/*-----------------------------------------------------------------------------
| Copyright (c) 2013-2024, Nucleic Development Team.
|
| Distributed under the terms of the Modified BSD License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
#include <algorithm>
#include <sstream>
#include <cppy/cppy.h>
#include <kiwi/kiwi.h>
#include "types.h"
#include "util.h"

namespace kiwisolver
{

namespace
{

PyObject *
Constraint_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
    static const char *kwlist[] = {"expression", "op", "strength", 0};
    PyObject *pyexpr;
    PyObject *pyop;
    PyObject *pystrength = 0;
    if (!PyArg_ParseTupleAndKeywords(
            args, kwargs, "OO|O:__new__", const_cast<char **>(kwlist),
            &pyexpr, &pyop, &pystrength))
        return 0;
    if (!Expression::TypeCheck(pyexpr))
        return cppy::type_error(pyexpr, "Expression");
    kiwi::RelationalOperator op;
    if (!convert_to_relational_op(pyop, op))
        return 0;
    double strength = kiwi::strength::required;
    if (pystrength && !convert_to_strength(pystrength, strength))
        return 0;
    cppy::ptr pycn(PyType_GenericNew(type, args, kwargs));
    if (!pycn)
        return 0;
    Constraint *cn = reinterpret_cast<Constraint *>(pycn.get());
    cn->expression = reduce_expression(pyexpr);
    if (!cn->expression)
        return 0;

    ACQUIRE_GLOBAL_LOCK();
    kiwi::Expression expr(convert_to_kiwi_expression(cn->expression));
    new (&cn->constraint) kiwi::Constraint(expr, op, strength);
    RELEASE_GLOBAL_LOCK();

    return pycn.release();
}

void Constraint_clear(Constraint *self)
{
    Py_CLEAR(self->expression);
}

int Constraint_traverse(Constraint *self, visitproc visit, void *arg)
{
    Py_VISIT(self->expression);
#if PY_VERSION_HEX >= 0x03090000
    // This was not needed before Python 3.9 (Python issue 35810 and 40217)
    Py_VISIT(Py_TYPE(self));
#endif
    return 0;
}

void Constraint_dealloc(Constraint *self)
{
    PyObject_GC_UnTrack(self);
    Constraint_clear(self);
    ACQUIRE_GLOBAL_LOCK();
    self->constraint.~Constraint();
    RELEASE_GLOBAL_LOCK();
    Py_TYPE(self)->tp_free(pyobject_cast(self));
}

PyObject *
Constraint_repr(Constraint *self)
{
    std::stringstream stream;
    Expression *expr = reinterpret_cast<Expression *>(self->expression);
    Py_ssize_t size = PyTuple_GET_SIZE(expr->terms);
    for (Py_ssize_t i = 0; i < size; ++i)
    {
        PyObject *item = PyTuple_GET_ITEM(expr->terms, i);
        Term *term = reinterpret_cast<Term *>(item);
        stream << term->coefficient << " * ";
        ACQUIRE_GLOBAL_LOCK();
        std::string name = reinterpret_cast<Variable *>(term->variable)->variable.name();
        RELEASE_GLOBAL_LOCK();
        stream << name;
        stream << " + ";
    }
    stream << expr->constant;

    ACQUIRE_GLOBAL_LOCK();
    kiwi::RelationalOperator op = self->constraint.op();
    double strength = self->constraint.strength();
    bool violated = self->constraint.violated();
    RELEASE_GLOBAL_LOCK();

    switch (op)
    {
    case kiwi::OP_EQ:
        stream << " == 0";
        break;
    case kiwi::OP_LE:
        stream << " <= 0";
        break;
    case kiwi::OP_GE:
        stream << " >= 0";
        break;
    }
    stream << " | strength = " << strength;
    if (violated)
    {
        stream << " (VIOLATED)";
    }
    return PyUnicode_FromString(stream.str().c_str());
}

PyObject *
Constraint_expression(Constraint *self)
{
    return cppy::incref(self->expression);
}

PyObject *
Constraint_op(Constraint *self)
{
    PyObject *res = 0;

    ACQUIRE_GLOBAL_LOCK();
    kiwi::RelationalOperator op = self->constraint.op();
    RELEASE_GLOBAL_LOCK();

    switch (op)
    {
    case kiwi::OP_EQ:
        res = PyUnicode_FromString("==");
        break;
    case kiwi::OP_LE:
        res = PyUnicode_FromString("<=");
        break;
    case kiwi::OP_GE:
        res = PyUnicode_FromString(">=");
        break;
    }
    return res;
}

PyObject *
Constraint_strength(Constraint *self)
{
    ACQUIRE_GLOBAL_LOCK();
    double strength = self->constraint.strength();
    RELEASE_GLOBAL_LOCK();
    return PyFloat_FromDouble(strength);
}

PyObject *
Constraint_violated(Constraint *self)
{
    ACQUIRE_GLOBAL_LOCK();
    bool violated = self->constraint.violated();
    RELEASE_GLOBAL_LOCK();

    if (violated) {
        Py_RETURN_TRUE;
    } else {
        Py_RETURN_FALSE;
    }
}

PyObject *
Constraint_or(PyObject *pyoldcn, PyObject *value)
{
    if (!Constraint::TypeCheck(pyoldcn))
        std::swap(pyoldcn, value);
    double strength;
    if (!convert_to_strength(value, strength))
        return 0;
    PyObject *pynewcn = PyType_GenericNew(Constraint::TypeObject, 0, 0);
    if (!pynewcn)
        return 0;
    Constraint *oldcn = reinterpret_cast<Constraint *>(pyoldcn);
    Constraint *newcn = reinterpret_cast<Constraint *>(pynewcn);
    newcn->expression = cppy::incref(oldcn->expression);

    ACQUIRE_GLOBAL_LOCK();
    new (&newcn->constraint) kiwi::Constraint(oldcn->constraint, strength);
    RELEASE_GLOBAL_LOCK();

    return pynewcn;
}

static PyMethodDef
    Constraint_methods[] = {
        {"expression", (PyCFunction)Constraint_expression, METH_NOARGS,
         "Get the expression object for the constraint."},
        {"op", (PyCFunction)Constraint_op, METH_NOARGS,
         "Get the relational operator for the constraint."},
        {"strength", (PyCFunction)Constraint_strength, METH_NOARGS,
         "Get the strength for the constraint."},
        {"violated", (PyCFunction)Constraint_violated, METH_NOARGS,
         "Return whether or not the constraint was violated "
         "during the last solver pass."},
        {0} // sentinel
};

static PyType_Slot Constraint_Type_slots[] = {
    {Py_tp_dealloc, void_cast(Constraint_dealloc)},   /* tp_dealloc */
    {Py_tp_traverse, void_cast(Constraint_traverse)}, /* tp_traverse */
    {Py_tp_clear, void_cast(Constraint_clear)},       /* tp_clear */
    {Py_tp_repr, void_cast(Constraint_repr)},         /* tp_repr */
    {Py_tp_methods, void_cast(Constraint_methods)},   /* tp_methods */
    {Py_tp_new, void_cast(Constraint_new)},           /* tp_new */
    {Py_tp_alloc, void_cast(PyType_GenericAlloc)},    /* tp_alloc */
    {Py_tp_free, void_cast(PyObject_GC_Del)},         /* tp_free */
    {Py_nb_or, void_cast(Constraint_or)},             /* nb_or */
    {0, 0},
};

} // namespace

// Initialize static variables (otherwise the compiler eliminates them)
PyTypeObject *Constraint::TypeObject = NULL;

PyType_Spec Constraint::TypeObject_Spec = {
    "kiwisolver.Constraint", /* tp_name */
    sizeof(Constraint),      /* tp_basicsize */
    0,                       /* tp_itemsize */
    Py_TPFLAGS_DEFAULT |
        Py_TPFLAGS_HAVE_GC |
        Py_TPFLAGS_BASETYPE, /* tp_flags */
    Constraint_Type_slots    /* slots */
};

bool Constraint::Ready()
{
    // The reference will be handled by the module to which we will add the type
    TypeObject = pytype_cast(PyType_FromSpec(&TypeObject_Spec));
    if (!TypeObject)
    {
        return false;
    }
    return true;
}

} // namespace kiwisolver
