/*
    SPDX-FileCopyrightText: 2021 Valentin Boettcher <hiro at protagon.space; @hiro98:tchncs.de>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include <pybind11/pybind11.h>
#include <pybind11/chrono.h>
#include "skyobjects/skypoint.h"
#include "skymesh.h"
#include "cachingdms.h"
#include "sqlstatements.cpp"
#include "catalogobject.h"
#include "catalogsdb.h"
#include <iostream>

using namespace pybind11::literals;
namespace py = pybind11;
namespace pybind11
{
namespace detail
{
template <>
struct type_caster<QString>
{
    public:
        PYBIND11_TYPE_CASTER(QString, _("QString"));

        bool load(handle src, bool)
        {
            try
            {
                value = QString::fromStdString(src.cast<std::string>());
            }
            catch (const py::cast_error &)
            {
                return false;
            }

            return !PyErr_Occurred();
        }

        static handle cast(QString src, return_value_policy /* policy */, handle /* parent */)
        {
            const handle *obj = new py::object(py::cast(src.toUtf8().constData()));
            return *obj;
        }
};

template <>
struct type_caster<QDateTime>
{
    public:
        PYBIND11_TYPE_CASTER(QDateTime, _("QDateTime"));

        bool load(handle src, bool)
        {
            try
            {
                value = QDateTime::fromMSecsSinceEpoch(
                            std::chrono::duration_cast<std::chrono::milliseconds>(
                                src.cast<std::chrono::system_clock::time_point>().time_since_epoch())
                            .count());
            }
            catch (const py::cast_error &)
            {
                return false;
            }

            return !PyErr_Occurred();
        }

        static handle cast(QDateTime src, return_value_policy /* policy */,
                           handle /* parent */)
        {
            const handle *obj = new py::object(py::cast(std::chrono::system_clock::time_point(
                                                   std::chrono::milliseconds(src.currentMSecsSinceEpoch()))));
            return *obj;
        }
};
} // namespace detail
} // namespace pybind11

/**
 * @struct Indexer
 * Provides a simple wrapper to generate trixel ids from python code.
 */
struct Indexer
{
    Indexer(int level) : m_mesh{ SkyMesh::Create(level) } {};

    int getLevel() const
    {
        return m_mesh->level();
    };
    void setLevel(int level)
    {
        m_mesh = SkyMesh::Create(level);
    };

    int getTrixel(double ra, double dec, bool convert_epoch = false) const
    {
        SkyPoint p{ dms(ra), dms(dec) };
        if (convert_epoch)
        {
            p.B1950ToJ2000();
            p = SkyPoint{ p.ra(), p.dec() }; // resetting ra0, dec0
        }

        return m_mesh->index(&p);
    };

    SkyMesh *m_mesh;
};

///////////////////////////////////////////////////////////////////////////////
//                                   PYBIND                                  //
///////////////////////////////////////////////////////////////////////////////

const CatalogObject DEFAULT_CATALOG_OBJECT{};

template <typename T>
T cast_default(const py::object &value, const T &default_value)
{
    try
    {
        return py::cast<T>(value);
    }
    catch (const py::cast_error &)
    {
        return default_value;
    }
}

