/********************************************************
 *  ██████╗  ██████╗████████╗██╗
 * ██╔════╝ ██╔════╝╚══██╔══╝██║
 * ██║  ███╗██║        ██║   ██║
 * ██║   ██║██║        ██║   ██║
 * ╚██████╔╝╚██████╗   ██║   ███████╗
 *  ╚═════╝  ╚═════╝   ╚═╝   ╚══════╝
 * Geophysical Computational Tools & Library (GCTL)
 *
 * Copyright (c) 2023  Yi Zhang (yizhang-geo@zju.edu.cn)
 *
 * GCTL is distributed under a dual licensing scheme. You can redistribute 
 * it and/or modify it under the terms of the GNU Lesser General Public 
 * License as published by the Free Software Foundation, either version 2 
 * of the License, or (at your option) any later version. You should have 
 * received a copy of the GNU Lesser General Public License along with this 
 * program. If not, see <http://www.gnu.org/licenses/>.
 * 
 * If the terms and conditions of the LGPL v.2. would prevent you from using 
 * the GCTL, please consider the option to obtain a commercial license for a 
 * fee. These licenses are offered by the GCTL's original author. As a rule, 
 * licenses are provided "as-is", unlimited in time for a one time fee. Please 
 * send corresponding requests to: yizhang-geo@zju.edu.cn. Please do not forget 
 * to include some description of your company and the realm of its activities. 
 * Also add information on how to contact you by electronic and paper mail.
 ******************************************************/

#include "dsviewer.h"
// We use the editline library to handle all inputs
#include "editline.h"

cmd_pair curr_cmd; // Executing command
gctl::dsv_io tc; // Grid object

extern "C" {

/*
 * Strip whitespace from the start and end of STRING.  Return a pointer
 * into STRING.
 */
char *stripwhite(char *string)
{
    char *s, *t;

    for (s = string; isspace(*s); s++) ;

    if (*s == 0)
	return s;

    t = s + strlen(s) - 1;
    while (t > s && isspace(*t))
	t--;
    *++t = '\0';

    return s;
}

/* Generator function for command completion.  STATE lets us
   know whether to start from scratch; without any state
   (i.e. STATE == 0), then we start at the top of the list. */
char *command_generator(const char *text, int state)
{
    static int list_index, len;

    /* If this is a new word to complete, initialize now.  This
       includes saving the length of TEXT for efficiency, and
       initializing the index variable to 0. */
    if (!state)
    {
        list_index = 0;
        len = strlen(text);
    }

    /* Return the next name which partially matches from the command list. */
    while (list_index < CMD_NUM)
    {
        if (std::string(text) == commands[list_index].name.substr(0, len))
        {
            //const char* name = commands[list_index].name.data();
            return strdup(commands[list_index].name.data());
        }
        else list_index++;
    }

    /* If no names matched, then return NULL. */
    return nullptr;
}

/*
 * Attempt to complete on the contents of TEXT.  START and END
 * bound the region of rl_line_buffer that contains the word to
 * complete.  TEXT is the word to complete.  We can use the entire
 * contents of rl_line_buffer in case we want to do some simple
 * parsing. Return the array of matches, or NULL if there aren't any.
 */
char **dsviewer_completion(const char *text, int start, int end)
{
    char **matches = nullptr;

    /* If this word is at the start of the line, then it is a command
       to complete.  Otherwise it is the name of a file in the current
       directory. */
    if (start == 0) matches = rl_completion_matches(text, command_generator);

    return matches;
}

void initialize_readline(void)
{
    /* Allow conditional parsing of the ~/.inputrc file. */
    rl_readline_name = "dsviewer";

    /* Tell the completer that we want a crack first. */
    rl_attempted_completion_function = dsviewer_completion;
}

} // End C section

void display_cmds()
{
    std::clog << "Command:\n";
    for (size_t i = 0; i < CMD_NUM - 1; i++)
    {
        std::clog << std::setw(12) << commands[i].name << ":\t" << commands[i].brief << "\n";
    }

    std::clog << "\nEnter \"<command>?\" to see detailed instructions.\n";

    std::clog << "\nCmdFile:\n";
    std::clog << "Each line of the input file(s) will be parsed as a command.\nEmpty lines and lines start with '#' will be skipped.\n";

    std::clog << "\nHereDoc:\n";
    std::clog << "You can use HereDoc to input commands. A simple example is:\n";
    std::clog << "dsviewer << EOF\n";
    std::clog << "open file1.csv\n";
    std::clog << "info\n";
    std::clog << "EOF\n";
    return;
}

