/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include "orcus/orcus_xml.hpp"
#include "orcus/xml_namespace.hpp"
#include "orcus/xml_structure_tree.hpp"
#include "orcus/dom_tree.hpp"
#include "orcus/spreadsheet/factory.hpp"
#include "orcus/spreadsheet/document.hpp"
#include "orcus/stream.hpp"
#include "orcus/sax_parser_base.hpp"

#include "orcus_filter_global.hpp"
#include "cli_global.hpp"

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <fstream>
#include <boost/program_options.hpp>
#include <mdds/sorted_string_map.hpp>

#include "filesystem_env.hpp"

using namespace orcus;
namespace po = boost::program_options;

namespace {

namespace output_mode {

enum class type {
    unknown,
    dump,
    map,
    map_gen,
    transform_xml,
    structure,
};

using map_type = mdds::sorted_string_map<type>;

// Keys must be sorted.
constexpr map_type::entry_type entries[] = {
    { "dump",      type::dump          },
    { "map",       type::map           },
    { "map-gen",   type::map_gen       },
    { "structure", type::structure     },
    { "transform", type::transform_xml },
};

const map_type& get()
{
    static const map_type mt(entries, std::size(entries), type::unknown);
    return mt;
}

} // namespace output_mode

std::string to_string(output_mode::type t)
{
    for (const output_mode::map_type::entry_type& e : output_mode::entries)
        if (t == e.value)
            return std::string(e.key);

    return std::string();
}

void print_usage(std::ostream& os, const po::options_description& desc)
{
    os << "Usage: orcus-xml [OPTIONS] FILE" << std::endl << std::endl;
    os << desc;
}

std::string build_output_help_text()
{
    std::ostringstream os;
    os << "Path to either an output directory, or an output file.";
    return os.str();
}

std::string build_mode_help_text()
{
    std::ostringstream os;
    os << "Mode of operation. Select one of the following options: ";
    auto it = output_mode::entries, ite = output_mode::entries + std::size(output_mode::entries);
    --ite;

    for (; it != ite; ++it)
        os << std::string(it->key) << ", ";

    os << "or " << std::string(it->key) << ".";
    return os.str();
}

std::string build_map_help_text()
{
    std::ostringstream os;
    os << "Path to the map file. A map file is required for all modes except for the "
        << to_string(output_mode::type::structure) << " mode.";
    return os.str();
}

bool parse_and_dump_structure(const file_content& content, const std::string& output)
{
    xmlns_repository repo;
    xmlns_context cxt = repo.create_context();
    xml_structure_tree tree(cxt);
    tree.parse(content.str());

    if (output.empty())
    {
        tree.dump_compact(std::cout);
        return true;
    }

    std::ofstream file(output);
    if (!file)
    {
        std::cerr << "failed to create output file: " << output << std::endl;
        return false;
    }

    tree.dump_compact(file);

    return true;
}

void dump_document_structure(const file_content& content, output_stream& os)
{
    xmlns_repository repo;
    xmlns_context cxt = repo.create_context();
    dom::document_tree tree(cxt);
    tree.load(content.str());

    tree.dump_compact(os.get());
}

} // anonymous namespace

