/*
 * Copyright (C) 1999-2007  Lorenzo Bettini, www.lorenzobettini.it
 *
 * 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; either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "startapp.h"

#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <fstream>
#include <assert.h>
#include <string.h>

#include "colors.h"
#include "cmdline.h"
#include "fileutil.h"
#include "utils.h"
#include "messages.h"

#include "copyright.h"
#include "reportbugs.h"
#include "parsestyles.h"

#include "generatorfactory.h"
#include "srcuntabifier.h"
#include "chartranslator.h"
#include "langdefloader.h"
#include "outputgenerator.h"
#include "langmap.h"
#include "regexpengine.h"
#include "regexpenginedebug.h"
#include "docgenerator.h"
#include "textstyles.h"
#include "outlangdefparserfun.h"
#include "fileinfo.h"
#include "stopwatch.h"
#include "textformatter.h"

#include "languageinfer.h"

// for command line arguments
#include "cmdlineargs.h"

using namespace std;

#ifdef BUILD_AS_CGI
#include "envmapper.h"
#endif // BUILD_AS_CGI

gengetopt_args_info args_info; // command line structure

static void print_cgi_header();
static void run_ctags(const string &cmd);

/**
 * Print progress status information (provided --quiet is not specified)
 * @param message
 */
static void progressInfo(const string &message) {
    if (args_info.quiet_given)
        return;

    cerr << message;
}

StartApp::StartApp() :
    docgenerator(0), preformatter(0), langmap(new LangMap),
            outlangmap(new LangMap), styledefaults(new LangMap), 
            generator_factory(0), entire_doc(0),
            verbose(0), cssUrl(0), use_css(0), is_cgi(0), gen_version(true),
            generate_line_num(false), generate_ref(false) {
}

StartApp::~StartApp() {
    // cout << "destroying StartApp..." << endl;
    cmdline_parser_free(&args_info);

    if (preformatter)
        delete preformatter;

    if (docgenerator)
        delete docgenerator;

    if (generator_factory)
        delete generator_factory;
}

