#include <ESP8266WiFi.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <EEPROM.h>

#define PRINT_CALLBACK  0
#define DEBUG 1

#if DEBUG
#define PRINT(s, v) { Serial.print(F(s)); Serial.print(v); }
#define PRINTS(s)   { Serial.print(F(s)); }
#else
#define PRINT(s, v)
#define PRINTS(s)
#endif

#define MAX_DEVICES 4
#define CLK_PIN   13
#define DATA_PIN  15
#define CS_PIN    2

#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);

// EEPROM storage
#define EEPROM_SIZE 512
#define SSID_ADDR 0
#define SSID_LEN 32
#define PASS_ADDR 32
#define PASS_LEN 64
#define VALID_FLAG_ADDR 96
#define VALID_FLAG 0xAB
#define DEFAULT_TEXT_ADDR 97
#define DEFAULT_TEXT_LEN 128

char ssid[SSID_LEN];
char password[PASS_LEN];
char defaultText[DEFAULT_TEXT_LEN];

WiFiServer server(80);

const uint8_t MESG_SIZE = 255;
const uint8_t CHAR_SPACING = 1;
const uint8_t SCROLL_DELAY = 55;

char curMessage[MESG_SIZE];
char newMessage[MESG_SIZE];
bool newMessageAvailable = false;
bool wifiConnected = false;

char WebResponse[] = "HTTP/1.1 200 OK\nContent-Type: text/html\n\n";
char WebPage[] =
  "<!DOCTYPE html>"
  "<html>"
  "<head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
  "<title>ESP8266 AND MAX7219</title>"
  "<style>"
  "html, body { font-family: Helvetica; display: block; margin: 0 auto; text-align: center; background-color: #cad9c5; }"
  "#container { width: 100%; height: 100%; margin-left: 5px; margin-top: 20px; border: solid 2px; padding: 10px; background-color: #2dfa53; }"
  "</style>"
  "<script>"
  "function SendText(event) {"
  "  event.preventDefault();"
  "  var txt = document.getElementById('txt_form').Message.value;"
  "  var request = new XMLHttpRequest();"
  "  request.open('GET','/&MSG='+encodeURIComponent(txt)+'/&' + '/&nocache='+Math.random()*1000000,false);"
  "  request.send(null);"
  "  document.getElementById('txt_form').Message.value = '';"
  "  return false;"
  "}"
  "</script>"
  "</head>"
  "<body>"
  "<h1><b>ESP8266 and MAX7219 LED Matrix WiFi Control</b></h1>"
  "<div id='container'>"
  "<form id='txt_form' name='frmText' onsubmit='SendText(event);'>"
  "<label>Message:<input type='text' name='Message' maxlength='255'></label><br><br>"
  "<input type='submit' value='Send Text'>"
  "</form>"
  "</div>"
  "</body>"
  "</html>";

//--------- EEPROM Functions ---------
void readCredentialsFromEEPROM()
{
  EEPROM.begin(EEPROM_SIZE);
  uint8_t validFlag = EEPROM.read(VALID_FLAG_ADDR);

  if (validFlag == VALID_FLAG)
  {
    PRINTS("Czytam dane z EEPROM...\n");
    for (int i = 0; i < SSID_LEN - 1; i++)
      ssid[i] = EEPROM.read(SSID_ADDR + i);
    ssid[SSID_LEN - 1] = '\0';

    for (int i = 0; i < PASS_LEN - 1; i++)
      password[i] = EEPROM.read(PASS_ADDR + i);
    password[PASS_LEN - 1] = '\0';

    for (int i = 0; i < DEFAULT_TEXT_LEN - 1; i++)
      defaultText[i] = EEPROM.read(DEFAULT_TEXT_ADDR + i);
    defaultText[DEFAULT_TEXT_LEN - 1] = '\0';
  }
  else
  {
    ssid[0] = '\0';
    password[0] = '\0';
    strcpy(defaultText, "Siemaneczko. Kopsnij SUBA! Dzieki! :]");
  }
}

