/*******************************************************************************
 * Copyright 2009-2025 Jörg Müller
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
#include "PyAnimateableProperty.h"

#include "Exception.h"

#include "sequence/AnimateableProperty.h"

#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
#include <memory>

#include <numpy/ndarrayobject.h>

using namespace aud;

extern PyObject* AUDError;

static PyObject* AnimateableProperty_new(PyTypeObject* type, PyObject* args, PyObject* kwds)
{
	AnimateablePropertyP* self = (AnimateablePropertyP*) type->tp_alloc(type, 0);

	int count;
	float value;

	if(self != nullptr)
	{
		if(!PyArg_ParseTuple(args, "i|f:animateableProperty", &count, &value))
			return nullptr;

		try
		{
			if(PyTuple_Size(args) == 1)
			{
				self->animateableProperty = new std::shared_ptr<aud::AnimateableProperty>(new aud::AnimateableProperty(count));
			}
			else
			{
				self->animateableProperty = new std::shared_ptr<aud::AnimateableProperty>(new aud::AnimateableProperty(count, value));
			}
		}
		catch(aud::Exception& e)
		{
			Py_DECREF(self);
			PyErr_SetString(AUDError, e.what());
			return nullptr;
		}
	}

	return (PyObject*) self;
}

static void AnimateableProperty_dealloc(AnimateablePropertyP* self)
{
	if(self->animateableProperty)
		delete reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty);
	Py_TYPE(self)->tp_free((PyObject*) self);
}

static PyObject* AnimateableProperty_read(AnimateablePropertyP* self, PyObject* args)
{
	float position;

	if(!PyArg_ParseTuple(args, "f", &position))
		return nullptr;

	int count = (*reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty))->getCount();
	npy_intp dims[1] = {count};
	PyObject* np_array = PyArray_SimpleNew(1, dims, NPY_FLOAT32);
	if(!np_array)
		return nullptr;

	float* out = static_cast<float*>(PyArray_DATA(reinterpret_cast<PyArrayObject*>(np_array)));

	try
	{
		(*reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty))->read(position, out);
		return np_array;
	}
	catch(aud::Exception& e)
	{
		Py_DECREF(np_array);
		PyErr_SetString(AUDError, e.what());
		return nullptr;
	}
}

PyDoc_STRVAR(M_aud_AnimateableProperty_read_doc, ".. method:: read(position)\n\n"
                                                 "   Reads the properties value at the given position.\n\n"
                                                 "   :param position: The position in the animation in frames.\n"
                                                 "   :type position: float\n"
                                                 "   :return: A numpy array of values representing the properties value.\n"
                                                 "   :rtype: :class:`numpy.ndarray`\n");

static PyObject* AnimateableProperty_readSingle(AnimateablePropertyP* self, PyObject* args)
{
	float position;

	if(!PyArg_ParseTuple(args, "f", &position))
		return nullptr;

	try
	{
		float value = (*reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty))->readSingle(position);
		return Py_BuildValue("f", value);
	}
	catch(aud::Exception& e)
	{
		PyErr_SetString(AUDError, e.what());
		return nullptr;
	}
}

PyDoc_STRVAR(M_aud_AnimateableProperty_readSingle_doc, ".. method:: readSingle(position)\n\n"
                                                       "   Reads the properties value at the given position, assuming there is exactly one value.\n\n"
                                                       "   :param position: The position in the animation in frames.\n"
                                                       "   :type position: float\n"
                                                       "   :return: The value at that position.\n"
                                                       "   :rtype: float\n\n");

static PyObject* AnimateableProperty_write(AnimateablePropertyP* self, PyObject* args)
{
	PyObject* array_obj;
	int position = -1;

	if(!PyArg_ParseTuple(args, "O|i", &array_obj, &position))
		return nullptr;

	PyArrayObject* np_array = reinterpret_cast<PyArrayObject*>(PyArray_FROM_OTF(array_obj, NPY_FLOAT32, NPY_ARRAY_IN_ARRAY | NPY_ARRAY_FORCECAST));

	if(!np_array)
	{
		PyErr_SetString(PyExc_TypeError, "data must be a numpy array of dtype float32");
		return nullptr;
	}

	auto& prop = *reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty);
	int prop_count = prop->getCount();
	npy_intp size = PyArray_SIZE(np_array);

	int ndim = PyArray_NDIM(np_array);

	bool valid_shape = false;

	// For 1D arrays, the total number of elements must be a multiple of the property count
	if(ndim == 1)
	{
		valid_shape = (size % prop_count == 0);
	}
	// For 2D arrays, the number of elements in the second dimension must be the property count
	else if(ndim == 2)
	{
		npy_intp* shape = PyArray_DIMS(np_array);
		valid_shape = (shape[1] == prop_count);
	}

	if(!valid_shape)
	{
		PyErr_SetString(PyExc_ValueError, "array shape is invalid: must be 1D with length multiple of property count or 2D with the last dimension equal to property count");
		Py_DECREF(np_array);
		return nullptr;
	}

	int count = static_cast<int>(size / prop_count);

	if(count < 1)
	{
		PyErr_SetString(PyExc_ValueError, "input array must have at least 1 element");
		Py_DECREF(np_array);
		return nullptr;
	}

	float* data_ptr = reinterpret_cast<float*>(PyArray_DATA(np_array));
	try
	{
		if(position == -1)
		{
			if(count != 1)
			{
				PyErr_SetString(PyExc_ValueError, "input array must have exactly 1 element when position is not specified");
				Py_DECREF(np_array);
				return nullptr;
			}
			prop->write(data_ptr);
		}
		else
		{
			prop->write(data_ptr, position, count);
		}
	}
	catch(aud::Exception& e)
	{
		PyErr_SetString(AUDError, e.what());
	}

	Py_DECREF(np_array);
	Py_RETURN_NONE;
}

PyDoc_STRVAR(M_aud_AnimateableProperty_write_doc, ".. method:: write(data[, position])\n\n"
                                                  "   Writes the properties value.\n\n"
                                                  "   If `position` is also given, the property is marked animated and\n"
                                                  "   the values are written starting at `position`.\n\n"
                                                  "   :param data: numpy array of float32 values.\n"
                                                  "   :type data: numpy.ndarray\n"
                                                  "   :param position: The starting position in frames.\n"
                                                  "   :type position: int\n\n");

static PyObject* AnimateableProperty_writeConstantRange(AnimateablePropertyP* self, PyObject* args)
{
	PyObject* array_obj;
	int position_start;
	int position_end;

	if(!PyArg_ParseTuple(args, "Oii", &array_obj, &position_start, &position_end))
		return nullptr;

	PyArrayObject* np_array = reinterpret_cast<PyArrayObject*>(PyArray_FROM_OTF(array_obj, NPY_FLOAT32, NPY_ARRAY_IN_ARRAY | NPY_ARRAY_FORCECAST));
	if(!np_array)
	{
		PyErr_SetString(PyExc_TypeError, "data must be a numpy array of dtype float32");
		return nullptr;
	}

	int ndim = PyArray_NDIM(np_array);
	if(ndim != 1)
	{
		PyErr_SetString(PyExc_ValueError, "data must be a 1D numpy array");
		Py_DECREF(np_array);
		return nullptr;
	}

	float* data_ptr = reinterpret_cast<float*>(PyArray_DATA(np_array));

	auto& prop = *reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty);
	int prop_count = prop->getCount();

	npy_intp size = PyArray_SIZE(np_array);

	if(size != prop_count)
	{
		PyErr_Format(PyExc_ValueError, "input array length (%lld) does not match property count (%d)", size, prop_count);
		Py_DECREF(np_array);
		return nullptr;
	}

	try
	{
		prop->writeConstantRange(data_ptr, position_start, position_end);
	}
	catch(aud::Exception& e)
	{
		PyErr_SetString(AUDError, e.what());
		return nullptr;
	}

	Py_DECREF(np_array);
	Py_RETURN_NONE;
}

PyDoc_STRVAR(M_aud_AnimateableProperty_writeConstantRange_doc, ".. method:: writeConstantRange(data, position_start, position_end)\n\n"
                                                               "   Fills the properties frame range with a constant value and marks it animated.\n\n"
                                                               "   :param data: numpy array of float values representing the constant value.\n"
                                                               "   :type data: numpy.ndarray\n"
                                                               "   :param position_start: The start position in frames.\n"
                                                               "   :type position_start: int\n"
                                                               "   :param position_end: The end position in frames.\n"
                                                               "   :type position_end: int\n\n");

static PyMethodDef AnimateableProperty_methods[] = {

    {(char*) "read", (PyCFunction) AnimateableProperty_read, METH_VARARGS, M_aud_AnimateableProperty_read_doc},
    {(char*) "readSingle", (PyCFunction) AnimateableProperty_readSingle, METH_VARARGS, M_aud_AnimateableProperty_readSingle_doc},
    {(char*) "write", (PyCFunction) AnimateableProperty_write, METH_VARARGS, M_aud_AnimateableProperty_write_doc},
    {(char*) "writeConstantRange", (PyCFunction) AnimateableProperty_writeConstantRange, METH_VARARGS, M_aud_AnimateableProperty_writeConstantRange_doc},
    {nullptr} /* Sentinel */
};

