//! @file hfst-xfst2fst.cc
//!
//! @brief Implemetation of command line program for parsing xfst commands

//       This program 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, version 3 of the License.
//
//       This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

#if HAVE_CONFIG_H
#include <config.h>
#endif

#include "XfstCompiler.h"

#ifdef WINDOWS
#include <io.h>
#include <windows.h>
#endif

#include "hfst-string-conversions.h"

#ifdef HAVE_READLINE
#include <readline/history.h>
#include <readline/readline.h>
#endif

#ifdef _MSC_VER
#include "../hfst-getopt.h"
#else
#include <getopt.h>
#endif

#include "hfst-commandline.h"
#include "hfst-file-to-mem.h"
#include "hfst-program-options.h"
#include "hfst-tool-metadata.h"
#include "inc/globals-common.h"

static hfst::ImplementationType output_format = hfst::UNSPECIFIED_TYPE;
static char *scriptfilename = NULL;
static char *startupfilename = NULL;
static std::vector<std::string> execute_commands;
static char *execute_command_and_quit = NULL;
static bool pipe_input = false;
static bool pipe_output = false; // this has no effect on non-windows platforms
static bool restricted_mode = false;

#ifdef HAVE_READLINE
static bool use_readline = true;
#else
static bool use_readline = false;
#endif
static bool print_weight = false;

void
print_usage()
{
    // c.f. http://www.gnu.org/prep/standards/standards.html#g_t_002d_002dhelp
    // Usage line
    fprintf(message_out,
            "Usage: %s [OPTIONS...]\n"
            "Compile XFST scripts or execute XFST commands interactively\n"
            "\n",
            "hfst-xfst" /*program_name*/);

    print_common_program_options(message_out);
    fprintf(message_out, "\n");
    fprintf(message_out, "Xfst-specific options:\n");
    fprintf(
        message_out,
        "  -e, --execute=CMD          Execute command CMD on startup\n"
        "  -E, --execute-and-quit=CMD Execute command CMD, and quit\n"
        "  -f, --format=FMT           Write result using FMT as backend "
        "format\n"
        "  -F, --scriptfile=FILE      Read commands from FILE, and quit\n"
        "  -l, --startupfile=FILE     Read commands from FILE on startup\n"
        "  -p, --pipe-mode[=STREAM]   Control input and output streams\n"
        "  -r, --no-readline          Do not use readline library for input\n"
        "  -w, --print-weight         Print weights for each operation\n"
        "  -R, --restricted-mode      Allow read and write operations only in "
        "current\n"
        "                             directory, do not allow system calls\n"
        //          "  -k, --no-console         Do not output directly to
        //          console (Windows-specific)\n"
        "\n"
        "Option --execute can be invoked many times.\n"
        "If FMT is not given, OpenFst's tropical format will be used.\n"
        "The possible values for FMT are { foma, openfst-tropical, "
        "openfst-log, sfst }.\n"
        "Readline library, if enabled when configuring, is used for input by "
        "default.\n"
        "Input files are always treated as UTF-8.\n"
        "\n"
        "STREAM can be { input, output, both }. If not given, defaults to "
        "{both}.\n"
#ifdef _MSC_VER
        "If input file is not specified with -F, input is read interactively "
        "via the\n"
        "console, i.e. line by line from the user. If you redirect input from "
        "a file,\n"
        "use --pipe-mode=input. Output is by default printed to the console. "
        "If you\n"
        "redirect output to a file, use --pipe-mode=output.\n");
#else
        "If input file is not specified with -F, input is read interactively "
        "line by\n"
        "line from the user. If you redirect input from a file, use "
        "--pipe-mode=input.\n"
        "--pipe-mode=output is ignored on non-windows platforms.\n");
#endif
    fprintf(message_out, "\n");
    print_report_bugs();
    fprintf(message_out, "\n");
    print_more_info();
}

