/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2025 Univ. Grenoble Alpes, CNRS, Grenoble INP - UGA, TIMC, 38000 Grenoble, France
 *
 * Visit http://camitk.imag.fr for more information
 *
 * This file is part of CamiTK.
 *
 * CamiTK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * CamiTK is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with CamiTK.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $CAMITK_LICENCE_END$
 ****************************************************************************/

#include "qt_bindings.h"

#include <QRegularExpression>
#include <QVector3D>
#include <QColor>
#include <QPoint>

namespace py = pybind11;

namespace pybind11::detail {

// --------------- QString ---------------
// Conversion from Python PyObject to C++ QString
bool type_caster<QString>::load(handle src, bool /* not used : indicates whether implicit conversions should be applied */) {
    if (!src) {
        return false;
    }
    value = QString::fromUtf8(src.cast<std::string>().c_str());
    return true;
}

// Conversion from C++ QString to Python PyObject
handle type_caster<QString>::cast(const QString& src, return_value_policy /* policy */, handle /* parent */) {
    return py::str(src.toUtf8().constData()).release();
}


// --------------- QVariant ---------------
// Greatly inspired by https://github.com/ModOrganizer2/modorganizer-plugin_python/tree/master/src/pybind11-qt
// Conversion from Python PyObject to C++
bool type_caster<QVariant>::load(handle src, bool) {
    // test for string first otherwise PyList_Check also works
    if (PyBytes_Check(src.ptr()) || PyUnicode_Check(src.ptr())) {
        value = src.cast<QString>();
        // Similarly to what is done in HotPlugAction constructor to get the value from the JSon field
        // in the camitk extension file, now check for other types that can be
        // transmitted from python as QString using specific patterns:
        // - "QVector(.,.,.)"
        // - "QColor(.,.,.)"
        // - "QPoint(.,.)"
        QRegularExpression regExp;
        regExp.setPattern(R"(QVector3D\((\s*[-]?[\d\.]+\s*),(\s*[-]?[\d\.]+\s*),(\s*[-]?[\d\.]+\s*)\))");
        QRegularExpressionMatch match = regExp.match(value.toString());
        if (match.hasMatch()) {
            value = QVector3D(match.captured(1).toDouble(), match.captured(2).toDouble(), match.captured(3).toDouble());
        }
        else {
            regExp.setPattern(R"(QColor\(\s*(25[0-5]|2[0-4]\d|[01]?\d{1,2})\s*,\s*(25[0-5]|2[0-4]\d|[01]?\d{1,2})\s*,\s*(25[0-5]|2[0-4]\d|[01]?\d{1,2})\s*\))");
            match = regExp.match(value.toString());
            if (match.hasMatch()) {
                value = QColor(match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt());
            }
            else {
                regExp.setPattern(R"(QPoint\(\s*([-]?[\d]+)\s*,\s*([-]?[\d]+\s*)\))");
                match = regExp.match(value.toString());
                if (match.hasMatch()) {
                    value = QPoint(match.captured(1).toInt(), match.captured(2).toInt());
                }
                // else do nothing as value is already a regular QString
            }
        }
        return true;
    }
    else if (PySequence_Check(src.ptr())) {
        // we could check if all the elements can be converted to QString
        // and store a QStringList in the QVariant but I am not sure that is
        // really useful.
        value = src.cast<QVariantList>();
        return true;
    }
    else if (PyMapping_Check(src.ptr())) {
        value = src.cast<QVariantMap>();
        return true;
    }
    else if (src.is(pybind11::none())) {
        value = QVariant();
        return true;
    }
    else if (PyDict_Check(src.ptr())) {
        value = src.cast<QVariantMap>();
        return true;
    }
    // PyBool will also return true for PyLong_Check but not the other way
    // around, so the order here is relevant.
    else if (PyBool_Check(src.ptr())) {
        value = src.cast<bool>();
        return true;
    }
    else if (PyLong_Check(src.ptr())) {
        // QVariant doesn't have long. It has int or long long.
        value = src.cast<int>();
        return true;
    }
    else if (PyFloat_Check(src.ptr())) {
        // added for CamiTK
        value = src.cast<double>();
        return true;
    }
    else {
        return false;
    }
}

// Conversion from C++ QVariant to Python PyObject
handle type_caster<QVariant>::cast(QVariant src, return_value_policy policy, handle parent) {
    switch (src.userType()) {
        case QMetaType::UnknownType:
            return Py_None;
        case QVariant::Int:
            return PyLong_FromLong(src.toInt());
        case QVariant::UInt:
            return PyLong_FromUnsignedLong(src.toUInt());
        case QVariant::Bool:
            return PyBool_FromLong(src.toBool());
        case QVariant::Double:
            // added for CamiTK
            return PyFloat_FromDouble(src.toDouble());
        case QVariant::String:
            return type_caster<QString>::cast(src.toString(), policy, parent);
        // We need to check for StringList here because these are not considered
        // List since List is QList<QVariant> will StringList is QList<QString>:
        case QVariant::StringList: {
            std::vector<QString> value(src.toStringList().constBegin(), src.toStringList().constEnd());
            return type_caster<std::vector<QString> >::cast(value, policy, parent);
        }
        case QVariant::Vector3D: {
            QVector3D vec3d = src.value<QVector3D> ();
            std::vector<float> value;
            value.push_back(vec3d.x());
            value.push_back(vec3d.y());
            value.push_back(vec3d.z());
            return type_caster<std::vector<float> >::cast(value, policy, parent);
        }
        case QVariant::Color: {
            QColor color = src.value<QColor> ();
            std::vector<float> value;
            value.push_back(color.redF());
            value.push_back(color.greenF());
            value.push_back(color.blueF());
            return type_caster<std::vector<float> >::cast(value, policy, parent);
        }
        case QVariant::List:
            return type_caster<QVariantList>::cast(src.toList(), policy, parent);
        case QVariant::Map:
            return type_caster<QVariantMap>::cast(src.toMap(), policy, parent);
        default: {
            PyErr_Format(PyExc_TypeError, "QVariant type unsupported: %d", src.userType());
            throw pybind11::error_already_set();
        }
    }
}

// --------------- QList<QVariant> ---------------
// Conversion from Python PyObject to C++ QList<QVariant>
bool type_caster<QList<QVariant>>::load(handle src, bool) {
    if (!isinstance<py::list>(src)) {
        return false;
    }

    auto pyList = reinterpret_borrow<py::list>(src);
    value.clear();
    for (auto item : pyList) {
        // Convert Python object to QVariant using QString intermediate
        if (py::isinstance<py::str>(item)) {
            value.append(QVariant(QString::fromUtf8(py::str(item).cast<std::string>().c_str())));
        }
        else if (py::isinstance<py::int_>(item)) {
            value.append(QVariant(item.cast<int>()));
        }
        else if (py::isinstance<py::float_>(item)) {
            value.append(QVariant(item.cast<double>()));
        }
        else if (py::isinstance<py::bool_>(item)) {
            value.append(QVariant(item.cast<bool>()));
        }
        else {
            // fallback: convert to string
            value.append(QVariant(QString::fromUtf8(py::str(item).cast<std::string>().c_str())));
        }
    }
    return true;
}

// Conversion from C++ QList<QVariant> to Python PyObject
handle type_caster<QList<QVariant>>::cast(const QList<QVariant>& src, return_value_policy policy, handle parent) {
    py::list pyList;
    for (const QVariant& item : src) {
        switch (item.userType()) {
            case QMetaType::UnknownType:
                pyList.append(Py_None);
                break;
            case QVariant::Int:
                pyList.append(item.toInt());
                break;
            case QVariant::Double:
                pyList.append(item.toDouble());
                break;
            case QVariant::Bool:
                pyList.append(item.toBool());
                break;
            case QVariant::String:
                pyList.append(py::str(item.toString().toUtf8().constData()));
                break;
            case QVariant::StringList: {
                std::vector<QString> value(item.toStringList().constBegin(), item.toStringList().constEnd());
                pyList.append(type_caster<std::vector<QString> >::cast(value, policy, parent));
            }
            case QVariant::Vector3D: {
                QVector3D vec3d = item.value<QVector3D>();
                std::vector<float> value;
                value.push_back(vec3d.x());
                value.push_back(vec3d.y());
                value.push_back(vec3d.z());
                pyList.append(type_caster<std::vector<float> >::cast(value, policy, parent));
            }
            case QVariant::Color: {
                QColor color = item.value<QColor>();
                std::vector<float> value;
                value.push_back(color.redF());
                value.push_back(color.greenF());
                value.push_back(color.blueF());
                pyList.append(type_caster<std::vector<float> >::cast(value, policy, parent));
            }
            default:
                pyList.append(py::str(item.toString().toUtf8().constData()));
                break;
        }
    }
    return pyList.release();
}

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
// Qt 5: QVector and QList are different
// --------------- QVector<QVariant> ---------------
// Conversion from Python PyObject to C++ QVector<QVariant>
bool type_caster<QVector<QVariant>>::load(handle src, bool) {
    if (!isinstance<py::list>(src)) {
        return false;
    }

    auto pyList = reinterpret_borrow<py::list>(src);
    value.clear();
    for (auto item : pyList) {
        // Convert Python object to QVariant using QString intermediate
        if (py::isinstance<py::str>(item)) {
            value.append(QVariant(QString::fromUtf8(py::str(item).cast<std::string>().c_str())));
        }
        else if (py::isinstance<py::int_>(item)) {
            value.append(QVariant(item.cast<int>()));
        }
        else if (py::isinstance<py::float_>(item)) {
            value.append(QVariant(item.cast<double>()));
        }
        else if (py::isinstance<py::bool_>(item)) {
            value.append(QVariant(item.cast<bool>()));
        }
        else {
            // fallback: convert to string
            value.append(QVariant(QString::fromUtf8(py::str(item).cast<std::string>().c_str())));
        }
    }
    return true;
}

// Conversion from C++ QVector<QVariant> to Python PyObject
handle type_caster<QVector<QVariant>>::cast(const QVector<QVariant>& src, return_value_policy policy, handle parent) {
    py::list pyList;
    for (const QVariant& item : src) {
        switch (item.userType()) {
            case QMetaType::UnknownType:
                pyList.append(Py_None);
                break;
            case QVariant::Int:
                pyList.append(item.toInt());
                break;
            case QVariant::Double:
                pyList.append(item.toDouble());
                break;
            case QVariant::Bool:
                pyList.append(item.toBool());
                break;
            case QVariant::String:
                pyList.append(py::str(item.toString().toUtf8().constData()));
                break;
            case QVariant::StringList: {
                std::vector<QString> value(item.toStringList().constBegin(), item.toStringList().constEnd());
                pyList.append(type_caster<std::vector<QString> >::cast(value, policy, parent));
            }
            case QVariant::Vector3D: {
                QVector3D vec3d = item.value<QVector3D>();
                std::vector<float> value;
                value.push_back(vec3d.x());
                value.push_back(vec3d.y());
                value.push_back(vec3d.z());
                pyList.append(type_caster<std::vector<float> >::cast(value, policy, parent));
            }
            case QVariant::Color: {
                QColor color = item.value<QColor>();
                std::vector<float> value;
                value.push_back(color.redF());
                value.push_back(color.greenF());
                value.push_back(color.blueF());
                pyList.append(type_caster<std::vector<float> >::cast(value, policy, parent));
            }
            default:
                pyList.append(py::str(item.toString().toUtf8().constData()));
                break;
        }
    }
    return pyList.release();
}
#endif

// --------------- QColor ---------------
// Conversion from Python sequence to C++ QColor
bool type_caster<QColor>::load(handle src, bool) {
    // Accept list or tuple
    if (!py::isinstance<py::sequence>(src)) {
        return false;
    }
    py::sequence seq = reinterpret_borrow<py::sequence>(src);

    // QColor has either rgb or rgba (float in [0..1])
    if (seq.size() != 3 && seq.size() != 4) {
        return false;
    }

    // convert sequence to QColor depending on the length
    try {
        float r = seq[0].cast<float>();
        float g = seq[1].cast<float>();
        float b = seq[2].cast<float>();
        if (seq.size() == 3) {
            value = QColor::fromRgbF(r, g, b);
        }
        else {
            float a = seq[3].cast<float>();
            value = QColor::fromRgbF(r, g, b, a);
        }
        return true;
    }
    catch (const py::cast_error&) {
        return false;
    }
}

// Conversion from C++ QColor to Python tuple
handle type_caster<QColor>::cast(const QColor& src, return_value_policy, handle) {
    py::tuple t(src.alphaF() == 1.0 ? 3 : 4);
    t[0] = src.redF();
    t[1] = src.greenF();
    t[2] = src.blueF();
    if (src.alphaF() != 1.0) {
        t[3] = src.alphaF();
    }
    return t.release();
}


// --------------- QStringList ---------------
// Conversion from Python sequence to C++ QStringList
bool type_caster<QStringList>::load(handle src, bool) {
    if (!isinstance<pybind11::sequence>(src)) {
        return false;
    }

    pybind11::sequence seq = reinterpret_borrow<pybind11::sequence>(src);
    for (auto item : seq) {
        if (!pybind11::isinstance<pybind11::str>(item)) {
            return false;
        }
        QString q = QString::fromUtf8(item.cast<std::string>().c_str());
        value.push_back(q);
    }
    return true;
}

// Conversion from C++ QStringList to Python tuple
handle type_caster<QStringList>::cast(const QStringList& src, return_value_policy, handle) {
    pybind11::list pylist;
    for (const QString& s : src) {
        pylist.append(pybind11::str(s.toUtf8().constData()));
    }
    return pylist.release();
}

} // namespace pybind11::detail
