#include <main.hpp>

/***********************************************
 *            Begin Configuration              *
 ***********************************************/

// Analog Card & Current Transformer Configuration
#ifdef ANALOG_ENABLE
bool analogCardAvailable = false;
AnalogCard analogCard = AnalogCard();
float voltage = CT_RMS_VOLTAGE;
#define CT_INTERVAL 5000
CurrentTransformerCard ct1 = CurrentTransformerCard(&analogCard, 0, &voltage, &adc2current, CT_INTERVAL);
CurrentTransformerCard ct2 = CurrentTransformerCard(&analogCard, 1, &voltage, &adc2current, CT_INTERVAL);
CurrentTransformerCard ct3 = CurrentTransformerCard(&analogCard, 2, &voltage, &adc2current, CT_INTERVAL);
CurrentTransformerCard ct4 = CurrentTransformerCard(&analogCard, 3, &voltage, &adc2current, CT_INTERVAL);
CurrentTransformerCard ct5 = CurrentTransformerCard(&analogCard, 6, &voltage, &adc2current, CT_INTERVAL);
CurrentTransformerCard ct6 = CurrentTransformerCard(&analogCard, 7, &voltage, &adc2current, CT_INTERVAL);
float adc2current(uint16_t adc_val)
{
  // float voltage = adc_val * 0.0007-0.1994;
  float adc_voltage = adc_val * 0.0007;
  // 0-10V Output with 30A Current Rating CT
  float ct_current = adc_voltage / 10.0 * 30.0;
  return ct_current;
}

#endif


// Remote Variables
RemoteVariable pm25_in = RemoteVariable();
RemoteVariable pm25_out = RemoteVariable();
RemoteVariable temp_out = RemoteVariable();
RemoteVariable weather = RemoteVariable();
RemoteVariable pm_switch = RemoteVariable();
RemoteVariable pm_fan_speed = RemoteVariable();
RemoteVariable pm_lock = RemoteVariable();
RemoteVariable ac_lock = RemoteVariable();



// Light Configuration
uint8_t row = 4;
uint8_t column = 2;
const uint8_t light_array[4][2] = {
    {LIGHT_ROW1_COLUMN1, LIGHT_ROW1_COLUMN2},
    {LIGHT_ROW2_COLUMN1, LIGHT_ROW2_COLUMN2},
    {LIGHT_ROW3_COLUMN1, LIGHT_ROW3_COLUMN2},
    {LIGHT_ROW4_COLUMN1, LIGHT_ROW4_COLUMN2}};

// Air Conditioner Configuration
const char *mode_names_daikin[] = {"off", "fan_only", "cool", "dry"};
const char *mode_names_york[] = {"off", "fan_only", "cool"};
const char *fan_speed_names[] = {"auto", "high", "medium", "low"};

AirConditioner ac_daikin = {
    .max_temperature = DAIKIN_MAX_TEMP,
    .min_temperature = DAIKIN_MIN_TEMP,
    .modes = 4,
    .mode_names = mode_names_daikin,
    .fan_speeds = 4,
    .fan_speed_names = fan_speed_names,
    .getInfraredCode = &getInfraredCode_daikin};

AirConditioner ac_york = {
    .max_temperature = YORK_MAX_TEMP,
    .min_temperature = YORK_MIN_TEMP,
    .modes = 3,
    .mode_names = mode_names_york,
    .fan_speeds = 4,
    .fan_speed_names = fan_speed_names,
    .getInfraredCode = &getInfraredCode_york};
/***********************************************
 *            End Configuration                *
 ***********************************************/

ESPMegaPRO espmega = ESPMegaPRO();
ISEDisplay iseDisplay = ISEDisplay(&iseDisplayAdapter, &light_array[0][0], row, column);

ESPMegaDisplayOTA iseDisplayOTA = ESPMegaDisplayOTA();
ESPMegaDisplayOTA internalDisplayOTA = ESPMegaDisplayOTA();

ClimateCard climateCard_daikin = ClimateCard(AIR_CONDITIONER_DAIKIN_IR_PIN, ac_daikin,
                                             AIR_CONDITIONER_SENSOR_TYPE, AIR_CONDITIONER_SENSOR_PIN,
                                             AIR_CONDITIONER_RMT_CHANNEL0);