int StartApp::start(int argc, char * argv[]) {
    char *docTitle;
    char *docHeader; // the buffer with the header
    char *docFooter; // the buffer with the footer
    const char *header_fileName = 0;
    const char *footer_fileName = 0;
    unsigned i;
    int v;
    int tabSpaces = 0;

#ifdef BUILD_AS_CGI
    // map environment to parameters if used as CGI
    char **temp_argv;
    temp_argv = map_environment(&argc, argv);
    is_cgi = temp_argv != argv;
    argv = temp_argv;
#endif // BUILD_AS_CGI
    if ((v = cmdline_parser(argc, argv, &args_info)) != 0)
        // calls cmdline parser. The user gived bag args if it doesn't return -1
        return EXIT_FAILURE;

    if (args_info.version_given) {
        print_version();
        print_copyright();
        return EXIT_SUCCESS;
    }

    if (args_info.help_given) {
        cout << "GNU ";
        cmdline_parser_print_help();
        print_reportbugs();
        return EXIT_SUCCESS;
    }

    gen_version = (args_info.gen_version_flag != 0);

    /* initialization of global symbols */
    inputFileName = outputFileName = 0;
    docTitle = 0;
    docHeader = 0;
    docFooter = 0;

    docTitle = args_info.title_arg;
    header_fileName = args_info.header_arg;
    footer_fileName = args_info.footer_arg;
    verbose = args_info.verbose_given;
    const string style_file = args_info.style_file_arg;

    if (args_info.tab_given > 0)
        tabSpaces = args_info.tab_arg;

    if (header_fileName)
        docHeader = read_file(header_fileName);

    if (footer_fileName)
        docFooter = read_file(footer_fileName);

    cssUrl = args_info.css_arg;
    use_css = ( cssUrl != 0 );

    entire_doc =(! args_info.no_doc_given) &&( args_info.doc_given || (docTitle != 0) || use_css );

    string inputFileName;
    if (args_info.input_given)
        inputFileName = args_info.input_arg;

    string outputFileName;
    if (inputFileName.size()&& ! is_cgi && args_info.output_given)
        outputFileName = args_info.output_arg;

    bool generate_to_stdout =(args_info.output_arg &&
            strcmp (args_info.output_arg, "STDOUT") == 0);

    if (verbose)
        setMessager(new DefaultMessages);

    printMessage( PACKAGE);
    printMessage( VERSION);
    printMessage(argv[0]);

    if (verbose) {
        printMessage("command line arguments: ");
        for (int i = 0; i < argc; ++i) {
            printMessage(argv[i]);
        }
    }

    /*
     the starting default path to search for files is computed at
     run-time: it is
     the path of the binary + ".." + RELATIVEDATADIR
     this should make the package relocable (i.e., not stuck
     with a fixed installation directory).
     Of course, the GNU standards for installation directories
     should be followed, but this is not a problem if you use
     configure and make install features.
     If no path is specified in the running program we go back to
     the absolute datadir.
     */
    // this is defined in fileutil.cc
    string prefix_dir = get_file_path(argv[0]);
    if (prefix_dir.size())
        start_path = get_file_path(argv[0])+ RELATIVEDATADIR;
    else
        start_path = ABSOLUTEDATADIR;

    if (args_info.data_dir_given)
        data_dir = args_info.data_dir_arg;

    if (args_info.show_regex_given) {
        if (LangDefLoader::show_regex(data_dir, args_info.show_regex_arg)) {
            return (EXIT_SUCCESS);
        }

        return (EXIT_FAILURE);
    }

    if (args_info.check_lang_given) {
        cout << "checking " << args_info.check_lang_arg << "... ";
        if (LangDefLoader::check_lang_def(data_dir, args_info.check_lang_arg)) {
            cout << "OK" << endl;
            return (EXIT_SUCCESS);
        }

        return (EXIT_FAILURE);
    }

    if (args_info.check_outlang_given) {
        cout << "checking " << args_info.check_outlang_arg << "... ";
        textstyles = parse_outlang_def(data_dir.c_str(),
                args_info.check_outlang_arg);
        cout << "OK" << endl;
        return (EXIT_SUCCESS);
    }

    if (args_info.show_lang_elements_given) {
        // we simply printe all the language elements defined in the
        // language definition file
        if (LangDefLoader::show_lang_elements(data_dir,
                args_info.show_lang_elements_arg))
            return EXIT_SUCCESS;

        return EXIT_FAILURE;
    }

    string lang_map = args_info.lang_map_arg;
    assert(lang_map.size());
    if (! args_info.lang_def_given)
        langmap = LangMapPtr(new LangMap(data_dir, lang_map));

    string outlang_map = args_info.outlang_map_arg;
    assert(outlang_map.size());
    if (! args_info.outlang_def_given)
        outlangmap = LangMapPtr(new LangMap(data_dir, outlang_map));

    string defaults_map = args_info.style_defaults_arg;
    assert(defaults_map.size());
    styledefaults = LangMapPtr(new LangMap(data_dir, defaults_map));

    if (args_info.lang_list_given) {
        cout << "Supported languages (file extensions)\nand associated language definition files\n\n";
        langmap->print();
        return (EXIT_SUCCESS);
    }

    if (args_info.outlang_list_given) {
        cout << "Supported output languages\nand associated language definition files\n\n";
        outlangmap->print();
        return (EXIT_SUCCESS);
    }

    // when debugging, always flush the output
    outputbuffer.setAlwaysFlush(args_info.debug_langdef_given);

    string title;
    string doc_header;
    string doc_footer;
    string css_url;

    if (docTitle)
        title = docTitle;
    if ((! docTitle) && inputFileName.size())
        title = inputFileName;
    if (docHeader)
        doc_header = docHeader;
    if (docFooter)
        doc_footer = docFooter;
    if (cssUrl)
        css_url = cssUrl;

    if (args_info.line_number_ref_given)
        args_info.line_number_given = args_info.line_number_ref_given;

    string outlangfile;

    if (! args_info.outlang_def_given) {
        string out_format = args_info.out_format_arg;

        if (use_css)
            out_format += "-css";

        if (entire_doc)
            out_format += "-doc";

        outlangfile = outlangmap->get_file(out_format);

        if (! outlangfile.size()) {
            cerr << PACKAGE << ": ";
            cerr << "output language " << out_format<< " not handled" << endl;
            return EXIT_FAILURE;
        }
    } else {
        outlangfile = args_info.outlang_def_arg;
    }

    textstyles = parse_outlang_def(data_dir.c_str(), outlangfile.c_str());

    if (! textstyles->file_extension.size() && ! outputFileName.size()) {
        cerr << PACKAGE << ": ";
        cerr << "empty file extension in output language file " <<outlangfile << endl;
        return EXIT_FAILURE;
    }

    const string ext = "." + textstyles->file_extension;

    RefPosition refposition;
    if (strcmp(args_info.gen_references_arg, "inline")==0)
        refposition = INLINE;
    else if (strcmp(args_info.gen_references_arg, "postline")==0)
        refposition = POSTLINE;
    else if (strcmp(args_info.gen_references_arg, "postdoc")==0)
        refposition = POSTDOC;
    else {
        cerr << PACKAGE << ": ";
        cerr << "Bug: unhandled reference position " <<args_info.gen_references_arg << endl;
        return EXIT_FAILURE;
    }

    if (args_info.gen_references_given && strlen(args_info.ctags_arg)> 0) {
        string ctags_cmd = args_info.ctags_arg;

        if (inputFileName.size()) {
            ctags_cmd += " ";
            ctags_cmd += inputFileName;
        } else if (args_info.inputs_num) {
            for (i = 0; i < (args_info.inputs_num); ++i) {
                ctags_cmd += " ";
                ctags_cmd += args_info.inputs[i];
            }
        }

        run_ctags(ctags_cmd);
    }

    if (tabSpaces)
        preformatter = new Untabifier (tabSpaces);
    else if (args_info.line_number_given)
        preformatter = new Untabifier(8);
    else
        preformatter = new PreFormatter();

    PreFormatterPtr chartranslator(textstyles->charTranslator);
    preformatter->setFormatter(chartranslator);

    string background_color;

    generator_factory =new GeneratorFactory(textstyles, preformatter, &outputbuffer,
            args_info.gen_references_given,
            args_info.ctags_file_arg,
            refposition, args_info.debug_langdef_given);

    if (args_info.style_css_file_given) {
        parseCssStyles(data_dir, args_info.style_css_file_arg,
                generator_factory, background_color);
    } else {
        parseStyles(data_dir, style_file, generator_factory, background_color);
    }
    
    generator_factory->addDefaultGenerator();

    setStylesFromDefaults();

    if (background_color != "")
        background_color = generator_factory->preprocessColor(background_color);

    docgenerator = new DocGenerator(title, inputFileName,
            doc_header, doc_footer,
            css_url, background_color, entire_doc,
            textstyles->docTemplate.toStringBegin(),
            textstyles->docTemplate.toStringEnd());;

    if (is_cgi)
        print_cgi_header();

    // let's start the translation :-)

    generate_line_num =(args_info.line_number_given || args_info.line_number_ref_given);
    generate_ref = args_info.line_number_ref_given;

    if (args_info.lang_def_arg)
        lang_file = args_info.lang_def_arg;

    int result= EXIT_SUCCESS;

    if (args_info.src_lang_given)
        source_language = args_info.src_lang_arg;

    // if a stopwatch is created, when it is deleted (automatically
    // since we're using a shared pointer, it will print the
    // elapsed seconds.
    boost::shared_ptr<StopWatch> stopwatch;
    if (args_info.statistics_given)
        stopwatch = boost::shared_ptr<StopWatch>(new StopWatch);

    // first the --input file
    if (! args_info.inputs_num) {
        result = processFile(inputFileName, (generate_to_stdout ? "" : outputFileName), ext);
    }

    // let's process other files, if there are any
    if (args_info.inputs_num && !is_cgi) {
        for (i = 0; i < (args_info.inputs_num); ++i) {
            progressInfo(string("Processing ")+ args_info.inputs[i] + " ... ");
            const string &outputFileName = createOutputFileName(
                    args_info.inputs[i], args_info.output_dir_arg, ext);
            result = processFile(args_info.inputs[i], (generate_to_stdout ? "" : outputFileName), ext);
            if (result == EXIT_FAILURE)
                break;
            progressInfo("created " + outputFileName + "\n");
        }
    }

    return (result);
}

