// libraries
#include "Wire.h" // voor i2c
#include "BluetoothSerial.h"
#include "driver/rmt.h"

#define DEVICE_ID "DKTL-TheaterTimeline-000"

// code uit doc week 11
#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)
#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it
#endif

// classes
class LCD {
  public:
    static const int ADDR = 0x27;

    // LCD commands
    static const int CLEARDISPLAY = 0x01;
    static const int RETURNHOME = 0x02;
    static const int ENTRYMODESET = 0x04;
    static const int DISPLAYCONTROL = 0x08;
    static const int CURSORSHIFT = 0x10;
    static const int FUNCTIONSET = 0x20;
    static const int SETCGRAMADDR = 0x40;
    static const int SETDDRAMADDR = 0x80;

    // Flags for display on/off control
    static const int DISPLAYON = 0x04;
    static const int DISPLAYOFF = 0x00;
    static const int CURSORON = 0x02;
    static const int CURSOROFF = 0x00;
    static const int BLINKON = 0x01;
    static const int BLINKOFF = 0x00;

    // Flags for display/cursor shift
    static const int DISPLAYMOVE = 0x08;
    static const int CURSORMOVE = 0x00;
    static const int MOVERIGHT = 0x04;
    static const int MOVELEFT = 0x00;

    static const int En = 0x04;  // Enable
    static const int Rs = 0x01;  // Register select

    // Initialize LCD
    void init() {
      delay(50);
      expanderWrite(0x08); //backlight
      delay(1000);
      
      // 4-bit mode
      write4bits(0x03 << 4);
      delayMicroseconds(4500);
      
      write4bits(0x03 << 4);
      delayMicroseconds(4500);
      
      write4bits(0x03 << 4);
      delayMicroseconds(150);
      
      write4bits(0x02 << 4);
      
      // 4-bit mode, 2 lines, 5x8 dots
      command(FUNCTIONSET | 0x00 | 0x08 | 0x00);
      
      command(DISPLAYCONTROL | DISPLAYON | CURSOROFF | BLINKOFF);
      command(CLEARDISPLAY);
      delayMicroseconds(2000);
      command(ENTRYMODESET | 0x00 | 0x00);
    }

    void command(uint8_t value) {
      send(value, 0);
    }

    void write(uint8_t value) {
      send(value, Rs);
    }

    void send(uint8_t value, uint8_t mode) {
      uint8_t highnib = value & 0xF0;
      uint8_t lownib = (value << 4) & 0xF0;
      write4bits((highnib) | mode);
      write4bits((lownib) | mode);
    }

    void write4bits(uint8_t value) {
      expanderWrite(value);
      pulseEnable(value);
    }

    void pulseEnable(uint8_t data) {
      expanderWrite(data | En);
      delayMicroseconds(1);
      expanderWrite(data & ~En);
      delayMicroseconds(50);
    }

    void expanderWrite(uint8_t data) {
      Wire.beginTransmission(ADDR);
      Wire.write((int)(data) | 0x08);
      Wire.endTransmission();
    }

    void clear() {
      command(CLEARDISPLAY);
      delayMicroseconds(2000);
    }

    void setCursor(uint8_t col, uint8_t row) {
      int row_offsets[] = {0x00, 0x40, 0x14, 0x54};
      if (row > 1) row = 1;
      command(SETDDRAMADDR | (col + row_offsets[row]));
    }

    void print(String str) {
      for (int i = 0; i < str.length(); i++) {
        write(str.charAt(i));
      }
    }

    void print(char c) {
      write(c);
    }

    void on(){
      command(DISPLAYCONTROL | DISPLAYON | CURSOROFF | BLINKOFF);
    }

    void off(){
      command(DISPLAYCONTROL | DISPLAYOFF | CURSOROFF | BLINKOFF);
    }
};

class LedRing {
  public:
    #define LED_RMT_TX_CHANNEL RMT_CHANNEL_0
    #define LED_RMT_TX_GPIO GPIO_NUM_2  // GPIO pin connected to NeoPixel data line
    #define NUM_LEDS 23                 // Number of LEDs in your strip

    // WS2812B timing specifications (in nanoseconds)
    #define T0H 400    // 0 bit high time
    #define T0L 850    // 0 bit low time  
    #define T1H 800    // 1 bit high time
    #define T1L 450    // 1 bit low time
    #define RES 50000  // Reset time

    #define NS_TO_RMT_TICKS(ns) ((uint32_t)((ns) * 8 / 100))

    // RMT items for bit patterns
    rmt_item32_t bit0 = {{{ NS_TO_RMT_TICKS(T0H), 1, NS_TO_RMT_TICKS(T0L), 0 }}};
    rmt_item32_t bit1 = {{{ NS_TO_RMT_TICKS(T1H), 1, NS_TO_RMT_TICKS(T1L), 0 }}};

