#include <boost/program_options/options_description.hpp>
#include <boost/program_options/variables_map.hpp>
#include <boost/program_options/parsers.hpp>
#include <iostream>
#include "commandline.h"
#include "probleminstance.h"
#include "dynamiclabeling.h"
#include "staticlabeler.h"
#include "labelmodel.h"

namespace {

std::unique_ptr<ProblemInstance> namedProblemInstance(std::string const& filepath)
{
    std::unique_ptr<ProblemInstance> p( new ProblemInstance(filepath.c_str()) );
    return std::move(p);
}

std::unique_ptr<DynamicLabeling> namedDynamicLabeler(
        std::string const& name, ProblemInstance const& instance, qreal timestep, qreal trimming)
{
    std::unique_ptr<DynamicLabeling> p;
    if (name == "AlwaysRelabel") {
        p.reset( new AlwaysRelabelInterpolator(instance) );
    } else if (name == "SlowestInterpolation") {
        p.reset( new SlowestInterpolation(instance, timestep, trimming) );
    } else {
        throw CommandLineError("unknown dynamic labeler '" + name + "'");
    }
    return std::move(p);
}

void assignStaticLabeler(DynamicLabeling& labeling, std::string const& name)
{
    if (name == "LeftToRightSweep") {
        labeling.newStaticLabeler<GreedySweepLabeler>(SweepDirection::LeftToRight());
    } else if (name == "RightToLeftSweep") {
        labeling.newStaticLabeler<GreedySweepLabeler>(SweepDirection::RightToLeft());
    } else if (name == "TopToBottomSweep") {
        labeling.newStaticLabeler<GreedySweepLabeler>(SweepDirection::TopToBottom());
    } else if (name == "BottomToTopSweep") {
        labeling.newStaticLabeler<GreedySweepLabeler>(SweepDirection::BottomToTop());
    } else if (name == "FourSweeps") {
        labeling.newStaticLabeler<FourGreedySweepsLabeler>();
    } else {
        throw CommandLineError("unknown static labeler '" + name + "'");
    }
}

void assignLabelModel(ProblemInstance& instance, std::string const& name)
{
    std::unique_ptr<LabelModel> p( LabelModel::fromName(name.c_str()) );
    if (p) {
        instance.setLabelModel(std::move(p));
    } else {
        throw CommandLineError("unknown label model '" + name + "'");
    }
}

QString fillInFilepathTemplate(QString const& filepathTemplate, int run)
{
    QString runStr = QString("%1").arg(run);
    QString result = filepathTemplate;
    return std::move(result.replace("%r", runStr));
}

QFile& openOutputFile(QFile& file, QString const& filepathTemplate, int run)
{
    if (filepathTemplate == "-") {
        if (!file.open(1, QIODevice::WriteOnly | QIODevice::Text)) {
            throw CommandLineError("cannot write to stdout"); // ?!
        }
    } else {
        QString filepath( fillInFilepathTemplate(filepathTemplate, run) );
        file.setFileName(filepath);
        if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
            throw CommandLineError("could not open '" + filepath.toStdString() + "' for writing");
        }
    }
    return file;
}

bool processCommandline(int argc, char* argv[])
{
    // OPTION SPECIFICATION
    namespace po = boost::program_options;
    po::options_description desc("Allowed options");
    desc.add_options()
        ("instance", po::value<std::string>(),
         "problem instance filename")

        ("label-model", po::value<std::string>(),
         "override label model in problem instance\n"
         "   \tLeftSlider, RightSlider, TopSlider, BottomSlider, FourSlider, BehindSlider")

        ("trimming", po::value<qreal>(),
         "amount to trim the label model to induce lower label speeds")

        ("runs", po::value<int>(), "number of runs")

        ("static-labeler", po::value<std::string>(),
         "static labeling algorithm\n"
         "   \tLeftToRightSweep, RightToLeftSweep, TopToBottomSweep, BottomToTopSweep, FourSweeps")

        ("dynamic-labeler", po::value<std::string>(),
         "interpolator for static labelings\n"
         "   \tAlwaysRelabel, SlowestInterpolation")

        ("timestep", po::value<qreal>(),
         "time (s) between static labelings")

        ("output", po::value<std::string>(),
         "output filename template (%r replaced by run number)")

        ("help,?",
         "produce this help message")
    ;

    po::positional_options_description p;
    p.add("instance", 1);
    p.add("dynamic-labeler", 1);

    po::variables_map vm;
    po::store(po::command_line_parser(argc, argv).
              options(desc).positional(p).allow_unregistered().run(), vm);
    po::notify(vm);
    if (vm.empty()) return false;

    // PROCESS COMMAND-LINE OPTIONS
    if (vm.count("help")) {
        std::cout << desc << std::endl;
        std::exit(0);
    }   

    qreal timestep = vm.count("timestep") ? vm["timestep"].as<qreal>() : 5.0;
    if (timestep <= 0.0) {
        throw CommandLineError("timestep must be greater than 0 seconds");
    }
    qreal trimming = vm.count("trimming") ? vm["trimming"].as<qreal>() : NO_TRIMMING;
    if (trimming < 0) {
        throw CommandLineError("trimming amount must be non-negative");
    }
    int numRuns = vm.count("runs") ? vm["runs"].as<int>() : 1;
    if (numRuns < 0) {
        throw CommandLineError("number of runs must be at least 1");
    }
    QString filepathTemplate("-");
    if (vm.count("output")) {
        filepathTemplate = vm["output"].as<std::string>().c_str();
    }
    if (numRuns > 1 && !filepathTemplate.contains("%r")) {
        throw CommandLineError("output filename template must contain '%r' when doing more than one run");
    }

    if (!vm.count("instance")) {
        throw CommandLineError("no problem instance specified");
    }
    std::unique_ptr<ProblemInstance> instance(
                namedProblemInstance(vm["instance"].as<std::string>()) );

    if (vm.count("label-model")) {
        assignLabelModel(*instance, vm["label-model"].as<std::string>());
    }

    if (!vm.count("dynamic-labeler")) {
        throw CommandLineError("no dynamic labeler specified");
    }
    std::unique_ptr<DynamicLabeling> labeling(
                namedDynamicLabeler(vm["dynamic-labeler"].as<std::string>(), *instance, timestep, trimming) );

    if (vm.count("static-labeler")) {
        assignStaticLabeler(*labeling, vm["static-labeler"].as<std::string>());
    } else {
        labeling->newStaticLabeler<FourGreedySweepsLabeler>();
    }

    // PERFORM THE SPECIFIED WORK
    for (int run = 0; run < numRuns; ++run) {
        labeling->updateStatistics();
        QFile file;
        labeling->statistics().writeAsCSV( openOutputFile(file, filepathTemplate, run) );
        file.close();
        instance->rerandomizeAirplaneSpawns();
    }
    return true;
}

} // unnamed namespace

bool performWorkFromCommandline(int argc, char* argv[])
{
    try {
        return processCommandline(argc, argv);
    } catch (CommandLineError const& e) {
        std::cerr << e.what() << std::endl;
        std::exit(1);
    }
}
