// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc.  All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include <cstdint>
#include <string>

#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include "google/protobuf/descriptor.pb.h"
#include "absl/log/absl_check.h"
#include "absl/log/absl_log.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "google/protobuf/descriptor.h"
#include "google/protobuf/descriptor_database.h"
#include "google/protobuf/dynamic_message.h"
#include "google/protobuf/message_lite.h"
#include "google/protobuf/proto_api.h"
#include "google/protobuf/pyext/descriptor.h"
#include "google/protobuf/pyext/descriptor_pool.h"
#include "google/protobuf/pyext/message.h"
#include "google/protobuf/pyext/message_factory.h"
#include "google/protobuf/stubs/status_macros.h"

// Must be included last.
#include "google/protobuf/port_def.inc"

namespace {

class ProtoAPIDescriptorDatabase : public google::protobuf::DescriptorDatabase {
 public:
  ProtoAPIDescriptorDatabase(PyObject* py_pool) : pool_(py_pool) {};

  ~ProtoAPIDescriptorDatabase() {};

  bool FindFileByName(absl::string_view filename,
                      google::protobuf::FileDescriptorProto* output) override {
    if (PyErr_Occurred()) {
      // If an error has already occurred, PyObject_CallMethod will assert.
      // Reset it so we can continue.
      PyErr_Clear();
    }

    PyObject* pyfile_name =
        PyUnicode_FromStringAndSize(filename.data(), filename.size());
    if (pyfile_name == nullptr) {
      PyErr_Format(PyExc_TypeError, "Fail to convert proto file name");
      return false;
    }

    PyObject* pyfile =
        PyObject_CallMethod(pool_, "FindFileByName", "O", pyfile_name);
    Py_DECREF(pyfile_name);
    if (pyfile == nullptr) {
      PyErr_Format(PyExc_TypeError, "Default python pool fail to find %s",
                   filename.data());
      return false;
    }

    PyObject* pyfile_serialized =
        PyObject_GetAttrString(pyfile, "serialized_pb");
    Py_DECREF(pyfile);
    if (pyfile_serialized == nullptr) {
      PyErr_Format(PyExc_TypeError,
                   "Python file has no attribute 'serialized_pb'");
      return false;
    }

    bool ok = output->ParsePartialFromArray(
        reinterpret_cast<uint8_t*>(PyBytes_AS_STRING(pyfile_serialized)),
        PyBytes_GET_SIZE(pyfile_serialized));
    if (!ok) {
      ABSL_LOG(ERROR) << "Failed to parse descriptor for " << filename;
    }
    Py_DECREF(pyfile_serialized);
    return ok;
  }

  bool FindFileContainingSymbol(absl::string_view symbol_name,
                                google::protobuf::FileDescriptorProto* output) override {
    return false;
  }

  bool FindFileContainingExtension(
      absl::string_view containing_type, int field_number,
      google::protobuf::FileDescriptorProto* output) override {
    return false;
  }

  PyObject* pool() { return pool_; }

 private:
  PyObject* pool_;
};

struct DescriptorPoolState {
  // clang-format off
  PyObject_HEAD

  std::unique_ptr<google::protobuf::DescriptorPool> pool;
  std::unique_ptr<ProtoAPIDescriptorDatabase> database;
};

void DeallocDescriptorPoolState (DescriptorPoolState* self) {
  self->database.reset();
  self->pool.reset();
  Py_TYPE(self)->tp_free((PyObject *)self);
}

PyTypeObject PyDescriptorPoolState_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0) FULL_MODULE_NAME
    ".DescriptorPoolState",       //  tp_name
    sizeof(DescriptorPoolState),  //  tp_basicsize
    0,                            //  tp_itemsize
    (destructor)DeallocDescriptorPoolState,   //  tp_dealloc
    0,                            //  tp_vectorcall_offset
    nullptr,                      //  tp_getattr
    nullptr,                      //  tp_setattr
    nullptr,                      //  tp_compare
    nullptr,                      //  tp_repr
    nullptr,                      //  tp_as_number
    nullptr,                      //  tp_as_sequence
    nullptr,                      //  tp_as_mapping
    PyObject_HashNotImplemented,  //  tp_hash
    nullptr,                      //  tp_call
    nullptr,                      //  tp_str
    nullptr,                      //  tp_getattro
    nullptr,                      //  tp_setattro
    nullptr,                      //  tp_as_buffer
    Py_TPFLAGS_DEFAULT,           //  tp_flags
    "DescriptorPoolState",        //  tp_doc
};

