#include "cmd_helpers.h"

#include "topology_loader.h"
#include "topology_data.h"
#include "common.h"
#include "topology_group.h"

#include "area_checker.h"

#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>

#include <boost/program_options/variables_map.hpp>
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/parsers.hpp>

#include <iostream>
#include <sstream>
#include <fstream>
#include <string>
#include <vector>
#include <list>

namespace po = boost::program_options;
namespace fixer = maps::wiki::topology_fixer;
namespace cmd = fixer::cmd;

namespace {

struct Params {
    std::string conn;
    std::string originalSchema;
    std::string fixedSchema;
    std::string ftGroup;
    fixer::SRID srid;
    double boxSize;
    double areaRelativeDifference;
    double perimeterRelativeDifference;
    size_t threads;
    std::string verbLevel;
};

int run(const Params& params)
{
    ASSERT(!params.ftGroup.empty());
    auto group = fixer::topologyGroupsRegistry()[params.ftGroup];
    if (group->topologyType() == fixer::TopologyType::Linear) {
        WARN() << "Parameter '" << cmd::OPT_AREA_RELATIVE_DIFFERENCE
            << "' will be ignored";
    } else {
        WARN() << "Parameter '" << cmd::OPT_PERIMETER_RELATIVE_DIFFERENCE
            << "' will be ignored";
    }

    std::shared_ptr<fixer::IdGenerator> gen = std::make_shared<fixer::IdGenerator>(0);

    fixer::TopologyLoader originalDataLoader(
        params.conn, params.originalSchema, gen, params.srid);
    INFO() << "Loading original data from schema " << params.originalSchema;
    fixer::TopologyData originalData = originalDataLoader.loadData(group);

    fixer::TopologyLoader fixedDataLoader(
        params.conn, params.fixedSchema, gen, params.srid);
    INFO() << "Loading fixed data from schema " << params.fixedSchema;
    fixer::TopologyData fixedData = fixedDataLoader.loadData(group);

    INFO() << "Checking " << params.ftGroup << " topology group";
    INFO() << cmd::OPT_BOX_SIZE << ": " << params.boxSize;
    if (group->topologyType() == fixer::TopologyType::Linear) {
        INFO() << cmd::OPT_PERIMETER_RELATIVE_DIFFERENCE << ": " << params.perimeterRelativeDifference;
    } else {
        INFO() << cmd::OPT_AREA_RELATIVE_DIFFERENCE << ": " << params.areaRelativeDifference;
    }
    INFO() << cmd::OPT_THREADS << ": " << params.threads;

    INFO() << "Checking data...";
    size_t errorsCount = 0;
    if (group->topologyType() == fixer::TopologyType::Linear) {
        fixer::PerimeterDiffChecker checker(
            params.boxSize, params.perimeterRelativeDifference, params.threads);
        errorsCount = checker(originalData, fixedData);
    } else {
        fixer::AreaDiffChecker checker(
            params.boxSize, params.areaRelativeDifference, params.threads);
        errorsCount = checker(originalData, fixedData);
    }

    if (errorsCount) {
        WARN() << "Data differs, " << errorsCount << " boxes with delta too big found";
        return 1;
    }

    INFO() << "Done.";
    return 0;
}

} // namespace

int main(int argc, char* argv[])
{
    po::options_description desc("Allowed options");
    cmd::addOptionDescription(desc,
        cmd::g_connectionOption(),
        cmd::g_originalSchemaOption(), cmd::g_fixedSchemaOption(),
        cmd::g_ftGroupOption(), cmd::g_sridOption(),
        cmd::g_boxSizeOption(),
        cmd::g_areaRelativeDifferenceOption(),
        cmd::g_perimeterRelativeDifferenceOption(),
        cmd::g_threadsOption(),
        cmd::g_verboseOption(), cmd::g_helpOption());

    po::variables_map vm;
    po::store(po::parse_command_line(argc, argv, desc), vm);
    po::notify(vm);

    if (argc < 2 || vm.count(cmd::OPT_HELP)) {
        std::cerr << "Usage: topology_checker <options> " << std::endl;
        std::cerr << desc << std::endl;
        return 1;
    }

    try {
        Params params = {
            cmd::loadParam(vm, cmd::g_connectionOption()),
            cmd::loadParam(vm, cmd::g_originalSchemaOption()),
            cmd::loadParam(vm, cmd::g_fixedSchemaOption()),
            cmd::loadParam(vm, cmd::g_ftGroupOption()),
            fixer::sridFromString(cmd::loadParam(vm, cmd::g_sridOption())),
            cmd::loadParam(vm, cmd::g_boxSizeOption()),
            cmd::loadParam(vm, cmd::g_areaRelativeDifferenceOption()),
            cmd::loadParam(vm, cmd::g_perimeterRelativeDifferenceOption()),
            cmd::loadParam(vm, cmd::g_threadsOption()),
            cmd::loadParam(vm, cmd::g_verboseOption())};

        maps::log8::setLevel(params.verbLevel);
        return run(params);
    }
    catch (const maps::Exception& e) {
         FATAL() << "FAIL: " << e;
         return 1;
    }
    catch (const std::exception& e) {
        FATAL() << "FAIL: " << e.what();
        return 1;
    }
}
