/**
 * @file itender.ino
 * @brief Official proxy code for iTender GPIO Communication via Serial.
 *
 * This code acts as a communication bridge between an Arduino and a Raspberry Pi
 * using JSON over a serial connection (USB). It allows the Raspberry Pi to
 * control Arduino GPIO pins (set digital/analog output, get digital/analog input)
 * and read sensor data (specifically from multiple HX711 load cells dynamically).
 */

#include <ArduinoJson.h> // Include the ArduinoJson library for JSON parsing and serialization.
#include "HX711.h"     // Include the HX711 library for interacting with the load cell amplifier.

// Define the size of the JSON buffer for incoming messages.
// Adjust this size based on the maximum expected size of incoming JSON messages.
#define JSON_BUFFER_SIZE 256

// Create a static JSON document for incoming messages.
// Static allocation is efficient but has a fixed size.
StaticJsonDocument<JSON_BUFFER_SIZE> incomingJson;

// Create a dynamic JSON document for outgoing messages.
// Dynamic allocation is more flexible but can lead to memory fragmentation on small microcontrollers.
DynamicJsonDocument outgoingJson(JSON_BUFFER_SIZE);

// --- Multiple HX711 Handling ---

// Define the maximum number of HX711 sensors we can manage tare offsets for.
// Adjust this based on available Arduino memory if needed.
#define MAX_SENSORS 5

// Structure to hold the pins and tare offset for a single sensor.
struct SensorTare {
  int dataPin = -1;
  int clockPin = -1;
  long tareOffset = 0;
};

// Array to store the tare offsets for multiple sensors.
SensorTare sensorTareOffsets[MAX_SENSORS];

/**
 * @brief Finds the index of a sensor in the sensorTareOffsets array based on its pins.
 * @param dataPin The data pin of the sensor.
 * @param clockPin The clock pin of the sensor.
 * @return The index of the sensor, or -1 if not found.
 */
int findSensorTareIndex(int dataPin, int clockPin) {
  for (int i = 0; i < MAX_SENSORS; i++) {
    if (sensorTareOffsets[i].dataPin == dataPin && sensorTareOffsets[i].clockPin == clockPin) {
      return i;
    }
  }
  return -1; // Sensor not found
}

/**
 * @brief Adds or updates the tare offset for a sensor based on its pins.
 * @param dataPin The data pin of the sensor.
 * @param clockPin The clock pin of the sensor.
 * @param offset The tare offset to store.
 * @return The index where the offset was stored/updated, or -1 if the array is full.
 */
int addOrUpdateSensorTare(int dataPin, int clockPin, long offset) {
  int index = findSensorTareIndex(dataPin, clockPin);

  if (index != -1) {
    // Sensor found, update the offset
    sensorTareOffsets[index].tareOffset = offset;
    return index;
  } else {
    // Sensor not found, find the first available slot
    for (int i = 0; i < MAX_SENSORS; i++) {
      if (sensorTareOffsets[i].dataPin == -1) { // Found an empty slot
        sensorTareOffsets[i].dataPin = dataPin;
        sensorTareOffsets[i].clockPin = clockPin;
        sensorTareOffsets[i].tareOffset = offset;
        return i;
      }
    }
  }
  return -1; // Array is full
}

/**
 * @brief Gets the stored tare offset for a sensor based on its pins.
 * @param dataPin The data pin of the sensor.
 * @param clockPin The clock pin of the sensor.
 * @return The stored tare offset, or 0 if the sensor is not found.
 */
long getSensorTareOffset(int dataPin, int clockPin) {
  int index = findSensorTareIndex(dataPin, clockPin);
  if (index != -1) {
    return sensorTareOffsets[index].tareOffset;
  }
  return 0; // Return 0 offset if sensor not found (effectively untared)
}

// --- End Multiple HX711 Handling ---


void setup() {
  // Initialize serial communication at 9600 baud rate.
  // This speed should match the serial communication speed on the Raspberry Pi.
  Serial.begin(9600);

  // Initialize the sensorTareOffsets array.
  for (int i = 0; i < MAX_SENSORS; i++) {
    sensorTareOffsets[i].dataPin = -1; // Mark as empty
  }

  // Optional: Wait for the serial port to connect.
  while (!Serial);
}