PyObject* PyDescriptorPoolState_New(PyObject* pyfile_pool) {
  PyObject* pypool_state = PyType_GenericAlloc(&PyDescriptorPoolState_Type, 0);
  if (pypool_state == nullptr) {
    PyErr_SetString(PyExc_MemoryError,
                    "Fail to new PyDescriptorPoolState_Type");
    return nullptr;
  }
  DescriptorPoolState* pool_state =
      reinterpret_cast<DescriptorPoolState*>(pypool_state);
  pool_state->database =
      std::make_unique<ProtoAPIDescriptorDatabase>(pyfile_pool);
  pool_state->pool =
      std::make_unique<google::protobuf::DescriptorPool>(pool_state->database.get());
  return pypool_state;
}

PyObject* InitAndGetPoolMap() {
#if PY_VERSION_HEX >= 0x030C0000
  // Returns a WeakKeyDictionary. The key will be a python pool and
  // the value will be PyDescriptorPoolState_Type.
  // PyDescriptorPoolState_Type should be ready for the usage.
  if (PyType_Ready(&PyDescriptorPoolState_Type) < 0) {
    return nullptr;
  }
  PyObject* weakref = PyImport_ImportModule("weakref");
  PyObject* pypool_map =
      PyObject_CallMethod(weakref, "WeakKeyDictionary", NULL);
  Py_DECREF(weakref);
  return pypool_map;
#else
  return PyDict_New();
#endif
}

absl::StatusOr<const google::protobuf::Descriptor*> FindMessageDescriptor(
    PyObject* pyfile, const char* descriptor_full_name) {
  static PyObject* pypool_map = InitAndGetPoolMap();
  if (pypool_map == nullptr) {
    return absl::InternalError("Fail to create pypool_map");
  }
  PyObject* pyfile_name = nullptr;
  PyObject* pyfile_pool = nullptr;
  PyObject* pypool_state = nullptr;
  google::protobuf::DescriptorPool* pool;
  DescriptorPoolState* pool_state;
  const char* pyfile_name_char_ptr;
  const google::protobuf::FileDescriptor* file_descriptor;
  absl::StatusOr<const google::protobuf::Descriptor*> ret;

  pyfile_name = PyObject_GetAttrString(pyfile, "name");
  if (pyfile_name == nullptr) {
    ret = absl::InvalidArgumentError("FileDescriptor has no attribute 'name'");
    goto err;
  }
  pyfile_pool = PyObject_GetAttrString(pyfile, "pool");
  if (pyfile_pool == nullptr) {
    ret = absl::InvalidArgumentError("FileDescriptor has no attribute 'pool'");
    goto err;
  }

  pypool_state = PyObject_GetItem(pypool_map, pyfile_pool);
  if (pypool_state == nullptr) {
    if (PyErr_ExceptionMatches(PyExc_KeyError)) {
      // Ignore the KeyError
      PyErr_Clear();
    }
    PyErr_Print();
    pypool_state = PyDescriptorPoolState_New(pyfile_pool);
    if (pypool_state == nullptr) {
      ret = absl::InternalError("Fail to create PyDescriptorPoolState_Type");
      goto err;
    }
    if (PyObject_SetItem(pypool_map, pyfile_pool, pypool_state) < 0) {
      ret = absl::InternalError(
          "Fail to insert PyDescriptorPoolState_Type into pypool_map");
      goto err;
    }
  }
  pool_state = reinterpret_cast<DescriptorPoolState*>(pypool_state);
  pool = pool_state->pool.get();
  pyfile_name_char_ptr = PyUnicode_AsUTF8(pyfile_name);
  if (pyfile_name_char_ptr == nullptr) {
    ret = absl::InvalidArgumentError(
        "FileDescriptor 'name' PyUnicode_AsUTF8() failure.");
    goto err;
  }
  file_descriptor = pool->FindFileByName(pyfile_name_char_ptr);
  if (file_descriptor == nullptr) {
    // This error should never be reached.
    ABSL_DLOG(ERROR) << "MEANT TO BE UNREACHABLE.";
    std::string error_msg = absl::StrCat("Fail to find/build file ",
                                         pyfile_name_char_ptr);
    ret = absl::InternalError(error_msg);
    goto err;
  }

  ret = pool->FindMessageTypeByName(descriptor_full_name);
  if (ret.value() == nullptr) {
    ret = absl::InternalError("Fail to find descriptor by name.");
  }

err:
  Py_XDECREF(pyfile_name);
  Py_XDECREF(pyfile_pool);
  Py_XDECREF(pypool_state);
  return ret;
}