ClimateCard climateCard_york = ClimateCard(AIR_CONDITIONER_YORK_IR_PIN, ac_york,
                                           AC_SENSOR_TYPE_NONE, 0,
                                           AIR_CONDITIONER_RMT_CHANNEL1);

void handleMqttMessage(char *topic, char *payload)
{
  // Unused for now
}
void subscribeToIoTCallbacks()
{
  espmega.updateTimeFromNTP();
}

void setup()
{
  // ------------ GPIO 2 Factory Reset Check ------------
  gpio_config_t gpio_2_conf;
  gpio_2_conf.intr_type = GPIO_INTR_DISABLE;
  gpio_2_conf.mode = GPIO_MODE_INPUT;
  gpio_2_conf.pull_up_en = GPIO_PULLUP_ENABLE;
  gpio_config(&gpio_2_conf);
  // If GPIO 2 is pulled low, clear the FRAM then reboot (Reset the device to factory defaults)
  bool clear_fram = !gpio_get_level(GPIO_NUM_2);
  // ------------ End GPIO 2 Factory Reset Check ------------

  
  // ------------ Display Pre Initialization Routine ------------
  Serial.begin(115200);
  iseDisplayAdapter.begin(ISE_DISPLAY_BAUD_RATE, SERIAL_8N1, ISE_DISPLAY_RX_PIN, ISE_DISPLAY_TX_PIN);
  sendExtStopBytes();
  iseDisplayAdapter.print("rest");
  sendExtStopBytes();
  sendStopBytes();
  Serial.print("rest");
  sendStopBytes();
  Serial.print("boot_state.txt=\"Core Initializing . . .\"");
  sendStopBytes();
  // ------------ End Display Pre Initialization Routine ------------
  

  // Give flow of control to OS and scheduler
  espmega.begin();

  
  // // ------------ Factory Reset Routine ------------
  // Disable factory reset for now
  espmega.inputs.loop();

  // set debounce time to 200 for pin 0-7
  for (uint16_t i = 0; i < 8; i++)
  {
    espmega.inputs.setDebounceTime(i, 200);
  }
  
  // if (clear_fram)
  // {
  //   Serial.print("boot_state.txt=\"Factory Resetting . . .\"");
  //   sendStopBytes();
  //   for (uint16_t i = 0; i < 32768; i++)
  //   {
  //     espmega.fram.write8(i, 0);
  //   }
  //   esp_restart();
  // }
  // // ------------ End Factory Reset Routine ------------


  // ------------ IoT Module Initialization Routine ------------
  Serial.print("boot_state.txt=\"IoT Initializing . . .\"");
  sendStopBytes();
  espmega.enableIotModule();
  ETH.begin();
  espmega.iot->bindEthernetInterface(&ETH);
  Serial.print("boot_state.txt=\"Network Initializing . . .\"");
  sendStopBytes();
  espmega.iot->loadNetworkConfig();
  espmega.iot->connectNetwork();
  Serial.print("boot_state.txt=\"MQTT Initializing . . .\"");
  sendStopBytes();
  espmega.iot->loadMqttConfig();
  espmega.iot->registerSubscribeCallback(&subscribeToIoTCallbacks);
  espmega.iot->connectToMqtt();
  Serial.print("boot_state.txt=\"Display Initializing . . .\"");
  espmega.enableWebServer(80);
  // ------------ End IoT Module Initialization Routine ------------

  // ------------ Display Post Initialization Routine ------------
  espmega.enableInternalDisplay(&Serial);
  // ------------ End Display Post Initialization Routine ------------

  // ------------ Inputs and Outputs Initialization Routine ------------
  espmega.inputs.registerCallback(on_pin_change);
  espmega.outputs.setAutoSaveToFRAM(true);
  // Set value of pin 0-12 to 4095
  for (uint8_t i = 0; i < 13; i++)
  {
    espmega.outputs.setValue(i, 4095);
  }
  // ------------ End Inputs and Outputs Initialization Routine ------------
  espmega.outputs.setState(12, true);
  espmega.outputs.setValue(12, 4095);
  espmega.installCard(2, &climateCard_daikin);
  climateCard_daikin.bindFRAM(&espmega.fram, 5000);
  climateCard_daikin.loadStateFromFRAM();
  climateCard_daikin.setFRAMAutoSave(true);
  espmega.display->bindClimateCard(&climateCard_daikin);

// Current Transformers
#ifdef ANALOG_ENABLE
  espmega.installCard(4, &analogCard);
  espmega.installCard(5, &ct1);
  ct1.bindFRAM(&espmega.fram, 6000);
  espmega.iot->registerCard(5);
  espmega.installCard(6, &ct2);
  ct2.bindFRAM(&espmega.fram, 6100);
  espmega.iot->registerCard(6);
  espmega.installCard(7, &ct3);
  ct3.bindFRAM(&espmega.fram, 6200);
  espmega.iot->registerCard(7);
  espmega.installCard(8, &ct4);
  ct4.bindFRAM(&espmega.fram, 6300);
  espmega.iot->registerCard(8);
  espmega.installCard(9, &ct5);
  ct5.bindFRAM(&espmega.fram, 6400);
  espmega.iot->registerCard(9);
  espmega.installCard(10, &ct6);
  ct6.bindFRAM(&espmega.fram, 6500);
  espmega.iot->registerCard(10);
#endif

  // ------------ Climate Cards Initialization Routine ------------
  ESP_LOGD("ISE OS", "Setting up climate cards");

  // Daikin Climate Card
  ESP_LOGD("ISE OS", "Installing daikin climate card");
  espmega.installCard(2, &climateCard_daikin);
  climateCard_daikin.bindFRAM(&espmega.fram, 5000);
  climateCard_daikin.loadStateFromFRAM();
  climateCard_daikin.setFRAMAutoSave(true);
  // Bind daikin climate card to the internal display
  espmega.display->bindClimateCard(&climateCard_daikin);

  // York Climate Card
  ESP_LOGD("ISE OS", "Installing york climate card");
  espmega.installCard(3, &climateCard_york);
  climateCard_york.bindFRAM(&espmega.fram, 5005);
  climateCard_york.loadStateFromFRAM();
  climateCard_york.setFRAMAutoSave(true);
  // ------------ End Climate Cards Initialization Routine ------------
  
  // ------------ Current Transformer Cards Initialization Routine ------------
  #ifdef ANALOG_ENABLE
  ESP_LOGD("ISE OS", "Installing current transformer cards");
  // First try to install the analog card
  analogCardAvailable = espmega.installCard(4, &analogCard);
  // If the analog card is available, install the current transformer cards
  // If the analog card is not available, current transformer cards will not be installed
  // Unless CT_FORCE_ENABLE is set to true
  // This is to prevent soft locking the device when the device tries to read from an ADC channel that does not exist
  if (analogCardAvailable || CT_FORCE_ENABLE)
  {
    ESP_LOGV("ISE OS", "Analog card available, installing current transformer cards");
    espmega.installCard(5, &ct1);
    ct1.bindFRAM(&espmega.fram, 5010);
    espmega.installCard(6, &ct2);
    ct2.bindFRAM(&espmega.fram, 5020);
    espmega.installCard(7, &ct3);
    ct3.bindFRAM(&espmega.fram, 5030);
    espmega.installCard(8, &ct4);
    ct4.bindFRAM(&espmega.fram, 5040);
    espmega.installCard(9, &ct5);
    ct5.bindFRAM(&espmega.fram, 5050);
    espmega.installCard(10, &ct5);
    ct6.bindFRAM(&espmega.fram, 5060);
    espmega.iot->registerCard(5);
    espmega.iot->registerCard(6);
    espmega.iot->registerCard(7);
    espmega.iot->registerCard(8);
    espmega.iot->registerCard(9);
    espmega.iot->registerCard(10);
  }
  else
  {
    ESP_LOGE("ISE OS", "Analog card not available, current transformer cards cannot and will not be installed.");
  }
  #endif
  // ------------ End Current Transformer Cards Initialization Routine ------------


  // ------------ Remote Variables Initialization Routine ------------
  pm25_out.registerCallback(&pm25outupdatedisplay);
  pm25_in.registerCallback(&pm25inupdatedisplay);
  temp_out.registerCallback(&tempoutupdatedisplay);
  weather.registerCallback(&weatherupdatedisplay);
  pm_switch.registerCallback(&pmswitchupdatedisplay);
  pm_fan_speed.registerCallback(&pmfanspeedupdatedisplay);
  pm_lock.registerCallback(&pmlockupdatedisplay);
  ac_lock.registerCallback(&aclockupdatedisplay);


  // PM2.5 PPM Remote Variable
  pm25_out.begin(6, "/aqi/value", espmega.iot, true, "/aqi/request_value");
  // Temperature Remote Variable
  temp_out.begin(6, "/temp/value", espmega.iot, true, "/temp/request_value");
  // Weather Remote Variable
  weather.begin(45, "/weather/value", espmega.iot, true, "/weather/request_value");
  // PM2.5 PPM Remote Variable
  pm25_in.begin(6, "/pm/value", espmega.iot, true, "/pm/request_value");
  // Air Purifier Switch Remote Variable
  pm_switch.begin(6, "/pm/switch_state", espmega.iot, true, "/pm/request_switch_state");
  pm_switch.enableSetValue("/pm/set_switch_state");
  // Air Purifier Fan Speed Remote Variable
  pm_fan_speed.begin(6, "/pm/fan_speed", espmega.iot, true, "/pm/request_fan_speed");
  pm_fan_speed.enableSetValue("/pm/set_fan_speed");
  // Air Purifier Lock Remote Variable
  pm_lock.begin(6, "/pm/lock_state", espmega.iot, true, "/pm/request_lock_pm_state");
  pm_lock.enableSetValue("/pm/set_lock_pm_state");
  // Air Conditioner Lock Remote Variable
  ac_lock.begin(6, "/ac/lock_state", espmega.iot, true, "/ac/request_lock_ac_state");
  ac_lock.enableSetValue("/ac/set_lock_ac_state");
  // ------------ End Remote Variables Initialization Routine ------------


  // ------------ IoT Card Registration Routine ------------
  espmega.iot->registerCard(0);  // Register the Input Card
  espmega.iot->registerCard(1);  // Register the Output Card
  espmega.iot->registerCard(2);  // Register the Climate Card Daikin
  espmega.iot->registerCard(3); // Register the Climate Card York
  // ------------ End IoT Card Registration Routine ------------


  // ------------ External Display Initialization Routine ------------
  auto bindedGetTime = std::bind(&ESPMegaPRO::getTime, &espmega);
  iseDisplay.begin(&espmega.inputs, &espmega.outputs, &climateCard_daikin, &climateCard_york, &pm_switch, &pm_fan_speed, &pm_lock, &ac_lock, espmega.iot);
  espmega.iot->registerRelativeMqttCallback(&handleMqttMessage);
  iseDisplayOTA.begin("/isedisp", &iseDisplay, espmega.webServer);
  internalDisplayOTA.begin("/intdisp", espmega.display, espmega.webServer);
  iseDisplay.registerPageChangeCallback(&handlePageChange);
  // ------------ End External Display Initialization Routine ------------
}