void saveCredentialsToEEPROM()
{
  EEPROM.begin(EEPROM_SIZE);

  // Wyczyść stare dane
  for (int i = 0; i < SSID_LEN; i++)
    EEPROM.write(SSID_ADDR + i, 0);
  for (int i = 0; i < PASS_LEN; i++)
    EEPROM.write(PASS_ADDR + i, 0);
  for (int i = 0; i < DEFAULT_TEXT_LEN; i++)
    EEPROM.write(DEFAULT_TEXT_ADDR + i, 0);

  // Zapisz nowe dane
  for (int i = 0; i < SSID_LEN && ssid[i] != '\0'; i++)
    EEPROM.write(SSID_ADDR + i, ssid[i]);
  for (int i = 0; i < PASS_LEN && password[i] != '\0'; i++)
    EEPROM.write(PASS_ADDR + i, password[i]);
  for (int i = 0; i < DEFAULT_TEXT_LEN && defaultText[i] != '\0'; i++)
    EEPROM.write(DEFAULT_TEXT_ADDR + i, defaultText[i]);

  // Zapisz flagę
  EEPROM.write(VALID_FLAG_ADDR, VALID_FLAG);
  EEPROM.commit();

  PRINTS("Dane zapisane w EEPROM\n");
}

//--------- Polish Characters Support ---------
// Custom character definitions for Polish letters
const uint8_t PROGMEM polish_chars[][8] = {
  { 0x0, 0x7e, 0x9, 0xc9, 0x7e, 0x0, 0x0, 0x0}, // Ą (A z ogonkiem)
  { 0x38, 0x44, 0x44, 0xc4, 0x7c, 0x0, 0x0, 0x0}, // ą (a z ogonkiem)
  { 0x3c, 0x42, 0x46, 0x43, 0x42, 0x0, 0x0, 0x0}, // Ć (C z akcentem)
  { 0x38, 0x44, 0x4c, 0x46, 0x0, 0x0, 0x0, 0x0}, // ć (c z akcentem)
  { 0x3e, 0x49, 0x49, 0x49, 0xc9, 0x0, 0x0, 0x0}, // Ę (E z ogonkiem)
  { 0x38, 0x54, 0x54, 0xd4, 0x58, 0x0, 0x0, 0x0}, // ę (e z ogonkiem)
  { 0x7f, 0x50, 0x48, 0x44, 0x40, 0x0, 0x0, 0x0}, // Ł (L z kreską) - Kreska powinna być przez środek
  { 0x10, 0x7f, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0}, // ł (l z kreską) - Kreska pozioma
  { 0x7f, 0x8, 0x13, 0x20, 0x7f, 0x0, 0x0, 0x0}, // Ń (N z akcentem)
  { 0x7c, 0x4, 0xc, 0x6, 0x78, 0x0, 0x0, 0x0}, // ń (n z akcentem)
  { 0x7c, 0x82, 0x86, 0x83, 0x7c, 0x0, 0x0, 0x0}, // Ó (O z akcentem)
  { 0x38, 0x44, 0x46, 0x39, 0x0, 0x0, 0x0, 0x0}, // ó (o z akcentem)
  { 0x8c, 0x92, 0x96, 0x93, 0x62, 0x0, 0x0, 0x0}, // Ś (S z akcentem)
  { 0xb0, 0xa8, 0xa8, 0xac, 0x6a, 0x0, 0x0, 0x0}, // ś (s z akcentem)
  { 0xc4, 0xa4, 0x96, 0x8d, 0x84, 0x0, 0x0, 0x0}, // Ź (Z z akcentem)
  { 0x64, 0x54, 0x56, 0x4d, 0x44, 0x0, 0x0, 0x0}, // ź (z z akcentem)
  { 0xc2, 0xa2, 0x93, 0x8a, 0x86, 0x0, 0x0, 0x0}, // Ż (Z z kropką)
  { 0x64, 0x54, 0x55, 0x4c, 0x0, 0x0, 0x0, 0x0}
};

uint8_t getPolishChar(uint8_t c, uint8_t *buf)
{
  // Używamy prostego kodowania: 128-145 dla polskich znaków
  switch (c)
  {
    case 128: memcpy_P(buf, polish_chars[0], 8); return 5;  // Ą
    case 129: memcpy_P(buf, polish_chars[1], 8); return 5;  // ą
    case 130: memcpy_P(buf, polish_chars[2], 8); return 5;  // Ć
    case 131: memcpy_P(buf, polish_chars[3], 8); return 5;  // ć
    case 132: memcpy_P(buf, polish_chars[4], 8); return 5;  // Ę
    case 133: memcpy_P(buf, polish_chars[5], 8); return 5;  // ę
    case 134: memcpy_P(buf, polish_chars[6], 8); return 5;  // Ł
    case 135: memcpy_P(buf, polish_chars[7], 8); return 5;  // ł
    case 136: memcpy_P(buf, polish_chars[8], 8); return 5;  // Ń
    case 137: memcpy_P(buf, polish_chars[9], 8); return 5;  // ń
    case 138: memcpy_P(buf, polish_chars[10], 8); return 5; // Ó
    case 139: memcpy_P(buf, polish_chars[11], 8); return 5; // ó
    case 140: memcpy_P(buf, polish_chars[12], 8); return 5; // Ś
    case 141: memcpy_P(buf, polish_chars[13], 8); return 5; // ś
    case 142: memcpy_P(buf, polish_chars[14], 8); return 5; // Ź
    case 143: memcpy_P(buf, polish_chars[15], 8); return 5; // ź
    case 144: memcpy_P(buf, polish_chars[16], 8); return 5; // Ż
    case 145: memcpy_P(buf, polish_chars[17], 8); return 5; // ż
    default: return 0;
  }
}

