#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <Adafruit_NeoPixel.h>
#include <EEPROM.h>
#include <time.h>
#include <WiFiManager.h>


// ================= LED =================
#define PIN D4
#define NUMPIXELS 110
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

// ================= SERVER =================
ESP8266WebServer server(80);

// ================= NTP =================
String ntpServer = "de.pool.ntp.org";
String tzString = "CET-1CEST,M3.5.0/02,M10.5.0/03";

// ================= SETTINGS =================
struct Settings {
  int brightness;
  int nightBrightness;
  bool autoNight;
  int nightStart;
  int nightEnd;
  uint32_t color;
  uint32_t bgColor;
  bool backgroundMode;
  char ntpServer[64];
  char tzString[64];
} settings;

// ================= VAR =================
uint32_t currentColor = 0x00FFFF;
uint32_t lastColor = 0x00FFFF;
uint32_t bgColor = 0x000000;
bool backgroundMode = false;
bool forceInstant = false;
bool updateNeeded = true;

int brightness = 120;
int nightBrightness = 10;
bool autoNight = true;
int nightStart = 22;
int nightEnd = 6;

uint32_t targetBuffer[NUMPIXELS];
uint32_t currentBuffer[NUMPIXELS];

int lastMinute = -1;

// ================= EEPROM =================
void saveSettings(){
  settings.brightness = brightness;
  settings.nightBrightness = nightBrightness;
  settings.autoNight = autoNight;
  settings.nightStart = nightStart;
  settings.nightEnd = nightEnd;
  settings.color = currentColor;
  settings.bgColor = bgColor;
  settings.backgroundMode = backgroundMode;
  strncpy(settings.ntpServer, ntpServer.c_str(), sizeof(settings.ntpServer));
  strncpy(settings.tzString, tzString.c_str(), sizeof(settings.tzString));

  EEPROM.put(0, settings);
  EEPROM.commit();
}



void loadSettings(){
  EEPROM.begin(512);
  EEPROM.get(0, settings);

  // Prüfen, ob EEPROM leer ist (über Helligkeit)
  if(settings.brightness == 0 || settings.brightness == 255){ 
    // Standardwerte setzen
    brightness = 120;
    nightBrightness = 10;
    autoNight = true;
    nightStart = 22;
    nightEnd = 6;
    currentColor = 0x00FFFF;
    bgColor = 0x000000;
    backgroundMode = false;
    ntpServer = "de.pool.ntp.org";
    tzString = "CET-1CEST,M3.5.0/02,M10.5.0/03";
  } else {
    brightness = settings.brightness;
    nightBrightness = settings.nightBrightness;
    autoNight = settings.autoNight;
    nightStart = settings.nightStart;
    nightEnd = settings.nightEnd;
    currentColor = settings.color;
    bgColor = settings.bgColor;
    backgroundMode = settings.backgroundMode;
    ntpServer = String(settings.ntpServer);
    tzString = String(settings.tzString);
  }
  
  // Zeitkonfiguration sofort anwenden
  configTime(tzString.c_str(), ntpServer.c_str());
}



// ================= HELFER =================
void clearTarget(){
  for(int i=0;i<NUMPIXELS;i++) targetBuffer[i]=0;
}

void setRange(int a,int b){
  for(int i=a;i<=b;i++) targetBuffer[i]=currentColor;
}

void setRangeColor(int a,int b, uint32_t col){
  for(int i=a;i<=b;i++) targetBuffer[i] = col;
}


uint32_t blend(uint32_t c1, uint32_t c2, float t){
  uint8_t r1=(c1>>16)&255, g1=(c1>>8)&255, b1=c1&255;
  uint8_t r2=(c2>>16)&255, g2=(c2>>8)&255, b2=c2&255;

  return pixels.Color(
    r1 + (r2 - r1) * t,
    g1 + (g2 - g1) * t,
    b1 + (b2 - b1) * t
  );
}

uint32_t applyBrightness(uint32_t c, float brightnessFactor){
  uint8_t r = ((c >> 16) & 255) * brightnessFactor;
  uint8_t g = ((c >> 8) & 255) * brightnessFactor;
  uint8_t b = (c & 255) * brightnessFactor;

  return pixels.Color(r,g,b);
}

uint8_t gamma8(uint8_t x){
  if(x<5) return 0;
  float f = x / 255.0;
  f = pow(f, 2.2);   // Gamma 2.2
  return (uint8_t)(f * 255);
}