// Declare a function pointer to address 0.
// Calling this function effectively resets the Arduino.
void (*resetFunc)(void) = 0;

void loop() {
  // Check if there is data available to read from the serial port.
  if (Serial.available()) {
    // Read the incoming JSON message from the serial buffer.
    // deserializeJson reads from the Serial stream until it finds a complete JSON object.
    DeserializationError error = deserializeJson(incomingJson, Serial);

    // Check if deserialization was successful.
    if (error) {
      // Handle deserialization error.
      // You might want to send an error message back to the Raspberry Pi.
      Serial.print(F("Deserialization failed: "));
      Serial.println(error.f_str());
      // Clear the serial buffer to prevent processing incomplete messages.
      while (Serial.available()) Serial.read();
    } else {
      // Deserialization was successful. Process the incoming message.

      // Extract the "id", "type", and "data" fields from the JSON object.
      // The "id" is used to correlate requests and responses.
      String id = incomingJson["id"];
      // The "type" determines the action to perform.
      int type = incomingJson["type"];
      // The "data" field contains parameters for the action.
      JsonVariant data = incomingJson["data"];

      // Create a nested object named "data" within the root outgoing JSON object.
      // This will hold the response data.
      JsonObject outgoingData = outgoingJson.to<JsonObject>().createNestedObject("data");

      // Assume success by default, set to false in case of errors within cases.
      outgoingData["success"] = true;

      // Check if the 'data' field exists and is not null before accessing its members.
      if (!data.isNull()) {
        // Handle the message based on the "type" field using a switch statement.
        switch (type) {
          case 1: // Handle SET_PIN message: Set the state of a GPIO pin.
            { // Use a block to scope variables declared within the case.
              int pin = data["pin"];
              String mode = data["mode"];
              int value = data["value"];

              // Set the pin mode to OUTPUT.
              pinMode(pin, OUTPUT);

              // Check the mode (DIGITAL or ANALOG).
              if (mode == "DIGITAL") {
                // For digital mode, write HIGH or LOW.
                digitalWrite(pin, value); // Assuming value is 0 for LOW, 1 for HIGH.
              } else if (mode == "ANALOG") {
                // For analog mode (PWM), write a value between 0 and 255.
                // Ensure the pin supports PWM output.
                analogWrite(pin, value); // Use the value directly for PWM.
              } else {
                 outgoingData["success"] = false;
                 outgoingData["error"] = "Invalid mode for SET_PIN";
              }
            }
            break;

          case 2: // Handle GET_VAL message: Get the current value of a GPIO pin.
            { // Use a block to scope variables declared within the case.
              int pin = data["pin"];
              String mode = data["mode"];
              int val;

              // Set the pin mode to INPUT.
              pinMode(pin, INPUT);

              // Check the mode (DIGITAL or ANALOG).
              if (mode == "DIGITAL") {
                // For digital mode, read HIGH or LOW.
                val = digitalRead(pin);
              } else if (mode == "ANALOG") {
                // For analog mode, read the analog value (0-1023 for most Arduinos).
                val = analogRead(pin);
              } else {
                 outgoingData["success"] = false;
                 outgoingData["error"] = "Invalid mode for GET_VAL";
                 break; // Exit the case after setting the error.
              }
              // Add the read value to the outgoing JSON data.
              outgoingData["value"] = val;
            }
            break;

          case 3: // Handle GET_SENSOR message: Read data from a sensor (specifically HX711).
            { // Use a block to scope variables declared within the case.
              // Get the data and clock pins for the HX711 from the incoming data.
              const int LOADCELL_DATA_PIN = (int)data["pin_data"];
              const int LOADCELL_CLOCK_PIN = (int)data["pin_clock"];

              // Check if pins are valid.
              if (LOADCELL_DATA_PIN < 0 || LOADCELL_CLOCK_PIN < 0) {
                 outgoingData["success"] = false;
                 outgoingData["error"] = "HX711 data and clock pins not provided or invalid";
                 break;
              }

              // Create a temporary HX711 object for this specific sensor.
              HX711 currentScale;
              currentScale.begin(LOADCELL_DATA_PIN, LOADCELL_CLOCK_PIN);

              // Wait for the HX711 to be ready.
              if (currentScale.wait_ready_timeout(1000)) { // Wait up to 1000ms
                 // Get the raw value from the scale.
                 long raw_value = currentScale.get_value(); // Use get_value() for raw reading.

                 // Get the stored tare offset for these pins.
                 long tare_offset = getSensorTareOffset(LOADCELL_DATA_PIN, LOADCELL_CLOCK_PIN);

                 // Calculate the taried value.
                 long taried_value = raw_value - tare_offset;

                 // Add the taried value to the outgoing JSON data.
                 outgoingData["value"] = taried_value;

                 /*
                 // Example calculation for weight in grams using a scale factor (requires calibration).
                 // You would need to store and retrieve the scale factor per sensor as well.
                 // currentScale.set_scale(calibration_factor);
                 // long weight_grams = currentScale.get_units(10); // Get average of 10 readings with scale factor
                 // outgoingData["grams"] = weight_grams;
                 */

              } else {
                 outgoingData["success"] = false;
                 outgoingData["error"] = "HX711 not ready or timeout";
              }
            } // Closing brace for case 3
            break;

          case 4: // Handle TARE message: Tare a specific HX711 sensor.
            { // Use a block to scope variables declared within the case.
              // Get the data and clock pins for the HX711 from the incoming data.
              const int LOADCELL_DATA_PIN = (int)data["pin_data"];
              const int LOADCELL_CLOCK_PIN = (int)data["pin_clock"];

              // Check if pins are valid.
              if (LOADCELL_DATA_PIN < 0 || LOADCELL_CLOCK_PIN < 0) {
                 outgoingData["success"] = false;
                 outgoingData["error"] = "HX711 data and clock pins not provided or invalid";
                 break;
              }

              // Create a temporary HX711 object for this specific sensor.
              HX711 currentScale;
              currentScale.begin(LOADCELL_DATA_PIN, LOADCELL_CLOCK_PIN);

              // Wait for the HX711 to be ready.
              if (currentScale.wait_ready_timeout(1000)) { // Wait up to 1000ms
                 // Get the current raw value to use as the new tare offset.
                 long current_raw_value = currentScale.get_value();

                 // Store this value as the tare offset for these pins.
                 int index = addOrUpdateSensorTare(LOADCELL_DATA_PIN, LOADCELL_CLOCK_PIN, current_raw_value);

                 if (index != -1) {
                    outgoingData["message"] = "Sensor taried successfully";
                    // Optionally return the new tare offset or taried value (which should be close to 0)
                    // outgoingData["new_tare_offset"] = current_raw_value;
                    // outgoingData["taried_value"] = current_raw_value - current_raw_value; // Should be 0
                 } else {
                    outgoingData["success"] = false;
                    outgoingData["error"] = "Could not store tare offset, sensor array full";
                 }

              } else {
                 outgoingData["success"] = false;
                 outgoingData["error"] = "HX711 not ready or timeout for tare";
              }
            } // Closing brace for case 4
            break;


          case 5: // Handle RESET message: Reset the Arduino.
            outgoingData["message"] = "Resetting Arduino";
            serializeJson(outgoingJson, Serial); // Send response before reset
            Serial.println(); // Send newline
            delay(100); // Small delay to ensure message is sent
            resetFunc();  // Call the reset function.
            break;


          default: // Handle unknown message type.
            outgoingData["success"] = false;
            outgoingData["error"] = "Unknown message type";
            break;
        }
      } else {
         // Handle case where the 'data' field is missing or null.
         outgoingData["success"] = false;
         outgoingData["error"] = "Missing or null 'data' field in incoming JSON";
      }


      // Prepare the root of the outgoing JSON message.
      // Set the 'id' to match the incoming message for correlation.
      outgoingJson["id"] = id;
      // Set the 'type' for the response (e.g., 0 for acknowledgment/response).
      outgoingJson["type"] = 0;
      // The 'data' field was already created as a nested object.

      // Send the outgoing JSON message to the serial port.
      serializeJson(outgoingJson, Serial);
      // Send a newline character to indicate the end of the JSON message.
      Serial.println();

      // Clear the incoming JSON document to free up memory for the next message.
      incomingJson.clear();
      // Clear the outgoing JSON document.
      outgoingJson.clear();
    }
  }
  // Add a small delay to prevent the loop from running too fast when no data is available.
  delay(1);
}
