#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <TimeLib.h>

#include "config.h"

//***********************************//

// 6 byte
#define TIMER_DATA_HH      0
#define TIMER_DATA_MM      1
#define TIMER_DATA_CH1     2 // the brightness of the channel
#define TIMER_DATA_CH2     3 // the brightness of the channel
#define TIMER_DATA_CH3     4 // the brightness of the channel
#define TIMER_DATA_CH4     5 // the brightness of the channel

#define LENGTH_OF_TIMER_DATA_BLOCK (TIMER_DATA_CH4 + 1)
#define NUMBER_OF_TIMER_DATA_BLOCKS 10

//***********************************//
/* Globals */

bool tc_testOngoing = false;

uint32_t tc_last_check = 60000;

uint8_t current_target_data_block = 255;

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, MY_NTP_SERVER);

struct tc_data_st {
  uint8_t hh;
  uint8_t mm;
  uint8_t ch1;
  uint8_t ch2;
  uint8_t ch3;
  uint8_t ch4;
};

struct tc_data_st tc_data[10];

uint8_t example_timer_data_block[] = {
// hour  min  ch1  ch2  ch3  ch3
   8,  0,   0,   0,   0,   0, // 0: off
   8, 30,  25,   0,   0,   0, // 1: 10% ch1 blues
  11,  0,  25,   0,  25,   0, // 2: 10% all blues
  13,  0, 205, 205, 205, 205, // 3: 80% all
  18,  0, 205, 205, 205, 205, // 4: 80% all for 5 hours
  19,  0,  50,  50,  50,  50, // 5: 20% all
  19, 30,  50,   0,  50,   0, // 6: 20% all blues
  20,  0,  25,   0,  25,   0, // 9: 10% all blues
  20, 30,  25,   0,   0,   0, // 7: 10% ch1 blues
  21,  0,   0,   0,   0,   0, // 8: 0% all
};

uint8_t test_timer_data_block[] = {
// hour  min  ch1  ch2  ch3  ch3
   9, 20,   0,   0,   0,   0, // 0: off
   9, 30,  25,   0,   0,   0, // 1: 10% ch1 blues
   9, 40,  25,   0,  25,   0, // 2: 10% all blues
   9, 50, 205, 205, 205, 205, // 3: 80% all
  10,  0, 100, 100, 100, 100, // 4: 80% all for 5 hours
  10, 10,  50,  50,  50,  50, // 5: 20% all
  10, 20,  50,   0,  50,   0, // 6: 20% all blues
  10, 30,  25,   0,   0,   0, // 7: 10% ch1 blues
  10, 40,   0,   0,   0,   0, // 8: all off
  11,  0,   0,   0,   0,   0, // 9: all off
};

//***********************************//

void tc_init()
{

  if (tc_check_no_data_block() == true)
  {
    //Serial.println("TC: No data block found, writing example block to EEPROM.");
    tc_write_default();
  }

  while ( WiFi.status() != WL_CONNECTED )
  {
    delay (500);
    Serial.print ( "." );
  }

  tc_last_check = millis();

  // Set the timezone to Europe/Berlin
  setenv("TZ", LOCAL_TIMEZONE_STRING, 1);
  tzset();

  timeClient.begin();

  Serial.println("TC: Read data block from eeprom");
  tc_readConfig();

  if (tc_testOngoing == true)
  {
    Serial.println("TC: Test ongoing, reading test data");
    for (uint8_t i = 0; i < NUMBER_OF_TIMER_DATA_BLOCKS * LENGTH_OF_TIMER_DATA_BLOCK; i += LENGTH_OF_TIMER_DATA_BLOCK)
    {
      tc_data[i / LENGTH_OF_TIMER_DATA_BLOCK].hh      = test_timer_data_block[i];
      tc_data[i / LENGTH_OF_TIMER_DATA_BLOCK].mm      = test_timer_data_block[i+1];
      tc_data[i / LENGTH_OF_TIMER_DATA_BLOCK].ch1     = test_timer_data_block[i+2];
      tc_data[i / LENGTH_OF_TIMER_DATA_BLOCK].ch2     = test_timer_data_block[i+3];
      tc_data[i / LENGTH_OF_TIMER_DATA_BLOCK].ch3     = test_timer_data_block[i+4];
      tc_data[i / LENGTH_OF_TIMER_DATA_BLOCK].ch4     = test_timer_data_block[i+5];

      Serial.print("data block: "); Serial.println(i / LENGTH_OF_TIMER_DATA_BLOCK);
      Serial.print("  hh:  "); Serial.println(tc_data[i / LENGTH_OF_TIMER_DATA_BLOCK].hh);
      Serial.print("  mm:  "); Serial.println(tc_data[i / LENGTH_OF_TIMER_DATA_BLOCK].mm);
      Serial.print("  ch1: "); Serial.println(tc_data[i / LENGTH_OF_TIMER_DATA_BLOCK].ch1);
      Serial.print("  ch2: "); Serial.println(tc_data[i / LENGTH_OF_TIMER_DATA_BLOCK].ch2);
      Serial.print("  ch3: "); Serial.println(tc_data[i / LENGTH_OF_TIMER_DATA_BLOCK].ch3);
      Serial.print("  ch4: "); Serial.println(tc_data[i / LENGTH_OF_TIMER_DATA_BLOCK].ch4);
    }
  }

  if (tc_enabled == TIMING_CONTROL_ENABLED)
  {
    Serial.println("tc_enabled = " + (String)tc_enabled);
    tc_update_main();
  }
}