uint32_t applyGammaBrightness(uint32_t c, float brightnessFactor){

  uint8_t r = ((c >> 16) & 255) * brightnessFactor;
  uint8_t g = ((c >> 8) & 255) * brightnessFactor;
  uint8_t b = (c & 255) * brightnessFactor;

  // 🔥 Gamma Korrektur
  r = gamma8(r);
  g = gamma8(g);
  b = gamma8(b);

  return pixels.Color(r,g,b);
}

void updateDisplayNow(){
  time_t now = time(nullptr);
  struct tm *t = localtime(&now);
  if(t){
    showTimeWords(t->tm_hour, t->tm_min);
  }
}

void showInstant(){

  time_t now = time(nullptr);
  struct tm *t = localtime(&now);

  float b = getBrightness(t) / 255.0;

  for(int i=0;i<NUMPIXELS;i++){
    uint32_t c = applyGammaBrightness(targetBuffer[i], b);
    pixels.setPixelColor(i, c);
  }

  pixels.show();
}

// ================= WORTUHR =================

// ES IST
void word_ES(){ setRange(0,1); }
void word_IST(){ setRange(3,5); }

// Minuten
void word_FUENF_M(){ setRange(7,10); }
void word_ZEHN_M(){ setRange(18,20); }
void word_ZWANZIG(){ setRange(11,17); }
void word_VIERTEL(){ setRange(22,28); }
void word_VOR(){ setRange(30,32); }
void word_NACH(){ setRange(40,43); }
void word_HALB(){ setRange(33,36); }

// Stunden
void word_EINS(){ setRange(62,65); }
void word_EIN(){ setRange(62,64); }
void word_ZWEI(){ setRange(55,58); }
void word_DREI(){ setRange(66,69); }
void word_VIER(){ setRange(73,76); }
void word_FUENF_H(){ setRange(51,54); }
void word_SECHS(){ setRange(83,87); }
void word_SIEBEN(){ setRange(88,93); }
void word_ACHT(){ setRange(77,80); }
void word_NEUN(){ setRange(44,47); }
void word_ZEHN_H(){ setRange(106,109); }
void word_ELF(){ setRange(103,105); }
void word_ZWOELF(){ setRange(94,98); }

// UHR
void word_UHR(){ setRange(99,101); }

// ================= TEXT =================
String hourToText(int h){
  String arr[]={"Zwölf","Eins","Zwei","Drei","Vier","Fünf","Sechs","Sieben","Acht","Neun","Zehn","Elf"};
  return arr[h%12];
}

String hourText(int h,bool full){
  int hh = h % 12;

  if(hh == 1){
    return full ? "ein" : "eins";
  }
  return hourToText(h);
}

String getTimeText(int h,int m){
  int m5=(m/5)*5;
  if(m5>=25) h++;

  String t="Es ist ";

  switch(m5){
    case 0: t+= hourText(h, true)+" Uhr"; break;
    case 5: t+="fünf nach "+hourToText(h); break;
    case 10: t+="zehn nach "+hourToText(h); break;
    case 15: t+="viertel nach "+hourToText(h); break;
    case 20: t+="zwanzig nach "+hourToText(h); break;
    case 25: t+="fünf vor halb "+hourToText(h); break;
    case 30: t+="halb "+hourToText(h); break;
    case 35: t+="fünf nach halb "+hourToText(h); break;
    case 40: t+="zwanzig vor "+hourToText(h); break;
    case 45: t+="viertel vor "+hourToText(h); break;
    case 50: t+="zehn vor "+hourToText(h); break;
    case 55: t+="fünf vor "+hourToText(h); break;
  }
  return t;
}

// ================= NACHTMODUS =================
int getBrightness(struct tm *t){
  if(!autoNight) return brightness;

  int h=t->tm_hour;
  if(nightStart > nightEnd){
    if(h >= nightStart || h < nightEnd) return nightBrightness;
  } else {
    if(h >= nightStart && h < nightEnd) return nightBrightness;
  }
  return brightness;
}