static PyObject* AnimateableProperty_get_count(AnimateablePropertyP* self, void* nothing)
{
	try
	{
		int count = (*reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty))->getCount();
		return Py_BuildValue("i", count);
	}
	catch(aud::Exception& e)
	{
		PyErr_SetString(AUDError, e.what());
		return nullptr;
	}
}

PyDoc_STRVAR(M_aud_AnimateableProperty_count_doc, "The count of floats for a property.");

static PyObject* AnimateableProperty_get_animated(AnimateablePropertyP* self, void* nothing)
{
	try
	{
		bool animated = (*reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty))->isAnimated();
		return PyBool_FromLong(animated);
	}
	catch(aud::Exception& e)
	{
		PyErr_SetString(AUDError, e.what());
		return nullptr;
	}
}

PyDoc_STRVAR(M_aud_AnimateableProperty_animated_doc, "Whether the property is animated.");

static PyGetSetDef AnimateableProperty_properties[] = {
    {(char*) "count", (getter) AnimateableProperty_get_count, nullptr, M_aud_AnimateableProperty_count_doc, nullptr},
    {(char*) "animated", (getter) AnimateableProperty_get_animated, nullptr, M_aud_AnimateableProperty_animated_doc, nullptr},
    {nullptr} /* Sentinel */
};

