// Copyright 2022 Alibaba Group Holding Limited.
//
// 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 "Pemja.h"
#include "java_class/JavaClass.h"
#include "python_class/PythonClass.h"

static int pyjobject_init(JNIEnv *env, PyJObject *self) {
  int len;
  JcpThread *jcpThread;

  jstring className;
  jobjectArray methods, fields;
  jobject method, field;

  PyObject *cachedAttrs, *cachedMethod;
  PyJMultiMethodObject *multi_methods;

  if ((*env)->PushLocalFrame(env, 16) != 0) {
    return -1;
  }

  className = JavaClass_getName(env, self->clazz);
  if (!className) {
    goto EXIT_ERROR;
  }

  self->class_name = JcpPyString_FromJString(env, className);
  jcpThread = JcpThread_Get();

  if (jcpThread->name_to_attrs == NULL) {
    jcpThread->name_to_attrs = PyDict_New();
  }

  cachedAttrs = PyDict_GetItem(jcpThread->name_to_attrs, self->class_name);

  if (cachedAttrs == NULL) {
    cachedAttrs = PyDict_New();

    // cache methods
    methods = JavaClass_getMethods(env, self->clazz);
    len = (*env)->GetArrayLength(env, methods);

    for (int i = 0; i < len; i++) {
      method = (*env)->GetObjectArrayElement(env, methods, i);
      PyJMethodObject *pyjmethod = JcpPyJMethod_New(env, method);
      (*env)->DeleteLocalRef(env, method);

      if (!pyjmethod) {
        continue;
      }

      if (pyjmethod->md_name) {
        cachedMethod = PyDict_GetItem(cachedAttrs, pyjmethod->md_name);
        if (cachedMethod == NULL) {
          if (PyDict_SetItem(cachedAttrs, pyjmethod->md_name,
                             (PyObject *)pyjmethod) < 0) {
            Py_DECREF(pyjmethod);
            goto EXIT_ERROR;
          }
        } else {
          if (PyJMethod_Check(cachedMethod)) {
            multi_methods = JcpPyJMultiMethod_New();
            if (JcpPyJMultiMethod_Append(multi_methods,
                                         (PyJMethodObject *)cachedMethod) < 0) {
              Py_DECREF(pyjmethod);
              Py_DECREF(multi_methods);
              goto EXIT_ERROR;
            }

            if (PyDict_SetItem(cachedAttrs, pyjmethod->md_name,
                               (PyObject *)multi_methods) < 0) {
              Py_DECREF(pyjmethod);
              Py_DECREF(multi_methods);
              goto EXIT_ERROR;
            }
          } else {
            multi_methods = (PyJMultiMethodObject *)cachedMethod;
          }

          if (JcpPyJMultiMethod_Append(multi_methods, pyjmethod) < 0) {
            Py_DECREF(pyjmethod);
            Py_DECREF(multi_methods);
            goto EXIT_ERROR;
          }
        }
      }

      Py_DECREF(pyjmethod);
    }
    (*env)->DeleteLocalRef(env, methods);

    // cache fields
    fields = JavaClass_getFields(env, self->clazz);
    len = (*env)->GetArrayLength(env, fields);

    for (int i = 0; i < len; i++) {
      field = (*env)->GetObjectArrayElement(env, fields, i);
      PyJFieldObject *pyjfield = JcpPyJField_New(env, field);
      (*env)->DeleteLocalRef(env, field);

      if (!pyjfield) {
        continue;
      }

      if (pyjfield->fd_name) {
        if (PyDict_SetItem(cachedAttrs, pyjfield->fd_name,
                           (PyObject *)pyjfield) < 0) {
          Py_DECREF(pyjfield);
          goto EXIT_ERROR;
        }
      }
      Py_DECREF(pyjfield);
    }
    (*env)->DeleteLocalRef(env, fields);

    PyDict_SetItem(jcpThread->name_to_attrs, self->class_name, cachedAttrs);
    Py_DECREF(cachedAttrs);
  }

  if (self->object) {
    Py_INCREF(cachedAttrs);
    self->attr = cachedAttrs;
  } else {
    self->attr = PyDict_Copy(cachedAttrs);
  }

  (*env)->PopLocalFrame(env, NULL);
  return 0;

EXIT_ERROR:
  (*env)->PopLocalFrame(env, NULL);
  return -1;
}

