/*
This file is part of pilang.

pilang is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

pilang is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with pilang.  If not, see <https://www.gnu.org/licenses/>.
*/

#include <iostream>
#include <string>
#include <string_view>
#include <exception>
#include <map>
#include <vector>
#include <regex>
#include <fstream>

#include <cstdio>
#include <cstring>
#include <csignal>
#include <dlfcn.h>
#include <dirent.h>
#include <unistd.h>


// Return type defs
std::string success = "Success";
std::string failed = "Failed";
std::string nosuchcommand = "No such command";
std::string nosuchvariable = "No such variable";
std::string badarguments = "Bad arguments";

// Exception defs
class markergoto : public std::string {};
class pilerror : public std::string {};
std::string markergotoname;

// Environment defs
std::vector<std::string> cmdargs;
std::map<std::string, std::string> variables;
std::map<std::string, int> markers;
bool interactivecli;
bool catcherrs = false;
std::string lasterr = "";
std::vector<std::string> markersetqueue;
std::string execline(std::string commandstr);

// Restart program on segmenation fault
void sigsegv_handler(int signal) {
    std::cerr << std::endl << "Pilang crashed: signal " << signal << "=SIGSEGV (memory error)" << std::endl;
    exit(177);
}

// Helper functions
bool is_float(std::string s) {
    std::istringstream iss(s);
    float f;
    iss >> std::noskipws >> f;
    return iss.eof() && !iss.fail();
}
std::string str_lower(std::string string) {
    std::transform(string.begin(), string.end(), string.begin(), ::tolower);
    return string;
}
std::string str_upper(std::string string) {
    std::transform(string.begin(), string.end(), string.begin(), ::toupper);
    return string;
}
std::string ltrim(const std::string& s) {
    static const std::regex lws{"^[[:space:]]*", std::regex_constants::extended};
    return std::regex_replace(s, lws, "");
}
std::string rtrim(const std::string& s) {
    static const std::regex tws{"[[:space:]]*$", std::regex_constants::extended};
    return std::regex_replace(s, tws, "");
}
std::string trim(const std::string& s) {
    return ltrim(rtrim(s));
}
std::vector<std::string> strsplit(std::string_view s, char delimiter, std::vector<std::string>::size_type times = 0) {
    std::vector<std::string> to_return;
    decltype(s.size()) start = 0, finish = 0;
    while ((finish = s.find_first_of(delimiter, start)) != std::string_view::npos) {
        to_return.emplace_back(s.substr(start, finish - start));
        start = finish + 1;
        if (to_return.size() == times) { break; }
    }
    to_return.emplace_back(s.substr(start));
    return to_return;
}
void do_markersetqueue(int currline) {
    for (const std::string & markername : markersetqueue) {
        markers[markername] = currline;
    }
    markersetqueue = {};
}
std::string get_rstring(std::string rstring) {
    if (rstring == nosuchcommand or rstring == nosuchvariable or rstring == badarguments) {
        lasterr = rstring;
        if (catcherrs)
            throw pilerror();
    }
    return rstring;
}