// Funkcja do konwersji UTF-8 na własne kodowanie
void convertUtf8ToCustom(char *str)
{
  int i = 0, j = 0;
  char result[MESG_SIZE];

  while (str[i] != '\0' && j < MESG_SIZE - 1)
  {
    if ((uint8_t)str[i] == 0xC4) // Polskie znaki z C4
    {
      switch ((uint8_t)str[i + 1])
      {
        case 0x84: result[j++] = 128; i += 2; break; // Ą
        case 0x85: result[j++] = 129; i += 2; break; // ą
        case 0x86: result[j++] = 130; i += 2; break; // Ć
        case 0x87: result[j++] = 131; i += 2; break; // ć
        case 0x98: result[j++] = 132; i += 2; break; // Ę
        case 0x99: result[j++] = 133; i += 2; break; // ę
        default: result[j++] = str[i++]; break;
      }
    }
    else if ((uint8_t)str[i] == 0xC5) // Polskie znaki z C5
    {
      switch ((uint8_t)str[i + 1])
      {
        case 0x81: result[j++] = 134; i += 2; break; // Ł
        case 0x82: result[j++] = 135; i += 2; break; // ł
        case 0x83: result[j++] = 136; i += 2; break; // Ń
        case 0x84: result[j++] = 137; i += 2; break; // ń
        case 0x9A: result[j++] = 140; i += 2; break; // Ś
        case 0x9B: result[j++] = 141; i += 2; break; // ś
        case 0xB9: result[j++] = 142; i += 2; break; // Ź
        case 0xBA: result[j++] = 143; i += 2; break; // ź
        case 0xBB: result[j++] = 144; i += 2; break; // Ż
        case 0xBC: result[j++] = 145; i += 2; break; // ż
        default: result[j++] = str[i++]; break;
      }
    }
    else if ((uint8_t)str[i] == 0xC3) // Ó i ó
    {
      switch ((uint8_t)str[i + 1])
      {
        case 0x93: result[j++] = 138; i += 2; break; // Ó
        case 0xB3: result[j++] = 139; i += 2; break; // ó
        default: result[j++] = str[i++]; break;
      }
    }
    else
    {
      result[j++] = str[i++];
    }
  }
  result[j] = '\0';
  strcpy(str, result);
}

//--------------------------
uint8_t htoi(char c)
{
  c = toupper(c);
  if ((c >= '0') && (c <= '9')) return (c - '0');
  if ((c >= 'A') && (c <= 'F')) return (c - 'A' + 0xa);
  return (0);
}

// obsługa zarówno nowego jak i starego formatu
boolean getText(char *szMesg, char *psz, uint8_t len)
{
  char *pStart = strstr(szMesg, "/&MSG=");
  if (!pStart) pStart = strstr(szMesg, "/?MSG=");
  if (pStart)
  {
    pStart += (szMesg[2] == '&' ? 6 : 6);
    char *pEnd = strstr(pStart, "/&");
    if (!pEnd) pEnd = szMesg + strlen(szMesg);
    uint8_t i = 0;
    while (pStart < pEnd && i < len - 1)
    {
      if (*pStart == '%' && isxdigit(*(pStart + 1)) && isxdigit(*(pStart + 2)))
      {
        char val = (htoi(*(pStart + 1)) << 4) + htoi(*(pStart + 2));
        psz[i++] = val;
        pStart += 3;
      }
      else
      {
        psz[i++] = *pStart++;
      }
    }
    psz[i] = '\0';

    // Konwertuj UTF-8 na własne kodowanie
    convertUtf8ToCustom(psz);
    return true;
  }
  return false;
}