void display_help(std::string input_cmd)
{
    std::string install_dir = GCTL_INSTALL_PREFIX;
    std::ifstream helpin;
    open_infile(helpin, install_dir + "/sbin/share/dsviewer", ".md");

    std::string tmp_l, tmp_help;
    std::vector<std::string> cmds;
    std::vector<std::string> helps;
    while (getline(helpin, tmp_l))
    {
        if (tmp_l.substr(0, 4) == "####")
        {
            cmds.push_back(tmp_l.substr(5));
            tmp_help = "";
            while (getline(helpin, tmp_l))
            {
                if (tmp_l == "") break;
                else tmp_help += tmp_l + "\n";
            }
            helps.push_back(tmp_help);
        }        
    }
    helpin.close();

    std::string cmd_str; 
    for (size_t j = 0; j < cmds.size(); j++)
    {
        parse_string_to_value(cmds[j], ' ', true, cmd_str);
        if (input_cmd == cmd_str)
        {
            replace_all(tmp_l, cmds[j], "\\", "");
            std::cout << GCTL_BOLDGREEN << tmp_l << GCTL_RESET << "\n";
            std::cout << helps[j] << "\n";
            break;
        }
    }
    return;
}

void exec_cmd(std::string cmd)
{
    std::string cmd_name;
    parse_string_to_value(cmd, ' ', true, cmd_name);

    // show instruction if there is a question mark at end of the command
    if (cmd_name.back() == '?')
    {
        cmd_name = cmd_name.substr(0, cmd_name.length() - 1);
        display_help(cmd_name);
        return;
    }

    // set default command to null
    curr_cmd = commands[CMD_NUM - 1];
    for (size_t i = 0; i < CMD_NUM - 1; i++)
    {
        if (cmd_name == commands[i].name)
        {
            curr_cmd = commands[i]; break;
        }
    }

    if (curr_cmd.func_p == nullptr) throw gctl::runtime_error("Invalid command: " + cmd_name);

    std::vector<std::string> cmd_units;
    parse_string_with_quotes(cmd, cmd_units);

    return curr_cmd.func_p(cmd_units);
}

// This function is defined to avoid potential breakdown while running in script mode.
void quit(const std::vector<std::string> &cmd_units)
{
    return;
}

void info(const std::vector<std::string> &cmd_units)
{
    std::smatch ret;
    std::regex pata("att"), patt("tag"), patc("col"), patr("row"), path("hdr");

    // info [att|tag|hdr|col|row]
    int info_code = 0;
    if (cmd_units.size() > 1)
    {
        if (regex_search(cmd_units[1], ret, pata)) info_code = AttInfo;
        if (regex_search(cmd_units[1], ret, patt)) info_code = info_code|TagInfo;
        if (regex_search(cmd_units[1], ret, patc)) info_code = info_code|ColInfo;
        if (regex_search(cmd_units[1], ret, patr)) info_code = info_code|RowInfo;
        if (regex_search(cmd_units[1], ret, path)) info_code = info_code|HeadInfo;
        tc.info(info_code);
    }
    else tc.info(ColInfo);
    return;
}

void head(const std::vector<std::string> &cmd_units)
{
    // head [number]
    int h = 10;
    if (cmd_units.size() > 1) h = atoi(cmd_units[1].c_str());
    if (h <= 0) throw std::runtime_error("head: invalid number.");

    _1s_vector cnames = tc.column_names();
    display_vector(cnames, std::cout, tc.delimeter());

    _1s_array line;
    for (size_t i = 1; i <= h; i++)
    {
        tc.get_row(line, i);
        line.show(std::cout, tc.delimeter());
    }
    return;
}