int
parse_options(int argc, char **argv)
{
    extend_options_getenv(&argc, &argv);
    // use of this function requires options are settable on global scope
    while (true)
    {
        static const struct option long_options[]
            = { HFST_GETOPT_COMMON_LONG,
                // add tool-specific options here
                { "format", required_argument, 0, 'f' },
                { "scriptfile", required_argument, 0, 'F' },
                { "execute", required_argument, 0, 'e' },
                { "execute-and-quit", required_argument, 0, 'E' },
                { "startupfile", required_argument, 0, 'l' },
                { "pipe-mode", optional_argument, 0, 'p' },
                { "no-readline", no_argument, 0, 'r' },
                { "print-weight", no_argument, 0, 'w' },
                { "restricted-mode", no_argument, 0, 'R' },
                //            {"no-console", no_argument, 0, 'k'},
                { 0, 0, 0, 0 } };
        int option_index = 0;
        // add tool-specific options here
        int c = getopt_long(argc, argv,
                            HFST_GETOPT_COMMON_SHORT "f:F:e:E:l:p::rwkR",
                            long_options, &option_index);
        if (-1 == c)
        {
            break;
        }

        switch (c)
        {
            // copied from "inc/getopt-cases-common.h" (with exceptions)
        case 'd':
            debug = true;
            break;
        case 'h':
            print_usage();
            return EXIT_SUCCESS;
            break;
        case 'V':
            print_version();
            return EXIT_SUCCESS;
            break;
        case 'v':
            verbose = true;
            silent = false;
            break;
        case 'q':
        case 's':
            verbose = false;
            silent = true;
            break;
        // not using standard case o
        case GETOPT_COLOUR:
            if (optarg == 0)
            {
                colour = COLOUR_ALWAYS;
            }
            else if (strcmp(optarg, "always") == 0)
            {
                colour = COLOUR_ALWAYS;
            }
            else if (strcmp(optarg, "never") == 0)
            {
                colour = COLOUR_NEVER;
            }
            else if (strcmp(optarg, "auto") == 0)
            {
                colour = COLOUR_NEVER;
            }
            else
            {
                hfst_error(
                    EXIT_FAILURE, 0,
                    "--colour must be one of always, never, or auto, not %s",
                    optarg);
            }
            break;
        case 'f':
            output_format = hfst_parse_format_name(optarg);
            break;
        case 'F':
            scriptfilename = hfst_strdup(optarg);
            break;
            // todo: on windows getopt_long does not support unicode:
            // e.g. option  -e 'regex U;'  where U is a unicode character
            // is not possible
        case 'e':
            execute_commands.push_back(std::string(optarg));
            break;
        case 'E':
            execute_command_and_quit = hfst_strdup(optarg);
            break;
        case 'l':
            startupfilename = hfst_strdup(optarg);
            break;

        case 'p':
            if (optarg == NULL)
            {
                pipe_input = true;
                pipe_output = true;
            }
            else if (strcmp(optarg, "both") == 0
                     || strcmp(optarg, "BOTH") == 0)
            {
                pipe_input = true;
                pipe_output = true;
            }
            else if (strcmp(optarg, "input") == 0
                     || strcmp(optarg, "INPUT") == 0
                     || strcmp(optarg, "in") == 0 || strcmp(optarg, "IN") == 0)
            {
                pipe_input = true;
            }
            else if (strcmp(optarg, "output") == 0
                     || strcmp(optarg, "OUTPUT") == 0
                     || strcmp(optarg, "out") == 0
                     || strcmp(optarg, "OUT") == 0)
            {
                pipe_output = true;
            }
            else
            {
                error(EXIT_FAILURE, 0, "--pipe-mode argument %s unrecognised",
                      optarg);
            }
            break;

            //          case 'p':
            // pipe_input = true;
            // use_readline = false;
            // break;
        case 'r':
            use_readline = false;
            break;
        case 'w':
            print_weight = true;
            break;
        case 'R':
            restricted_mode = true;
            break;
        case 'k':
            pipe_output = true;
            break;
#include "inc/getopt-cases-error.h"
        }
    }

    if (output_format == hfst::UNSPECIFIED_TYPE)
    {
        output_format = hfst::TROPICAL_OPENFST_TYPE;
        verbose_printf("Using default output format OpenFst "
                       "with tropical weight class\n");
    }

    return EXIT_CONTINUE;
}

// Parse file 'filename' using compiler 'comp'.
// Filename "<stdin>" uses stdin for reading.
int
parse_file(const char *filename, hfst::xfst::XfstCompiler &comp)
{
    char *line = hfst_file_to_mem(filename);
    if (NULL == line)
    {
        hfst_error(EXIT_FAILURE, 0, "error when reading file %s\n", filename);
        return EXIT_FAILURE;
    }

    if (0 != comp.parse_line(line))
    {
        hfst_error(EXIT_FAILURE, 0, "error when parsing file %s\n", filename);
        return EXIT_FAILURE;
    }
    free(line);
    return 0;
}

void
insert_zeroes(char *array, unsigned int number)
{
    for (unsigned int i = 0; i < number; i++)
    {
        array[i] = '\0';
    }
}

static bool
expression_continues(std::string &expr)
{
    size_t s = expr.size();

    // get rid of extra newlines...
    if (s > 0 && expr.at(s - 1) == '\n')
    {
        expr.erase(s - 1);
    }
    s = expr.size();
    // and carriage returns
    if (s > 0 && expr.at(s - 1) == '\r')
    {
        expr.erase(s - 1);
    }
    s = expr.size();
    if (s > 0 && expr.at(s - 1) == '\\')
    {
        expr.at(s - 1) = '\n';
        return true;
    }
    return false;
}

