// 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 CCommunity.h
 * @brief Wraps the various C++ community objects for accessing via Python.
 * @copyright <a href="https://opensource.org/licenses/MIT">MIT Licence.</a>
 */

#ifndef NECSIM_C_COMMUNITY_H
#define NECSIM_C_COMMUNITY_H

#include <Python.h>
#include <string>
#include <structmember.h>
#include <memory>
#include "PyImports.h"
#include "necsim/Community.h"
#include "necsim/Metacommunity.h"
#include "PyTemplates.h"
#include "necsim.h"

using namespace std;
using namespace necsim;

/**
 * @brief Template for CCommunity and CMetacommunity objects for exporting to Python
 * @tparam T the type of the Community
 */
template<class T>
class PyCommunityTemplate : public PyTemplate<T>
{
public:
    shared_ptr<SpecSimParameters> specSimParameters;
};

/**
 * @brief Sets up the speciation parameters.
 * @tparam T the type of the Community
 * @param self the reference to the Python self object
 * @param args the arguments to pass
 * @return Py_RETURN_NONE
 */
template<class T> static PyObject* setupApplySpeciation(PyCommunityTemplate<T>* self, PyObject* args)
{
    char* database;
    int record_spatial;
    char* sample_file;
    char* fragment_file;
    PyObject* list_speciation_rates;
    PyObject* list_times;
    if(!PyArg_ParseTuple(args,
                         "sissO!O!|kdsk",
                         &database,
                         &record_spatial,
                         &sample_file,
                         &fragment_file,
                         &PyList_Type,
                         &list_speciation_rates,
                         &PyList_Type,
                         &list_times))
    {
        return nullptr;
    }
    setGlobalLogger(self->logger, self->log_function);


    // Convert all our variables to the relevant form
    string database_str = database;
    auto use_spatial = static_cast<bool>(record_spatial);
    string sample_file_str = sample_file;
    string fragment_file_str = fragment_file;
    vector<double> speciation_rates;
    vector<double> times;
    if(!importPyListToVectorDouble(list_speciation_rates, speciation_rates, "Speciation rates must be floats."))
    {
        return nullptr;
    }
    if(!importPyListToVectorDouble(list_times, times, "Times must be floats."))
    {
        return nullptr;
    }
    try
    {
        self->specSimParameters->setup(std::move(database_str),
                                       use_spatial,
                                       sample_file_str,
                                       times,
                                       fragment_file_str,
                                       speciation_rates);
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/**
 * @brief Adds a time to the speciation parameters.
 * @tparam T the type of the Community
 * @param self the reference to the Python self object
 * @param args the arguments to pass
 * @return Py_RETURN_NONE
 */
template<class T> static PyObject* addTime(PyCommunityTemplate<T>* self, PyObject* args)
{
    double time;
    if(!PyArg_ParseTuple(args, "d", &time))
    {
        return nullptr;
    }
    try
    {
        setGlobalLogger(self->logger, self->log_function);
        self->specSimParameters->addTime(time);
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/**
 * @brief Adds a set of protracted parameters to the speciation parameters.
 * @tparam T the type of the Community
 * @param self the reference to the Python self object
 * @param args the arguments to pass
 * @return Py_RETURN_NONE
 */
template<class T> static PyObject* addProtractedParameters(PyCommunityTemplate<T>* self, PyObject* args)
{
    double proc_min, proc_max;
    if(!PyArg_ParseTuple(args, "dd", &proc_min, &proc_max))
    {
        return nullptr;
    }
    try
    {
        setGlobalLogger(self->logger, self->log_function);
        self->specSimParameters->addProtractedParameters(proc_min, proc_max);
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/**
 * @brief Adds a set of metacommunity parameters to the speciation parameters.
 * @tparam T the type of the Community
 * @param self the reference to the Python self object
 * @param args the arguments to pass
 * @return Py_RETURN_NONE
 */
template<class T> static PyObject* pyAddMetacommunityParameters(PyCommunityTemplate<T>* self, PyObject* args)
{
    unsigned long metacommunity_size;
    double speciation_rate;
    char* metacommunity_option;
    unsigned long metacommunity_reference;
    if(!PyArg_ParseTuple(args,
                         "kdsk",
                         &metacommunity_size,
                         &speciation_rate,
                         &metacommunity_option,
                         &metacommunity_reference))
    {
        return nullptr;
    }
    try
    {
        setGlobalLogger(self->logger, self->log_function);
        self->specSimParameters->addMetacommunityParameters(metacommunity_size,
                                                            speciation_rate,
                                                            metacommunity_option,
                                                            metacommunity_reference);
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/**
 * @brief Wipes the protracted parameters vectors
 * @tparam T the type of the Community
 * @param self the reference to the Python self object
 * @return Py_RETURN_NONE
 */
template<class T> static PyObject* wipeProtractedParameters(PyCommunityTemplate<T>* self, PyObject* args)
{
    try
    {
        setGlobalLogger(self->logger, self->log_function);
        self->specSimParameters->wipeProtractedParameters();
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/**
 * @brief Applies the speciation parameters to the coalescence tree to generate the community.
 * @tparam T the type of the Community
 * @param self the reference to the Python self object
 * @return Py_RETURN_NONE
 */
template<class T> static PyObject* apply(PyCommunityTemplate<T>* self, PyObject* args)
{

    // Now run the actual simulation
    try
    {
        setGlobalLogger(self->logger, self->log_function);
        self->base_object->Community::applyNoOutput(self->specSimParameters);
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/**
 * @brief Outputs the community to database.
 * @tparam T the type of the Community
 * @param self the reference to the Python self object
 * @return Py_RETURN_NONE
 */
template<class T> static PyObject* output(PyCommunityTemplate<T>* self, PyObject* args)
{

    // Now run the actual simulation
    try
    {
        setGlobalLogger(self->logger, self->log_function);
        self->base_object->output();
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/**
 * @brief Resets the internal community object.
 * @tparam T the type of the Community
 * @param self the reference to the Python self object
 * @return Py_RETURN_NONE
 */
template<class T> static PyObject* reset(PyCommunityTemplate<T>* self, PyObject* args)
{

    // Now run the actual simulation
    try
    {
        setGlobalLogger(self->logger, self->log_function);
        if(self->base_object != nullptr)
        {
            self->base_object.reset();
        }
        self->base_object = make_unique<T>();
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

template<class T> static PyObject* pySpeciateRemainingLineages(PyCommunityTemplate<T>* self, PyObject* args)
{
    try
    {
        char* database_char;
        if(!PyArg_ParseTuple(args, "s", &database_char))
        {
            return nullptr;
        }
        string database_str(database_char);
        setGlobalLogger(self->logger, self->log_function);
        self->base_object->speciateRemainingLineages(database_str);
    }
    catch(exception &e)
    {
        removeGlobalLogger();
        PyErr_SetString(necsimError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

template<class T> static void PyCommunity_dealloc(PyCommunityTemplate<T>* self)
{
    if(self->specSimParameters != nullptr)
    {
        self->specSimParameters.reset();
        self->specSimParameters = nullptr;
    }
    PyTemplate_dealloc<T>(self);
}

/**
 * @brief Initialises the Community object.
 * @tparam T the template type
 * @param self the Python self object
 * @param args arguments to pass to constructor
 * @param kwds keyword arguments to pass to constructor
 * @return
 */
template<class T> static int PyCommunity_init(PyCommunityTemplate<T>* self, PyObject* args, PyObject* kwds)
{
    self->specSimParameters = make_shared<SpecSimParameters>();
    return PyTemplate_init<T>(self, args, kwds);
}

/**
 * @brief Methods associated with the Python CCommunity object
 * @tparam T the type of the Community
 */
template<class T> PyMethodDef* genCommunityMethods()
{
    const char* ml_name = nullptr;   /* The name of the built-in function/method */
    PyCFunction ml_meth = nullptr;    /* The C function that implements it */
    int ml_flags = 0;   /* Combination of METH_xxx flags, which mostly
                               describe the args expected by the C func */
    const char* ml_doc = nullptr;
    static PyMethodDef CommunityMethods[] = {{"setup",                        *reinterpret_cast<PyCFunction *>(&setupApplySpeciation<T>),         METH_VARARGS, "Sets the speciation current_metacommunity_parameters to be applied to the tree."},
                                             {"add_time",                     *reinterpret_cast<PyCFunction *>(&addTime<T>),                      METH_VARARGS, "Adds a time to apply to the simulation."},
                                             {"wipe_protracted_parameters",   *reinterpret_cast<PyCFunction *>(&wipeProtractedParameters<T>),     METH_NOARGS,  "Wipes the protracted current_metacommunity_parameters."},
                                             {"add_protracted_parameters",    *reinterpret_cast<PyCFunction *>(&addProtractedParameters<T>),      METH_VARARGS, "Adds protracted speciation current_metacommunity_parameters to apply to the simulation."},
                                             {"add_metacommunity_parameters", *reinterpret_cast<PyCFunction *>(&pyAddMetacommunityParameters<T>), METH_VARARGS, "Adds metacommunity current_metacommunity_parameters to be applied"},
                                             {"apply",                        *reinterpret_cast<PyCFunction *>(&apply<T>),                        METH_NOARGS,  "Applies the new speciation rate(s) to the coalescence tree."},
                                             {"output",                       *reinterpret_cast<PyCFunction *>(&output<T>),                       METH_NOARGS,  "Outputs the database to file."},
                                             {"reset",                        *reinterpret_cast<PyCFunction *>(&reset<T>),                        METH_NOARGS,  "Resets the internal object."},
                                             {"speciate_remaining_lineages",  *reinterpret_cast<PyCFunction *>(&pySpeciateRemainingLineages<T>),  METH_VARARGS, "Speciates the remaining lineages in a paused simulation to force it to appear complete"},

                                             {ml_name,                        ml_meth, ml_flags,                                                            ml_doc}};
    return CommunityMethods;
}

/**
 * @brief Generates the community type using the object name and description
 * @tparam T the C++ class to generate a Python object for
 * @param tp_name the Python class name
 * @param tp_doc the Python documentation
 * @return
 */
template<class T> static PyTypeObject genCommunityType(char* tp_name, char* tp_doc)
{
    PyTypeObject ret_Community_Type = {PyVarObject_HEAD_INIT(nullptr, 0)};
    ret_Community_Type.tp_name = tp_name;
    ret_Community_Type.tp_doc = tp_doc;
    ret_Community_Type.tp_basicsize = sizeof(PyCommunityTemplate<T>);
    ret_Community_Type.tp_itemsize = 0;
    ret_Community_Type.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC;
    ret_Community_Type.tp_new = PyTemplate_new<T>;
    ret_Community_Type.tp_init = (initproc) PyCommunity_init<T>;
    ret_Community_Type.tp_dealloc = (destructor) PyCommunity_dealloc<T>;
    ret_Community_Type.tp_traverse = (traverseproc) PyTemplate_traverse<T>;
    //		.tp_members = PyTemplate_members<T>,
    ret_Community_Type.tp_methods = genCommunityMethods<T>();
    ret_Community_Type.tp_getset = PyTemplate_gen_getsetters<T>();
    //	static PyTypeObject outType = ret_Community_Type;
    return ret_Community_Type;
}

/**
 * @brief Generates the community type using the object name and description
 * @tparam T the C++ class to generate a Python object for
 * @param tp_name the Python class name
 * @param tp_doc the Python documentation
 * @return
 */
template<class T> static PyTypeObject genCommunityType(string tp_name, string tp_doc)
{
    return genCommunityType<T>(const_cast<char*>(tp_name.c_str()), const_cast<char*>(tp_doc.c_str()));
}

/**
 * @brief The type object containing the Community to pass on to Python.
 */
static PyTypeObject C_CommunityType = genCommunityType<Community>((char*) "libnecsim.CCommunity",
                                                                  (char*) "C class for generating communities from neutral simulations");

/**
 * @brief The type object containing the Metacommunity to pass on to Python.
 */
static PyTypeObject C_MetacommunityType = genCommunityType<Metacommunity>((char*) "libnecsim.CMetacommunity",
                                                                          (char*) "C class for generating communities from neutral simulations");

#endif //NECSIM_C_COMMUNITY_H