void tail(const std::vector<std::string> &cmd_units)
{
    // tail [number]
    int h = 10;
    if (cmd_units.size() > 1) h = atoi(cmd_units[1].c_str());
    if (h <= 0) throw std::runtime_error("tail: invalid number.");

    _1s_vector cnames = tc.column_names();
    display_vector(cnames, std::cout, tc.delimeter());

    _1s_array line;
    for (size_t i = tc.row_number() - h + 1; i <= tc.row_number(); i++)
    {
        tc.get_row(line, i);
        line.show(std::cout, tc.delimeter());
    }
    return;
}

void set_enable(const std::vector<std::string> &cmd_units)
{
    // enable table|column|row [<name1>,<name2>...]
    if (cmd_units.size() < 2) throw std::runtime_error("enable: insufficient parameters.");

    if (cmd_units[1] == "table") tc.table_output(Enable);
    else if (cmd_units[1] == "column")
    {
        for (size_t i = 2; i < cmd_units.size(); i++)
        {
            tc.column_output(cmd_units[i], Enable);
        }
    }
    else if (cmd_units[1] == "row")
    {
        for (size_t i = 2; i < cmd_units.size(); i++)
        {
            tc.row_output(cmd_units[i], Enable);
        }
    }
    else throw std::runtime_error("enable: invalid parameters.");
    return;
}

void set_disable(const std::vector<std::string> &cmd_units)
{
    // disable table|column|row [<name1>,<name2>...]
    if (cmd_units.size() < 2) throw std::runtime_error("disable: insufficient parameters.");

    if (cmd_units[1] == "table") tc.table_output(Disable);
    else if (cmd_units[1] == "column")
    {
        for (size_t i = 2; i < cmd_units.size(); i++)
        {
            tc.column_output(cmd_units[i], Disable);
        }
    }
    else if (cmd_units[1] == "row")
    {
        for (size_t i = 2; i < cmd_units.size(); i++)
        {
            tc.row_output(cmd_units[i], Disable);
        }
    }
    else throw std::runtime_error("disable: invalid parameters.");
    return;
}

void load_file(const std::vector<std::string> &cmd_units)
{
    // load <file> [nohead|column|row|both] [head_num] [<delimeter>] [att_sym] [tag_sym] 
    if (cmd_units.size() < 2) throw std::runtime_error("open: insufficient parameters.");

    gctl::array<std::string> copy_str(5, "null");
    for (size_t i = 0; i < GCTL_MIN(cmd_units.size() - 2, 5); i++)
    {
        copy_str[i] = cmd_units[i + 2];
    }

    int ht = NoHead;
    if (copy_str[0] == "nohead") ht = NoHead;
    else if (copy_str[0] == "column") ht = ColHead;
    else if (copy_str[0] == "row") ht = RowHead;
    else if (copy_str[0] == "both") ht = RowHead|ColHead;

    int hnum = 0;
    if (copy_str[1] != "null") hnum = atoi(copy_str[1].c_str());
    if (hnum != 0) tc.head_number(hnum);

    if (copy_str[2] != "null") tc.delimeter(copy_str[2][0]);
    if (copy_str[3] != "null") tc.annotation_symbol(copy_str[3][0]);
    if (copy_str[4] != "null") tc.tag_symbol(copy_str[4][0]);

    std::string naked_name, exten_name;
    parse_filename(cmd_units[1], naked_name, exten_name);

    if (exten_name == ".csv") tc.load_csv(naked_name);
    else tc.load_text(naked_name, exten_name, ht);
    return;
}

void save_file(const std::vector<std::string> &cmd_units)
{
    // save <file> [<delimeter>] [att_sym] [tag_sym]
    if (cmd_units.size() < 2) throw std::runtime_error("save: insufficient parameters.");

    gctl::array<std::string> copy_str(3, "null");
    for (size_t i = 0; i < GCTL_MIN(cmd_units.size() - 2, 3); i++)
    {
        copy_str[i] = cmd_units[i + 2];
    }

    if (copy_str[0] != "null") tc.delimeter(copy_str[0][0]);
    if (copy_str[1] != "null") tc.annotation_symbol(copy_str[1][0]);
    if (copy_str[2] != "null") tc.tag_symbol(copy_str[2][0]);

    std::string naked_name, exten_name;
    parse_filename(cmd_units[1], naked_name, exten_name);

    if (exten_name == ".csv") tc.save_csv(naked_name);
    else tc.save_text(naked_name, exten_name);
    return;
}