void handleWiFi()
{
  static enum { S_IDLE, S_WAIT_CONN, S_READ, S_EXTRACT, S_RESPONSE, S_DISCONN } state = S_IDLE;
  static char szBuf[1024];
  static uint16_t idxBuf = 0;
  static WiFiClient client;
  static uint32_t timeStart;

  switch (state)
  {
    case S_IDLE:
      idxBuf = 0; state = S_WAIT_CONN; break;

    case S_WAIT_CONN:
      client = server.available();
      if (!client || !client.connected()) break;
      timeStart = millis(); state = S_READ;
      break;

    case S_READ:
      while (client.available())
      {
        char c = client.read();
        if (c == '\r' || c == '\n') {
          szBuf[idxBuf] = '\0';
          client.flush();
          state = S_EXTRACT;
          break;
        }
        else szBuf[idxBuf++] = c;
      }
      if (millis() - timeStart > 1000) state = S_DISCONN;
      break;

    case S_EXTRACT:
      newMessageAvailable = getText(szBuf, newMessage, MESG_SIZE);
      state = S_RESPONSE;
      break;

    case S_RESPONSE:
      client.print(WebResponse);
      client.print(WebPage);
      state = S_DISCONN;
      break;

    case S_DISCONN:
      client.flush();
      client.stop();
      state = S_IDLE;
      break;

    default: state = S_IDLE;
  }
}

uint8_t scrollDataSource(uint8_t dev, MD_MAX72XX::transformType_t t)
{
  static enum { S_IDLE, S_NEXT_CHAR, S_SHOW_CHAR, S_SHOW_SPACE } state = S_IDLE;
  static char *p;
  static uint16_t curLen, showLen;
  static uint8_t cBuf[8];
  uint8_t colData = 0;

  switch (state)
  {
    case S_IDLE:
      p = curMessage;
      if (newMessageAvailable) {
        strcpy(curMessage, newMessage);
        newMessageAvailable = false;
      }
      state = S_NEXT_CHAR; break;

    case S_NEXT_CHAR:
      if (*p == '\0') state = S_IDLE;
      else
      {
        // Sprawdzamy czy to polski znak z własną definicją
        uint8_t customLen = getPolishChar(*p, cBuf);

        if (customLen > 0)
        {
          showLen = customLen;
        }
        else
        {
          // Użyj standardowej czcionki z biblioteki
          showLen = mx.getChar(*p, sizeof(cBuf) / sizeof(cBuf[0]), cBuf);
        }
        p++;
        curLen = 0;
        state = S_SHOW_CHAR;
      }
      break;

    case S_SHOW_CHAR:
      colData = cBuf[curLen++];
      if (curLen >= showLen) {
        showLen = (*p != '\0' ? CHAR_SPACING : (MAX_DEVICES * 8) / 2);
        curLen = 0;
        state = S_SHOW_SPACE;
      }
      break;

    case S_SHOW_SPACE: colData = 0; curLen++; if (curLen == showLen) state = S_NEXT_CHAR; break;

    default: state = S_IDLE;
  }
  return colData;
}

void scrollText()
{
  static uint32_t prevTime = 0;
  if (millis() - prevTime >= SCROLL_DELAY) {
    mx.transform(MD_MAX72XX::TSL);
    prevTime = millis();
  }
}

