#include "BilinearInterpolation.h"
#include "NasaWorldWindToOpenStreetMapConverter.h"
#include "NearestNeighborInterpolation.h"
#include "OsmTileClusterRenderer.h"
#include "ReadOnlyMapDefinition.h"
#include "Thread.h"
#include "mapreproject.h"

#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QList>
#include <QPair>
#include <QThread>

#include <getopt.h>

#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>

/* example usage
mapreproject --simulate --output-directory=/home/jmho --jobs 7 --cluster-size 64 --output-tile-level=10 \
    --input=type=NasaWW,base-directory=/home,tile-level=8,interpolation-method=bilinear,cache-size=200000000 \
    --input type=Bathymetry,file=BLAH.tiff
*/

void printUsage()
{
    std::cout << "Usage: mapreproject [OPTIONS] [INPUT] ...\n"
                 "      --help                display this help and exit\n"
                 "      --output-directory    output base directory, where the resulting tiles will be stored\n"
                 "      --output-tile-level   tile level of resulting map\n"
                 "      --cluster-size        edge length of tile clusters in tiles\n"
                 "      --jobs                number of threads, use to override default of one thread per cpu core\n"
                 "      --simulate            \n"
                 "      --input INPUT_OPTS    INPUT_OPTS can be a combination of the following options, separated by comma:\n"
                 "          type              \n"
                 "          base-directory    \n"
                 "          tile-level        \n"
                 "          cache-size        \n"
                 "          file              \n"
                 "          interpolation-method  one of \"integer\", \"nearest-neighbor\" (default), \"average\" or \"bilinear\"\n";
}

MapSourceType parseType(char const *const value)
{
    MapSourceType result = UnknownMapSource;
    if (!value)
        qFatal("Suboption 'type' does not have a value.");
    if (strcmp(value, "NasaWW") == 0)
        result = NasaWorldWindMap;
    else if (strcmp(value, "Bathymetry") == 0)
        result = BathymetryMap;
    else
        qFatal("Suboption 'type': Unrecognized value '%s'.", value);
    return result;
}

QString parseString(char const *const value)
{
    QString result;
    if (!value)
        qFatal("Suboption does not have a value.");
    result = value;
    return result;
}

int parseInt(char const *const value)
{
    if (!value)
        qFatal("Suboption does not have a value.");
    QString str(value);
    bool ok;
    int const result = str.toInt(&ok);
    if (!ok)
        qFatal("Suboption does not have an integer value.");
    return result;
}

EInterpolationMethod parseInterpolationMethod(char const *const value)
{
    EInterpolationMethod result = UnknownInterpolationMethod;
    if (!value)
        qFatal("Suboption 'interpolation-method' does not have a value.");
    if (strcmp(value, "integer") == 0)
        result = IntegerInterpolationMethod;
    if (strcmp(value, "nearest-neighbor") == 0)
        result = NearestNeighborInterpolationMethod;
    if (strcmp(value, "average") == 0)
        result = AverageInterpolationMethod;
    else if (strcmp(value, "bilinear") == 0)
        result = BilinearInterpolationMethod;
    else
        qFatal("Suboption 'interpolation-method': Unrecognized value '%s'.", value);
    return result;
}