void pm25outupdatedisplay(char *value)
{
  iseDisplay.updatePMoutside(get_pm25_out());
}
void pm25inupdatedisplay(char *value)
{
  iseDisplay.updatePMinside(get_pm25_in());
}
void tempoutupdatedisplay(char *value)
{
  //iseDisplay.updateTempOutside(get_temp_out());
}
void weatherupdatedisplay(char *value)
{
  //iseDisplay.updateWeather(weather.getValue());
}
void pmfanspeedupdatedisplay(char *value)
{
  iseDisplay.updateAirPurifierState();
}
void pmswitchupdatedisplay(char *value)
{
  ESP_LOGI("PM switch", "getting PM switch state from MQTT: %d", pm_switch.getValue());
  iseDisplay.updateAirPurifierState();
  iseDisplay.updateSystemtoggle();
  iseDisplay.updateAllStandbyToggle();
  ESP_LOGI("PM switch", "toggling PM switch state from: %d to %d", pm_switch.getValue(), !pm_switch.getValue());
}
void pmlockupdatedisplay(char *value)
{
  ESP_LOGI("PM lock", "getting PM lock state from MQTT: %d", pm_lock.getValue());
  iseDisplay.updateAirPurifierState();
  ESP_LOGI("PM lock", "toggling PM lock state from: %d to %d", pm_lock.getValue(), !pm_lock.getValue());
}
void aclockupdatedisplay(char *value)
{
  ESP_LOGI("AC lock", "getting AC lock state from MQTT: %d", ac_lock.getValue());
  iseDisplay.updateACState();
  ESP_LOGI("AC lock", "toggling AC lock state from: %d to %d", ac_lock.getValue(), !ac_lock.getValue());
}