void fadeToTargetSmooth(int steps = 10, int delayMs = 5){

  time_t now = time(nullptr);
  struct tm *t = localtime(&now);

  float bTarget = getBrightness(t) / 255.0;

  for(int s = 0; s <= steps; s++){
    float tBlend = (float)s / steps;

    for(int i=0;i<NUMPIXELS;i++){

      uint32_t c = blend(currentBuffer[i], targetBuffer[i], tBlend);

      uint32_t finalColor = applyGammaBrightness(c, bTarget);

      pixels.setPixelColor(i, finalColor);
    }

    pixels.show();
    delay(delayMs);
  }
}
// ================= ANZEIGE =================
void showTimeWords(int h, int m){

  // 🔥 aktuellen Zustand als Start für Fade holen
  for(int i=0;i<NUMPIXELS;i++){
    currentBuffer[i] = pixels.getPixelColor(i);
  }

  // 🔥 Zielbuffer komplett neu aufbauen
  for(int i=0;i<NUMPIXELS;i++){
    if(backgroundMode){
      targetBuffer[i] = bgColor;   // frei wählbarer Hintergrund
    } else {
      targetBuffer[i] = 0;         // aus
    }
  }

  // Grundtext
  word_ES();
  word_IST();

  int m5 = (m/5)*5;
  bool full = (m5 == 0);

  if(m5 >= 25) h++;

  // Minuten
  switch(m5){
    case 0:  word_UHR(); break;
    case 5:  word_FUENF_M(); word_NACH(); break;
    case 10: word_ZEHN_M(); word_NACH(); break;
    case 15: word_VIERTEL(); word_NACH(); break;
    case 20: word_ZWANZIG(); word_NACH(); break;
    case 25: word_FUENF_M(); word_VOR(); word_HALB(); break;
    case 30: word_HALB(); break;
    case 35: word_FUENF_M(); word_NACH(); word_HALB(); break;
    case 40: word_ZWANZIG(); word_VOR(); break;
    case 45: word_VIERTEL(); word_VOR(); break;
    case 50: word_ZEHN_M(); word_VOR(); break;
    case 55: word_FUENF_M(); word_VOR(); break;
  }

  // Stunden
  int hh = h % 12;

  if(hh == 1 && full) word_EIN();
  else if(hh == 1) word_EINS();
  else if(hh == 2) word_ZWEI();
  else if(hh == 3) word_DREI();
  else if(hh == 4) word_VIER();
  else if(hh == 5) word_FUENF_H();
  else if(hh == 6) word_SECHS();
  else if(hh == 7) word_SIEBEN();
  else if(hh == 8) word_ACHT();
  else if(hh == 9) word_NEUN();
  else if(hh == 10) word_ZEHN_H();
  else if(hh == 11) word_ELF();
  else word_ZWOELF();

  // 🔥 jetzt alles weich darstellen (inkl. Gamma & Brightness)
  if(forceInstant){
    showInstant();
    forceInstant = false;
  }else{
    fadeToTargetSmooth(10,5);
  }
}
// ================= WEB =================
void handleNTP() {
  if (server.hasArg("val")) {
    ntpServer = server.arg("val");
    // Konfiguration sofort mit neuem Server aktualisieren
    configTime(tzString.c_str(), ntpServer.c_str());
    Serial.println("NTP Server geändert: " + ntpServer);
  }
  server.send(200, "text/plain", "OK");
}

void handleTZ() {
  if (server.hasArg("val")) {
    tzString = server.arg("val");
    // Konfiguration sofort mit neuer Zeitzone aktualisieren
    configTime(tzString.c_str(), ntpServer.c_str());
    Serial.println("Zeitzone geändert: " + tzString);
  }
  server.send(200, "text/plain", "OK");
}

void handleColor(){
  if(server.hasArg("c")){
    lastColor=currentColor;
    currentColor=strtoul(server.arg("c").c_str(),NULL,16);
    forceInstant = true;
    updateDisplayNow();

  }
  server.send(200,"text/plain","OK");
}

void handleBGColor(){
  if(server.hasArg("c")){
    bgColor=strtoul(server.arg("c").c_str(),NULL,16);
    forceInstant = true;
    updateNeeded = true;
  }
  server.send(200,"text/plain","OK");
}


void handleBrightness(){
  brightness=server.arg("val").toInt();
  forceInstant = true;
  updateNeeded = true;
  server.send(200,"text/plain","OK");
}

void handleNight(){
  autoNight=server.arg("val").toInt();
  forceInstant = true;
  updateNeeded = true;
  server.send(200,"text/plain","OK");
}

void handleBG(){
  backgroundMode=server.arg("val").toInt();
  forceInstant = true;
  updateNeeded = true;
  server.send(200,"text/plain","OK");
}

void handleSave(){
  saveSettings();
  server.send(200,"text/plain","saved");
}

void handleNightStart(){
  int val = server.arg("val").toInt();
  if(val >= 0 && val <= 23) {
    nightStart = val;
    forceInstant = true;
    updateNeeded = true;
    server.send(200, "text/plain", "OK");
  } else {
    server.send(400, "text/plain", "Ungueltiger Wert");
  }
}