//********************************//

void tc_reset()
{
  current_target_data_block = 255;
}

void tc_update_loop()
{
  static uint8_t last_min_check = 255;

  if (tc_enabled == TIMING_CONTROL_DISABLED)
  {
    return;
  }

  if ((minute() % 10) != 0 || last_min_check == minute()) // && tc_testOngoing == false
  {
    return; // only run every 10 minutes
  }

  last_min_check = minute();

  tc_update_main();
}

void tc_update_main()
{
  uint8_t target_data_block = 255;

  if (tc_enabled == TIMING_CONTROL_DISABLED)
  {
    return;
  }

  tc_updateTime();

  // calculate the current time as minutes
  uint16_t time_now = (((uint16_t)hour()) * 60) + minute();

  // search for the current active time slot
  for (int i = NUMBER_OF_TIMER_DATA_BLOCKS-1; i >= 0 && target_data_block == 255; --i)
  {
    //Serial.println((String)i + " - " + (String)tc_data[i].hh + ":" + (String)tc_data[i].mm);

    // calculate the time of the data block to minutes
    uint16_t time_tc_data = (((uint16_t)tc_data[i].hh) * 60) + tc_data[i].mm;

    if (time_now >= time_tc_data)
    {
      target_data_block = i+1; // found the next block to load
      //Serial.println((String)i + " => " + target_data_block);
    }
  }

  if (target_data_block == 255)
  {
    // no new predecessor or successor found, start over
    current_target_data_block = 255;

    Serial.println("No predecessor or successor found.");

    // disable the lights
    for (uint8_t i = 0; i < LIGHTS_COUNT; i++)
    {
      bri[i]            = 0;
      current_bri[i]    = 1; // set it to a value to force the light engine to fix the current brightness
      transitiontime[i] = default_transitiontime;
      process_lightdata(i, transitiontime[i]);
    }

    return;
  }

  if (current_target_data_block != target_data_block)
  {
    // new target block should be reached
    current_target_data_block = target_data_block;

  } else {
    // drop the found target block, we are already going on to reach it
    return;
  }

  if (target_data_block >= NUMBER_OF_TIMER_DATA_BLOCKS)
  {
    // we are not between two valid data points, do nothing
    Serial.println("tdb is >= num data blocks, abort operation...");
    for (uint8_t i = 0; i < LIGHTS_COUNT; i++)
    {
      light_state[i]    = false;
      bri[i]            = 0;
      current_bri[i]    = 1; // set it to a value to force the light engine to fix the current brightness
      transitiontime[i] = default_transitiontime;
      process_lightdata(i, transitiontime[i]);
    }
    target_data_block = 255;
    current_target_data_block = 255;
    return;
  }

  // print out the current light state
  Serial.println("-----\nCurrent values");
  for (uint8_t i = 0; i < LIGHTS_COUNT; i++)
  {
    Serial.println("current_bri[" + (String)i + "] = " + (String)current_bri[i]);
  }
  for (uint8_t i = 0; i < LIGHTS_COUNT; i++)
  {
    Serial.println("bri[" + (String)i + "] = " + (String)bri[i]);
  }
  for (uint8_t i = 0; i < LIGHTS_COUNT; i++)
  {
    Serial.println("step_level[" + (String)i + "] = " + (String)step_level[i]);
  }

  Serial.println("-----\ntdb = " + (String)target_data_block);
  Serial.print("target dataset time: ");
  Serial.print(tc_data[target_data_block].hh);
  Serial.print(":");
  Serial.print(tc_data[target_data_block].mm);
  Serial.println();

  // set the channels current and target brightness
  bri[0] = tc_data[target_data_block].ch1;
  bri[1] = tc_data[target_data_block].ch2;
  bri[2] = tc_data[target_data_block].ch3;
  bri[3] = tc_data[target_data_block].ch4;

  // make sure that the current brightness is correct
  if (tc_data[target_data_block-1].ch1 != current_bri[0])
  {
    current_bri[0] = tc_data[target_data_block-1].ch1;
  }
  if (tc_data[target_data_block-1].ch2 != current_bri[1])
  {
    current_bri[1] = tc_data[target_data_block-1].ch2;
  }
  if (tc_data[target_data_block-1].ch3 != current_bri[2])
  {
    current_bri[2] = tc_data[target_data_block-1].ch3;
  }
  if (tc_data[target_data_block-1].ch4 != current_bri[3])
  {
    current_bri[3] = tc_data[target_data_block-1].ch4;
  }

  for (uint8_t i = 0; i < LIGHTS_COUNT; i++)
  {
    Serial.println("current_bri[" + (String)i + "] = " + (String)current_bri[i]);
  }
  for (uint8_t i = 0; i < LIGHTS_COUNT; i++)
  {
    Serial.println("bri[" + (String)i + "] = " + (String)bri[i]);
  }

  // enable the lights
  for (uint8_t i = 0; i < LIGHTS_COUNT; i++)
  {
    light_state[i] = true;
    Serial.println("light_state[" + (String)i + "] = " + (String)light_state[i]);
  }

  // set the transition time
  int t_time = default_transitiontime;
  if (target_data_block > 0)
  {
    // hours as seconds from now on to the next enabled block
    t_time  = ((uint32_t)tc_data[target_data_block].hh * 60 * 60) - ((uint32_t)hour() * 60 * 60);
    // add the left over seconds to the next enabled block
    t_time += ((uint16_t)tc_data[target_data_block].mm * 60) - ((uint16_t)minute() * 60);

    if (t_time <= 0)
    {
      t_time = 1; // 0 could lead to a division by zero
    }
  }
  for (uint8_t i = 0; i < LIGHTS_COUNT; i++)
  {
    transitiontime[i] = t_time;
  }

  // calculate the step level
  for (uint8_t i = 0; i < LIGHTS_COUNT; i++)
  {
    process_lightdata(i, transitiontime[i]);

    // set the PWM for the channel
    current_pwm[i] = calcPWM(current_bri[i]);
    //Serial.println("lon: pin" + (String)i + " = PWM(" + (String)tmp_pwm + ")");
    analogWrite(pins[i], current_pwm[i]);

    Serial.println("transitiontime[" + (String)i + "] = " + (String)transitiontime[i]);
  }
  for (uint8_t i = 0; i < LIGHTS_COUNT; i++)
  {
    Serial.println("step_level[" + (String)i + "] = " + (String)step_level[i]);
  }

}

