/*
 * (C) Copyright 2013 ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation
 * nor does it submit to any jurisdiction.
 */

#include "atlas/library/FloatingPointExceptions.h"

#include <cfenv>
#include <cstring>
#include <iomanip>

#include "eckit/config/LibEcKit.h"
#include "eckit/config/Resource.h"
#include "eckit/utils/StringTools.h"
#include "eckit/utils/Translator.h"

#include "atlas/library/config.h"
#include "atlas/parallel/mpi/mpi.h"
#include "atlas/runtime/Exception.h"
#include "atlas/runtime/Log.h"
#include "atlas/runtime/Trace.h"

namespace {
int getEnv( const std::string& env, int default_value ) {
    if ( ::getenv( env.c_str() ) ) {
        return eckit::Translator<std::string, int>()( ::getenv( env.c_str() ) );
    }
    return default_value;
}
}  // namespace

static int atlas_feenableexcept( int excepts ) {
#if ATLAS_HAVE_FEENABLEEXCEPT
    return ::feenableexcept( excepts );
#else
    return 0;
#endif
}

#ifdef UNUSED
static int atlas_fedisableexcept( int excepts ) {
#if ATLAS_HAVE_FEDISABLEEXCEPT
    return ::fedisableexcept( excepts );
#else
    return 0;
#endif
}
#endif

namespace atlas {
namespace library {

static std::map<std::string, int> str_to_except = {{"FE_INVALID", FE_INVALID},     {"FE_INEXACT", FE_INEXACT},
                                                   {"FE_DIVBYZERO", FE_DIVBYZERO}, {"FE_OVERFLOW", FE_OVERFLOW},
                                                   {"FE_UNDERFLOW", FE_UNDERFLOW}, {"FE_ALL_EXCEPT", FE_ALL_EXCEPT}};
static std::map<int, std::string> except_to_str = {{FE_INVALID, "FE_INVALID"},     {FE_INEXACT, "FE_INEXACT"},
                                                   {FE_DIVBYZERO, "FE_DIVBYZERO"}, {FE_OVERFLOW, "FE_OVERFLOW"},
                                                   {FE_UNDERFLOW, "FE_UNDERFLOW"}, {FE_ALL_EXCEPT, "FE_ALL_EXCEPT"}};

static std::map<std::string, int> str_to_signal = {{"SIGINT", SIGINT},  {"SIGILL", SIGILL},   {"SIGABRT", SIGABRT},
                                                   {"SIGFPE", SIGFPE},  {"SIGKILL", SIGKILL}, {"SIGSEGV", SIGSEGV},
                                                   {"SIGTERM", SIGTERM}};
static std::map<int, std::string> signal_to_str = {{SIGINT, "SIGINT"},  {SIGILL, "SIGILL"},   {SIGABRT, "SIGABRT"},
                                                   {SIGFPE, "SIGFPE"},  {SIGKILL, "SIGKILL"}, {SIGSEGV, "SIGSEGV"},
                                                   {SIGTERM, "SIGTERM"}};

// ------------------------------------------------------------------------------------

class Signal {
    // Not sure if this should be made public (in header file) just yet

public:
    Signal();

    Signal( int signum );

    Signal( int signum, signal_action_t );

    Signal( int signum, signal_handler_t signal_handler );

    operator int() const { return signum_; }
    int signum() const { return signum_; }

    const std::string& code() const { return signal_to_str[signum_]; }

    std::string str() const { return str_; }
    const signal_handler_t& handler() const { return signal_action_.sa_handler; }
    const struct sigaction* action() const { return &signal_action_; }

private:
    friend std::ostream& operator<<( std::ostream&, const Signal& );