    // LED strip array
    int leds[NUM_LEDS];


    float percentage = 0.0;

    void init() {
      rmt_config_t config = RMT_DEFAULT_CONFIG_TX(LED_RMT_TX_GPIO, LED_RMT_TX_CHANNEL);
      config.clk_div = 1;
      
      rmt_config(&config);
      rmt_driver_install(config.channel, 0, 0);
      
      clearStrip();
      showStrip();
    }

    void loop() {
      percentage += 0.00001;
      if (percentage > 1) percentage = 0;
      loadingBar(0xFFFFFF, percentage);
      clearStrip();
    }

    // Send color data to LED strip
    void showStrip() {
      rmt_item32_t items[NUM_LEDS * 24 + 1];  // 24 bits per LED + reset
      int item_idx = 0;
      
      for (int led = 0; led < NUM_LEDS; led++) {
        uint32_t color = leds[led];
        
        // Send 24 bits (MSB first)
        for (int bit = 23; bit >= 0; bit--) {
          if (color & (1UL << bit)) {
            items[item_idx++] = bit1;
          } else {
            items[item_idx++] = bit0;
          }
        }
      }
      
      // Add reset pulse - using proper integer calculation
      rmt_item32_t reset_item = {{{ 0, 0, NS_TO_RMT_TICKS(RES), 0 }}};
      items[item_idx] = reset_item;
      
      // Send data via RMT
      rmt_write_items(LED_RMT_TX_CHANNEL, items, item_idx + 1, true);
      rmt_wait_tx_done(LED_RMT_TX_CHANNEL, portMAX_DELAY);
    }

    void setPixel(int pixel, int color) {
      pixel += 5;
      pixel %= NUM_LEDS;
      if (pixel >= 0 && pixel < NUM_LEDS) {
        leds[pixel] = color;
      }
    }

    void clearStrip() {
      for (int i = 0; i < NUM_LEDS; i++) {
        leds[i] = 0;
      }
    }

    void fillStrip(int color) {
      for (int i = 0; i < NUM_LEDS; i++) {
        setPixel(i, color);
      }
      showStrip();
    }

    int lowerBrightness(int color, float brightness){
      int r = (color >> 8) & 0xFF;
      int g = (color >> 16) & 0xFF;
      int b = color & 0xFF;

      r *= brightness;
      g *= brightness;
      b *= brightness; 

      return ((uint32_t)g << 16) | ((uint32_t)r << 8) | b;
    }

    void loadingBar(int color, float percentage) {
      clearStrip();
      for (int i = 0; i < NUM_LEDS; i++) {
        if (float(i)/NUM_LEDS < percentage){
          setPixel(i, color);
        }
        else if ((i-1.0)/NUM_LEDS < percentage){
          float brightness = percentage - float(i-1)/NUM_LEDS;
          setPixel(i, lowerBrightness(color, brightness));
        }
      }
      showStrip();
    }

    void spinner(int color, float percentage) {
      clearStrip();

      int position0 = NUM_LEDS * percentage;
      int position1 = (position0 + 1) % NUM_LEDS;
      int position2 = (position1 + 1) % NUM_LEDS;
      int position3 = (position2 + 1) % NUM_LEDS;
      int position4 = (position3 + 1) % NUM_LEDS;

      setPixel(position0, lowerBrightness(color, 0.02));
      setPixel(position1, lowerBrightness(color, 0.07));
      setPixel(position2, lowerBrightness(color, 0.15));
      setPixel(position3, lowerBrightness(color, 0.07));
      setPixel(position4, lowerBrightness(color, 0.02));

      showStrip();
    }
};

// I2C pins
#define SDA_PIN 21
#define SCL_PIN 22

#define PIR_PIN 35


// instanties
LCD lcd;
BluetoothSerial SerialBT;
LedRing ledRing;

float spinnerPosition = 0;
bool isConnected = false;
bool isInitiated = false;
bool lastConnectionState = false;
bool printingConnecting = false;
bool printingInitiating = false;

bool previousPirState = false;

bool isSleeping = false;
bool prevIsSleeping = false;


bool pulsing = false;
int pulsingColor = 0;


void setup() {
  // serial
  Serial.begin(115200);
  Serial.println("[SETUP] Theater Timeline booting...");

  // i2c
  Wire.begin(SDA_PIN, SCL_PIN);
  Serial.println("[SETUP] I2C gestart.");
  lcd.init();
  Serial.println("[SETUP] LCD gestart.");

  // bt
  SerialBT.begin(DEVICE_ID); //Bluetooth device name
  Serial.println("[SETUP] Bluetooth seriele bus gestart.");

  // ledring
  ledRing.init();

  // bewegings sensor
  pinMode(PIR_PIN, INPUT);


  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Theater Timeline");
  lcd.setCursor(0, 1);  
  lcd.print("by Dante Deketele");
  delay(1000);
  
  Serial.println("Init complete.");
}