// Builtin functions
std::string cmd_ret(std::vector<std::string> args) {
    std::string retstring;
    for (const std::string & arg : args){
        retstring.append(arg);
    }
    return retstring;
}
std::string cmd_catch(std::vector<std::string> args) {
    std::cout << "reached" << std::endl;
    if (args.size() != 1) {
        return badarguments;
    }
    std::string boolstr = *args.begin();
    if (str_lower(boolstr) == "true")
        catcherrs = true;
    else if (str_lower(boolstr) == "false")
        catcherrs = false;
    else
        return badarguments;
    return success;
}
std::string cmd_get(std::vector<std::string> args) {
    if (args.size() != 2) {
        return badarguments;
    }
    auto argsit = args.begin();
    std::string string = *argsit;
    argsit++;
    long unsigned int position = std::stoi(*argsit);
    if (position < string.length()) {
        std::string res;
        res.push_back(string.at(position));
        return res;
    } else {
        return badarguments;
    }
}
std::string cmd_len(std::vector<std::string> args) {
    int result = 0;
    for (const std::string & arg : args){
        result += arg.length();
    }
    return std::to_string(result);
}
std::string cmd_exit(std::vector<std::string> args) {
    //Check amount of arguments
    if (args.size() != 1) {
        return badarguments;
    }
    std::string exitcode = *args.begin();
    if (is_float(exitcode))
        exit(std::stoi(exitcode));
    else
        return badarguments;
}
std::string cmd_marker(std::vector<std::string> args) {
    // Check amount of arguments
    if (args.size() != 2) {
        return badarguments;
    }
    // Get arguments
    auto argsit = args.begin();
    std::string action = *argsit;
    argsit++;
    std::string markername = *argsit;
    // Set or goto
    if (action == "goto") {
        markergotoname = markername;
        throw markergoto();
    } else if (action == "set") {
        markersetqueue.push_back(markername);
    } else {
        return badarguments;
    }
    return success;
}
std::string cmd_printnnl(std::vector<std::string> args) {
    std::cout << cmd_ret(args) << std::flush;
    return success;
}
std::string cmd_print(std::vector<std::string> args) {
    auto result = cmd_printnnl(args);
    std::cout << std::endl;
    return result;
}
std::string cmd_input(std::vector<std::string> args) {
    cmd_printnnl(args);
    std::string result;
    std::getline(std::cin, result);
    return result;
}
std::string cmd_set(std::vector<std::string> args) {
    //Check amount of arguments
    if (args.size() != 2) {
        return badarguments;
    }
    // Get arguments
    auto argsit = args.begin();
    std::string key = *argsit;
    argsit++;
    std::string value = *argsit;
    // Store in environment
    variables[key] = value;
    // Return success
    return success;
}
std::string cmd_append(std::vector<std::string> args) {
    //Check amount of arguments
    if (args.size() < 2) {
        return badarguments;
    }
    // Get arguments
    auto argsit = args.begin();
    std::string variablename = *argsit;
    argsit++;
    for (int dummy; argsit != args.end(); argsit++)
        variables[variablename].append(*argsit);
    return success;
}
std::string cmd_incdec(bool type, std::vector<std::string> args) {
    //Check amount of arguments
    if (args.size() < 1 or args.size() > 2) {
        return badarguments;
    }
    // Get arguments
    auto argsit = args.begin();
    std::string variablename = *argsit;
    float incby;
    if (args.size() == 2) {
        argsit++;
        incby = std::atof((*argsit).c_str());
    } else {
        incby = 1;
    }
    // Modify variable if it exists
    if (variables.find(variablename) == variables.end()) {
        return nosuchvariable;
    } else {
        float currval = std::atof(variables[variablename].c_str());
        if (type)
            currval += incby;
        else
            currval -= incby;
        variables[variablename] = std::to_string(currval);
    }
    return success;
}
std::string cmd_del(std::vector<std::string> args) {
    if (args.size() < 1) {
        return badarguments;
    }
    for (const std::string & arg : args) {
        auto varit = variables.find(arg);
        if (varit != variables.end()) {
            variables.erase(variables.find(arg));
        }
    }
    return success;
}
std::string cmd_eval(std::vector<std::string> args) {
    if (args.size() < 1) {
        return badarguments;
    }
    std::string result;
    for (const std::string & arg : args) {
        result = execline(arg);
    }
    return result;
}
std::string cmd_cmp(std::vector<std::string> args) {
    bool matches;
    //Check amount of arguments
    if (args.size() < 4) {
        return badarguments;
    }
    // Get arguments
    auto argsit = args.begin();
    std::string action = *argsit;
    argsit++;
    std::string var1 = *argsit;
    argsit++;
    std::string var2 = *argsit;
    argsit++;
    // Compare
    if      (action == "equal")
        matches = var1 == var2;
    else if (action == "nonequal")
        matches = var1 != var2;
    else if (action == "bigger")
        matches = std::atof(var1.c_str()) > std::atof(var2.c_str());
    else if (action == "smaller")
        matches = std::atof(var1.c_str()) < std::atof(var2.c_str());
    else if (action == "isnum" or action == "isnotnum") {
        matches = is_float(var1) and is_float(var2);
        if (action == "isnotnum")
            matches = !matches;
    } else
        return badarguments;
    // Eval on match
    if (matches) {
        std::string result;
        while (argsit != args.end()) {
            result = execline(*argsit);
            argsit++;
        }
        return result;
    } else {
        return failed;
    }
}
std::string cmd_lowerupper(bool type, std::vector<std::string> args) {
    if (args.size() < 1) {
        return badarguments;
    }
    std::string result;
    for (auto argsit = args.begin(); argsit != args.end(); argsit++) {
        if (type)
            result.append(str_upper(*argsit));
        else
            result.append(str_lower(*argsit));
    }
    return result;
}
std::string cmd_addsub(bool type, std::vector<std::string> args) {
    //Check amount of arguments
    if (args.size() < 2) {
        return badarguments;
    }
    // Initialise variables
    bool asint = false;
    float numarg;
    // Get arguments
    auto argsit = args.begin();
    if (*argsit == "asint") {
        asint = true;
        argsit++;
    }
    // Initialise initial result
    if (!is_float(*argsit))
        return badarguments;
    float result = std::atof((*argsit).c_str());
    argsit++;
    // Calculaté
    for (int dummy; argsit != args.end(); argsit++) {
        if (!is_float(*argsit))
            return badarguments;
        numarg = std::atof((*argsit).c_str());
        if (type)
            result += numarg;
        else
            result -= numarg;
    }
    // Return result
    if (asint)
        return std::to_string((int) result);
    else
        return std::to_string(result);
}
std::string cmd_sleep(std::vector<std::string> args) {
    // Check amount of arguments
    if (args.size() != 1) {
        return badarguments;
    }
    // Check argument
    auto argsit = args.begin();
    if (!is_float(*argsit))
        return badarguments;
    // Get argument
    float sleepnsecs = std::atof((*argsit).c_str()) * 1000000;
    usleep(sleepnsecs);
    return success;
}
std::string cmd_argv(std::vector<std::string> args) {
    // Check amount of arguments
    if (args.size() != 1) {
        return badarguments;
    }
    // Check argument
    auto argsit = args.begin();
    if (!is_float(*argsit))
        return badarguments;
    long unsigned int argnum = std::atoi((*argsit).c_str());
    if (argnum >= cmdargs.size())
        return badarguments;
    // Get argument
    return cmdargs[argnum];
}
std::string cmd_argc(std::vector<std::string> args) {
    return std::to_string(cmdargs.size());
}