ReadOnlyMapDefinition parseInput(char *subopts)
{
    if (!subopts)
        qFatal("Missing argument for '--input'");

    enum {
        TypeOption = 0,
        BaseDirectoryOption,
        FileOption,
        TileLevelOption,
        InterpolationOption,
        CacheSizeOption,
        TheEnd
    };

    char optionType[] = "type";
    char optionBaseDirectory[] = "base-directory";
    char optionFile[] = "file";
    char optionTileLevel[] = "tile-level";
    char optionInterpolationMethod[] = "interpolation-method";
    char optionCacheSize[] = "cache-size";
    char *const input_opts[] = {optionType, optionBaseDirectory, optionFile, optionTileLevel, optionInterpolationMethod, optionCacheSize, nullptr};

    ReadOnlyMapDefinition mapDefinition;

    char *value;
    while (*subopts != '\0') {
        switch (getsubopt(&subopts, input_opts, &value)) {
        case TypeOption:
            mapDefinition.setMapType(parseType(value));
            break;

        case BaseDirectoryOption:
            mapDefinition.setBaseDirectory(parseString(value));
            break;

        case FileOption:
            mapDefinition.setFileName(parseString(value));
            break;

        case TileLevelOption:
            mapDefinition.setTileLevel(parseInt(value));
            break;

        case InterpolationOption:
            mapDefinition.setInterpolationMethod(parseInterpolationMethod(value));
            break;

        case CacheSizeOption:
            mapDefinition.setCacheSizeBytes(parseInt(value));
            break;

        default:
            qFatal("Unrecognized input suboption.");
        }
    }
    return mapDefinition;
}

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    // --interpolation-method=NearestNeighbor|Bilinear

    QString outputDirectory;
    int outputTileLevel = -1;

    int threadCount = QThread::idealThreadCount();
    int clusterSize = 0; // cluster size 0 makes no sense
    bool onlySimulate = false;

    QList<ReadOnlyMapDefinition> mapSources;

    // input: type, tile-level, base-dir|file
    // --input,type=NasaWW,tile-level=8,base-directory=<dir>,interpolation-method=Bilinear
    // --input,type=Image,file=<file>

    enum {
        HelpOption = 1,
        InputOption,
        OutputDirectoryOption,
        OutputTileLevelOption,
        JobsOption,
        ClusterSizeOption,
        SimulateOption
    };

    static struct option long_options[] = {{"help", no_argument, nullptr, HelpOption},
                                           {"input", required_argument, nullptr, InputOption},
                                           {"output-directory", required_argument, nullptr, OutputDirectoryOption},
                                           {"output-tile-level", required_argument, nullptr, OutputTileLevelOption},
                                           {"jobs", required_argument, nullptr, JobsOption},
                                           {"cluster-size", required_argument, nullptr, ClusterSizeOption},
                                           {"simulate", no_argument, nullptr, SimulateOption},
                                           {nullptr, 0, nullptr, 0}};

    while (true) {
        int option_index = 0;

        int const opt = getopt_long(argc, argv, "", long_options, &option_index);
        if (opt == -1)
            break;

        switch (opt) {
        case HelpOption:
            printUsage();
            exit(EXIT_SUCCESS);

        case InputOption:
            mapSources.push_back(parseInput(optarg));
            break;

        case OutputDirectoryOption:
            outputDirectory = parseString(optarg);
            break;

        case OutputTileLevelOption:
            outputTileLevel = parseInt(optarg);
            break;

        case JobsOption:
            threadCount = parseInt(optarg);
            break;

        case ClusterSizeOption:
            clusterSize = parseInt(optarg);
            break;

        case SimulateOption:
            onlySimulate = true;
            break;

        case '?':
            break;
        }
    }

    qDebug() << "\noutput directory:" << outputDirectory << "\noutput tile level:" << outputTileLevel << "\ncluster size:" << clusterSize
             << "\nthreads:" << threadCount << "\ninputs:" << mapSources;

    if (onlySimulate)
        exit(EXIT_SUCCESS);

    NasaWorldWindToOpenStreetMapConverter converter;
    converter.setMapSources(mapSources);
    converter.setOsmBaseDirectory(QDir(outputDirectory));
    converter.setOsmTileLevel(outputTileLevel);
    converter.setOsmTileClusterEdgeLengthTiles(clusterSize);
    converter.setThreadCount(threadCount);

    QObject::connect(&converter, &NasaWorldWindToOpenStreetMapConverter::finished, &app, &QCoreApplication::quit);

    QList<QPair<Thread *, OsmTileClusterRenderer *>> renderThreads = converter.start();
    app.exec();

    QList<QPair<Thread *, OsmTileClusterRenderer *>>::iterator pos = renderThreads.begin();
    QList<QPair<Thread *, OsmTileClusterRenderer *>>::iterator const end = renderThreads.end();
    for (; pos != end; ++pos) {
        (*pos).first->stop();
        (*pos).first->wait();
        delete (*pos).second;
    }

    return EXIT_SUCCESS;
}