google::protobuf::DynamicMessageFactory* GetFactory() {
  static google::protobuf::DynamicMessageFactory* factory =
      new google::protobuf::DynamicMessageFactory;
  return factory;
}

absl::StatusOr<google::protobuf::Message*> CreateNewMessage(PyObject* py_msg) {
  PyObject* pyd = PyObject_GetAttrString(py_msg, "DESCRIPTOR");
  if (pyd == nullptr) {
    return absl::InvalidArgumentError("py_msg has no attribute 'DESCRIPTOR'");
  }

  PyObject* fn = PyObject_GetAttrString(pyd, "full_name");
  if (fn == nullptr) {
    return absl::InvalidArgumentError(
        "DESCRIPTOR has no attribute 'full_name'");
  }

  const char* descriptor_full_name = PyUnicode_AsUTF8(fn);
  if (descriptor_full_name == nullptr) {
    return absl::InternalError("Fail to convert descriptor full name");
  }

  PyObject* pyfile = PyObject_GetAttrString(pyd, "file");
  Py_DECREF(pyd);
  if (pyfile == nullptr) {
    return absl::InvalidArgumentError("DESCRIPTOR has no attribute 'file'");
  }
  auto gen_d = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName(
      descriptor_full_name);
  if (gen_d) {
    Py_DECREF(pyfile);
    Py_DECREF(fn);
    return google::protobuf::MessageFactory::generated_factory()
        ->GetPrototype(gen_d)
        ->New();
  }
  auto d = FindMessageDescriptor(pyfile, descriptor_full_name);
  Py_DECREF(pyfile);
  Py_DECREF(fn);
  RETURN_IF_ERROR(d.status());
  return GetFactory()->GetPrototype(*d)->New();
}

bool CopyToOwnedMsg(google::protobuf::Message** copy, const google::protobuf::Message& message) {
  *copy = message.New();
  std::string wire;
  // TODO: Remove this suppression.
      (void)message.SerializePartialToString(&wire);
  // TODO: Remove this suppression.
      (void)(*copy)->ParsePartialFromString(wire);
  return true;
}

// C++ API.  Clients get at this via proto_api.h
struct ApiImplementation : google::protobuf::python::PyProto_API {
  absl::StatusOr<google::protobuf::python::PythonMessageMutator> GetClearedMessageMutator(
      PyObject* py_msg) const override {
    if (PyObject_TypeCheck(py_msg, google::protobuf::python::CMessage_Type)) {
      google::protobuf::Message* message =
          google::protobuf::python::PyMessage_GetMutableMessagePointer(py_msg);
      if (message == nullptr) {
        return absl::InternalError(
            "Fail to get message pointer. The message "
            "may already had a reference.");
      }
      message->Clear();
      return CreatePythonMessageMutator(nullptr, message, py_msg);
    }

    auto msg = CreateNewMessage(py_msg);
    RETURN_IF_ERROR(msg.status());
    return CreatePythonMessageMutator(*msg, *msg, py_msg);
  }

  absl::StatusOr<google::protobuf::python::PythonConstMessagePointer>
  GetConstMessagePointer(PyObject* py_msg) const override {
    if (PyObject_TypeCheck(py_msg, google::protobuf::python::CMessage_Type)) {
      const google::protobuf::Message* message =
          google::protobuf::python::PyMessage_GetMessagePointer(py_msg);
      google::protobuf::Message* owned_msg = nullptr;
      ABSL_DCHECK(CopyToOwnedMsg(&owned_msg, *message));
      return CreatePythonConstMessagePointer(owned_msg, message, py_msg);
    }
    auto msg = CreateNewMessage(py_msg);
    RETURN_IF_ERROR(msg.status());
    PyObject* serialized_pb(
        PyObject_CallMethod(py_msg, "SerializePartialToString", nullptr));
    if (serialized_pb == nullptr) {
      return absl::InternalError("Fail to serialize py_msg");
    }
    char* data;
    Py_ssize_t len;
    if (PyBytes_AsStringAndSize(serialized_pb, &data, &len) < 0) {
      Py_DECREF(serialized_pb);
      return absl::InternalError(
          "Fail to get bytes from py_msg serialized data");
    }
    if (!(*msg)->ParsePartialFromString(absl::string_view(data, len))) {
      Py_DECREF(serialized_pb);
      return absl::InternalError(
          "Couldn't parse py_message to google::protobuf::Message*!");
    }
    Py_DECREF(serialized_pb);
    return CreatePythonConstMessagePointer(*msg, *msg, py_msg);
  }