static void pyjobject_dealloc(PyJObject *self) {
  JNIEnv *env;

  env = JcpThreadEnv_Get();

  if (self->clazz) {
    (*env)->DeleteGlobalRef(env, self->clazz);
  }

  if (self->object) {
    (*env)->DeleteGlobalRef(env, self->object);
  }

  Py_CLEAR(self->attr);
  Py_DECREF(self->class_name);

  PyObject_Del(self);
}

static PyObject *pyjobject_str(PyJObject *self) {
  JNIEnv *env;
  jstring str;

  env = JcpThreadEnv_Get();

  if (self->object) {
    str = JavaObject_toString(env, self->object);
  } else {
    str = JavaObject_toString(env, self->clazz);
  }

  return JcpPyString_FromJString(env, str);
}

static PyObject *pyjobject_getattro(PyObject *self, PyObject *name) {
  PyObject *attr, *cachedAttrs, *result;

  cachedAttrs = ((PyJObject *)self)->attr;
  attr = PyDict_GetItem(cachedAttrs, name);

  result = NULL;
  if (attr == NULL) {
    return PyObject_GenericGetAttr(self, name);
  } else if (PyJMethod_Check(attr) || PyJMultiMethod_Check(attr)) {
    result = PyMethod_New(attr, self);
  } else if (PyJField_Check(attr)) {
    result = JcpPyJField_Get((PyJFieldObject *)attr, (PyJObject *)self);
  }
  return result;
}

static int pyjobject_setattro(PyJObject *self, PyObject *name,
                              PyObject *value) {
  PyObject *attr, *cachedAttrs;

  cachedAttrs = self->attr;
  attr = PyDict_GetItem(cachedAttrs, name);

  if (attr == NULL) {
    return PyObject_GenericSetAttr(self->attr, name, value);
  }

  if (!PyJField_Check(attr)) {
    PyErr_Format(PyExc_AttributeError, "'%s' object cannot set attribute '%s'.",
                 PyUnicode_AsUTF8(self->class_name), PyUnicode_AsUTF8(name));
  }

  return JcpPyJField_Set((PyJFieldObject *)attr, self, value);
}

PyObject *JcpPyJObject_New(JNIEnv *env, PyTypeObject *type, jobject object,
                           jclass clazz) {
  PyJObject *self;

  self = PyObject_NEW(PyJObject, type);

  if (object) {
    self->object = (*env)->NewGlobalRef(env, object);
  } else {
    // Class Type
    self->object = NULL;
  }

  if (clazz) {
    // Class Type
    self->clazz = (*env)->NewGlobalRef(env, clazz);
  } else {
    clazz = (*env)->GetObjectClass(env, object);
    self->clazz = (*env)->NewGlobalRef(env, clazz);
    (*env)->DeleteLocalRef(env, clazz);
    clazz = NULL;
  }

  if (pyjobject_init(env, self) < 0) {
    Py_DECREF((PyObject *)self);
    self = NULL;
  }

  return (PyObject *)self;
}

PyTypeObject PyJObject_Type = {
    PyVarObject_HEAD_INIT(NULL, 0) "pemja.PyJObject", /* tp_name */
    sizeof(PyJObject),                                /* tp_basicsize */
    0,                                                /* tp_itemsize */
    (destructor)pyjobject_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 */
    (reprfunc)pyjobject_str,                          /* tp_str */
    (getattrofunc)pyjobject_getattro,                 /* tp_getattro */
    (setattrofunc)pyjobject_setattro,                 /* tp_setattro */
    0,                                                /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,         /* tp_flags */
    "Java Object",                                    /* tp_doc */
    0,                                                /* tp_traverse */
    0,                                                /* tp_clear */
    0,                                                /* tp_richcompare */
    0,                                                /* tp_weaklistoffset */
    0,                                                /* tp_iter */
    0,                                                /* tp_iternext */
    0,                                                /* tp_methods */
    0,                                                /* tp_members */
    0,                                                /* 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 */
    0,                                                /* tp_new */
};
