#include <assert.h>
#include <errno.h>
#include <fstream>
#include <iostream>
#include <signal.h>
#include <sstream>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <string>
#ifdef _WIN32
  #include <windows.h>
  #include <imagehlp.h>
#else
  #include <err.h>
  #include <execinfo.h>
#endif
#include "error.h"
#include "strlib.h"

#include "stacktrace/call_stack.hpp"

namespace exceptions {
static bool topLevelExceptionHandlerEnabled = false;
static void (*old_terminate)() = NULL;
static std::string PROGRAM_NAME = "";

static void stanfordCppLibTerminateHandler();

bool getTopLevelExceptionHandlerEnabled() {
    return topLevelExceptionHandlerEnabled;
}

void setTopLevelExceptionHandlerEnabled(bool enabled) {
    if (!topLevelExceptionHandlerEnabled && enabled) {
        old_terminate = std::set_terminate(stanfordCppLibTerminateHandler);
    } else if (topLevelExceptionHandlerEnabled && !enabled) {
        std::set_terminate(old_terminate);
    }
    topLevelExceptionHandlerEnabled = enabled;
}

int execAndCapture(std::string cmd, std::string& output) {
    cmd += " 2>&1";
    FILE* pipe = popen(cmd.c_str(), "r");
    if (!pipe) {
        return -1;
    }
    char buffer[1024];
    output = "";
    while (!feof(pipe)) {
        if (fgets(buffer, 1024, pipe) != NULL) {
            output += buffer;
        }
    }
    return pclose(pipe);
}


// "_Z5errorSs at /home/stepp/Dropbox/data/docs/stanford/StanfordCPPLib/build/Autograder_QtCreatorProject-Desktop_Qt_5_3_GCC_64bit-Debug/../../Autograder_QtCreatorProject/lib/StanfordCPPLib/error.cpp:47 (discriminator 2)"
// _Z1cv at mainfunc.cpp:71 (discriminator 1)

// http://en.wikipedia.org/wiki/Name_mangling#Name_mangling_in_C.2B.2B

// MyNS::LoL::cCcCc_cCcCc123(int, char*, double, char, bool)
// becomes
// _ZN4MyNS3LoL14cCcCc_cCcCc123EiPcdcb

// _Z N4 MyNS 3 LoL 14 cCcCc_cCcCc123 EiPcdcb

static int extractIntFromFront(std::string& s) {
    int result = 0;
    while (!s.empty() && isdigit(s[0])) {
        result = (result * 10) + (s[0] - '0');
        s.erase(0, 1);
    }
    return result;
}

static std::string demangle(std::string line) {
    line = trim(line);
    if (startsWith(line, "?") || stringContains(line, "??:?")) {
        return "";   // no stack trace info; give up
    }
    
    // /home/stepp/Dropbox/data/docs/stanford/StanfordCPPLib/foo.cpp -> foo.cpp
    int firstSlash = stringIndexOf(line, "/");
    int lastSlash = stringLastIndexOf(line, "/");
    if (firstSlash >= 0 && lastSlash >= 0 && firstSlash != lastSlash) {
        line = line.erase(firstSlash, lastSlash - firstSlash + 1);
    }
    int firstSlash2 = stringIndexOf(line, "\\");
    int lastSlash2 = stringLastIndexOf(line, "\\");
    if (firstSlash2 >= 0 && lastSlash2 >= 0 && firstSlash2 != lastSlash2) {
        line = line.erase(firstSlash2, lastSlash2 - firstSlash2 + 1);
    }
    
    // remove "(discriminator 2)" and such info
    int leftParen = stringIndexOf(line, " (");
    if (leftParen >= 0) {
        line = line.substr(0, leftParen);
    }
    
    // extract function name and other info
    // MyNS::LoL::cCcCc_cCcCc123(int, char*, double, char, bool)
    // becomes
    // _ZN4MyNS3LoL14cCcCc_cCcCc123EiPcdcb
    // _Z N4 MyNS 3 LoL 14 cCcCc_cCcCc123 EiPcdcb
    
    // b becomes
    // _Z1bv
    
    int firstSpace = stringIndexOf(line, " ");
    if (firstSpace >= 0) {
        std::string funcName = line.substr(0, firstSpace);
        line = line.substr(firstSpace + 1);
        
        if (startsWith(funcName, "_Z")) {
            funcName = funcName.substr(2);
            if (isdigit(funcName[0])) {
                // a simple name such as
                // _Z5errorSs at error.cpp:47
                int len = extractIntFromFront(funcName);
                funcName = funcName.substr(0, len);
            } else if (funcName[0] == 'N') {
                // a more complex name such as the above
                std::string scrubbed = funcName;
                funcName = "";
                scrubbed.erase(0, 1);
                while (!scrubbed.empty() && isdigit(scrubbed[0])) {
                    int len = extractIntFromFront(scrubbed);
                    std::string symbolName = scrubbed.substr(0, len);
                    funcName += (funcName.empty() ? "" : "::") + symbolName;
                    scrubbed.erase(0, len);
                }
            }
            
            // replace custom Stanford C++ lib 'main' function names with real 'main' name
            if (funcName == "Main" || funcName == "studentMain") {
                funcName = "main";
            }
            
            line += " (function " + funcName + ")";
        }
    }
    
    return line;
}

/*
 * Resolve symbol name and source location given the path to the executable 
 * and an address
 */
int addr2line(const void* const addr, std::string& line) {
    char addr2line_cmd[512] = {0};
    
    // have addr2line map the address to the relent line in the code
#ifdef __APPLE__
    sprintf(addr2line_cmd, "atos -o %.256s %p", PROGRAM_NAME.c_str(), addr); 
#else
    sprintf(addr2line_cmd, "addr2line -f -p -e %.256s %p", PROGRAM_NAME.c_str(), addr); 
#endif
    
    return execAndCapture(addr2line_cmd, line);
}

#ifdef _WIN32
// TODO: implement
void print_stack_trace() {
    
}

#else
// linux/mac
#define MAX_STACK_FRAMES 64
static void* stack_traces[MAX_STACK_FRAMES];

void print_stack_trace() {
    int i, trace_size = 0;
    char **messages = (char **)NULL;
    
    trace_size = backtrace(stack_traces, MAX_STACK_FRAMES);
    messages = backtrace_symbols(stack_traces, trace_size);
    
    // skip the first couple stack frames (as they are this function and
    // our handler) and also skip the last frame as it's (always?) junk.
    // for (i = 3; i < (trace_size - 1); ++i)
    for (i = 0; i < trace_size; ++i) {
        std::string line;
        if (addr2line(stack_traces[i], line) == 0) {
            line = demangle(line);
            if (!line.empty()) {
                // stop showing the stack trace when it gets down to the low-level functions
                if (stringContains(line, "at main.cpp") || stringContains(line, "at platform.cpp")) {
                    break;
                }
                std::cerr << " ***     " << line << std::endl;
            }
        } else {
            // printf("  error determining line # for: %s\n", messages[i]);
        }
        
    }
    if (messages) {
        free(messages);
    }
}
#endif


static void stanfordCppLibTerminateHandler() {
    std::ostream& out = std::cerr;
    try {
        throw;   // re-throws the exception that already occurred
    } catch (const ErrorException& ex) {
        std::string msg = "\n ***\n";
        msg += " *** STANFORD C++ LIBRARY \n";
        msg += " *** An ErrorException occurred during program execution: \n";
        msg += " *** ";
        msg += ex.what();
        msg += "\n ***\n";
        std::cout.flush();
        out << msg;
        
        print_stack_trace();
        std::cerr << "About to print stack trace ..." << std::endl;
        stacktrace::call_stack trace;
        std::cerr << trace.to_string() << std::endl;
        std::cerr << "Done printing stack trace." << std::endl;
        
        throw ex;
    } catch (const InterruptedIOException& /* iex */) {
        // blocked console I/O was interrupted; just exit program immediately
        // (doesn't close any other JBE-generated GUI windows, but oh well)
        std::cout.flush();
        exit(0);
    } catch (const std::exception& ex) {
        std::string msg = "\n ***\n";
        msg += " *** STANFORD C++ LIBRARY \n";
        msg += " *** An exception occurred during program execution: \n";
        msg += " *** ";
        msg += ex.what();
        msg += "\n ***\n";
        std::cout.flush();
        out << msg;
        print_stack_trace();
        throw ex;
    } catch (std::string str) {
        std::string msg = "\n ***\n";
        msg += " *** STANFORD C++ LIBRARY \n";
        msg += " *** A string exception occurred during program execution: \n";
        msg += " *** \"";
        msg += str;
        msg += "\"\n ***\n";
        std::cout.flush();
        out << msg;
        print_stack_trace();
        throw str;
    } catch (char const* str) {
        std::string msg = "\n ***\n";
        msg += " *** STANFORD C++ LIBRARY \n";
        msg += " *** A string exception occurred during program execution: \n";
        msg += " *** \"";
        msg += str;
        msg += "\"\n ***\n";
        std::cout.flush();
        out << msg;
        print_stack_trace();
        throw str;
    } catch (int n) {
        char buf[128];
        snprintf(buf, 128, "%d", n);
        std::string msg = "\n ***\n";
        msg += " *** STANFORD C++ LIBRARY \n";
        msg += " *** An int exception occurred during program execution: \n";
        msg += " *** ";
        msg += buf;
        msg += "\n ***\n";
        std::cout.flush();
        out << msg;
        print_stack_trace();
        throw n;
    } catch (long l) {
        char buf[128];
        snprintf(buf, 128, "%ld", l);
        std::string msg = "\n ***\n";
        msg += " *** STANFORD C++ LIBRARY \n";
        msg += " *** A long exception occurred during program execution: \n";
        msg += " *** ";
        msg += buf;
        msg += "\n ***\n";
        std::cout.flush();
        out << msg;
        print_stack_trace();
        throw l;
    } catch (char c) {
        std::string msg = "\n ***\n";
        msg += " *** STANFORD C++ LIBRARY \n";
        msg += " *** A char exception occurred during program execution: \n";
        msg += " *** '";
        msg += c;
        msg += "'\n ***\n";
        std::cout.flush();
        out << msg;
        print_stack_trace();
        throw c;
    } catch (bool b) {
        std::string msg = "\n ***\n";
        msg += " *** STANFORD C++ LIBRARY \n";
        msg += " *** A bool exception occurred during program execution: \n";
        msg += " *** ";
        msg += (b ? "true" : "false");
        msg += "\n ***\n";
        std::cout.flush();
        out << msg;
        std::cout << msg;
        throw b;
    } catch (double d) {
        char buf[128];
        snprintf(buf, 128, "%lf", d);
        std::string msg = "\n ***\n";
        msg += " *** STANFORD C++ LIBRARY \n";
        msg += " *** A double exception occurred during program execution: \n";
        msg += " *** ";
        msg += buf;
        msg += "\n ***\n";
        std::cout.flush();
        out << msg;
        print_stack_trace();
        throw d;
    }
}

} // namespace exceptions

void setProgramNameForStackTrace(char* programName) {
    exceptions::PROGRAM_NAME = programName;
}
