// This file is part of necsim project which is released under MIT license.
// See file **LICENSE.txt** or visit https://opensource.org/licenses/MIT) for full license details
/**
 * @author Samuel Thompson
 * @file CSimulation.h
 * @brief Wraps the various C++ tree objects for accessing via Python.
 * @copyright <a href="https://opensource.org/licenses/MIT">MIT Licence.</a>
 */
#ifndef PY_TREE_NECSIM
#define PY_TREE_NECSIM

#include <Python.h>
#include <structmember.h>
#include <memory>
#include "ConfigParser.h"
#include "necsim/Tree.h"
#include "necsim/SpatialTree.h"
#include "necsim/ProtractedTree.h"
#include "necsim/ProtractedSpatialTree.h"
#include "PyLogging.h"
#include "necsim.h"
#include "PyImports.h"
#include "PyTemplates.h"

using namespace std;
using namespace necsim;

/**
 * @brief Imports the config parameters from a file. The file should be of the format generated by
 * ConfigFileParser in Python.
 * @tparam T the simulation type
 * @param self the Python self object
 * @param args arguments to parse
 */
template<class T> static PyObject* importConfig(PyTemplate<T>* self, PyObject* args)
{
    char* input;
    // parse arguments
    if(!PyArg_ParseTuple(args, "s", &input))
    {
        return nullptr;
    }
    try
    {
        string config_file = input;
        setGlobalLogger(self->logger, self->log_function);
        self->base_object->wipeSimulationVariables();
        self->base_object->importSimulationVariables(config_file);
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/**
 * @brief Imports the config parameters from a string. The string should be equivalent to a file written by
 * ConfigFileParser in Python.
 * @tparam T the simulation type
 * @param self the Python self object
 * @param args arguments to parse
 */
template<class T> static PyObject* importConfigFromString(PyTemplate<T>* self, PyObject* args)
{
    char* input;
    // parse arguments
    if(!PyArg_ParseTuple(args, "s", &input))
    {
        return nullptr;
    }
    try
    {

        setGlobalLogger(self->logger, self->log_function);
        self->base_object->wipeSimulationVariables();
        self->base_object->importSimulationVariablesFromString(config);
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/**
 * @brief Sets up the simulation, including importing map files and generating in-memory objects.
 * @tparam T the simulation type
 * @param self the Python self object
 * @param args arguments to parse
 */
template<class T> static PyObject* setup(PyTemplate<T>* self, PyObject* args)
{
    // Set up the simulation, catch and return any errors.
    try
    {
        setGlobalLogger(self->logger, self->log_function);
        self->base_object->setup();
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

template<class T> static PyObject* addGillespie(PyTemplate<T>* self, PyObject* args)
{
    // Set up the simulation, catch and return any errors.
    double generation_threshold;
    if(!PyArg_ParseTuple(args, "d", &generation_threshold))
    {
        return nullptr;
    }
    try
    {
        setGlobalLogger(self->logger, self->log_function);
        self->base_object->addGillespie(generation_threshold);
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/**
 * @brief Runs the simulation.
 * @tparam T the simulation type
 * @param self the Python self object
 * @param args arguments to parse
 * @return Py_RETURN_TRUE if the simulation completes, Py_RETURN_FALSE otherwise.
 */
template<class T> static PyObject* run(PyTemplate<T>* self, PyObject* args)
{
    // Run the program, catch and return any errors.
    try
    {
        setGlobalLogger(self->logger, self->log_function);
        if(self->base_object->runSimulation())
        {
            Py_RETURN_TRUE;
        }
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_FALSE;
}

/**
 * @brief Applies the provided speciation rates to the simulation to generate multiple communities.
 * @tparam T the simulation type
 * @param self the Python self object
 * @param args arguments to parse
 */
template<class T> static PyObject* applySpeciationRates(PyTemplate<T>* self, PyObject* args)
{
    // parse arguments
    // Mimic a command-line simulation call
    // Run the program, catch and return any errors.
    try
    {
        PyObject* list_speciation_rates;
        vector<double> spec_rates;
        if(!PyArg_ParseTuple(args, "|O!", &PyList_Type, &list_speciation_rates))
        {
            return nullptr;
        }
        if(!importPyListToVectorDouble(list_speciation_rates, spec_rates, "Speciation rates must be floats."))
        {
            return nullptr;
        }
        setGlobalLogger(self->logger, self->log_function);
        if(!spec_rates.empty())
        {
            vector<long double> spec_rates_long(spec_rates.begin(), spec_rates.end());
            self->base_object->addSpeciationRates(spec_rates_long);
        }
        self->base_object->applyMultipleRates();
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/**
 * @brief Sets up the simulation by reading the dumped files from a paused simulation.
 * @tparam T the simulation type
 * @param self the Python self object
 * @param args arguments to parse
 */
template<class T> static PyObject* setupResume(PyTemplate<T>* self, PyObject* args)
{
    char* pause_directory;
    char* out_directory;
    int seed, task, max_time;
    // parse arguments
    if(!PyArg_ParseTuple(args, "ssiii", &pause_directory, &out_directory, &seed, &task, &max_time))
    {
        return nullptr;
    }
    // Set up the resume current_metacommunity_parameters.
    string pause_directory_str, out_directory_str;
    pause_directory_str = pause_directory;
    out_directory_str = out_directory;
    try
    {
        setGlobalLogger(self->logger, self->log_function);
        self->base_object->wipeSimulationVariables();
        self->base_object->setResumeParameters(pause_directory_str, out_directory_str, seed, task, max_time);
        self->base_object->checkSims(pause_directory_str, seed, task);
        if(self->base_object->hasPaused())
        {
            self->base_object->setup();
        }
        else
        {
            throw runtime_error("Couldn't find paused simulation");
        }
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

template<class T> static PyMethodDef* genPySimulationMethods()
{
    static PyMethodDef PySimulationMethods[] = {{"import_from_config",        reinterpret_cast<PyCFunction>( importConfig<T>),           METH_VARARGS, "Import the simulation variables from a config file"},
                                                {"import_from_config_string", reinterpret_cast<PyCFunction>( importConfigFromString<T>), METH_VARARGS, "Import the simulation variables from a config file"},
                                                {"add_gillespie",             reinterpret_cast<PyCFunction>( addGillespie<T>),           METH_VARARGS, "Use the Gillespie algorithm in the simulation at the specified time"},
                                                {"run",                       reinterpret_cast<PyCFunction>( run<T>),                    METH_VARARGS, "Run the simulation"},
                                                {"setup",                     reinterpret_cast<PyCFunction>( setup<T>),                  METH_VARARGS, "Set up the simulation, importing the maps and assigning the variables"},
                                                {"apply_speciation_rates",    reinterpret_cast<PyCFunction>( applySpeciationRates<T>),   METH_VARARGS, "Applies the speciation rates to the completed simulation. Can optionally provide a list of additional speciation rates to apply"},
                                                {"setup_resume",              reinterpret_cast<PyCFunction>( setupResume<T>),            METH_VARARGS, "Sets up for resuming from a paused simulation"},
                                                {nullptr, nullptr, 0, nullptr}  /* Sentinel */
    };
    return PySimulationMethods;
}

template<class T> PyTypeObject genSimulationType(char* tp_name, char* tp_doc)
{
    auto genPyTemplateGetSetters = PyTemplate_gen_getsetters<T>();
    auto genPyTemplateNew = PyTemplate_new<T>;
    auto genPyTemplateInit = PyTemplate_init<T>;
    auto genPyTemplateDealloc = PyTemplate_dealloc<T>;
    auto genPyTemplateTraverse = PyTemplate_traverse<T>;
    auto genPyTemplateMethods = genPySimulationMethods<T>();
    PyTypeObject ret_Simulation_Type = {PyVarObject_HEAD_INIT(nullptr, 0)};
    ret_Simulation_Type.tp_name = tp_name;
    ret_Simulation_Type.tp_doc = tp_doc;
    ret_Simulation_Type.tp_basicsize = sizeof(PyTemplate<T>);
    ret_Simulation_Type.tp_itemsize = 0;
    ret_Simulation_Type.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC;
    ret_Simulation_Type.tp_new = genPyTemplateNew;
    ret_Simulation_Type.tp_init = (initproc) genPyTemplateInit;
    ret_Simulation_Type.tp_dealloc = (destructor) genPyTemplateDealloc;
    ret_Simulation_Type.tp_traverse = (traverseproc) genPyTemplateTraverse;
    ret_Simulation_Type.tp_methods = genPyTemplateMethods;
    ret_Simulation_Type.tp_getset = genPyTemplateGetSetters;
    return ret_Simulation_Type;
}

static PyTypeObject C_SpatialSimulationType = genSimulationType<SpatialTree>((char*) "libnecsim.CSpatialSimulation",
                                                                             (char*) "C class for spatial simulations.");
static PyTypeObject C_NSESimulationType = genSimulationType<Tree>((char*) "libnecsim.CNSESimulation",
                                                                  (char*) "C class for non-spatial simulations.");
static PyTypeObject C_ProtractedSpatialSimulationType = genSimulationType<ProtractedSpatialTree>((char*) "libnecsim.CPSpatialSimulation",
                                                                                                 (char*) "C class for protracted spatial simulations.");
static PyTypeObject C_ProtractedNSESimulationType = genSimulationType<ProtractedTree>((char*) "libnecsim.CPNSESimulation",
                                                                                      (char*) "C class for protracted non-spatial simulations.");

#endif // PY_TREE_NECSIM