#ifdef HAVE_READLINE
#include "abbrcmd.h"
#include "cmd.h"

/* Variable to pass the position of rl completion to our completer */
static int smatch;

// By Mans Hulden
char *
my_generator(const char *text, int state)
{
    static int list_index, list_index2, len, nummatches;
    const char *name;
    text = rl_line_buffer;
    if (!state)
    {
        list_index = 0;
        list_index2 = 0;
        nummatches = 0;
        len = strlen(text);
    }

    while ((name = cmd[list_index]))
    {
        list_index++;

        if (strncmp(name, text, len) == 0)
        {
            nummatches++;
            return (strdup(name + smatch));
        }
    }

    if (rl_point > 0)
    {
        while ((name = abbrcmd[list_index2]))
        {
            list_index2++;

            if (strncmp(name, text, len) == 0)
                return (strdup(name + smatch));
        }
    }

    /* If no names matched, then return NULL. */
    return ((char *)NULL);
}

// By Mans Hulden
static char **
my_completion(const char *text, int start, int end)
{
    char **matches;

    matches = (char **)NULL;
    smatch = start;
    matches = rl_completion_matches((char *)text, &my_generator);

    return (matches);
}
#endif // HAVE_READLINE

int
main(int argc, char **argv)
{

    hfst_set_program_name(argv[0], "0.1", "HfstXfst2Fst");
    int retval = parse_options(argc, argv);
    if (retval != EXIT_CONTINUE)
    {
        return retval;
    }

    switch (output_format)
    {
    case hfst::SFST_TYPE:
        verbose_printf("Using SFST as output handler\n");
        break;
    case hfst::TROPICAL_OPENFST_TYPE:
        verbose_printf("Using OpenFst's tropical weights as output\n");
        break;
    case hfst::LOG_OPENFST_TYPE:
        verbose_printf("Using OpenFst's log weight output\n");
        break;
    case hfst::FOMA_TYPE:
        verbose_printf("Using foma as output handler\n");
        break;
    case hfst::HFST_OL_TYPE:
        verbose_printf("Using optimized lookup output\n");
        break;
    case hfst::HFST_OLW_TYPE:
        verbose_printf("Using optimized lookup weighted output\n");
        break;
    default:
        error(EXIT_FAILURE, 0, "Unknown format cannot be used as output\n");
        return EXIT_FAILURE;
    }

    if (pipe_input && (scriptfilename != NULL))
    {
        hfst_error(EXIT_FAILURE, 0,
                   "--pipe-mode and --scriptfile cannot be used "
                   "simultaneously\n");
        return EXIT_FAILURE;
    }

    if ((startupfilename != NULL) && (scriptfilename != NULL))
    {
        hfst_error(EXIT_FAILURE, 0,
                   "--startupfile and --scriptfile cannot be used "
                   "simultaneously\n");
        return EXIT_FAILURE;
    }

    // Create XfstCompiler
    hfst::xfst::XfstCompiler comp(output_format);
#ifdef HAVE_READLINE
    comp.setReadline(use_readline);
#else
    comp.setReadline(false);
#endif
    comp.setVerbosity(!silent);

    if (print_weight)
    {
        comp.setPromptVerbosity(false);
        comp.set("print-weight", "ON");
        comp.setPromptVerbosity(true);
    }

    if (restricted_mode)
    {
        comp.setRestrictedMode(true);
    }

    if (!pipe_output)
        comp.setOutputToConsole(true);

    try
    {
        // If needed, execute scripts given in command line
        for (auto const &cmd : execute_commands)
        {
            verbose_printf(
                "Executing xfst command '%s' given on command line\n",
                cmd.c_str());
            if (0 != comp.parse_line(cmd))
            {
                hfst_error(EXIT_FAILURE, 0,
                           "command '%s' could not be parsed\n", cmd.c_str());
                return EXIT_FAILURE;
            }
        }
        // If needed, execute script given in command line, and quit
        if (execute_command_and_quit != NULL)
        {
            verbose_printf(
                "Executing xfst command '%s' given on command line\n",
                execute_command_and_quit);
            if (0 != comp.parse_line(execute_command_and_quit))
            {
                hfst_error(EXIT_FAILURE, 0,
                           "command '%s' could not be parsed\n",
                           execute_command_and_quit);
                return EXIT_FAILURE;
            }
            return EXIT_SUCCESS;
        }
        // If needed, execute script in startup file
        if (startupfilename != NULL)
        {
            verbose_printf("Executing startup file '%s'...\n",
                           startupfilename);
            if (parse_file(startupfilename, comp) == EXIT_FAILURE)
            {
                return EXIT_FAILURE;
            }
        }

        if (pipe_input)
        {
            verbose_printf("Reading from standard input...\n");
            comp.setReadInteractiveTextFromStdin(false);
            comp.setPromptVerbosity(verbose);
            if (parse_file("<stdin>", comp) == EXIT_FAILURE)
            // if (0 != comp.parse(stdin)) segfaults with scriptfiles..
            {
                return EXIT_FAILURE;
            }
        }
        else if (scriptfilename != NULL)
        {
            verbose_printf("Reading from script file '%s'\n", scriptfilename);
            if (parse_file(scriptfilename, comp) == EXIT_FAILURE)
            {
                return EXIT_FAILURE;
            }
        }
        // Use interactive mode
        else if (!use_readline)
        {
            verbose_printf("Starting interactive mode...\n");
            comp.setPromptVerbosity(!silent);
            comp.setReadInteractiveTextFromStdin(true);
            if (!silent)
                comp.prompt();
            // support for backspace

            std::string expression = "";
            // unsigned int lines = 0;
            const unsigned int MAX_LINE_LENGTH = 1024;

#ifdef WINDOWS
            /*SetConsoleCP(65001);
            const HANDLE stdIn = GetStdHandle(STD_INPUT_HANDLE);
            WCHAR buffer[0x1000];
            DWORD numRead = 0;
            while (ReadConsoleW(stdIn, buffer, size_t((0x1000)/4),
            &numRead, NULL))
              {
                std::wstring wstr(buffer, numRead);
                std::string linestr = hfst::wide_string_to_string(wstr);
                expression += linestr;*/
            std::string str("");
            while (hfst::get_line_from_console(str, MAX_LINE_LENGTH))
            {
                expression += str;
                str = std::string("");
#else
            char line[MAX_LINE_LENGTH];
            insert_zeroes(line, MAX_LINE_LENGTH);
            while (std::cin.getline(line, MAX_LINE_LENGTH))
            {
                std::string linestr(line);
                expression += linestr;
#endif
                if (expression_continues(expression))
                {
                    if (!silent)
                        comp.prompt();
                    continue;
                }

                if (0 != comp.parse_line((expression + "\n").c_str()))
                {
#ifdef WINDOWS
                    if (!pipe_output)
                        hfst::hfst_fprintf_console(
                            stderr, "expression '%s' could not be parsed\n",
                            expression.c_str());
                    else
#endif
                        fprintf(stderr,
                                "expression '%s' could not be parsed\n",
                                expression.c_str());
                    if (comp.get("quit-on-fail") == "ON")
                    {
                        return EXIT_FAILURE;
                    }
                    if (!silent)
                        comp.prompt();
                }
                if (comp.quit_requested())
                {
                    break;
                }

                expression = "";
            }
        }
        else
        {
#ifdef HAVE_READLINE
            // support for backspace and Up/Down keys, needs readline
            // library

            verbose_printf("Starting interactive mode...\n");
            comp.setPromptVerbosity(false); // prompts handled manually
            comp.setReadInteractiveTextFromStdin(true);
            char *buf = NULL; // result from readline
            // rl_bind_key('\t',rl_abort);     // disable auto-complet
            std::string expression
                = ""; // the whole expression (possibly several lines)

            char *promptline = (!silent) ? comp.get_prompt() : strdup("");

            rl_attempted_completion_function = my_completion;

            while ((buf = readline(promptline)) != NULL)
            {
                std::string bufstr(buf);
                expression += bufstr;

                if (expression.size() > 0
                    && expression.at(expression.size() - 1) == '\\')
                {
                    expression.at(expression.size() - 1) = '\n';
                    free(promptline);
                    promptline = (!silent) ? comp.get_prompt() : strdup("");
                    continue;
                }

                if (buf[0] != 0)
                {
                    add_history(expression.c_str());
                }

                free(buf);

                if (0 != comp.parse_line((expression + "\n").c_str()))
                {
#ifdef WINDOWS
                    if (!pipe_output)
                        hfst::hfst_fprintf_console(
                            stderr, "expression '%s' could not be parsed\n",
                            expression.c_str());
                    else
#endif
                        fprintf(stderr,
                                "expression '%s' could not be parsed\n",
                                expression.c_str());
                    if (comp.get("quit-on-fail") == "ON")
                    {
                        return EXIT_FAILURE;
                    }
                }
                if (comp.quit_requested())
                {
                    break;
                }

                expression = "";
                free(promptline);
                promptline = (!silent) ? comp.get_prompt() : strdup("");
            }

            free(promptline);
#else
            fprintf(stderr, "ERROR: missing readline library\n");
            return EXIT_FAILURE;
#endif
        }
    }
    catch (TransducerTypeMismatchException &ttme)
    {
        fprintf(stderr, "ERROR: trying to load automata in different "
                        "formats (OpenFst, foma) is not supported in XFST "
                        "scripts\n");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}