void loop()
{
  espmega.loop();
  iseDisplay.loop();

  // Update the time every 15 seconds
  static uint32_t last_time_updated = 0;

  if (millis() - last_time_updated > 15000)
  {
    rtctime_t time = espmega.getTime();
    iseDisplay.updateDateTimeText(time);
    last_time_updated = millis();
  }
#ifdef ANALOG_ENABLE
  // Send out analog Data every 6 seconds
  static uint32_t last_analog_sent = 0;
  if (millis() - last_analog_sent > 6000)
  {
    espmega.iot->publish("/debug/up", "1");
    if (analogCardAvailable || CT_FORCE_ENABLE)
    {
      espmega.iot->publish("/debug/log", "Sending Analog Card Data");
      char topic_buffer[50];
      char payload_buffer[50];
      // Publish ADC Pin 0-7
      for (uint8_t i = 0; i < 8; i++)
      {
        sprintf(topic_buffer, "/debug/analog/%d", i);
        sprintf(payload_buffer, "%d", analogCard.analogRead(i));
        espmega.iot->publish(topic_buffer, payload_buffer);
      }
    }
    last_analog_sent = millis();
  }
#endif
}

void on_pin_change(uint8_t pin, uint8_t value)
{
  switch (pin)
  {
  case LIGHT_ROW1_COLUMN1_IN:
    espmega.outputs.setState(LIGHT_ROW1_COLUMN1, !espmega.outputs.getState(LIGHT_ROW1_COLUMN1));
    break;
  case LIGHT_ROW1_COLUMN2_IN:
    espmega.outputs.setState(LIGHT_ROW1_COLUMN2, !espmega.outputs.getState(LIGHT_ROW1_COLUMN2));
    break;
  case LIGHT_ROW2_COLUMN1_IN:
    espmega.outputs.setState(LIGHT_ROW2_COLUMN1, !espmega.outputs.getState(LIGHT_ROW2_COLUMN1));
    break;
  case LIGHT_ROW2_COLUMN2_IN:
    espmega.outputs.setState(LIGHT_ROW2_COLUMN2, !espmega.outputs.getState(LIGHT_ROW2_COLUMN2));
    break;
  case LIGHT_ROW3_COLUMN1_IN:
    espmega.outputs.setState(LIGHT_ROW3_COLUMN1, !espmega.outputs.getState(LIGHT_ROW3_COLUMN1));
    break;
  case LIGHT_ROW3_COLUMN2_IN:
    espmega.outputs.setState(LIGHT_ROW3_COLUMN2, !espmega.outputs.getState(LIGHT_ROW3_COLUMN2));
    break;
  case LIGHT_ROW4_COLUMN1_IN:
    espmega.outputs.setState(LIGHT_ROW4_COLUMN1, !espmega.outputs.getState(LIGHT_ROW4_COLUMN1));
    break;
  case LIGHT_ROW4_COLUMN2_IN:
    espmega.outputs.setState(LIGHT_ROW4_COLUMN2, !espmega.outputs.getState(LIGHT_ROW4_COLUMN2));
    break;
  case COMPUTER_DESK_SWITCH_IN:
    espmega.outputs.setState(COMPUTER_DESK_SWITCH_OUT, !espmega.outputs.getState(COMPUTER_DESK_SWITCH_OUT));
    break;
  }
}

