/*
  ____                                   _             _
 / ___|  ___  _   _ _ __ ___ ___   _ __ | |_   _  __ _(_)_ __
 \___ \ / _ \| | | | '__/ __/ _ \ | '_ \| | | | |/ _` | | '_ \
  ___) | (_) | |_| | | | (_|  __/ | |_) | | |_| | (_| | | | | |
 |____/ \___/ \__,_|_|  \___\___| | .__/|_|\__,_|\__, |_|_| |_|
                                  |_|            |___/
# A Template for SerialPlugin, a Source Plugin
# Generated by the command: plugin --type source --dir . serial
# Hostname: Fram-IV-3.local
# Current working directory: /Users/p4010/Develop/mads_doc/ai_example
# Creation date: 2026-04-10T17:08:04.988+0200
# NOTICE: MADS Version 2.0.4
*/
// Mandatory included headers
#include <source.hpp>
#include <nlohmann/json.hpp>
#include <pugg/Kernel.h>
#include <serialport.hpp>
#include "test_helpers.hpp"

#include <cctype>
#include <deque>
#include <iostream>
#include <limits>
#include <memory>
#include <random>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

// Define the name of the plugin
#ifndef PLUGIN_NAME
#define PLUGIN_NAME "serial"
#endif

using namespace std;
using json = nlohmann::json;

/*!
 * @brief Source plugin that decodes framed scalar samples from a serial stream.
 */
class SerialPlugin : public Source<json> {
public:
  /*!
   * @brief Inherit the base source constructors.
   */
  using Source::Source;

  /*!
   * @brief Return the plugin kind used by the MADS plugin loader.
   * @return The plugin name.
   */
  string kind() override { return PLUGIN_NAME; }

  /*!
   * @brief Fetch the next decoded frame from the serial stream.
   * @param[out] out JSON object receiving the next valid payload.
   * @param[in,out] blob Unused binary payload pointer required by the API.
   * @return `return_type::success` when one frame is published, `retry` when no
   * complete frame is available yet, or `error` on operational failures.
   */
  return_type get_output(json &out,
                         vector<unsigned char> *blob = nullptr) override {
    (void)blob;
    out.clear();
    if (_has_setup_error) {
      return return_type::error;
    }

    if (_pending_frames.empty()) {
      const return_type read_status = read_next_chunk();
      if (read_status != return_type::success &&
          read_status != return_type::retry) {
        return read_status;
      }
    }

    if (_pending_frames.empty()) {
      return return_type::retry;
    }

    out["data"] = _pending_frames.front();
    _pending_frames.pop_front();
    if (!_agent_id.empty()) {
      out["agent_id"] = _agent_id;
    }
    return return_type::success;
  }

  /*!
   * @brief Configure the serial source parameters and open the serial port when
   * required.
   * @param params JSON configuration containing `address` and `baud_rate`.
   */
  void set_params(const json &params) override {
    Source::set_params(params);
    _params = json::object();
    _params["address"] = "";
    _params["baud_rate"] = 115200;
    _params.merge_patch(params);

    _raw_buffer.clear();
    _pending_frames.clear();
    _test_chunks.clear();
    _random_chunks.clear();
    _random_chunk_index = 0;
    _use_test_chunks = false;
    _serial_port.reset();
    _has_setup_error = false;
    _error = "No error";

    if (_params["address"].get<string>().empty()) {
      return;
    }

    try {
      _serial_port = make_unique<SerialPort>(
          _params["address"].get<string>().c_str(),
          _params["baud_rate"].get<unsigned>());
    } catch (const exception &e) {
      _error = e.what();
      _has_setup_error = true;
    }
  }

  /*!
   * @brief Report the current serial configuration.
   * @return Map containing the configured address and baud rate.
   */
  map<string, string> info() override {
    return {
        {"address", _params.value("address", "")},
        {"baud_rate", to_string(_params.value("baud_rate", 115200u))},
    };
  }

