    /*
     * Copyright (C) 1999-2009  Lorenzo Bettini <http://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"

    // this is glib related so don't use it when compiling with qmake
    #include "progname.h"

    #else

    #define set_program_name(x) ;

    #endif

    #include <cstdlib>
    #include <iostream>
    #include <exception>
    #include <boost/shared_ptr.hpp>

    #include "srchilite/sourcehighlight.h"

    #include "srchilite/fileutil.h"
    #include "srchilite/verbosity.h"
    #include "srchilite/langmap.h"
    #include "srchilite/languageinfer.h"
    #include "srchilite/utils.h"
    #include "srchilite/highlightbuilderexception.h"
    #include "srchilite/parserexception.h"
    #include "srchilite/debuglistener.h"
    #include "srchilite/ctagsmanager.h"
    #include "srchilite/stopwatch.h"
    #include "srchilite/lineranges.h"
    #include "srchilite/regexranges.h"
    #include "srchilite/versions.h"
    #include "srchilite/settings.h"

    #include "cmdline.h"

    using namespace std;
    using namespace srchilite;

    /**
     * how language inference should be made: NOINFERENCE = no inference at all,
     * INFERFIRST = inference has priority (e.g., if --infer-lang is specified),
     * INFERATLAST = try inference as the last chance
     */
    enum InferPolicy {
        NOINFERENCE = 0, INFERFIRST, INFERATLAST
    };

    static void print_copyright();
    static void print_reportbugs();
    static void print_version();
    static string inferLang(const string &inputFileName);
    static string getMappedLang(const string &s, LangMap &langmap);
    static void printError(const string &s);
    static void print_cgi_header();

    /**
     * Exits for a generic error (i.e., file not found)
     */
    static void exitError(const string &s);

    static string getLangFileName(InferPolicy infer, const string &inputFileName,
            const string &langFileName, const string &langName, LangMap &langMap);

    static bool verbose = false;
    static bool failsafe = false;

    #ifdef BUILD_AS_CGI
    #include "envmapper.h"
    #endif // BUILD_AS_CGI
    /**
     * Print progress status information (provided --quiet is not specified)
     * @param message
     */
    #define PROGRESSINFO(message) if (!args_info.quiet_given) cerr << message;

    int main(int argc, char * argv[]) {
        set_program_name(argv[0]);
        gengetopt_args_info args_info; // command line structure
        bool is_cgi = false;

    #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 (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();
            cmdline_parser_free(&args_info);
            return EXIT_SUCCESS;
        }

        if (args_info.help_given || args_info.detailed_help_given) {
            cout << "GNU ";
            if (args_info.detailed_help_given) {
                cmdline_parser_print_detailed_help();
            } else {
                cmdline_parser_print_help();
            }
            print_reportbugs();
            cmdline_parser_free(&args_info);
            return EXIT_SUCCESS;
        }

        // set possible line ranges
        boost::shared_ptr<LineRanges> lineRanges;
        if (args_info.line_range_given) {
            lineRanges = boost::shared_ptr<LineRanges>(new LineRanges);
            for (unsigned int i = 0; i < args_info.line_range_given; ++i) {
                if (lineRanges->addRange(args_info.line_range_arg[i]) != NO_ERROR) {
                    string invalid_range = args_info.line_range_arg[i];
                    cmdline_parser_free(&args_info);
                    exitError("invalid line range: " + invalid_range);
                }
            }
        }

        // set possible regex ranges
        boost::shared_ptr<RegexRanges> regexRanges;
        if (args_info.regex_range_given) {
            regexRanges = boost::shared_ptr<RegexRanges>(new RegexRanges);
            for (unsigned int i = 0; i < args_info.regex_range_given; ++i) {
                if (!regexRanges->addRegexRange(args_info.regex_range_arg[i])) {
                    string invalid_range = args_info.regex_range_arg[i];
                    cmdline_parser_free(&args_info);
                    exitError("invalid regex range: " + invalid_range);
                }
            }
        }

        if (args_info.range_context_given) {
            lineRanges->setContextLines(args_info.range_context_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);
        }

        verbose = args_info.verbose_given;
        failsafe = args_info.failsafe_given;

        string inputFile;
        string outputFile;
        string srcLang;
        string outputFormat;
        string langFile;
        string outLangFile;
        string dataDir;
        string outputDir;

        Verbosity::setVerbosity(verbose);

        if (args_info.input_given)
            inputFile = args_info.input_arg;

        if (args_info.output_given)
            outputFile = args_info.output_arg;

        // for cgi force output to standard output
        if (is_cgi)
            outputFile = "STDOUT";

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

        if (args_info.lang_def_given)
            langFile = args_info.lang_def_arg;

        if (args_info.outlang_def_given)
            outLangFile = args_info.outlang_def_arg;

        // the default for output format is html
        outputFormat = args_info.out_format_arg;

        if (args_info.output_dir_given) {
            outputDir = args_info.output_dir_arg;
        }

        /*
         The starting default path is from Settings::retrieveDataDir() which
         does the heavy lifting of finding the data dir by looking at config-time
         values, environment variables, user settings at $HOME, etc. See settings.h

         From here, invoking with --data-dir=<my-datadir> overrides the above value.

         We also use a fallback dir which is calculated in runtime relative to this
         executable (by default <executable-dir>/../share/source-highlight/).

         this should make the package relocatable (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.
        */

        dataDir = Settings::retrieveDataDir();
        if (args_info.data_dir_given)
            dataDir = args_info.data_dir_arg;

        // start_path is global fallback for dataDir - used at fileutil.h
        string executable_dir = get_file_path(argv[0]);
        if (executable_dir.size())
            start_path = executable_dir + RELATIVEDATADIR;

        try {
            // initialize map files
            LangMap langmap(dataDir, args_info.lang_map_arg);

            // just print the input language list and associations
            if (args_info.lang_list_given) {
                langmap.open();
                langmap.print();
                return (EXIT_SUCCESS);
            }

            LangMap outlangmap(dataDir, args_info.outlang_map_arg);

            // just print the input language list and associations
            if (args_info.outlang_list_given) {
                outlangmap.open();
                outlangmap.print();
                return (EXIT_SUCCESS);
            }

            string cssUrl;
            if (args_info.css_given)
                cssUrl = args_info.css_arg;

            string docTitle;
            if (args_info.title_given)
                docTitle = args_info.title_arg;

            if (args_info.check_lang_given || args_info.check_outlang_given
                    || args_info.show_regex_given
                    || args_info.show_lang_elements_given
                    || args_info.lang_list_given || args_info.outlang_list_given) {
                // FIXME just a bogus outlang file specification not to search in outlang.map
                outLangFile = "html.outlang";
            }

            // initialize the main source highlight object
            SourceHighlight sourcehighlight(getLangFileName(NOINFERENCE,
                    outputFile, outLangFile, outputFormat
                            + (cssUrl.size() ? +"-css" : ""), outlangmap));

            // and sets all its properties according to the command line args
            sourcehighlight.setDataDir(dataDir);

            boost::shared_ptr<DebugListener> debugListener;

            // if a simple check is required...
            if (args_info.check_lang_given) {
                cout << "checking " << args_info.check_lang_arg << "... ";
                sourcehighlight.checkLangDef(args_info.check_lang_arg);
                // if we're here, everything went fine!
                cout << "OK" << endl;
                // otherwise an exception has already been thrown
                return (EXIT_SUCCESS);
            }

            if (args_info.check_outlang_given) {
                cout << "checking " << args_info.check_outlang_arg << "... ";
                sourcehighlight.checkOutLangDef(args_info.check_outlang_arg);
                // if we're here, everything went fine!
                cout << "OK" << endl;
                // otherwise an exception has already been thrown
                return (EXIT_SUCCESS);
            }

            // just print the highlightstate if show-regex is specified
            if (args_info.show_regex_given) {
                sourcehighlight.printHighlightState(args_info.show_regex_arg, cout);
                return (EXIT_SUCCESS);
            }

            // just print the lang elems if show-lang-elements is specified
            if (args_info.show_lang_elements_given) {
                sourcehighlight.printLangElems(args_info.show_lang_elements_arg,
                        cout);
                return (EXIT_SUCCESS);
            }

            sourcehighlight.setGenerateEntireDoc((!args_info.no_doc_given)
                    && (args_info.doc_given || (docTitle.size()) || cssUrl.size()));
            sourcehighlight.setStyleDefaultFile(args_info.style_defaults_arg);
            sourcehighlight.setStyleFile(args_info.style_file_arg);
            if (args_info.style_css_file_given)
                sourcehighlight.setStyleCssFile(args_info.style_css_file_arg);
            sourcehighlight.setGenerateVersion(args_info.gen_version_flag != 0);
            sourcehighlight.setCss(cssUrl);
            sourcehighlight.setTitle(docTitle);
            sourcehighlight.setOutputDir(outputDir);
            sourcehighlight.setLineRanges(lineRanges.get());
            sourcehighlight.setRegexRanges(regexRanges.get());
            sourcehighlight.setBinaryOutput(args_info.binary_output_given);

            if (args_info.debug_langdef_given) {
                debugListener = boost::shared_ptr<DebugListener>(new DebugListener);
                const string debugType = args_info.debug_langdef_arg;
                if (debugType == "interactive")
                    debugListener->setInteractive(true);
                sourcehighlight.setHighlightEventListener(debugListener.get());
                // during debugging disable optimizations
                sourcehighlight.setOptimize(false);
            }

            int tabs = 0;
            if (args_info.tab_given) {
                tabs = args_info.tab_arg;
            }

            if (args_info.header_given)
                sourcehighlight.setHeaderFileName(args_info.header_arg);

            if (args_info.footer_given)
                sourcehighlight.setFooterFileName(args_info.footer_arg);

            bool generateLineNumbers = args_info.line_number_given
                    || args_info.line_number_ref_given;
            sourcehighlight.setGenerateLineNumbers(generateLineNumbers);

            if (args_info.line_number_given) {
                // set the line number padding char (default is always '0')
                sourcehighlight.setLineNumberPad(args_info.line_number_arg[0]);
            }

            if (args_info.line_number_ref_given) {
                sourcehighlight.setGenerateLineNumberRefs(true);
                sourcehighlight.setLineNumberAnchorPrefix(
                        args_info.line_number_ref_arg);
            }

            if (generateLineNumbers) {
                // when generating line numbers tabs must be translated
                // otherwise the output line will not be aligned
                // due to the presence of line numbers
                sourcehighlight.setTabSpaces(tabs ? tabs : 8);
            }

            if (tabs) {
                sourcehighlight.setTabSpaces(tabs);
            }

            // whether to give precedence to language inference
            InferPolicy inferPolicy = (args_info.infer_lang_given ? INFERFIRST
                    : INFERATLAST);

            RefPosition refposition = INLINE;
            string gen_references_arg = args_info.gen_references_arg;
            if (gen_references_arg == "inline")
                refposition = INLINE;
            else if (gen_references_arg == "postline")
                refposition = POSTLINE;
            else if (gen_references_arg == "postdoc")
                refposition = POSTDOC;

            boost::shared_ptr<CTagsManager> ctagsManager;
            if (args_info.gen_references_given) {
                string ctags = args_info.ctags_arg;
                string ctags_file = args_info.ctags_file_arg;

                // build the additional arguments for the ctags command
                if (args_info.gen_references_given && ctags != "") {
                    if (inputFile.size()) {
                        ctags += " ";
                        ctags += inputFile;
                    } else if (args_info.inputs_num) {
                        for (unsigned int i = 0; i < (args_info.inputs_num); ++i) {
                            ctags += " ";
                            ctags += args_info.inputs[i];
                        }
                    }
                }

                // the ctags command must be executed if --ctags is specified with an empty string
                ctagsManager = boost::shared_ptr<CTagsManager>(new CTagsManager(
                        ctags_file, ctags, ctags != "", refposition));
                sourcehighlight.setCTagsManager(ctagsManager.get());
            }

            if (args_info.range_separator_given) {
                sourcehighlight.setRangeSeparator(args_info.range_separator_arg);
            }

            // OK, let's highlight!

            if (args_info.inputs_num && (args_info.input_given
                    || args_info.output_given)) {
                // do not mix command line invocation modes
                cerr << "Please, use one of the two syntaxes for invocation: "
                        << endl;
                cerr
                        << "source-highlight [OPTIONS]... -i input_file -o output_file"
                        << endl;
                cerr << "source-highlight [OPTIONS]... [FILES]..." << endl;
                exit(EXIT_FAILURE);
            }

            // for cgi we can only process one file specified with --input
            if (args_info.inputs_num && !is_cgi) {
                // the output file name is empty, but we don't want to generate to stdout
                sourcehighlight.setCanUseStdOut(false);
                // in case multiple input files were specified (without --input)
                for (unsigned int i = 0; i < (args_info.inputs_num); ++i) {
                    PROGRESSINFO(string("Processing ")+ args_info.inputs[i] + " ... ");
                    sourcehighlight.highlight(args_info.inputs[i], "",
                            getLangFileName(inferPolicy, args_info.inputs[i],
                                    langFile, srcLang, langmap));
                    PROGRESSINFO("created " + sourcehighlight.createOutputFileName(args_info.inputs[i]) + "\n");
                }
            } else {
                if (is_cgi)
                    print_cgi_header();

                // in case only one input file was specified with --input
                sourcehighlight.highlight(inputFile, outputFile, getLangFileName(
                        inferPolicy, inputFile, langFile, srcLang, langmap));
            }
        } catch (const HighlightBuilderException &e) {
            cerr << e << endl;
            exit(EXIT_FAILURE);
        } catch (const ParserException &e) {
            cerr << e << endl;
            exit(EXIT_FAILURE);
        } catch (const exception &e) {
            exitError(e.what());
        }

        cmdline_parser_free(&args_info);

        return 0;
    }

    void print_copyright() {
        int i;
        int copyright_text_length = 5;
        const char *copyright_text[] = {
      "copyright.text",
      "Copyright (C) 1999-2008 Lorenzo Bettini <http://www.lorenzobettini.it>",
      "This program comes with ABSOLUTELY NO WARRANTY.",
      "This is free software; you may redistribute copies of the program",
      "under the terms of the GNU General Public License.",
      "For more information about these matters, see the file named COPYING.",
      0 };

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

    void print_reportbugs() {
        int i;
        int reportbugs_text_length = 3;
        const char *reportbugs_text[] = {
      "reportbugs.text",
      "",
      "Maintained by Lorenzo Bettini <http://www.lorenzobettini.it>",
      "Report bugs to <bug-source-highlight at gnu.org>",
      0 };

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

    void print_version() {
        cout << Versions::getCompleteVersion() << endl;
    }

    string inferLang(const string &inputFileName) {
        VERBOSELN("inferring input language...");
        if (!inputFileName.size()) {
            printError("missing feature: language inference requires input file");
            return "";
        }

        LanguageInfer languageInfer;

        const string &result = languageInfer.infer(inputFileName);
        if (result.size()) {
            VERBOSELN("inferred input language: " + result);
        } else {
            VERBOSELN("couldn't infer input language");
        }

        return result;
    }

    string getMappedLang(const string &s, LangMap &langmap) {
        // OK now map it into a .lang file
        string mapped_lang = langmap.getMappedFileName(s);

        if (!mapped_lang.size()) {
            // try the lower version
            mapped_lang = langmap.getFileName(Utils::tolower(s));
        }

        return mapped_lang;
    }

    string getLangFileName(InferPolicy infer, const string &inputFileName,
            const string &langFileName, const string &langName, LangMap &langMap) {
        string langFile;

        // language inference has the precedence
        if (infer == INFERFIRST) {
            langFile = inferLang(inputFileName);
            langFile = getMappedLang(langFile, langMap);
            if (langFile.size())
                return langFile;
        }

        // then if a lang file name was specified we're done
        if (langFileName.size())
            return langFileName;

        // now try with the langName
        langFile = getMappedLang(langName, langMap);
        if (langFile.size())
            return langFile;

        // otherwise try with the inputFileName (its file extension
        // and its name)
        langFile = langMap.getMappedFileNameFromFileName(inputFileName);
        if (langFile.size())
            return langFile;

        // OK, as a last chance let's try with language infer
        if (infer == INFERATLAST) {
            langFile = getMappedLang(inferLang(inputFileName), langMap);
            if (langFile.size())
                return langFile;
        }

        // if we're here we failed all checks
        // if failsafe is specified we default to default.lang
        if (failsafe)
            langFile = "default.lang";

        if (!langFile.size()) {
            // if we're here we must exit with failure
            if (langName.size())
                exitError("could not find a language definition for " + langName);
            else
                exitError("could not find a language definition for input file "
                        + inputFileName);
        }

        return langFile;
    }

    void printError(const string &s) {
        cerr << PACKAGE << ": " << s << endl;
    }

    void exitError(const string &s) {
        printError(s);
        exit(EXIT_FAILURE);
    }

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