void handleNightEnd(){
  int val = server.arg("val").toInt();
  
  if(val >= 0 && val <= 23) {
    nightEnd = val;
    forceInstant = true;
    updateNeeded = true;
    server.send(200, "text/plain", "OK");
  } else {
    server.send(400, "text/plain", "Ungueltiger Wert");
  }
}

void handleNightBrightness(){
  nightBrightness = server.arg("val").toInt();
  forceInstant = true;
  updateNeeded = true;
  server.send(200, "text/plain", "OK");
}

void handleStatus() {
  time_t now = time(nullptr);
  struct tm *t = localtime(&now);
  char hexCol[10];
  snprintf(hexCol, sizeof(hexCol), "#%06X", currentColor);
  char hexBg[10];
  snprintf(hexBg, sizeof(hexBg), "#%06X",bgColor);

  String ntpStat = (now > 100000) ? "OK" : "SYNC...";
  Serial.println(ntpStat);
  String json = "{";
  json += "\"txt\":\"" + getTimeText(t->tm_hour, t->tm_min) + "\",";
  json += "\"ip\":\"" + WiFi.localIP().toString() + "\",";
  json += "\"ntp\":\"" + ntpStat + "\",";
  json += "\"ntpServer\":\"" + ntpServer + "\",";
  json += "\"tzString\":\"" + tzString + "\",";
  json += "\"brightness\":" + String(brightness) + ",";
  json += "\"nightBrightness\":" + String(nightBrightness) + ",";
  json += "\"color\":\"" + String(hexCol) + "\",";
  json += "\"bgColor\":\"" + String(hexBg) + "\",";
  json += "\"night\":" + String(autoNight ? 1 : 0) + ",";
  json += "\"nightStart\":" + String(nightStart) + ",";
  json += "\"nightEnd\":" + String(nightEnd) + ",";
  json += "\"bg\":" + String(backgroundMode ? 1 : 0); // Hier KEIN Komma am Ende
  json += "}";

  server.send(200, "application/json", json);
}