void StartApp::setStylesFromDefaults() {
    for (LangMap::const_iterator it = styledefaults->begin(); it != styledefaults->end(); ++it) {
        if (generator_factory->createMissingGenerator(it->first, it->second))
            printMessage("style for " + it->first + " defaults to style for " + it->second);
    }
}

void StartApp::print_copyright() {
    int i;

    for (i = 1; i <= copyright_text_length; ++i)
        cout << copyright_text[i] << endl;
    ;
}

void StartApp::print_reportbugs() {
    int i;

    for (i = 1; i <= reportbugs_text_length; ++i)
        cout << reportbugs_text[i] << endl;
}

void StartApp::print_version() {
    cout << "GNU " << PACKAGE << " " << VERSION << endl;
}

int process_file(const char *file, TextFormatter *pre, const string &path,
        const string &lang_file, FileInfo *fileinfo, bool verbose) {
    RegExpStatePtr initial_state = LangDefLoader::get_lang_def(path, lang_file);

    try {
        printMessage("Processing " + string((file ? file : "standard input")) + " with regex");
        printMessage("Using language definition " + lang_file);
        RegExpEnginePtr engine;
        if (args_info.debug_langdef_given) {
            RegExpEngineDebug *debugEngine = new RegExpEngineDebug(initial_state, pre, fileinfo);
            debugEngine->setInteractive( strcmp(args_info.debug_langdef_arg, "interactive" ) == 0);
            engine = RegExpEnginePtr(debugEngine);
        } else {
            engine = RegExpEnginePtr(new RegExpEngine(initial_state, pre, fileinfo));
        }
        engine->process_file(file);
    }
    catch(...)
    {
        exitError("error during regex processing");
    }
    return 0;
}

