// An extension module to test proto_api.h.

#include <memory>
#include <stdexcept>
#include <string>
#include <utility>

#include "google/protobuf/descriptor.pb.h"
#include "google/protobuf/descriptor.h"
#include "google/protobuf/descriptor_database.h"
#include "google/protobuf/dynamic_message.h"
#include "google/protobuf/message.h"
#include "google/protobuf/message_lite.h"
#include "google/protobuf/text_format.h"
#include "google/protobuf/unittest.pb.h"
#include "google/protobuf/proto_api.h"
#include "third_party/pybind11/include/pybind11/eval.h"
#include "third_party/pybind11/include/pybind11/pybind11.h"
#include "third_party/pybind11/include/pybind11/stl.h"

namespace google {
namespace protobuf {
namespace python {

namespace py = pybind11;
using ::google_protobuf_unittest::TestAllTypes;

const PyProto_API* GetProtoApi() {
  py::module_::import("google.protobuf.pyext._message");
  const PyProto_API* py_proto_api = static_cast<const PyProto_API*>(
      PyCapsule_Import(PyProtoAPICapsuleName(), 0));
  if (!py_proto_api) {
    throw py::error_already_set();
  }
  return py_proto_api;
}

// Test for GetConstMessagePointer
auto GetConstMessage(py::handle py_msg) {
  const PyProto_API* api = GetProtoApi();
  auto msg_ptr = api->GetConstMessagePointer(py_msg.ptr());
  if (!msg_ptr.ok()) {
    throw std::runtime_error(msg_ptr.status().ToString());
  }
  const auto* msg = DynamicCastMessage<TestAllTypes>(&msg_ptr->get());
  if (!msg) {
    throw std::runtime_error("Invalid message type");
  }
  return py::make_tuple(msg->optional_int32(), msg->optional_string());
}

// Test for GetClearedMessageMutator
auto SetMessageFieldWithMutator(py::handle py_msg, int value) {
  const PyProto_API* api = GetProtoApi();
  auto status_or_mutator = api->GetClearedMessageMutator(py_msg.ptr());
  if (!status_or_mutator.ok()) {
    throw std::runtime_error(status_or_mutator.status().ToString());
  }
  TestAllTypes* msg = DownCastMessage<TestAllTypes>(status_or_mutator->get());
  msg->set_optional_int32(value);
  // On destruction, the mutator will copy content back to python message.
}

// Test for DescriptorPool_FromPool and NewMessageOwnedExternally
auto ReprDynamicMessage(int value) {
  const PyProto_API* api = GetProtoApi();

  // Create a descriptor pool which copies everything from the linked protos.
  DescriptorPool pool(DescriptorPool::internal_generated_database());
  // FileDescriptorProto file_descriptor;
  // TestAllTypes::descriptor()->file()->CopyTo(&file_descriptor);
  // if (!pool.BuildFile(file_descriptor)) {
  //   throw std::runtime_error("Failed to build file descriptor");
  // }
  const Descriptor* descriptor =
      pool.FindMessageTypeByName("proto2_unittest.TestAllTypes");
  if (!descriptor) {
    throw std::runtime_error("Failed to find file descriptor");
  }
  DynamicMessageFactory factory(&pool);
  const Message* prototype = factory.GetPrototype(descriptor);
  if (!prototype) {
    throw std::runtime_error("Failed to get prototype for descriptor");
  }
  std::unique_ptr<Message> msg(prototype->New());
  if (!msg) {
    throw std::runtime_error("Failed to create message");
  }
  msg->GetReflection()->SetInt32(
      msg.get(), descriptor->FindFieldByName("optional_int32"), value);

  // These calls to NewMessage fail because the descriptor pool is not
  // known to Python yet.
  {
    auto py_msg =
        py::reinterpret_steal<py::object>(api->NewMessage(descriptor, nullptr));
    if (py_msg) {
      throw std::runtime_error("NewMessage succeeded unexpectedly");
    }
    py_msg = py::reinterpret_steal<py::object>(
        api->NewMessageOwnedExternally(msg.get(), nullptr));
    if (py_msg) {
      throw std::runtime_error("NewMessage succeeded unexpectedly");
    }
  }

  // Create the Python DescriptorPool...
  auto py_pool =
      py::reinterpret_steal<py::object>(api->DescriptorPool_FromPool(&pool));
  if (!py_pool) {
    throw py::error_already_set();
  }

  // ... And now the API Can use it to create the messages.
  std::string result_string;
  {
    auto py_msg =
        py::reinterpret_steal<py::object>(api->NewMessage(descriptor, nullptr));
    if (!py_msg) {
      throw py::error_already_set();
    }

    py_msg = py::reinterpret_steal<py::object>(
        api->NewMessageOwnedExternally(msg.get(), nullptr));
    if (!py_msg) {
      throw py::error_already_set();
    }
    result_string = py::repr(py_msg);
  }

  // The code above is dangerous! It relies on the C++ DescriptorPool being
  // alive for whole duration of the test.
  // At this point, there are no external references to the Python Message
  // classes, but they always form a reference cycle with their Python
  // MessageFactory.
  // So it is necessary to run the garbage collector.
  py::exec("import gc; gc.collect()");
  // Now the Python MessageFactory has been deleted, and it is safe to destroy
  // the C++ DescriptorPool.

  return result_string;
}

auto ReprDynamicMessageSharedPool(int value) {
  const PyProto_API* api = GetProtoApi();

  auto pool = std::make_shared<DescriptorPool>(
      DescriptorPool::internal_generated_database());

  // Create the Python DescriptorPool using shared_ptr...
  auto py_pool = py::reinterpret_steal<py::object>(
      api->DescriptorPool_FromSharedPool(pool, nullptr));
  if (!py_pool) {
    throw py::error_already_set();
  }

  std::string result_string;
  {
    DynamicMessageFactory factory(pool.get());
    const Descriptor* descriptor =
        pool->FindMessageTypeByName("proto2_unittest.TestAllTypes");
    if (!descriptor) {
      throw std::runtime_error("Failed to find file descriptor");
    }
    const Message* prototype = factory.GetPrototype(descriptor);
    if (!prototype) {
      throw std::runtime_error("Failed to get prototype for descriptor");
    }
    std::unique_ptr<Message> msg(prototype->New());
    if (!msg) {
      throw std::runtime_error("Failed to create message");
    }
    msg->GetReflection()->SetInt32(
        msg.get(), descriptor->FindFieldByName("optional_int32"), value);

    auto py_msg = py::reinterpret_steal<py::object>(
        api->NewMessageOwnedExternally(msg.get(), nullptr));
    if (!py_msg) {
      throw py::error_already_set();
    }
    result_string = py::repr(py_msg);
  }  // msg, py_msg, and factory are safely destroyed here before pool.

  // Testing co-ownership: When C++ drops its handle, the
  // pool stays alive because Python still owns it.
  pool.reset();

  // Omit manual gc.collect() here and return naturally.
  //
  // When PyMessageFactory creates dynamic message classes (e.g.
  // CustomMessageClass), Python establishes two interlocking reference cycles
  // on the CPython heap:
  // (PyDescriptorPool <-> PyMessageFactory <-> CustomMessageClass).
  //
  // Even after local C++ stack objects (factory) exit
  // scope above, these Python objects sit in an unreferenced cyclic island. If
  // manual gc.collect() runs mid-flight, CPython runs gc_collect_main() to
  // break cycles via tp_clear in non-deterministic heap order.
  //
  // Natural return allows Python to tear down wrappers cleanly
  // during finalization.
  return result_string;
}

auto ReprDynamicMessageSharedPoolAndDb(int value) {
  const PyProto_API* api = GetProtoApi();

  // Create custom DB and Pool held by shared_ptr
  auto db = std::make_shared<SimpleDescriptorDatabase>();
  FileDescriptorProto file_proto;
  file_proto.set_name("custom_unittest.proto");
  file_proto.set_package("custom_unittest");
  DescriptorProto* msg_proto = file_proto.add_message_type();
  msg_proto->set_name("CustomMessage");
  FieldDescriptorProto* field_proto = msg_proto->add_field();
  field_proto->set_name("val");
  field_proto->set_number(1);
  field_proto->set_type(FieldDescriptorProto::TYPE_INT32);
  field_proto->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
  db->Add(file_proto);

  auto pool = std::make_shared<DescriptorPool>(db.get());

  auto py_pool = py::reinterpret_steal<py::object>(
      api->DescriptorPool_FromSharedPool(pool, db));
  if (!py_pool) {
    throw py::error_already_set();
  }

  std::string result_string;
  {
    DynamicMessageFactory factory(pool.get());
    const Descriptor* descriptor =
        pool->FindMessageTypeByName("custom_unittest.CustomMessage");
    const Message* prototype = factory.GetPrototype(descriptor);
    std::unique_ptr<Message> msg(prototype->New());
    msg->GetReflection()->SetInt32(msg.get(),
                                   descriptor->FindFieldByName("val"), value);

    auto py_msg = py::reinterpret_steal<py::object>(
        api->NewMessageOwnedExternally(msg.get(), nullptr));
    result_string = py::repr(py_msg);
  }  // msg, py_msg, and factory are safely destroyed here before pool.

  // Testing co-ownership: When C++ drops its handle, the
  // pool stays alive because Python still owns it.
  pool.reset();
  db.reset();

  return result_string;
}

py::object CreateDynamicPoolMessage() {
  FileDescriptorProto file_descriptor;
  file_descriptor.set_name("test_file");
  file_descriptor.set_package("test_package");
  DescriptorProto* message_descriptor = file_descriptor.add_message_type();
  message_descriptor->set_name("MyMessage");
  FieldDescriptorProto* field_descriptor = message_descriptor->add_field();
  field_descriptor->set_name("my_field");
  field_descriptor->set_number(1);
  field_descriptor->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
  field_descriptor->set_type(FieldDescriptorProto::TYPE_INT32);
  auto owned_pool = std::make_unique<DescriptorPool>();
  if (!owned_pool->BuildFile(file_descriptor)) {
    throw std::runtime_error("Failed to build file descriptor");
  }

  // Create a Python DescriptorPool from the C++ one.
  const PyProto_API* api = GetProtoApi();
  auto py_pool = py::reinterpret_steal<py::object>(
      api->DescriptorPool_FromPool(std::move(owned_pool), nullptr));
  if (!py_pool) {
    throw py::error_already_set();
  }

  const DescriptorPool* pool = api->DescriptorPool_AsPool(py_pool.ptr());
  if (!pool) {
    throw py::error_already_set();
  }

  // Navigate through the C++ Descriptors, and create a Python message.
  const Descriptor* descriptor =
      pool->FindMessageTypeByName("test_package.MyMessage");
  if (!descriptor) {
    throw std::runtime_error("Failed to find file descriptor");
  }
  auto py_msg =
      py::reinterpret_steal<py::object>(api->NewMessage(descriptor, nullptr));
  if (!py_msg) {
    throw py::error_already_set();
  }
  Message* msg = api->GetMutableMessagePointer(py_msg.ptr());
  if (!msg) {
    throw py::error_already_set();
  }

  // Populate the message, and return it.
  if (!google::protobuf::TextFormat::ParseFromString("my_field: 42", msg)) {
    throw std::runtime_error("Failed to parse message");
  }
  // This is safe: the Python object keeps a reference to the Python
  // DescriptorPool, which owns the C++ DescriptorPool.
  return py_msg;
}

PYBIND11_MODULE(proto_api_test_ext, m) {
  m.def("get_const_message", &GetConstMessage);
  m.def("set_message_field_with_mutator", &SetMessageFieldWithMutator);
  m.def("repr_dynamic_message", &ReprDynamicMessage);
  m.def("repr_dynamic_message_shared_pool", &ReprDynamicMessageSharedPool);
  m.def("repr_dynamic_message_shared_pool_and_db",
        &ReprDynamicMessageSharedPoolAndDb);
  m.def("create_dynamic_pool_message", &CreateDynamicPoolMessage);
}

}  // namespace python
}  // namespace protobuf
}  // namespace google