uint16_t get_pm25_out()
{
  uint16_t pm25_out_value = 0;
  // Read PM2.5 PPM from home assistant
  pm25_out_value = atoi(pm25_out.getValue());
  ESP_LOGI("PM2.5", "getting PM2.5 PPM from MQTT: %d", pm25_out_value);
  return pm25_out_value;
}

uint16_t get_pm25_in()
{
  uint16_t pm25_in_value = 0;
  // Read PM2.5 PPM from home assistant
  pm25_in_value = atoi(pm25_in.getValue());
  ESP_LOGI("PM2.5", "getting PM2.5 PPM from MQTT: %d", pm25_in_value);
  return pm25_in_value;
}

float get_temp_out()
{
  float temp_out_value = 0;
  // Read temperature from home assistant
  temp_out_value = atof(temp_out.getValue());
  ESP_LOGI("Temperature", "getting Temperature from MQTT: %f", temp_out_value);
  return temp_out_value;
}

uint8_t get_pm_fanspeed()
{
  uint8_t pm_fan_speed_value = 0;
  // Read PM2.5 fan speed from home assistant
  pm_fan_speed_value = (int)atof(pm_fan_speed.getValue());
  ESP_LOGI("PM fan speed", "getting PM2.5 PPM from MQTT: %d", pm_fan_speed_value);
  return pm_fan_speed_value;
}