// ================= HTML =================
void handleRoot(){
String html = R"rawliteral(
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<style>
  body{font-family:Arial,sans-serif; background:#fff; margin:0; color:#333; display:flex; flex-direction:column; align-items:center}
  .card{width:90%; max-width:400px; margin:10px; padding:15px; border-radius:12px; background:#f2f2f2; box-shadow: 0 2px 5px rgba(0,0,0,0.1)}
  .big{font-size:26px; text-align:center; font-weight:bold; color:#0055ff}
  #status{font-size:0.85em; color:#666; text-align:center}
  .grid{display:flex; gap:10px; align-items:center; margin-top:10px}
  .sub-label{font-size:0.85em; margin-top:10px; display:block; color:#555}
  input[type='number']{width:55px; padding:8px; border-radius:6px; border:1px solid #ccc; text-align:center}
  input[type='range'], input[type='color']{width:100%; cursor:pointer}
  button{width:100%; padding:12px; border-radius:8px; border:none; background:#0055ff; color:#fff; font-weight:bold; cursor:pointer}
  label{font-weight:bold; display:block; margin-bottom:5px}
</style>
</head>
<body>

<div class='card big' id='txt'>--:--</div>
<div class='card' id='status'>Verbinde...</div>

<div class='card'>
  <label>Wort-LED Farbe</label>
  <input type='color' id='col' onchange='c(this.value)'>
</div>

<div class='card'>
  <label>Standard Helligkeit</label>
  <input type='range' id='br' min='0' max='255' oninput='b(this.value)'>
</div>

<div class='card'>
  <input type='checkbox' id='night' onchange='n(this.checked)'> <b>Nachtmodus (Auto)</b>
    
  <span class='sub-label'>Zeitraum:</span>
  <div class='grid'>
    Von: <input type='number' id='nS' min='0' max='23' oninput='ns(this.value)' autocomplete='off'> Uhr
    Bis: <input type='number' id='nE' min='0' max='23' oninput='ne(this.value)' autocomplete='off'> Uhr
  </div>

  <span class='sub-label'>Nacht-Helligkeit:</span>
  <input type='range' id='nbr' min='0' max='255' oninput='nb(this.value)'>
</div>

<div class='card'>
  <input type='checkbox' id='bg' onchange='bgf(this.checked)'> <b>Hintergrund</b>
  <span class='sub-label'>Farbe</span>
  <input type='color' id='bgcol' onchange='bgc(this.value)'>
</div>

<div class='card'>
  <label>Zeit-Quellen</label>
  <span class='sub-label'>NTP Server:</span>
  <input type='text' id='ntpS' onchange='setNTP(this.value)' style='width:100%'>
  
  <span class='sub-label'>Zeitzone (POSIX TZ):</span>
  <input type='text' id='tzS' onchange='setTZ(this.value)' style='width:100%'>
  <a href='https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv' target='_blank' style='font-size:0.7em'>TZ-Datenbank Hilfe</a>
</div>

<div class='card'>
  <button onclick='save()'>Einstellungen im EEPROM speichern</button>
</div>




<script>
function c(v){fetch('/color?c='+v.substring(1))}
function bgc(v){fetch('/bgcolor?c='+v.substring(1))}
function b(v){fetch('/brightness?val='+v)}
function n(v){fetch('/night?val='+(v?1:0))}
function nb(v){fetch('/nightBrightness?val='+v)} // NEU: Nacht-Helligkeit
function bgf(v){fetch('/bg?val='+(v?1:0))}
function save(){fetch('/save').then(()=>alert('Gespeichert!'))}
function setNTP(v){ fetch('/setNTP?val=' + encodeURIComponent(v)); }
function setTZ(v){ fetch('/setTZ?val=' + encodeURIComponent(v)); }
function setNTP(v){ fetch('/setNTP?val=' + encodeURIComponent(v)); }
function setTZ(v){ fetch('/setTZ?val=' + encodeURIComponent(v)); }

function ns(v){
  if(v === "") return; // Warten, bis eine Zahl drin steht
  let val = parseInt(v);
  if(val < 0) val = 0; if(val > 23) val = 23;
  fetch('/nightStart?val=' + val);
}

function ne(v){
  if(v === "") return;
  let val = parseInt(v);
  if(val < 0) val = 0; if(val > 23) val = 23;
  fetch('/nightEnd?val=' + val);
}


function update(){
 fetch('/status').then(r=>r.json()).then(d=>{
  // Hilfsfunktion zum sicheren Setzen von Werten
  const setVal = (id, val, isCheck = false) => {
    const el = document.getElementById(id);
    if(!el) return; // Falls Element nicht existiert, überspringen
    if(document.activeElement === el) return; // Falls User gerade tippt, überspringen
    
    if(isCheck) el.checked = (val == 1);
    else el.value = val;
  };

  // Texte immer aktualisieren
  const txt = document.getElementById('txt');
  const stat = document.getElementById('status');
  if(txt) txt.innerHTML = d.txt;
  if(stat) stat.innerHTML = "IP: " + d.ip + " | NTP: " + d.ntp;
  
  // Alle Eingabefelder sicher aktualisieren
  setVal('col', d.color);
  setVal('bgcol', d.bgColor);
  setVal('br', d.brightness);
  setVal('nbr', d.nightBrightness);
  setVal('nS', d.nightStart);
  setVal('nE', d.nightEnd);
  setVal('ntpS', d.ntpServer);
  setVal('tzS', d.tzString);
  
  // Checkboxen
  setVal('night', d.night, true);
  setVal('bg', d.bg, true);

 }).catch(err => {
  console.error("Update-Fehler:", err);
  const stat = document.getElementById('status');
  if(stat) stat.innerHTML = "Verbindung unterbrochen...";
 });
}

setInterval(update, 2000);
update();
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}


// ================= SETUP =================
void setup(){
  Serial.begin(115200);

  WiFiManager wm;
  wm.autoConnect("Wortuhr");

  configTime(tzString.c_str(), ntpServer.c_str());
  for(int i=0;i<NUMPIXELS;i++){
  currentBuffer[i] = 0;
  }

  pixels.begin();
  loadSettings();

  server.on("/",handleRoot);
  server.on("/status",handleStatus);
  server.on("/color",handleColor);
  server.on("/bgcolor",handleBGColor);
  server.on("/brightness",handleBrightness);
  server.on("/night",handleNight);
  server.on("/nightStart",handleNightStart);
  server.on("/nightEnd",handleNightEnd);
  server.on("/nightBrightness",handleNightBrightness);
  server.on("/bg",handleBG);
  server.on("/save",handleSave);
  server.on("/setNTP", handleNTP);
  server.on("/setTZ", handleTZ);

  server.begin();
}

// ================= LOOP =================
void loop(){
  server.handleClient();

  time_t now=time(nullptr);
  struct tm *t=localtime(&now);

  if(t && t->tm_min !=lastMinute){
    lastMinute=t->tm_min;
    updateNeeded = true;
  }

  if(updateNeeded){
    showTimeWords(t->tm_hour,t->tm_min);
    updateNeeded = false;
  }
}