void loop() {
  loopMotionSensor();
  loopBT();


  if (!isConnected){
    spinnerPosition += 0.01f;
    if (spinnerPosition >= 1) spinnerPosition = 0;

    ledRing.spinner(0x0000FF, spinnerPosition);
    delay(25);
    
    if (!printingConnecting){
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Waiting for");
      lcd.setCursor(0, 1);  
      lcd.print("BT connection");
      printingConnecting = true;
    }
  } else{
    if (isSleeping) {
      if (prevIsSleeping != isSleeping){
        lcd.off();
        ledRing.fillStrip(0x000000);
        prevIsSleeping = isSleeping;
      }
      return;
    }else{
      if (prevIsSleeping != isSleeping){
        lcd.on();
        prevIsSleeping = isSleeping;
      }

      if (pulsing){
        spinnerPosition += 0.01f;
        if (spinnerPosition >= 1) spinnerPosition = 0;
        float x = (spinnerPosition * 2) - 1;
        x = x*x;
        ledRing.fillStrip(ledRing.lowerBrightness(pulsingColor, x * 0.05));
        delay(10);
      }
    }

    if (!isInitiated){
      spinnerPosition += 0.01f;
      if (spinnerPosition >= 1) spinnerPosition = 0;
      float x = (spinnerPosition * 2) - 1;
      x = x*x;
      ledRing.fillStrip(ledRing.lowerBrightness(0x0000FF, x * 0.05));
      delay(25);
      if (!printingInitiating){
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Initiating");
        lcd.setCursor(0, 1);  
        lcd.print("TheaterTimeline");
        printingInitiating = true;
      }
    }
  }
}

void loopMotionSensor() {
  if (!isConnected) return;
  bool pirState = digitalRead(PIR_PIN);
  if (pirState != previousPirState) {
    SerialBT.println("<tt><component>PIR</component><value>" + String(pirState) + "</value></tt>");
    previousPirState = pirState;
  }
}

String extractXMLContent(String data, String lineTag) {
  String startTag = "<" + lineTag + ">";
  String endTag = "</" + lineTag + ">";
  
  int startIndex = data.indexOf(startTag);
  if (startIndex == -1) return "";
  
  startIndex += startTag.length();
  int endIndex = data.indexOf(endTag, startIndex);
  if (endIndex == -1) return "";
  
  return data.substring(startIndex, endIndex);
}

void loopBT() {
  bool currentState = SerialBT.hasClient();
  
  if (currentState != lastConnectionState) {
    printingConnecting = false;
    printingInitiating = false;
    spinnerPosition = 0;

    if (currentState) {
      onConnect();
    } else {
      onDisconnect();
    }
    lastConnectionState = currentState;
  }

  if (SerialBT.available()) {
    String data = SerialBT.readStringUntil('|');
    Serial.println(data);

    
    if(data.indexOf("<tt>") == -1 || data.indexOf("</tt>") == -1){
      Serial.println("FORMAT ERR:" + data);
      return;
    }

    data = extractXMLContent(data, "tt");
    
    Serial.println(data);

    if(data.indexOf("<display>") != -1){
      data = extractXMLContent(data, "display");
      String line1 = extractXMLContent(data, "line1");
      String line2 = extractXMLContent(data, "line2");
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print(line1);
      lcd.setCursor(0, 1);  
      lcd.print(line2);
      Serial.println("Displaying: " + line1 + ", " + line2);
    } else if (data.indexOf("<sleepmode>") != -1){
      data = extractXMLContent(data, "sleepmode");
      if (data == "on"){
        isSleeping = true;
      } else if (data == "off"){
        isSleeping = false;
      }
    } else if (data.indexOf("<ledring>") != -1){
      pulsing = false;
      isInitiated = true;
      data = extractXMLContent(data, "ledring");
      String mode = extractXMLContent(data, "mode");
      String colorString = extractXMLContent(data, "color");
      String progressString = extractXMLContent(data, "progress");

      if (mode == "solid"){
        int color = colorString.toInt();
        ledRing.fillStrip(color);
        Serial.println("[BT] solid ledstrip.");
      }else if (mode == "progress"){
        int color = colorString.toInt();
        int progress = progressString.toInt();
        ledRing.loadingBar(color, progress/100.0);
        Serial.println("[BT] solid ledstrip.");
      } else if (mode == "pulse"){
        pulsingColor = colorString.toInt();
        pulsing = true;
      }
    }
    else{
      Serial.println("FORMAT ERR Unsupported format:" + data);
    }
  }
}

void onConnect() {
  Serial.println("[BT] Client connected.");
  isConnected = true;
  SerialBT.println("<tt><state>connected</state></tt>");
}

void onDisconnect() {
  isConnected = false;
  isInitiated = false;
  Serial.println("[BT] Client disconnected.");
}