  /*!
   * @brief Queue a deterministic raw input chunk for unit testing.
   * @param chunk Raw bytes to append to the parser input stream.
   */
  void push_test_chunk(const string &chunk) {
    _use_test_chunks = true;
    _test_chunks.push_back(chunk);
  }

private:
  vector<string> initialize_random_chunks() {
    constexpr size_t k_chunk_count = 20;
    constexpr unsigned int k_min_values = 1;
    constexpr unsigned int k_max_values = 10;
    constexpr unsigned int k_min_chunk_size = 1;

    mt19937 generator(random_device{}());
    uniform_int_distribution<unsigned int> value_count_dist(k_min_values,
                                                            k_max_values);
    uniform_int_distribution<unsigned int> value_dist(0, 2047);

    vector<string> frames;
    frames.reserve(k_chunk_count);
    for (size_t frame_index = 0; frame_index < k_chunk_count; ++frame_index) {
      ostringstream frame;
      frame << '^';
      const unsigned int value_count = value_count_dist(generator);
      for (unsigned int value_index = 0; value_index < value_count;
           ++value_index) {
        if (value_index > 0) {
          frame << ',';
        }
        frame << value_dist(generator);
      }
      frame << '$';
      frames.push_back(frame.str());
    }

    string stream;
    for (const auto &frame : frames) {
      stream += frame;
    }

    const string &first_frame = frames.front();
    const size_t first_frame_length = first_frame.size();
    const size_t split_position =
        first_frame_length > 2 ? first_frame_length / 2 : 1;
    const string rotated_stream =
        stream.substr(split_position) + stream.substr(0, split_position);

    vector<string> chunks;
    chunks.reserve(k_chunk_count);

    const size_t total_size = rotated_stream.size();
    const size_t base_size = total_size / k_chunk_count;
    const size_t remainder = total_size % k_chunk_count;
    size_t offset = 0;

    for (size_t chunk_index = 0; chunk_index < k_chunk_count; ++chunk_index) {
      size_t chunk_size = base_size + (chunk_index < remainder ? 1u : 0u);
      if (chunk_size < k_min_chunk_size) {
        chunk_size = k_min_chunk_size;
      }
      if (offset + chunk_size > total_size) {
        chunk_size = total_size - offset;
      }
      chunks.push_back(rotated_stream.substr(offset, chunk_size));
      offset += chunk_size;
    }

    if (offset < total_size) {
      chunks.back() += rotated_stream.substr(offset);
    }

    return chunks;
  }

  static bool parse_frame(const string &candidate,
                          vector<unsigned int> &values) {
    values.clear();
    if (candidate.size() < 3 || candidate.front() != '^' ||
        candidate.back() != '$') {
      return false;
    }

    const string payload = candidate.substr(1, candidate.size() - 2);
    if (payload.empty()) {
      return false;
    }

    size_t start = 0;
    while (start < payload.size()) {
      const size_t comma = payload.find(',', start);
      const size_t end = comma == string::npos ? payload.size() : comma;
      const string token = payload.substr(start, end - start);
      if (token.empty()) {
        return false;
      }
      for (char ch : token) {
        if (!isdigit(static_cast<unsigned char>(ch))) {
          return false;
        }
      }
      unsigned long parsed = 0;
      try {
        parsed = stoul(token);
      } catch (const exception &) {
        return false;
      }
      if (parsed > 2047ul) {
        return false;
      }
      values.push_back(static_cast<unsigned int>(parsed));
      if (comma == string::npos) {
        break;
      }
      start = comma + 1;
    }

    return !values.empty();
  }

  void extract_frames() {
    vector<unsigned int> values;
    while (true) {
      const size_t start = _raw_buffer.find('^');
      if (start == string::npos) {
        _raw_buffer.clear();
        return;
      }
      if (start > 0) {
        _raw_buffer.erase(0, start);
      }

      const size_t end = _raw_buffer.find('$');
      if (end == string::npos) {
        return;
      }

      const string candidate = _raw_buffer.substr(0, end + 1);
      _raw_buffer.erase(0, end + 1);

      if (parse_frame(candidate, values)) {
        _pending_frames.emplace_back(values.begin(), values.end());
      }
    }
  }

  return_type read_next_chunk() {
    if (_params.value("address", "").empty()) {
      if (!_test_chunks.empty()) {
        _raw_buffer += _test_chunks.front();
        _test_chunks.pop_front();
        extract_frames();
        return _pending_frames.empty() ? return_type::retry
                                       : return_type::success;
      }

      if (_use_test_chunks) {
        return return_type::retry;
      }

      if (_random_chunks.empty()) {
        _random_chunks = initialize_random_chunks();
        _random_chunk_index = 0;
      }

      _raw_buffer += _random_chunks[_random_chunk_index];
      _random_chunk_index =
          (_random_chunk_index + 1) % _random_chunks.size();
      extract_frames();
      return _pending_frames.empty() ? return_type::retry
                                     : return_type::success;
    }

    if (!_serial_port) {
      _error = "Serial port is not initialized";
      return return_type::error;
    }

    char buffer[256] = {};
    const int bytes_read = _serial_port->read(buffer, sizeof(buffer));
    if (bytes_read < 0) {
      _error = "Serial read failed";
      return return_type::error;
    }
    if (bytes_read == 0) {
      return return_type::retry;
    }

    _raw_buffer.append(buffer, static_cast<size_t>(bytes_read));
    extract_frames();
    return _pending_frames.empty() ? return_type::retry : return_type::success;
  }

  json _params;
  unique_ptr<SerialPort> _serial_port;
  deque<vector<unsigned int>> _pending_frames;
  deque<string> _test_chunks;
  vector<string> _random_chunks;
  string _raw_buffer;
  size_t _random_chunk_index = 0;
  bool _has_setup_error = false;
  bool _use_test_chunks = false;
};

INSTALL_SOURCE_DRIVER(SerialPlugin, json)

int main(int argc, char const *argv[]) {
  (void)argc;
  (void)argv;

  const bool ok = test_helpers::run_serial_tests<SerialPlugin>();

  if (!ok) {
    return 1;
  }

  cout << "serial tests passed" << endl;
  return 0;
}