string StartApp::inferLang(const string &inputFileName) {
    printMessage("inferring input language...", cerr);
    if (!inputFileName.size()) {
        cerr << PACKAGE << ": ";
        cerr << "missing feature: language inference requires input file" << endl;
        return "";
    }

    LanguageInfer languageInfer;

    const string &result = languageInfer.infer(inputFileName);
    if (result.size()) {
        printMessage("inferred input language: " + result, cerr);

        // OK now map it into a .lang file
        string mapped_lang = langmap->get_file(result);

        if (!mapped_lang.size()) {
            // try the lower version
            mapped_lang = langmap->get_file(Utils::tolower(result));
        }

        if (mapped_lang.size()) {
            return mapped_lang;
        }
    } else {
        printMessage("couldn't infer input language", cerr);
    }

    return "";
}

int StartApp::processFile(const string &inputFileName,
        const string &outputFileName, const string &file_extension) {
    FILE *in = 0;
    bool deleteOStream = false;
    bool langSpecFound = false;
    ostream* sout = 0;
    
    /// num of digits to represent line number
    unsigned int line_num_digit;
    /// character to use for padding the line number
    char line_num_padding;

    if (outputFileName.size()) {
        sout = new ofstream(outputFileName.c_str());
        if (! (*sout)) {
            cerr << "Error in creating " << outputFileName << " for output" << endl;
            return EXIT_FAILURE;
        }
        deleteOStream = true;
        printMessage("output file: " + inputFileName);
    }

    if (inputFileName.size()) {
        unsigned int lines = get_line_count(inputFileName);
        printMessage("input file: " + inputFileName);

        line_num_digit = 0;
        while (lines) {
            ++line_num_digit;
            lines /= 10;
        }
    } else
        line_num_digit = 5;
    // if we read from stdin, we can't read the file in advance and
    // check how many lines of code it contains.  In this case set
    // the number of digit for the line number to 5.

    /*
     * Use default values for any options not provided
     */
    if (sout == 0) {
        sout = &cout;
    }

    if (in == 0) {
        ; /* Well stdin already points to stdin so, .... */
    }

    OutputGenerator *outputgenerator = 0;

    if (generate_line_num) {
        // Read the padding character to use for line numbers
        line_num_padding = args_info.line_number_arg[0];
        outputgenerator =new OutputGenerator(*sout, generator_factory->getTextFormatter()->getGenerator("linenum"),
                &(textstyles->refstyle.anchor), generate_ref,
                (args_info.line_number_ref_given ? args_info.line_number_ref_arg : ""),
                textstyles->line_prefix);
        outputgenerator->setLineNumDigit(line_num_digit);
        outputgenerator->setLineNumPadding(line_num_padding);
    }
    else
        outputgenerator = new OutputGenerator(*sout, textstyles->line_prefix);

    // when debugging, always flush the output
    outputgenerator->setAlwaysFlush(args_info.debug_langdef_given);

    outputbuffer.setOutputGenerator(outputgenerator);

    docgenerator->set_gen_version(gen_version);

    printMessage("translating source code... ", cerr);

    string langfile = lang_file;

    if (args_info.infer_lang_given) {
        langfile = inferLang(inputFileName);

        if (langfile.size())
            langSpecFound = true;
    }

    // language inference has the precedence (if it succeeds)
    if (!langfile.size() && !langSpecFound) {
        // find the language definition file associated to a language
        if (source_language.size()) {
            langfile = langmap->get_file(source_language);
            if (! langfile.size()) {
                if (! args_info.failsafe_given) {
                    cerr << PACKAGE << ": ";
                    cerr << "source language " << source_language<< " not handled" << endl;
                    return EXIT_FAILURE;
                }
            } else
                langSpecFound = true;
        } else {
            if (! inputFileName.size()) {
                if (! args_info.failsafe_given) {
                    cerr << PACKAGE << ": ";
                    cerr << "when using stdin, please specify a source language"<< endl;
                    return EXIT_FAILURE;
                }
            }

            string file_ext = get_file_extension(inputFileName);

            if (file_ext != "")
                langfile = langmap->get_file(file_ext);

            if (langfile.size())
                langSpecFound = true;
        }
    } else
        langSpecFound = true;

    // language inference is always performed, if the other attempts failed
    // if --infer-lang was specified at command line, then the inference
    // has already been performed, otherwise we perform it now
    if (!langSpecFound && !args_info.infer_lang_given) {
        langfile = inferLang(inputFileName);

        if (langfile.size())
            langSpecFound = true;
    }

    if (!langSpecFound && args_info.failsafe_given) {
        // OK we use default.lang
        langfile = "default.lang";
        langSpecFound = true;
    }

    if (langSpecFound) {
        docgenerator->generate_start_doc(sout);

        const string &i_file_name = get_input_file_name(inputFileName);
        const char *input_file_name = (i_file_name.size() ? i_file_name.c_str() : 0);

        FileInfo fileinfo(i_file_name, outputFileName);
        process_file(input_file_name, generator_factory->getTextFormatter(),
                data_dir, langfile, &fileinfo, verbose);

        outputbuffer.flush();

        docgenerator->generate_end_doc(sout);

        printMessage("done !", cerr);
    } else {
        cerr << PACKAGE << ": ";
        cerr << "unknown input language for "<< (inputFileName.size() ? inputFileName : "(stdin)") << endl;
        return EXIT_FAILURE;
    }

    /*
     else // we're in failsafe mode so we simply copy the file to the output
     {
     istream *input;
     if(! inputFileName.size())
     input = &cin;
     else
     input = open_file_istream_or_error(inputFileName);

     *sout << input->rdbuf();

     if (input != &cin)
     delete input;
     }
     */

    sout->flush();

    if (deleteOStream)
        delete sout;

    delete outputgenerator;

    return EXIT_SUCCESS;
}

void run_ctags(const string &cmd) {
    printMessage("Running ctags: " + cmd);

    int res = system(cmd.c_str());

    if (res != 0) {
        exitError("error running ctags");
    }
}

void print_cgi_header() {
    printf("Content-type: text/html\n");
    printf("\n");
}