//********************************//

void tc_updateTime()
{
  /**/
  if (timeClient.update() || millis() > (tc_last_check + TIME_CHECK_INTERVAL_MS))
  {
    tc_last_check = millis();

    adjustTimeForDST();

    Serial.print("=====\nLocal time: ");
    Serial.print(hour());
    Serial.print(":");
    Serial.println(minute());
  }
  /**/
}

//********************************//

void adjustTimeForDST()
{

  // Get the time and date information
  time_t now = timeClient.getEpochTime();
  struct tm timeinfo;
  localtime_r(&now, &timeinfo);

  // set the local time
  setTime(timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year + 1900);

}

//********************************//

void tc_readConfig()
{
  for (uint8_t i = 0; i < NUMBER_OF_TIMER_DATA_BLOCKS; i++)
  {
    /*Serial.print("Reading from address: "); Serial.print(EEPROM_TIMING_DATA_ADDRESS);
    Serial.print(" + (");
    Serial.print(i);
    Serial.print(" * ");
    Serial.print(LENGTH_OF_TIMER_DATA_BLOCK);
    Serial.print(") + ");
    Serial.println(TIMER_DATA_ENSTATE);*/

    tc_data[i].hh      = EEPROM.read(EEPROM_TIMING_DATA_ADDRESS + i * LENGTH_OF_TIMER_DATA_BLOCK + TIMER_DATA_HH);
    tc_data[i].mm      = EEPROM.read(EEPROM_TIMING_DATA_ADDRESS + i * LENGTH_OF_TIMER_DATA_BLOCK + TIMER_DATA_MM);
    tc_data[i].ch1     = EEPROM.read(EEPROM_TIMING_DATA_ADDRESS + i * LENGTH_OF_TIMER_DATA_BLOCK + TIMER_DATA_CH1);
    tc_data[i].ch2     = EEPROM.read(EEPROM_TIMING_DATA_ADDRESS + i * LENGTH_OF_TIMER_DATA_BLOCK + TIMER_DATA_CH2);
    tc_data[i].ch3     = EEPROM.read(EEPROM_TIMING_DATA_ADDRESS + i * LENGTH_OF_TIMER_DATA_BLOCK + TIMER_DATA_CH3);
    tc_data[i].ch4     = EEPROM.read(EEPROM_TIMING_DATA_ADDRESS + i * LENGTH_OF_TIMER_DATA_BLOCK + TIMER_DATA_CH4);

    /*Serial.print("data block: "); Serial.print(i);
    Serial.print(" @ ");
    Serial.println((EEPROM_TIMING_DATA_ADDRESS + (i * LENGTH_OF_TIMER_DATA_BLOCK) + TIMER_DATA_ENSTATE));

    Serial.print("  es:  "); Serial.println(tc_data[i].enstate);    
    Serial.print("  hh:  "); Serial.println(tc_data[i].hh);
    Serial.print("  mm:  "); Serial.println(tc_data[i].mm);
    Serial.print("  ch1: "); Serial.println(tc_data[i].ch1);
    Serial.print("  ch2: "); Serial.println(tc_data[i].ch2);
    Serial.print("  ch3: "); Serial.println(tc_data[i].ch3);
    Serial.print("  ch4: "); Serial.println(tc_data[i].ch4);*/
  }
}

