/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */

#include <pybind11/functional.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl/filesystem.h>
#include <pybind11/stl_bind.h>

// #include "./py-logger.hpp"
#include "sh4lt/follower.hpp"
#include "sh4lt/infotree/json-serializer.hpp"
#include "sh4lt/logger/console.hpp"
#include "sh4lt/monitor/monitor.hpp"
#include "sh4lt/shtype/shtype-from-gst-caps.hpp"
#include "sh4lt/shtype/shtype.hpp"
#include "sh4lt/time.hpp"
#include "sh4lt/utils/any.hpp"
#include "sh4lt/writer.hpp"

namespace py = pybind11;
using namespace sh4lt;

PYBIND11_MAKE_OPAQUE(std::vector<std::string>);

PYBIND11_MODULE(sh4lt, m) {
  py::bind_vector<std::vector<std::string>>(m, "KeyList");
  /* generate comment: figlet -f lean Any | tr ' _/' ' ()'
   *
   *        ()()
   *     ()    ()  ()()()    ()    ()
   *    ()()()()  ()    ()  ()    ()
   *   ()    ()  ()    ()  ()    ()
   *  ()    ()  ()    ()    ()()()
   *                           ()
   *                      ()()
   */
  py::class_<Any>(m, "Any", R"__(
      The Any class provides type erasure with serialization capability and
      categorization of the "erased" type (floating point, integral, boolean, other
      or none).
      )__")
      .def("is_empty", &Any::empty, R"__(
      Checks whether the instance is empty.
      return True if empty.
      )__")
      .def("as_str", &Any::copy_as<std::string>, R"__(
      Get a copy of the internal object, as a string.
      )__")
      .def("as_bool", &Any::copy_as<bool>, R"__(
      Get a copy of the internal object, as a boolean.
      )__")
      .def("as_float", &Any::copy_as<double>, R"__(
      Get a copy of the internal object, as a float.
      )__")
      .def("as_int", &Any::copy_as<int64_t>, R"__(
      Get a copy of the internal object, as a interger.
      )__");
  /*
   *       ()()()  ()      ()()()()()
   *    ()        ()()()      ()      ()    ()  ()()()      ()()
   *     ()()    ()    ()    ()      ()    ()  ()    ()  ()()()()
   *        ()  ()    ()    ()      ()    ()  ()    ()  ()
   * ()()()    ()    ()    ()        ()()()  ()()()      ()()()
   *                                    ()  ()
   *                               ()()    ()
   */
  py::class_<ShType>(m, "ShType", R"__(
      ShType class provides a description of data shared with Sh4lt. It has a
      media specification (a string) and a list composed of key/value elements.
      
      For media specification, mime types compatible with GStreamer are
      highly suggested. Custom data type can use a specification as the following
      "application/x-myformat".
      
      It provides (de)serialization with JSON.
       )__")
      .def(py::init<const std::string&,
                    const std::string&,
                    const std::string&,
                    const std::string&>(),
           py::arg("media"),
           py::arg("label"),
           py::arg("group") = ShType::default_group(),
           py::arg("description") = std::string(),
           R"__(
      Construct a ShType with group label, label and media specification.
 
      param: media The media type.
      param: label The media label.
      param: group_labeg The group label.
       )__")
      .def_static("deserialize", &ShType::deserialize, R"__(
      Obtain a ShType from serialization.
 
      param: serialized A string description (JSON) of a Shtype.
 
      return: The corresponding Shtype.
       )__")
      .def_static("serialize", &ShType::serialize, R"__(
      Obtain a string representation of a ShType.
 
      param: shtype The ShType to serialize.
 
      return: The resulting serialization.
       )__")
      .def_static("default_group", &ShType::default_group, R"__(
      Get default group label
 
      return: The default group label
       )__")
      .def_static("get_path",
                  &ShType::get_path,
                  py::arg("label"),
                  py::arg("group") = ShType::default_group(),
                  R"__(
      Provide the absolute file path for the Sh4lt Unix socket.
 
      param: label The Sh4lt label.
      param: group_label The Sh4lt group label.
 
      return: The absolute file path.
       )__")
      .def_static("get_log_for_path_issue", &ShType::get_log_for_path_issue, R"__(
      Provide the log for issue with the socket path directory.
 
      return: The log message (empty if no issue found). 
       )__")
      .def_static("from_gst_caps",
                  py::overload_cast<const std::string&,
                                    const std::string&,
                                    const std::string&,
                                    const std::string&>(&shtype::shtype_from_gst_caps),
                  R"__(
       Construct a Shtype from a serialized GStreamer Caps.
    
       param:   gst_caps The serialized GStreamer Caps.
       param:   label The Sh4lt label.
       param:   group_label The ShType group.
       param:   description The ShType description.
    
       return:  The ShType created. Validity can be tested from the SafeBoolLog
       inheritence.
       )__")
      .def_static("to_gst_caps", &shtype::shtype_to_gst_caps, R"__(
      Produce a serialized GStreamer caps from a ShType.
      Note the ShType label and group label are not embedded in the GStreamer caps.
   
      param:   shtype the ShType instance to convert.
   
      return:  The string in the GStreamer caps format.
       )__")
      .def("media", &ShType::media, R"__(
      Get the media specification.
 
      return: The media specification.
        )__")
      .def("label", &ShType::label, R"__(
      Get the Sh4lt label.
 
      return: The Sh4lt label.
        )__")
      .def("group", &ShType::group, R"__(
      Get the Sh4lt group.
 
      return: The media group label.
         )__")
      .def("description", &ShType::description, R"__(
      Get the description.
 
      return: The media description.
         )__")
      .def("id", &ShType::id, R"__(
      Get the Sh4lt id.
 
      return: The id.
         )__")
      .def("get_prop", &ShType::get_prop, R"__(
      Get the value for a given key.
 
      param: key The key.
 
      return: The value, or empty Any.
       )__")
      .def("get_custom_type", &ShType::get_custom_type, R"__(
      Get the custom type for a given key.
 
      param: key The key.
 
      return: The value, or empty if no custom type is specified for this key.
       )__")
      .def("get_property_keys", &ShType::get_property_keys, R"__(
      Get the list of keys for the properties.
 
      return: the list of keys.
       )__")
      .def("get_custom_types", &ShType::get_custom_types, R"__(
      Get the custom property types.
 
      return: the custom property types.
        )__")
      .def("set_media", &ShType::set_media, R"__(
      Set the media mime type.
 
      param: media The media mime type.
        )__")
      .def("set_label", &ShType::set_label, R"__(
      Set the media label.
 
      param: media The media label.
        )__")
      .def("set_group", &ShType::set_group, R"__(
      Set the media group label.
 
      param: media The media group label.
        )__")
      .def("set_prop", &ShType::set_prop<const std::string&>, R"__(
      Set a string property.
 
      param: key The property key.
      param: value The property value.
       )__")
      // bool overload MUST be before int overload, or bool will be
      // casted to an integer
      .def("set_prop", &ShType::set_prop<bool>, R"__(
      Set a boolean property.
 
      param: key The property key.
      param: value The property value.
       )__")
      .def("set_prop", &ShType::set_prop<int64_t>, R"__(
      Set an integer property.
 
      param: key The property key.
      param: value The property value.
       )__")
      .def("set_prop", &ShType::set_prop<double>, R"__(
      Set a float property.
 
      param: key The property key.
      param: value The property value.
       )__")
      .def("set_custom_prop", &ShType::set_custom_prop, R"__(
      Set a property with a custom type.
 
      param: key The property key.
      param: custom_type The property custom type name.
      param: value The property value.
       )__")
      .def("set_custom_type", &ShType::set_custom_type, R"__(
      Set the custom type of a property.
 
      param: key The property key.
      param: custom_type The property custom type.
       )__")
      .def("path", &ShType::path, R"__(
      Provide the absolute file path for the Sh4lt Unix socket, using internal labels.
 
      return: The absolute file path, or empty in case of issue with the socket path directory.
              Check get_log_for_path_issue for message.
       )__")
      .def("deserialization_errors", &ShType::msg, R"__(
       )__");
  /*
   *     ()
   *    ()          ()()      ()()()    ()()()    ()()    ()  ()()
   *   ()        ()    ()  ()    ()  ()    ()  ()()()()  ()()
   *  ()        ()    ()  ()    ()  ()    ()  ()        ()
   * ()()()()    ()()      ()()()    ()()()    ()()()  ()
   *                          ()        ()
   *                     ()()      ()()
   */
  py::class_<logger::Console, std::shared_ptr<logger::Console>>(m, "PyLogger", R"__(
      Logger class that prints Sh4lt logs into the console.
      )__")
      .def(py::init<>([]() { return std::make_shared<logger::Console>(); }))
      .def("set_debug", [](std::shared_ptr<logger::Console>& self, bool debug) {
        return self->set_debug(debug);
      });
  /*
   *
   *   ()          ()            ()    ()
   *  ()          ()  ()  ()()      ()()()()    ()()    ()  ()()
   * ()    ()    ()  ()()      ()    ()      ()()()()  ()()
   *  ()  ()  ()    ()        ()    ()      ()        ()
   *   ()  ()      ()        ()      ()()    ()()()  ()
   */
  py::class_<Writer>(m, "Writer", R"__(
      Writer class that push buffer data into a Sh4lt.
      )__")
      .def(py::init([](const ShType& shtype,
                       size_t size,
                       std::shared_ptr<logger::Console> log,
                       const UnixSocketProtocol::ServerSide::onClientConnect& occ,
                       const UnixSocketProtocol::ServerSide::onClientDisconnect& ocd,
                       mode_t mode) {
             return new Writer(
                 shtype,
                 size,
                 log,
                 [occ](int id) {
                   if (!occ) return;
                   py::gil_scoped_acquire gil;
                   occ(id);
                 },
                 [ocd](int id) {
                   if (!ocd) return;
                   py::gil_scoped_acquire gil;
                   ocd(id);
                 },
                 mode);
           }),
           py::arg("shtype"),
           py::arg("size") = 1,
           py::arg("log") = std::make_shared<logger::Console>(false),
           py::arg("on_client_connect") = nullptr,
           py::arg("on_client_disconnect") = nullptr,
           py::arg("unix_permission") = 0600,  // octal since suffixed with '0'
           R"__(
       Construct a Writer object.
  
       param:   shtype                The ShType description for the frames to be transmitted.
       param:   size                  Initial size of the shared memory. Note the shared memory
                                      can be resized at each frame.
       param:   log                   Log object where to write internal logs.
       param:   on_client_connect     Callback to be triggered when a follower connects.
       param:   on_client_disconnect  Callback to be triggered when a follower disconnects.
       param:   unix_permission       Permission to apply to the internal Unix socket, shared memory and semaphore.
       )__")
      .def("num_of_followers",
           &Writer::num_of_followers,
           R"__(
      Get current number of follower connected to the Sh4lt.
        )__")
      .def("alloc_size",
           &Writer::alloc_size,
           R"__(
      Get currently allocated size of the shared memory used by the writer.
        )__")
      .def(
          "wait_readers_for",
          [](Writer& self, float timeout) -> bool {
            return self.wait_readers_for(static_cast<int64_t>(timeout * 1e6));
          },
          py::arg("timeout"),
          R"__(
      Wait for the the readers to finish reading the last buffer.

      param: timeout The maximum time to wait in milliseconds.

      return: True if readers has finished before timeout, False otherwise.
        )__")
      .def(
          "copy_to_shm",
          [](Writer& self, py::buffer& buf, int64_t date, int64_t dur, float speed) -> bool {
            py::gil_scoped_acquire gil;
            auto info = buf.request();
            return self.copy_to_shm(info.ptr, info.size, date, dur, speed);
          },
          py::arg("buf"),
          py::arg("date") = 0,
          py::arg("dur") = 0,
          py::arg("speed") = 1.0,
          R"__(
      Copy a frame of data to the sh4lt.
 
      param: data  Pointer to the begining of the frame.
      param: buffer_date Playhead date for the beginning of the buffer, in nanoseconds, or -1.
      param: buffer_duration Buffer duration, in nanoseconds, or -1.
      param: speed Clock speed for buffer date. 2.0 means twice fast, 0 means offline clock and -1 means reverse.
 
      return: Success of the copy to the shared memory
      )__")
      // support python "with ... as ... :
      .def("__enter__", [](Writer& self) { return py::cast(&self); })
      .def("__exit__", [](Writer&, py::handle, py::handle, py::handle) {});
  /*
   *
   *  ()()()()()  ()                            ()()()                ()()
   *     ()          ()()()  ()()      ()()      ()    ()()()      ()        ()()
   *    ()      ()  ()    ()    ()  ()()()()    ()    ()    ()  ()()()()  ()    ()
   *   ()      ()  ()    ()    ()  ()          ()    ()    ()    ()      ()    ()
   *  ()      ()  ()    ()    ()    ()()()  ()()()  ()    ()    ()        ()()
   */
  py::class_<sh4lt_time_info_t>(m, "TimeInfo")
      .def(py::init<>())
      .def_readwrite("system_clock_date", &sh4lt_time_info_t::system_clock_date, R"__(
      Time in nanoseconds since epoch.
      )__")
      .def_readwrite("steady_clock_date", &sh4lt_time_info_t::steady_clock_date, R"__(
      Time in nanoseconds with a steady clock time since epoch.
      )__")
      .def_readwrite("buffer_number", &sh4lt_time_info_t::buffer_number, R"__(
      Current buffer index, starting from zero.
      )__")
      .def_readwrite("buffer_date", &sh4lt_time_info_t::buffer_date, R"__(
      Playhead date for the beginning of the buffer, in nanoseconds.
      )__")
      .def_readwrite("buffer_duration", &sh4lt_time_info_t::buffer_duration, R"__(
      Buffer duration, in nanoseconds.
      )__")
      .def_readwrite("speed", &sh4lt_time_info_t::speed, R"__(
      Clock speed for buffer date. 2.0 mean twice fast, 0 means offline clock and -1 means reverse.
      )__");
  /*
   *      ()()()()          ()  ()
   *     ()        ()()    ()  ()    ()()    ()      ()      ()    ()()      ()  ()()
   *    ()()()  ()    ()  ()  ()  ()    ()  ()      ()      ()  ()()()()    ()()
   *   ()      ()    ()  ()  ()  ()    ()    ()  ()  ()  ()    ()          ()
   *  ()        ()()    ()  ()    ()()        ()      ()        ()()()    ()
   */
  using on_data_t = std::function<void(py::bytearray & buf, size_t, const sh4lt_time_info_t*)>;
  py::class_<Follower>(m, "Follower", R"__(
      Follower class that receives buffer data from a Sh4lt.
      )__")
      .def(py::init([](const fs::path& sockpath,
                       const on_data_t& on_data,
                       const Reader::onServerConnected& on_con,
                       const Reader::onServerDisconnected& on_disc,
                       std::shared_ptr<logger::Console> log) {
             return new Follower(
                 sockpath,
                 [on_data](void* data, size_t size, const sh4lt_time_info_t* info) {
                   py::gil_scoped_acquire gil;
                   // auto gstate = PyGILState_Ensure();
                   auto bytes = py::bytearray{static_cast<const char*>(data), size};
                   on_data(bytes, size, info);
                   // PyGILState_Release(gstate);
                 },
                 [on_con](const ShType& shtype) {
                   if (!on_con) return;
                   // auto gstate = PyGILState_Ensure();
                   py::gil_scoped_acquire gil;
                   on_con(shtype);
                   // PyGILState_Release(gstate);
                 },
                 [on_disc]() {
                   if (!on_disc) return;
                   // auto gstate = PyGILState_Ensure();
                   py::gil_scoped_acquire gil;
                   on_disc();
                   // PyGILState_Release(gstate);
                 },
                 log);
           }),
           py::arg("path"),
           py::arg("on_data"),
           py::arg("on_server_connected") = nullptr,
           py::arg("on_server_disconnected") = nullptr,
           py::arg("log") = std::make_shared<logger::Console>(),
           R"__(
      Construct a Follower object that read a sh4lt, and handle connection/disconnection
      of the writer. Information and data are provided asynchronously by the Follower.
      though callbacks.
 
      param:  path                    The socket file path of the Sh4lt to follow.
      param:  on_data                 Callback to be triggered when a frame is published.
      param:  on_server_connected     Callback to be triggered when the follower connects with the sh4lt writer.
      param:  on_server_disconnected  Callback to be triggered when the follower disconnects from the sh4lt writer.
      param:  log                     Log object where to write internal logs.
      )__")
      .def("is_connected", &Follower::is_connected, R"__(
      Return True is the Follower is connected to a Writer, False otherwise.
      )__")
      .def("set_monitor_relaunch", &Follower::set_monitor_relaunch, py::arg("relaunch"), R"__(
      Enable/disable monitor relaunch when writer has disconnected. When a follower is created,
      monitor relaunch is enabled.

      param: monotor                  Set to true if monitor must be relaunched, false otherwise.
      )__")
      // support python "with ... as ... :
      .def("__enter__", [](Follower& self) { return py::cast(&self); })
      .def("__exit__", [](Follower&, py::handle, py::handle, py::handle) {});
  /*
   *      ()      ()                      ()    ()
   *     ()()  ()()    ()()    ()()()        ()()()()    ()()    ()  ()()
   *    ()  ()  ()  ()    ()  ()    ()  ()    ()      ()    ()  ()()
   *   ()      ()  ()    ()  ()    ()  ()    ()      ()    ()  ()
   *  ()      ()    ()()    ()    ()  ()      ()()    ()()    ()
   */
  py::class_<monitor::Monitor>(m, "Monitor", R"__(
      Monitor class that discover and provides statistics about Sh4lt writers.
      )__")
      .def(py::init([](std::shared_ptr<logger::Console> log) {
             return new monitor::Monitor(std::move(log));
           }),
           py::arg("log") = std::make_shared<logger::Console>(false))
      .def("update",
           &monitor::Monitor::update,
           R"__(
      Update monitor internal state.
      )__")
      .def(
          "get_stats",
          [&](monitor::Monitor& self) {
            py::module json = py::module::import("json");
            return json.attr("loads")(infotree::json::serialize(self.get_stats()));
          },
          R"__(
      Get the current state of the statistics.
      Note that bytes-per-seconds and frame-per-seconds are updated only when the
      2 seconds duration has passed since last update.
     
      Here follows, as an exemple, a returned information tree, in the JSON format:
      {
         'connected': True,
         'shtype': {
              'group': 'Default',
              'label': 'video',
              'media': 'video/x-raw',
              'property': {
                    'format': 'ABGR',
                    'framerate': '30/1',
                    'height': 1080,
                    'width': 1920
               }
          },
          'stats': {
               'Mbps': 1987.07275390625,
               'buffer_date': 24981933333333,
               'buffer_number': 749458,
               'speed': 1.0,
               'data': '29 2D 31 35 3A 3F 44 49 4F 55 ...',
               'dur_ms': 33.33333206176758,
               'fps': 29.945999145507812,
               'reup': 0,
               'size_kB': 8294.400390625,
               'system_clock_date': 1722391410082696450,
               'timecode': '06:56:21.933',
               'total_size': 3035750400
           }
        }
     
      where :
         - connected is true if the writer is connected
         - shtype is the writer shtype as provided by the ShType::as_info_tree() method
         - buffer_date, buffer_number, dur_ms, size_KB, system_clock_date, speed and timecode are
           information related to the last received buffer before the call to stats.
         - data is the first value of the first buffer received after the last call to stats
         - Mbps (megabit-per-second) and fps (frame-per-second) profide throughput in bits and frame
         - total_size is the total size in bytes received since the creation of the follower stat
         - reup is the number of times the writer did re-appear
     
      Return The dictionary with the statistics.
      )__")
      .def(
          "get_data_printer_configuration",
          [&](monitor::Monitor& self, const fs::path& path) {
            py::module json = py::module::import("json");
            return json.attr("loads")(
                infotree::json::serialize(self.get_data_printer_configuration(path)));
          },
          R"__(
      Get configuration for buffer value printing format.

      Here is a configuration example : 
      {
        "format" : "x",
        "separator" : " ",
        "precision" : 3,
        "min_width" : 0,
        "num_vals" : 10
      }

      Where
      * format can be one of the following:
          'x'   for hexadecimal (default)
          'c'   for char
          'f'   for 32 bits float
          'd'   for 64 bits float
          'i8'  for 8 bits integer
          'i16' for 16 bits integer
          'i32' for 32 bits integer
          'i64' for 64 bits integer
          'ui8'  for unsigned 8 bits integer
          'ui16' for unsigned 16 bits integer
          'ui32' for unsigned 32 bits integer
          'ui64' for unsigned 64 bits integer
      * separator is the separator between values during print.
      * precision is the precision for float and int (between 1 and 16).
      * min_width is the minimum width of the printed value. 0 is disabling min width. Width does not
        apply for hexadecimal and char formats.
      * num_vals is the number of value to print. 0 for all values.
      )__")
      .def("set_data_printer_configuration",
           &monitor::Monitor::set_data_printer_configuration,
           R"__(
      Set configuration for buffer value printing format.

      param: path path of the Sh4lt to configure
      param: value_format printing format for values in buffer.
      param: separator separator between values during print.
      param: precision precision for float and int (between 1 and 16).
      param: min_width minimum width of the printed value. 0 is disabling min width. Width does not
             apply for hexadecimal and char formats.

      return: True if path was found, False otherwise.
      )__")
      .def("set_num_value_to_print",
           &monitor::Monitor::set_num_value_to_print,
           R"__(
      Set number of value to be printed for a given Sh4lt.
    
      param: path path of the Sh4lt to configure
      param: num_val The number of value to be printed. 0 for all values.
       
      return: True if path was found, False otherwise.
      )__");
}