bool get_pm_lock()
{
  ESP_LOGI("PM lock", "getting PM lock state from MQTT: %d", pm_lock.getValue());
  bool is_pm_lock_on = strcmp(pm_lock.getValue(), "on") == 0;
  return is_pm_lock_on;
}

bool get_ac_lock()
{
  ESP_LOGI("AC lock", "getting AC lock state from MQTT: %d", ac_lock.getValue());
  bool is_ac_lock_on = strcmp(ac_lock.getValue(), "on") == 0;
  return is_ac_lock_on;
}

/**
 * @brief Get the PM switch state from home assistant
 * @return true if the PM switch is on, false if the PM switch is off
 */
bool get_pm_switch()
{
  ESP_LOGI("PM switch", "getting PM switch state from MQTT: %d", pm_switch.getValue());
  bool is_pm_switch_on = strcmp(pm_switch.getValue(), "on") == 0;
  return is_pm_switch_on;
}

void toggle_pm_switch()
{
  bool is_pm_switch_on = get_pm_switch();
  ESP_LOGI("PM switch", "toggling PM switch state from: %d to %d", is_pm_switch_on, !is_pm_switch_on);
  pm_switch.setValue(is_pm_switch_on ? "0" : "1");
}

void toggle_ac_lock()
{
  bool is_ac_lock_on = get_ac_lock();
  ESP_LOGI("AC lock", "toggling AC lock state from: %d to %d", is_ac_lock_on, !is_ac_lock_on);
  ac_lock.setValue(is_ac_lock_on ? "0" : "1");
}

void toggle_pm_lock()
{
  bool is_pm_lock_on = get_pm_lock();
  ESP_LOGI("PM lock", "toggling PM lock state from: %d to %d", is_pm_lock_on, !is_pm_lock_on);
  pm_lock.setValue(is_pm_lock_on ? "0" : "1");
}

void set_pm_fanspeed(uint8_t speed)
{
  ESP_LOGI("PM fan speed", "setting PM fan speed to: %d", speed);
  char buffer[4];
  itoa(speed, buffer, DEC);
  pm_fan_speed.setValue(buffer);
}

void handlePageChange(uint8_t page)
{
  ESP_LOGI("Page", "Page change to: %d", page);
  rtctime_t time = espmega.getTime();
  iseDisplay.updateDateTimeText(time);
  switch (page)
  {
  case PAGE_STANDBY:
    iseDisplay.updateLightGroupStatePageStandby();
    iseDisplay.updateAirPurifierStateStandby();
    iseDisplay.updateAllStandbyToggle();
    break;
  case PAGE_DASHBOARD:
    iseDisplay.updateLightGroupStatePageDashboard();
    iseDisplay.updateAirPurifierState();
    iseDisplay.updateSystemtoggle();
    break;
  default:
    break;
  }

  iseDisplay.updatePMoutside(get_pm25_out());
  iseDisplay.updatePMinside(get_pm25_in());
  //iseDisplay.updateWeather(weather.getValue());
  //iseDisplay.updateTempOutside(get_temp_out());
  iseDisplay.updateACState();
}

void sendStopBytes()
{
  Serial.write(0xFF);
  Serial.write(0xFF);
  Serial.write(0xFF);
}

void sendExtStopBytes()
{
  iseDisplayAdapter.write(0xFF);
  iseDisplayAdapter.write(0xFF);
  iseDisplayAdapter.write(0xFF);
}