int main(int argc, char** argv) try
{
    po::options_description desc("Options");
    desc.add_options()
        ("help,h", "Print this help.")
        ("mode", po::value<std::string>(), build_mode_help_text().data())
        ("map,m", po::value<std::string>(), build_map_help_text().data())
        ("output,o", po::value<std::string>(), build_output_help_text().data())
        ("output-format,f", po::value<std::string>(), gen_help_output_format().data())
    ;

    po::options_description hidden("");
    hidden.add_options()
        ("input", po::value<std::string>(), "input file");

    po::positional_options_description po_desc;
    po_desc.add("input", 1);

    po::options_description cmd_opt;
    cmd_opt.add(desc).add(hidden);

    po::variables_map vm;
    try
    {
        po::store(
            po::command_line_parser(argc, argv).options(cmd_opt).positional(po_desc).run(), vm);
        po::notify(vm);
    }
    catch (const std::exception& e)
    {
        // Unknown options.
        std::cerr << e.what() << std::endl;
        print_usage(std::cout, desc);
        return EXIT_FAILURE;
    }

    if (vm.count("help"))
    {
        print_usage(std::cout, desc);
        return EXIT_FAILURE;
    }

    if (!vm.count("input"))
    {
        std::cerr << "No input file." << std::endl;
        print_usage(std::cout, desc);
        return EXIT_FAILURE;
    }

    if (!vm.count("mode"))
    {
        std::cerr << "Mode not specified." << std::endl;
        print_usage(std::cout, desc);
        return EXIT_FAILURE;
    }

    std::string s = vm["mode"].as<std::string>();
    output_mode::type mode = output_mode::get().find(s);

    if (mode == output_mode::type::unknown)
    {
        std::cerr << "Unknown output mode: " << s << std::endl;
        print_usage(std::cout, desc);
        return EXIT_FAILURE;
    }

    fs::path input_path = vm["input"].as<std::string>();

    if (!fs::is_regular_file(input_path))
    {
        std::cerr << input_path << " is not a valid file." << std::endl;
        return EXIT_FAILURE;
    }

    fs::path map_path;
    if (vm.count("map"))
    {
        map_path = vm["map"].as<std::string>();

        if (!fs::is_regular_file(map_path))
        {
            std::cerr << map_path << " is not a valid map file." << std::endl;
            return EXIT_FAILURE;
        }
    }

    std::string output;
    if (vm.count("output"))
        output = vm["output"].as<std::string>();

    file_content content(input_path.string().data());

    try
    {
        switch (mode)
        {
            case output_mode::type::structure:
            {
                bool success = parse_and_dump_structure(content, output);
                return success ? EXIT_SUCCESS : EXIT_FAILURE;
            }
            case output_mode::type::dump:
            {
                output_stream os(vm);
                dump_document_structure(content, os);
                return EXIT_SUCCESS;
            }
            case output_mode::type::map_gen:
            {
                output_stream os(vm);
                xmlns_repository repo;
                orcus_xml app(repo, nullptr, nullptr);
                app.write_map_definition(content.str(), os.get());
                return EXIT_SUCCESS;
            }
            default:
                ;
        }

        spreadsheet::range_size_t ss{1048576, 16384};
        spreadsheet::document doc{ss};
        spreadsheet::import_factory import_fact(doc);
        spreadsheet::export_factory export_fact(doc);

        xmlns_repository repo;
        orcus_xml app(repo, &import_fact, &export_fact);

        if (map_path.empty())
            app.detect_map_definition(content.str());
        else
        {
            file_content map_content(map_path.string().data());
            app.read_map_definition(map_content.str());
        }

        app.read_stream(content.str());

        switch (mode)
        {
            case output_mode::type::map:
            {
                dump_format_t format = dump_format_t::unknown;
                s.clear();

                if (vm.count("output-format"))
                {
                    s = vm["output-format"].as<std::string>();
                    format = to_dump_format_enum(s);
                }
                else
                {
                    std::cerr << "Output format is not specified." << std::endl;
                    print_usage(std::cout, desc);
                    return EXIT_FAILURE;
                }

                if (format == dump_format_t::unknown)
                {
                    std::cerr << "Unsupported output format: '" << s << "'" << std::endl;
                    return EXIT_FAILURE;
                }

                doc.dump(format, output);
                break;
            }
            case output_mode::type::transform_xml:
            {
                if (output.empty())
                {
                    std::cout << "output xml file name not provided" << std::endl;
                    print_usage(std::cout, desc);
                    return EXIT_FAILURE;
                }

                std::ofstream file(output);
                if (!file)
                {
                    std::cerr << "failed to create output file: " << output << std::endl;
                    return EXIT_FAILURE;
                }

                // Write transformed xml content to file.
                app.write(content.str(), file);
                break;
            }
            default:
                ;
        }
    }
    catch (const malformed_xml_error& e)
    {
        std::cerr << create_parse_error_output(content.str(), e.offset()) << std::endl;
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}
catch (const std::exception& e)
{
    std::cerr << e.what() << std::endl;
    return EXIT_FAILURE;
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