//********************************//

void tc_write_default()
{
  //Serial.print("-----\nWrite data block starting from address EEPROM_TIMING_DATA_ADDRESS = "); Serial.println(EEPROM_TIMING_DATA_ADDRESS);
  for (int i = 0; i < NUMBER_OF_TIMER_DATA_BLOCKS * LENGTH_OF_TIMER_DATA_BLOCK; i++)
  {
    //Serial.print(EEPROM_TIMING_DATA_ADDRESS + i); Serial.print(" <= "); Serial.print(example_timer_data_block[i]); Serial.print(" <= ");
    
    EEPROM.write(EEPROM_TIMING_DATA_ADDRESS + i, example_timer_data_block[i]);
    EEPROM.commit();
    
    //Serial.println(EEPROM.read(EEPROM_TIMING_DATA_ADDRESS + i));
  }
  //Serial.println("-----");
}

//********************************//

bool tc_check_no_data_block()
{
  //Serial.print("Check data block address EEPROM_TIMING_DATA_ADDRESS = "); Serial.println(EEPROM_TIMING_DATA_ADDRESS);
  uint8_t e = EEPROM.read(EEPROM_TIMING_DATA_ADDRESS);
  //Serial.println(e);
  
  if (e > 23) // the maximum value for tis memory section is 23
  {
    return true;
  }
  return false;
}

//********************************//