void handleSerialInput()
{
  static char buffer[256];
  static int bufIdx = 0;

  if (!Serial.available())
    return;

  char c = Serial.read();

  if (c == '\n' || c == '\r')
  {
    buffer[bufIdx] = '\0';
    bufIdx = 0;

    if (strncmp(buffer, "SSID:", 5) == 0)
    {
      char tempSSID[SSID_LEN];
      strncpy(tempSSID, buffer + 5, SSID_LEN - 1);
      tempSSID[SSID_LEN - 1] = '\0';
      strncpy(ssid, tempSSID, SSID_LEN - 1);
      ssid[SSID_LEN - 1] = '\0';
      saveCredentialsToEEPROM();
      Serial.print("SSID zmienione na: ");
      Serial.println(ssid);
    }
    else if (strncmp(buffer, "PASS:", 5) == 0)
    {
      char tempPass[PASS_LEN];
      strncpy(tempPass, buffer + 5, PASS_LEN - 1);
      tempPass[PASS_LEN - 1] = '\0';
      strncpy(password, tempPass, PASS_LEN - 1);
      password[PASS_LEN - 1] = '\0';
      saveCredentialsToEEPROM();
      Serial.print("Haslo zmienione na: ");
      Serial.println(password);
    }
    else if (strncmp(buffer, "TXT:", 4) == 0)
    {
      char tempText[DEFAULT_TEXT_LEN];
      strncpy(tempText, buffer + 4, DEFAULT_TEXT_LEN - 1);
      tempText[DEFAULT_TEXT_LEN - 1] = '\0';
      convertUtf8ToCustom(tempText); // Konwertuj polskie znaki
      strncpy(defaultText, tempText, DEFAULT_TEXT_LEN - 1);
      defaultText[DEFAULT_TEXT_LEN - 1] = '\0';
      strcpy(curMessage, defaultText);
      saveCredentialsToEEPROM();
      Serial.print("Tekst zmieniony na: ");
      Serial.println(defaultText);
    }
    else if (strcmp(buffer, "RESTART") == 0)
    {
      Serial.println("Restartowanie...");
      delay(500);
      ESP.restart();
    }
    else if (strcmp(buffer, "WIFI") == 0)
    {
      Serial.println("Laczenie z WiFi...");
      if (ssid[0] == '\0')
      {
        Serial.println("Brak danych WiFi!");
        return;
      }
      WiFi.begin(ssid, password);
      int attempts = 0;
      while (WiFi.status() != WL_CONNECTED && attempts < 20)
      {
        delay(500);
        Serial.print(".");
        attempts++;
      }
      Serial.println();

      if (WiFi.status() == WL_CONNECTED)
      {
        Serial.println("Polaczono!");
        char ipStr[20];
        sprintf(ipStr, "%03d:%03d:%03d:%03d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]);
        Serial.print("IP: ");
        Serial.println(ipStr);
        wifiConnected = true;
      }
      else
      {
        Serial.println("Blad podczas laczenia!");
        wifiConnected = false;
      }
    }
  }
  else if (c >= 32 && c < 127)
  {
    if (bufIdx < 255)
    {
      buffer[bufIdx++] = c;
    }
  }
}

void setup()
{
  Serial.begin(115200);
  delay(1000);

  mx.begin();
  mx.setShiftDataInCallback(scrollDataSource);

  curMessage[0] = newMessage[0] = '\0';

  readCredentialsFromEEPROM();

  // Konwertuj domyślny tekst na własne kodowanie
  convertUtf8ToCustom(defaultText);
  strcpy(curMessage, defaultText);

  if (ssid[0] == '\0')
  {
    if (Serial) Serial.println("Brak danych WiFi!");
    return;
  }

  if (Serial)
  {
    Serial.print("Lacze z WiFi: ");
    Serial.println(ssid);
  }
  WiFi.begin(ssid, password);

  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 20)
  {
    delay(500);
    if (Serial) Serial.print(".");
    attempts++;
  }
  if (Serial) Serial.println();

  if (WiFi.status() == WL_CONNECTED)
  {
    if (Serial) Serial.println("Polaczono!");

    char ipStr[20];
    sprintf(ipStr, "%03d:%03d:%03d:%03d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]);
    if (Serial)
    {
      Serial.print("IP: ");
      Serial.println(ipStr);
    }

    server.begin();
    wifiConnected = true;

    strcpy(curMessage, ipStr);
    unsigned long lastScrollTime = millis();
    bool ipDisplayed = false;
    while (!ipDisplayed) {
      scrollText();
      delay(10);
      if (millis() - lastScrollTime > 6600) ipDisplayed = true;
    }

    strcpy(curMessage, defaultText);
  }
  else
  {
    if (Serial) Serial.println("Brak polaczenia WiFi");
    wifiConnected = false;
  }
}

void loop()
{
  // Migaj piksel w prawym dolnym rogu jeśli brak WiFi
  if (!wifiConnected)
  {
    static uint32_t timeLastNoWiFi = 0;
    if (millis() - timeLastNoWiFi >= 500)
    {
      static bool ledOn = false;
      if (ledOn)
        mx.setPoint(MAX_DEVICES * 8 - 1, 7, false);
      else
        mx.setPoint(MAX_DEVICES * 8 - 1, 7, true);
      ledOn = !ledOn;
      timeLastNoWiFi = millis();
    }
  }

  handleSerialInput();
  handleWiFi();
  scrollText();

  // Co 5s wypisz wszystkie instrukcje
  static uint32_t lastSerialMsg = 0;

  if (millis() - lastSerialMsg >= 5000)
  {
    Serial.println("\nAby zmienić SSID, wyślij SSID:SSID");
    Serial.println("Aby zmienić HASŁO, wyślij PASS:HASŁO");
    Serial.println("Aby zmienić DOMYŚLNY TEKST, wyślij TXT:DOMYŚLNY TEKST");
    Serial.println("Aby restartować, wyślij RESTART");
    Serial.println("Aby połączyć z WiFi, wyślij WIFI");
    lastSerialMsg = millis();
  }
}