PYBIND11_MODULE(pykstars, m)
{
    m.doc() = "Thin bindings for KStars to facilitate trixel indexation from python.";

    py::class_<Indexer>(m, "Indexer")
    .def(py::init<int>(), "level"_a,
         "Initializes an `Indexer` with the given `level`.\n"
         "If the level is greater then approx. 10 the initialization can take some "
         "time.")
    .def_property("level", &Indexer::getLevel, &Indexer::setLevel,
                  "Sets the level of the HTMesh/SkyMesh used to index points.")
    .def(
        "get_trixel", &Indexer::getTrixel, "ra"_a, "dec"_a, "convert_epoch"_a = false,
        "Calculates the trixel number from the right ascention and the declination.\n"
        "The epoch of coordinates is assumed to be J2000.\n\n"
        "If the epoch is B1950, `convert_epoch` has to be set to `True`.")
    .def("__repr__", [](const Indexer & indexer)
    {
        std::ostringstream lvl;
        lvl << indexer.getLevel();
        return "<Indexer level=" + lvl.str() + ">";
    });

    {
        using namespace CatalogsDB;
        py::class_<DBManager>(m, "DBManager")
        .def(py::init([](const std::string & filename)
        {
            return new DBManager(QString::fromStdString(filename));
        }),
        "filename"_a)
        .def(
            "register_catalog",
            [](DBManager & self, const py::dict & cat)
        {
            return self.register_catalog(
                       py::cast<int>(cat["id"]), py::cast<QString>(cat["name"]),
                       py::cast<bool>(cat["mut"]), py::cast<bool>(cat["enabled"]),
                       py::cast<double>(cat["precedence"]),
                       py::cast<QString>(cat["author"]),
                       py::cast<QString>(cat["source"]),
                       py::cast<QString>(cat["description"]),
                       py::cast<int>(cat["version"]), py::cast<QString>(cat["color"]),
                       py::cast<QString>(cat["license"]),
                       py::cast<QString>(cat["maintainer"]),
                       py::cast<QDateTime>(cat["timestamp"]));
        },
        "catalog"_a)
        .def(
            "update_catalog_meta",
            [](DBManager & self, const py::dict & cat)
        {
            return self.update_catalog_meta(
            {
                py::cast<int>(cat["id"]), py::cast<QString>(cat["name"]),
                py::cast<double>(cat["precedence"]),
                py::cast<QString>(cat["author"]),
                py::cast<QString>(cat["source"]),
                py::cast<QString>(cat["description"]),
                py::cast<bool>(cat["mut"]), py::cast<bool>(cat["enabled"]),
                py::cast<int>(cat["version"]), py::cast<QString>(cat["color"]),
                py::cast<QString>(cat["license"]),
                py::cast<QString>(cat["maintainer"]),
                py::cast<QDateTime>(cat["timestamp"]) });
        },
        "catalog"_a)
        .def("__repr__",
             [](const DBManager & manager)
        {
            return QString("<DBManager filename=\"" + manager.db_file_name() +
                           "\">");
        })
        .def("update_catalog_views", &DBManager::update_catalog_views)
        .def("compile_master_catalog", &DBManager::compile_master_catalog)
        .def("dump_catalog", &DBManager::dump_catalog, "catalog_id"_a, "file_path"_a)
        .def("import_catalog", &DBManager::import_catalog, "file_path"_a,
             "overwrite"_a)
        .def("remove_catalog", &DBManager::remove_catalog, "catalog_id"_a);

        py::register_exception<DatabaseError>(m, "DatabaseError");
    }

    py::enum_<SkyObject::TYPE>(m, "ObjectType", "The types of CatalogObjects",
                               py::arithmetic())
    .value("STAR", SkyObject::STAR)
    .value("CATALOG_STAR", SkyObject::CATALOG_STAR)
    .value("PLANET", SkyObject::TYPE::PLANET)
    .value("OPEN_CLUSTER", SkyObject::TYPE::OPEN_CLUSTER)
    .value("GLOBULAR_CLUSTER", SkyObject::TYPE::GLOBULAR_CLUSTER)
    .value("GASEOUS_NEBULA", SkyObject::TYPE::GASEOUS_NEBULA)
    .value("PLANETARY_NEBULA", SkyObject::TYPE::PLANETARY_NEBULA)
    .value("SUPERNOVA_REMNANT", SkyObject::TYPE::SUPERNOVA_REMNANT)
    .value("GALAXY", SkyObject::TYPE::GALAXY)
    .value("COMET", SkyObject::TYPE::COMET)
    .value("ASTEROID", SkyObject::TYPE::ASTEROID)
    .value("CONSTELLATION", SkyObject::TYPE::CONSTELLATION)
    .value("MOON", SkyObject::TYPE::MOON)
    .value("ASTERISM", SkyObject::TYPE::ASTERISM)
    .value("GALAXY_CLUSTER", SkyObject::TYPE::GALAXY_CLUSTER)
    .value("DARK_NEBULA", SkyObject::TYPE::DARK_NEBULA)
    .value("QUASAR", SkyObject::TYPE::QUASAR)
    .value("MULT_STAR", SkyObject::TYPE::MULT_STAR)
    .value("RADIO_SOURCE", SkyObject::TYPE::RADIO_SOURCE)
    .value("SATELLITE", SkyObject::TYPE::SATELLITE)
    .value("SUPERNOVA", SkyObject::TYPE::SUPERNOVA)
    .value("NUMBER_OF_KNOWN_TYPES", SkyObject::TYPE::NUMBER_OF_KNOWN_TYPES)
    .value("TYPE_UNKNOWN", SkyObject::TYPE::TYPE_UNKNOWN)
    .export_values();

    m.def(
        "get_id",
        [](const py::dict & obj) -> py::bytes
    {
        return CatalogObject::getId(
            static_cast<SkyObject::TYPE>(py::cast<int>(obj["type"])),
            py::cast<double>(obj["ra"]), py::cast<double>(obj["dec"]),
            py::cast<QString>(obj["name"]),
            py::cast<QString>(obj["catalog_identifier"]))
        .toStdString();
    },
    "object"_a,
    R"(
        Calculate the id of an object.

        Parameters
        ----------)");

    ///////////////////////////////////////////////////////////////////////////
    //                             Sql Statements                            //
    ///////////////////////////////////////////////////////////////////////////
    auto s = m.def_submodule("sqlstatements");
    {
        using namespace CatalogsDB::SqlStatements;

        s.doc() = "Assorted sql statements to modify the catalog database.";

        s.def("insert_dso", &insert_dso, "catalog_id"_a);
        s.def("create_catalog_table", &create_catalog_table, "catalog_id"_a);

#define ATTR(name)            \
    {                         \
        s.attr(#name) = name; \
    }
        ATTR(create_catalog_list_table);
        ATTR(insert_catalog);
        ATTR(get_catalog_by_id);
        ATTR(all_catalog_view);
        ATTR(master_catalog);
        ATTR(dso_by_name);
#undef ATTR
    }
}