    int signum_;
    std::string str_;
    struct sigaction signal_action_;
};

// ------------------------------------------------------------------------------------

class Signals {
    // Not sure if this should be made public (in header file) just yet

private:
    Signals();

public:
    static Signals& instance();
    void setSignalHandlers();
    void setSignalHandler( const Signal& );
    void restoreSignalHandler( int signum );
    void restoreAllSignalHandlers();
    const Signal& signal( int signum ) const;

private:
    typedef std::map<int, Signal> registered_signals_t;
    registered_signals_t registered_signals_;
    eckit::Channel& out_;
};

// ------------------------------------------------------------------------------------


// ------------------------------------------------------------------------------------

[[noreturn]] void atlas_signal_handler( int signum, siginfo_t* si, void* /*unused*/ ) {
    Signal signal = Signals::instance().signal( signum );

    std::string signal_code;
    if ( signum == SIGFPE ) {
        switch ( si->si_code ) {
            case FPE_FLTDIV:
                signal_code = " [FE_DIVBYZERO]";
                break;
            case FPE_FLTINV:
                signal_code = " [FE_INVALID]";
                break;
            case FPE_FLTOVF:
                signal_code = " [FE_OVERFLOW]";
                break;
            case FPE_FLTUND:
                signal_code = " [FE_UNDERFLOW]";
                break;
            case FPE_FLTRES:
                signal_code = " [FE_INEXACT]";
                break;
        }
    }

    std::ostream& out = Log::error();
    out << "\n"
        << "=========================================\n"
        << signal << signal_code << " (signal intercepted by atlas)\n";
    out << "-----------------------------------------\n"
        << "BACKTRACE\n"
        << "-----------------------------------------\n"
        << backtrace() << "\n"
        << "=========================================\n"
        << std::endl;

    Signals::instance().restoreSignalHandler( signum );
    eckit::LibEcKit::instance().abort();

    // Just in case we end up here, which normally we shouldn't.
    std::_Exit( EXIT_FAILURE );
}

//----------------------------------------------------------------------------------------------------------------------


Signals::Signals() :
    out_( [&]() -> eckit::Channel& {
        if ( getEnv( "ATLAS_LOG_RANK", 0 ) == int( mpi::rank() ) ) {
            return Log::debug();
        }
        static eckit::Channel sink;
        return sink;
    }() ) {}

Signals& Signals::instance() {
    static Signals signals;
    return signals;
}

void Signals::restoreSignalHandler( int signum ) {
    if ( registered_signals_.find( signum ) != registered_signals_.end() ) {
        out_ << "\n";
        std::signal( signum, SIG_DFL );
        out_ << "Atlas restored default signal handler for signal " << std::setw( 7 ) << std::left
             << registered_signals_[signum].code() << " [" << registered_signals_[signum] << "]\n";
        out_ << std::endl;
        registered_signals_.erase( signum );
    }
}

void Signals::restoreAllSignalHandlers() {
    out_ << "\n";
    for ( registered_signals_t::const_iterator it = registered_signals_.begin(); it != registered_signals_.end();
          ++it ) {
        std::signal( it->first, SIG_DFL );
        out_ << "Atlas restored default signal handler for signal " << std::setw( 7 ) << std::left << it->second.code()
             << " [" << it->second.str() << "]\n";
    }
    out_ << std::endl;
    registered_signals_.clear();
}

const Signal& Signals::signal( int signum ) const {
    return registered_signals_.at( signum );
}

std::ostream& operator<<( std::ostream& out, const Signal& signal ) {
    out << signal.str();
    return out;
}

void Signals::setSignalHandlers() {
    setSignalHandler( SIGINT );
    setSignalHandler( SIGILL );
    setSignalHandler( SIGABRT );
    setSignalHandler( SIGFPE );
    setSignalHandler( SIGSEGV );
    setSignalHandler( SIGTERM );
}

void Signals::setSignalHandler( const Signal& signal ) {
    registered_signals_[signal] = signal;
    sigaction( signal, signal.action(), nullptr );
    out_ << "Atlas registered signal handler for signal " << std::setw( 7 ) << std::left << signal.code() << " ["
         << signal << "]" << std::endl;
}


Signal::Signal() : signum_( 0 ), str_() {
    signal_action_.sa_handler = SIG_DFL;
}

Signal::Signal( int signum ) : Signal( signum, atlas_signal_handler ) {}

Signal::Signal( int signum, signal_handler_t signal_handler ) : signum_( signum ), str_( strsignal( signum ) ) {
    memset( &signal_action_, 0, sizeof( signal_action_ ) );
    sigemptyset( &signal_action_.sa_mask );
    signal_action_.sa_handler = signal_handler;
    signal_action_.sa_flags   = 0;
}

Signal::Signal( int signum, signal_action_t signal_action ) : signum_( signum ), str_( strsignal( signum ) ) {
    memset( &signal_action_, 0, sizeof( signal_action_ ) );
    sigemptyset( &signal_action_.sa_mask );
    signal_action_.sa_sigaction = signal_action;
    signal_action_.sa_flags     = SA_SIGINFO;
}


void enable_floating_point_exceptions() {
    auto& out = [&]() -> eckit::Channel& {
        if ( getEnv( "ATLAS_LOG_RANK", 0 ) == int( mpi::rank() ) ) {
            return Log::debug();
        }
        static eckit::Channel sink;
        return sink;
    }();

    // Following line gives runtime errors with Cray 8.6 due to compiler bug (but works with Cray 8.5 and Cray 8.7)
    //   std::vector<std::string> floating_point_exceptions = eckit::Resource<std::vector<std::string>>( "atlasFPE;$ATLAS_FPE", {"false"} );
    // Instead, manually access environment
    std::vector<std::string> floating_point_exceptions{"false"};
    if ( ::getenv( "ATLAS_FPE" ) ) {
        std::string env( ::getenv( "ATLAS_FPE" ) );
        std::vector<std::string> tmp = eckit::Translator<std::string, std::vector<std::string>>()( env );
        floating_point_exceptions    = tmp;
        // Above trick with "tmp" is what avoids the Cray 8.6 compiler bug
    }
    else {
        floating_point_exceptions = eckit::Resource<std::vector<std::string>>( "atlasFPE", {"false"} );
    }
    {
        bool _enable = false;
        int _excepts = 0;
        auto enable  = [&]( int except ) {
            _excepts |= except;
            _enable = true;
            out << "Atlas enabled floating point exception " << except_to_str[except] << std::endl;
        };
        bool skip_map = false;
        if ( floating_point_exceptions.size() == 1 ) {
            std::string s = eckit::StringTools::lower( floating_point_exceptions[0] );
            if ( s == "no" || s == "off" || s == "false" || s == "0" ) {
                _enable  = false;
                skip_map = true;
            }
            else if ( s == "yes" || s == "on" || s == "true" || s == "1" ) {
                enable( FE_INVALID );
                enable( FE_DIVBYZERO );
                enable( FE_OVERFLOW );
                skip_map = true;
            }
        }
        if ( not skip_map ) {
            for ( auto& s : floating_point_exceptions ) {
                if ( str_to_except.find( s ) == str_to_except.end() ) {
                    throw eckit::UserError(
                        s + " is not a valid floating point exception code. "
                            "Valid codes: [FE_INVALID,FE_INEXACT,FE_DIVBYZERO,FE_OVERFLOW,FE_ALL_EXCEPT]",
                        Here() );
                }
                enable( str_to_except[s] );
            }
        }
        if ( _enable ) {
            atlas_feenableexcept( _excepts );
        }
    }
}

void enable_atlas_signal_handler() {
    if ( eckit::Resource<bool>( "atlasSignalHandler;$ATLAS_SIGNAL_HANDLER", false ) ) {
        Signals::instance().setSignalHandlers();
    }
}

}  // namespace library
}  // namespace atlas