void statistic(const std::vector<std::string> &cmd_units)
{
    // stats <column> <column>...
    if (cmd_units.size() < 2) throw std::runtime_error("stats: insufficient parameters.");

    _1d_array data;
    for (size_t i = 1; i < cmd_units.size(); i++)
    {
        tc.get_column(data, cmd_units[i]);
        std::clog << "column: " << cmd_units[i] 
            << " | " << data.min() << "/" << data.mean() << "/" << data.max() 
            << " | STD: " << data.std() << "\n"; 
    }
    return;
}

void set_type(const std::vector<std::string> &cmd_units)
{
    // type row|col int|float|string <name>
    if (cmd_units.size() < 4) throw std::runtime_error("type: insufficient parameters.");
    if (cmd_units[1] == "row")
    {
        if (cmd_units[2] == "int") tc.row_type(Int, cmd_units[3]);
        else if (cmd_units[2] == "float") tc.row_type(Float, cmd_units[3]);
        else if (cmd_units[2] == "string") tc.row_type(String, cmd_units[3]);
        else throw std::runtime_error("type: invalid parameters.");
    }
    else if (cmd_units[1] == "col")
    {
        if (cmd_units[2] == "int") tc.column_type(Int, cmd_units[3]);
        else if (cmd_units[2] == "float") tc.column_type(Float, cmd_units[3]);
        else if (cmd_units[2] == "string") tc.column_type(String, cmd_units[3]);
        else throw std::runtime_error("type: invalid parameters.");
    }
    else throw std::runtime_error("type: invalid parameters.");
    return;
}

void set_titles(const std::vector<std::string> &cmd_units)
{
    // title <row|col> <t1>,<t2>,<t3>,... [<id1>,<id2>,<id3>,...]
    if (cmd_units.size() < 3) throw std::runtime_error("title: insufficient parameters.");
    if (cmd_units[1] == "row")
    {
        std::vector<std::string> row_titles;
        std::vector<int> row_ids;
        parse_string_to_vector(cmd_units[2], ',', row_titles);
        if (cmd_units.size() >= 4)
        {
            parse_string_to_vector(cmd_units[3], ',', row_ids);
            if (row_titles.size() != row_ids.size()) throw std::runtime_error("title: invalid parameters.");
            tc.row_names(row_titles, row_ids);
        }
        else tc.row_names(row_titles);
    }
    else if (cmd_units[1] == "col")
    {
        std::vector<std::string> col_titles;
        std::vector<int> col_ids;
        parse_string_to_vector(cmd_units[2], ',', col_titles);
        if (cmd_units.size() >= 4)
        {
            parse_string_to_vector(cmd_units[3], ',', col_ids);
            if (col_titles.size()!= col_ids.size()) throw std::runtime_error("title: invalid parameters.");
            tc.column_names(col_titles, col_ids);
        }
        else tc.column_names(col_titles);
    }
    else throw std::runtime_error("title: invalid parameters.");
    return;
}

void math_func(const std::vector<std::string> &cmd_units)
{
    // math <func> <col1>,<col2>,<col3>...
    if (cmd_units.size() < 3) throw std::runtime_error("math: insufficient parameters.");

    std::vector<std::string> col_names;
    parse_string_to_vector(cmd_units[2], ',', col_names);

    tc.cal_column(cmd_units[1], col_names, 12);
    return;
}

void rand_data(const std::vector<std::string> &cmd_units)
{
    // random normal|uniform <p1> <p2> [<colname>]
    if (cmd_units.size() < 4) throw std::runtime_error("random: insufficient parameters.");

    random_type_e rd_type = RdNormal;
    if (cmd_units[1] == "normal") rd_type = RdNormal;
    else if (cmd_units[1] == "uniform") rd_type = RdUniform;
    else throw std::runtime_error("random: invalid parameters.");

    double p1 = atof(cmd_units[2].c_str());
    double p2 = atof(cmd_units[3].c_str());
    array<double> rd_data(tc.row_number());
    rd_data.random_float(p1, p2, rd_type);

    if (cmd_units.size() >= 5) tc.fill_column(rd_data, cmd_units[4], 12);
    else
    {
        std::string col_name = "RdData";
        tc.add_column(col_name);
        tc.fill_column(rd_data, col_name, 12);
    }
    return;
}