std::string run_command(std::string command, std::vector<std::string> args) {
    //std::cout << "Running command:" << command << "!" << std::endl;
    if (command == "ret")
        return cmd_ret(args);
    else if (command == "catch")
        return cmd_catch(args);
    else if (command == "get")
        return cmd_get(args);
    else if (command == "len")
        return cmd_len(args);
    else if (command == "exit")
        return cmd_exit(args);
    else if (command == "marker")
        return cmd_marker(args);
    else if (command == "print")
        return cmd_print(args);
    else if (command == "printnnl")
        return cmd_printnnl(args);
    else if (command == "input")
        return cmd_input(args);
    else if (command == "set")
        return cmd_set(args);
    else if (command == "append")
        return cmd_append(args);
    else if (command == "inc")
        return cmd_incdec(true, args);
    else if (command == "dec")
        return cmd_incdec(false, args);
    else if (command == "del")
        return cmd_del(args);
    else if (command == "eval")
        return cmd_eval(args);
    else if (command == "cmp")
        return cmd_cmp(args);
    else if (command == "lower")
        return cmd_lowerupper(false, args);
    else if (command == "upper")
        return cmd_lowerupper(true, args);
    else if (command == "add")
        return cmd_addsub(true, args);
    else if (command == "sub")
        return cmd_addsub(false, args);
    else if (command == "sleep")
        return cmd_sleep(args);
    else if (command == "argv")
        return cmd_argv(args);
    else if (command == "argc")
        return cmd_argc(args);
    else
        return nosuchcommand;
}

// Interpreter initialiser
void init_interpreter(int argc, char *argv[]) {
    // Load modules from modules directory
    struct dirent *dp;
    DIR *dfd;
    char filename_qfd[264];
    char moduledir [] = "modules";
    dfd = opendir(moduledir);
    while ((dp = readdir(dfd)) != NULL) {
        sprintf(filename_qfd , "%s/%s",moduledir,dp->d_name);
        // Skip . and ..
        if (strcmp(dp->d_name,"..") or strcmp(dp->d_name,"."))
            continue;
        // Load file
        std::cout << "Module loading not yet implemented for: " << filename_qfd << std::endl;
    }
    // Generate list of arguments
    for (int elemnum = 0; elemnum < argc; elemnum++) {
        cmdargs.push_back(std::string(argv[elemnum]));
    }
}