  const google::protobuf::Message* GetMessagePointer(PyObject* msg) const override {
    return google::protobuf::python::PyMessage_GetMessagePointer(msg);
  }
  google::protobuf::Message* GetMutableMessagePointer(PyObject* msg) const override {
    return google::protobuf::python::PyMessage_GetMutableMessagePointer(msg);
  }
  const google::protobuf::Descriptor* MessageDescriptor_AsDescriptor(
      PyObject* desc) const override {
    return google::protobuf::python::PyMessageDescriptor_AsDescriptor(desc);
  }
  const google::protobuf::EnumDescriptor* EnumDescriptor_AsDescriptor(
      PyObject* enum_desc) const override {
    return google::protobuf::python::PyEnumDescriptor_AsDescriptor(enum_desc);
  }

  const google::protobuf::DescriptorPool* GetDefaultDescriptorPool() const override {
    return google::protobuf::python::GetDefaultDescriptorPool()->pool->get();
  }

  google::protobuf::MessageFactory* GetDefaultMessageFactory() const override {
    return google::protobuf::python::GetDefaultDescriptorPool()
        ->py_message_factory->message_factory;
  }
  PyObject* NewMessage(const google::protobuf::Descriptor* descriptor,
                       PyObject* py_message_factory) const override {
    return google::protobuf::python::PyMessage_New(descriptor, py_message_factory);
  }
  PyObject* NewMessageOwnedExternally(
      google::protobuf::Message* msg, PyObject* py_message_factory) const override {
    return google::protobuf::python::PyMessage_NewMessageOwnedExternally(
        msg, py_message_factory);
  }
  PyObject* DescriptorPool_FromPool(
      const google::protobuf::DescriptorPool* pool) const override {
    return google::protobuf::python::PyDescriptorPool_FromPool(pool);
  }
  PyObject* DescriptorPool_FromSharedPool(
      std::shared_ptr<const google::protobuf::DescriptorPool> pool,
      std::shared_ptr<const google::protobuf::DescriptorDatabase> database)
      const override {
    return google::protobuf::python::PyDescriptorPool_FromSharedPool(pool, database);
  }
  PyObject* DescriptorPool_FromPool(
      std::unique_ptr<const google::protobuf::DescriptorPool> pool,
      std::unique_ptr<const google::protobuf::DescriptorDatabase> database)
      const override {
    return google::protobuf::python::PyDescriptorPool_FromPool(std::move(pool),
                                                     std::move(database));
  }
  const google::protobuf::DescriptorPool* DescriptorPool_AsPool(
      PyObject* pool) const override {
    return google::protobuf::python::PyDescriptorPool_AsPool(pool);
  }
};

}  // namespace

static const char module_docstring[] =
    "python-proto2 is a module that can be used to enhance proto2 Python API\n"
    "performance.\n"
    "\n"
    "It provides access to the protocol buffers C++ reflection API that\n"
    "implements the basic protocol buffer functions.";

static PyMethodDef ModuleMethods[] = {
    {"SetAllowOversizeProtos",
     (PyCFunction)google::protobuf::python::cmessage::SetAllowOversizeProtos, METH_O,
     "Enable/disable oversize proto parsing."},
    // DO NOT USE: For migration and testing only.
    {nullptr, nullptr}};

static struct PyModuleDef _module = {PyModuleDef_HEAD_INIT,
                                     "_message",
                                     module_docstring,
                                     -1,
                                     ModuleMethods, /* m_methods */
                                     nullptr,
                                     nullptr,
                                     nullptr,
                                     nullptr};

PyMODINIT_FUNC PyInit__message() {
  PyObject* m;
  m = PyModule_Create(&_module);
  if (m == nullptr) {
    return nullptr;
  }

  if (!google::protobuf::python::InitProto2MessageModule(m)) {
    Py_DECREF(m);
    return nullptr;
  }

  // Adds the C++ API
  if (PyObject* api = PyCapsule_New(
          new ApiImplementation(), google::protobuf::python::PyProtoAPICapsuleName(),
          [](PyObject* o) {
            delete (ApiImplementation*)PyCapsule_GetPointer(
                o, google::protobuf::python::PyProtoAPICapsuleName());
          })) {
    PyModule_AddObject(m, "proto_API", api);
  } else {
    return nullptr;
  }

  return m;
}

#include "google/protobuf/port_undef.inc"