String tc_getJsonData()
{
  String output = "{\"tcdata\":[";
  for (uint8_t i = 0; i < NUMBER_OF_TIMER_DATA_BLOCKS; i++)
  {
    output += "{\"hour\":" + (String)(int)tc_data[i].hh  + ",";
    output += "\"min\":"   + (String)(int)tc_data[i].mm  + ",";
    output += "\"ch1\":"   + (String)(int)tc_data[i].ch1 + ",";
    output += "\"ch2\":"   + (String)(int)tc_data[i].ch2 + ",";
    output += "\"ch3\":"   + (String)(int)tc_data[i].ch3 + ",";
    output += "\"ch4\":"   + (String)(int)tc_data[i].ch4 + "}";
    if (i < NUMBER_OF_TIMER_DATA_BLOCKS-1)
    {
      output += ",";
    }
  }
  if (tc_testOngoing == false)
  {
    output += "], \"currenttime\":{\"hour\":" + (String)hour() + ",\"min\":" + (String)minute() + "}}";
  } else {
    output += "], \"currenttime\":{\"hour\":16,\"min\":0}}";
  }
  return output;
}

//********************************//

void tc_jsonDataBlocksToEEPROM(String json_data_string)
{
  StaticJsonDocument<1289> doc;
  deserializeJson(doc, json_data_string);

  Serial.println("Reading data from json data:");
  // Loop through each data block in the JSON data and store it in the tc_data array
  for (uint8_t i = 0; i < NUMBER_OF_TIMER_DATA_BLOCKS; i++)
  {
    //Serial.println("<< " + (String)i);
    
    JsonObject obj = doc["tcdata"][i];

    // Check and set the limits of the hour value
    int hour = obj["hour"];
    if (hour < 0) {
      hour = 0;
    } else if (hour > 23) {
      hour = 23;
    }
    tc_data[i].hh = hour;
    Serial.print("hour = " + (String)tc_data[i].hh + " " + (String)obj["hour"] + " ");

    // Check and set the limits of the minute value
    int minute = obj["min"];
    if (minute < 0) {
      minute = 0;
    } else if (minute > 50) {
      minute = 50;
    }
    tc_data[i].mm = minute;
    Serial.print("minute = " + (String)tc_data[i].mm + " " + (String)obj["min"] + " ");

    // Check and set the limits of the ch1 value
    int ch1 = obj["ch1"];
    if (ch1 < 0) {
      ch1 = 0;
    } else if (ch1 > 255) {
      ch1 = 255;
    }
    tc_data[i].ch1 = ch1;
    Serial.print("ch1 = " + (String)tc_data[i].ch1 + " " + (String)obj["ch1"] + " ");

    // Check and set the limits of the ch2 value
    int ch2 = obj["ch2"];
    if (ch2 < 0) {
      ch2 = 0;
    } else if (ch2 > 255) {
      ch2 = 255;
    }
    tc_data[i].ch2 = ch2;
    Serial.print("ch2 = " + (String)tc_data[i].ch2 + " " + (String)obj["ch2"] + " ");

    // Check and set the limits of the ch3 value
    int ch3 = obj["ch3"];
    if (ch3 < 0) {
      ch3 = 0;
    } else if (ch3 > 255) {
      ch3 = 255;
    }
    tc_data[i].ch3 = ch3;
    Serial.print("ch3 = " + (String)tc_data[i].ch3 + " " + (String)obj["ch3"] + " ");

    // Check and set the limits of the ch4 value
    int ch4 = obj["ch4"];
    if (ch4 < 0) {
      ch4 = 0;
    } else if (ch4 > 255) {
      ch4 = 255;
    }
    tc_data[i].ch4 = ch4;
    Serial.println("ch4 = " + (String)tc_data[i].ch4 + " " + (String)obj["ch4"] + "\n---");
  }

  Serial.println("Writing to EEPROM...");
  // Write the tc_data array to the EEPROM
  for (uint16_t i = 0; i < (NUMBER_OF_TIMER_DATA_BLOCKS * LENGTH_OF_TIMER_DATA_BLOCK); i++)
  {
    //Serial.println("  " + (String)(EEPROM_TIMING_DATA_ADDRESS + i) + " " + (String)(*((uint8_t*)&tc_data + i)));

    EEPROM.write(EEPROM_TIMING_DATA_ADDRESS + i, *((uint8_t*)&tc_data + i));
    /*if (((i+1) % 6 == 0))
    {
      Serial.println("---");
    }*/

  }
  EEPROM.commit();

  // reset the prograss in the timing control engine
  tc_reset();
  // call the function which reads out the new set data
  tc_update_main();
}

//********************************//