PyDoc_STRVAR(M_aud_AnimateableProperty_doc,
	".. class:: AnimateableProperty(count, value=0.0, /)\n\n"
	"   An AnimateableProperty object stores an array of float values for animating sound properties (e.g. pan, volume, pitch-scale).\n\n"
	"   :arg count: The number of float values to store per frame.\n"
	"   :type count: int\n"
	"   :arg value: The initial value for all elements.\n"
	"   :type value: float\n");

// Note that AnimateablePropertyType name is already taken
PyTypeObject AnimateablePropertyPyType = {
    PyVarObject_HEAD_INIT(nullptr, 0) "aud.AnimateableProperty", /* tp_name */
    sizeof(AnimateablePropertyP),                                /* tp_basicsize */
    0,                                                           /* tp_itemsize */
    (destructor) AnimateableProperty_dealloc,                    /* tp_dealloc */
    0,                                                           /* tp_print */
    0,                                                           /* tp_getattr */
    0,                                                           /* tp_setattr */
    0,                                                           /* tp_reserved */
    0,                                                           /* tp_repr */
    0,                                                           /* tp_as_number */
    0,                                                           /* tp_as_sequence */
    0,                                                           /* tp_as_mapping */
    0,                                                           /* tp_hash  */
    0,                                                           /* tp_call */
    0,                                                           /* tp_str */
    0,                                                           /* tp_getattro */
    0,                                                           /* tp_setattro */
    0,                                                           /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT,                                          /* tp_flags */
    M_aud_AnimateableProperty_doc,                               /* tp_doc */
    0,                                                           /* tp_traverse */
    0,                                                           /* tp_clear */
    0,                                                           /* tp_richcompare */
    0,                                                           /* tp_weaklistoffset */
    0,                                                           /* tp_iter */
    0,                                                           /* tp_iternext */
    AnimateableProperty_methods,                                 /* tp_methods */
    0,                                                           /* tp_members */
    AnimateableProperty_properties,                              /* tp_getset */
    0,                                                           /* tp_base */
    0,                                                           /* tp_dict */
    0,                                                           /* tp_descr_get */
    0,                                                           /* tp_descr_set */
    0,                                                           /* tp_dictoffset */
    0,                                                           /* tp_init */
    0,                                                           /* tp_alloc */
    AnimateableProperty_new,                                     /* tp_new */
};

AUD_API PyObject* AnimateableProperty_empty()
{
	return AnimateablePropertyPyType.tp_alloc(&AnimateablePropertyPyType, 0);
}

AUD_API AnimateablePropertyP* checkAnimateableProperty(PyObject* animateableProperty)
{
	if(!PyObject_TypeCheck(animateableProperty, &AnimateablePropertyPyType))
	{
		PyErr_SetString(PyExc_TypeError, "Object is not of type AnimateableProperty!");
		return nullptr;
	}

	return (AnimateablePropertyP*) animateableProperty;
}

bool initializeAnimateableProperty()
{
	import_array1(false);
	return PyType_Ready(&AnimateablePropertyPyType) >= 0;
}

void addAnimateablePropertyToModule(PyObject* module)
{
	Py_INCREF(&AnimateablePropertyPyType);
	PyModule_AddObject(module, "AnimateableProperty", (PyObject*) &AnimateablePropertyPyType);
}