void filt_data(const std::vector<std::string> &cmd_units)
{
    // filter row|col regex|math <expression> <name1>,<name2>,<name3>...
    if (cmd_units.size() < 5) throw std::runtime_error("filter: insufficient parameters.");
    std::vector<std::string> tar_names;
    parse_string_to_vector(cmd_units[4], ',', tar_names);

    table_headtype_e thead;
    if (cmd_units[1] == "row") thead = RowHead;
    else if (cmd_units[1] == "col") thead = ColHead;
    else throw std::runtime_error("filter: invalid parameters.");

    if (cmd_units[2] == "regex") tc.filter(cmd_units[3], tar_names[0], thead);
    else if (cmd_units[2] == "math") tc.filter(cmd_units[3], tar_names, thead);
    else throw std::runtime_error("filter: invalid parameters.");
    return;
}

void insert_data(const std::vector<std::string> &cmd_units)
{
    // insert row|col [<new-name>] [<insert-name>]
    if (cmd_units.size() < 2) throw std::runtime_error("insert: insufficient parameters.");

    if (cmd_units[1] == "row")
    {
        if (cmd_units.size() == 2) tc.add_row();
        else if (cmd_units.size() == 3) tc.add_row(cmd_units[2]);
        else if (cmd_units.size() == 4) tc.add_row(cmd_units[2], cmd_units[3]);
    }
    else if (cmd_units[1] == "col")
    {
        if (cmd_units.size() == 2) tc.add_column();
        else if (cmd_units.size() == 3) tc.add_column(cmd_units[2]);
        else if (cmd_units.size() == 4) tc.add_column(cmd_units[2], cmd_units[3]);
    }
    else throw std::runtime_error("insert: invalid parameters.");
    return;
}

void set_data(const std::vector<std::string> &cmd_units)
{
    // set row|col \<name\> \<value\>
    if (cmd_units.size() < 4) throw std::runtime_error("set: insufficient parameters.");

    if (cmd_units[1] == "row")
    {
        _1s_array row_data(tc.row_number(), cmd_units[3]);
        tc.fill_row(row_data, cmd_units[2]);
    }
    else if (cmd_units[1] == "col")
    {
        _1s_array col_data(tc.col_number(), cmd_units[3]);
        tc.fill_column(col_data, cmd_units[2]);
    }
    else throw std::runtime_error("set: invalid parameters.");
    return;
}

void display_table(const std::vector<std::string> &cmd_units)
{
    // view
    tc.display();
    return;
}

int main(int argc, char *argv[])
{
    if (argc >= 2)
    {
        std::string tmp_l;
        std::ifstream cmdin;

        // Run commands from files. Each line is a command.
        // You can give more than one file
        for (size_t i = 1; i < argc; i++) try
        {
            open_infile(cmdin, argv[i]);
            while (getline(cmdin, tmp_l) && tmp_l != "" && tmp_l[0] != '#')
            {
                exec_cmd(tmp_l);
            }
            cmdin.close();
        }
        catch(std::exception& e)
        {
            GCTL_ShowWhatError(e.what(), GCTL_ERROR_ERROR, 0, "run dsviewer in the interactive mode for instructions.", 0);
        }
    }
    else
    {
        setlocale(LC_CTYPE, "");
        initialize_readline();	/* Bind our completer. */

        display_logo();
        std::clog << "dsviewer - read, manipulate and write dsv/csv files.\n";
        std::clog << "Usage: dsviewer [<cmdfile1> <cmdfile2>...]\n";
        std::clog << "Enter '?' to see all available commands.\n";

        std::string cmd_str;
        char *c_line = (char *)NULL;
        char *c_line_s = (char *)NULL;
        bool quit = false;

        while (!quit) try
        {
            c_line = readline(">> ");
            if (!c_line) break;

            c_line_s = stripwhite(c_line);
            cmd_str = c_line_s;

            if (cmd_str == "quit") quit = true;
            else if (cmd_str == "?") display_cmds();
            else exec_cmd(cmd_str);

            free(c_line);
        }
        catch(std::exception& e)
        {
            GCTL_ShowWhatError(e.what(), GCTL_ERROR_ERROR, 0, "Enter 'help' for instructions.", 0);
        }
    }
    return 0;
}