std::string execline(std::string commandstr) {
    commandstr = ltrim(commandstr.substr(0, commandstr.find("#", 0)));
    //std::cout << "$ " << commandstr << std::endl;
    if (rtrim(commandstr) == "") {               // Command is empty
        return commandstr;
    } else if (commandstr.rfind('~', 0) == 0) {  // Command is a variable
        commandstr.erase(0, 1);
        // Check if variable exists
        if (variables.find(commandstr) == variables.end()) {
            return nosuchvariable;
        } else {
            return variables[commandstr];
        }
    } else {                                     // Command is an actual command
        std::vector<std::string> cmdsplit = strsplit(commandstr, ' ', 1);
        auto cmdsplit_it = cmdsplit.begin();
        std::string command = *cmdsplit_it;
        std::string argsstr = "";
        cmdsplit_it++;
        if (cmdsplit.size() == 2) {
            argsstr = *cmdsplit_it;
            argsstr.push_back(',');
        }
        std::vector<std::string> args;
        if (argsstr != ",") {
            std::string currarg = "";
            bool instring = false;
            bool arginstring = false;
            bool charig = false;
            bool skipall = false;
            for (std::string::iterator currchar = argsstr.begin(); currchar < argsstr.end(); currchar++) {
                if (skipall) {
                    continue;
                } else if (*currchar == '"' and !skipall) {
                    instring = !instring;
                    if (currarg == "")
                        arginstring = true;
                } else if (*currchar == '#' and !charig and !instring) {
                    skipall = true;
                } else if (*currchar == ',' and !charig and !instring) {
                    if (currarg.length() != 0 and *currarg.begin() == '$' and !arginstring) {
                        currarg.erase(0, 1);
                        currarg = execline(currarg);
                    }
                    args.push_back(currarg);
                    currarg = "";
                    arginstring = false;
                } else if (*currchar == '\\' and !charig) {
                    charig = true;
                } else {
                    currarg.push_back(*currchar);
                    charig = false;
                }
            }
        }
        //std::cout << "Args len: " << args.size() << std::endl;
        return get_rstring(run_command(command, args));
    }
}




// Main CLI
int main(int argc, char *argv[]) {
    signal(SIGSEGV, sigsegv_handler);
    std::string line;
    using namespace std;
    if (argc == 1) {                // Start interactive CLI
        // Show copyright note
        cout << "pilang  Copyright (C) 2020  niansa" << endl;
        cout << "This program comes with ABSOLUTELY NO WARRANTY; for details type `warranty'." << endl;
        cout << "This is free software, and you are welcome to redistribute it" << endl;
        cout << "under certain conditions; type `license' for details." << endl;
        cout << endl;
        // Initialise stuff
        init_interpreter(argc, argv);
        interactivecli = true;
        // Input loop
        while (true) {
            line = "";
            cout << ">>> " << flush;
            std::getline(std::cin, line);
            cout << execline(line) << endl;
        }
    } else {
        // Read from argv[1] into vector
        std::ifstream scriptfile(argv[1]);
        std::vector<std::string> lines;
        std::string line;
        while (std::getline(scriptfile, line)) {
            lines.push_back(line);
            if (scriptfile.eof()) { break; } // EOF
        }
        scriptfile.close();
        // Initialise stuff
        init_interpreter(argc, argv);
        interactivecli = false;
        // Add initial marker in line 0
        markersetqueue.push_back("start");
        do_markersetqueue(0);
        // Execute vector line-by-line
        auto linesit = lines.begin();
        for (int currline = 0; linesit != lines.end(); currline++) {
            try {
                execline(*linesit);
                do_markersetqueue(currline);  // Do markerqueue
            } catch (markergoto) { // Catch marker goto
                do_markersetqueue(currline);  // Do markerqueue even if an exception occured
                if (markers.find(markergotoname) != markers.end()) {
                    currline = markers[markergotoname];
                    linesit = lines.begin() + currline;
                } else {
                    cerr << "No such marker: \"" << markergotoname << "\" (critical)" << endl;
                    return 78;
                }
            } catch (pilerror) {
                cerr << "Line " << currline << ": " << lasterr << " (catched)" << endl;
                return 77;
            }
            linesit++;
        }
    }
}