/*
  NodeMCU Benzinskilt WebConfig
  Version: 1.2
  Jens Krogsgaard / ChatGPT

  Board: NodeMCU 1.0 (ESP-12E Module) / ESP8266

  TFT 1.77" ST7735 160x128 - samme kabling som dit gamle projekt:
    TFT_CS  = D2 / GPIO4
    TFT_DC  = D1 / GPIO5
    TFT_RST = D0 / GPIO16
    SCK     = D5 / GPIO14
    MOSI    = D7 / GPIO13
    VCC     = 3V3
    GND     = GND

  Funktioner:
    - Benzinskiltet kører også uden WiFi
    - WiFi setup portal hvis der ikke er gemt WiFi, eller forbindelsen fejler
    - Webkonfiguration af priser, stationsnavn, interval og tilfældig variation
    - OTA firmware upload via /firmware
    - mDNS: http://benzinskilt.local når WiFi er forbundet
*/

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <DNSServer.h>
#include <Updater.h>
#include <EEPROM.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>

#define APP_NAME     "NodeMCU Benzinskilt"
#define APP_VERSION  "1.2"

#define TFT_CS   D2
#define TFT_DC   D1
#define TFT_RST  D0

#define WHITE   ST7735_WHITE
#define BLUE    ST7735_BLUE
#define BLACK   ST7735_BLACK
#define YELLOW  ST7735_YELLOW
#define GREEN   ST7735_GREEN
#define RED     ST7735_RED

#define EEPROM_SIZE 1024
#define CONFIG_MAGIC 0xBEEF1101
#define DNS_PORT 53

struct AppConfig {
  uint32_t magic;
  char hostname[32];
  char wifiSsid[64];
  char wifiPassword[64];
  char stationName[16];
  uint16_t base95;        // øre, fx 1301 = 13,01
  int16_t plus100;        // øre, fx 87
  int16_t dieselOffset;   // øre, fx -111
  uint16_t min95;         // øre
  uint16_t max95;         // øre
  uint16_t intervalSec;
  bool randomMode;
  char language[3];       // DK, GB eller D
};

AppConfig cfg;
ESP8266WebServer server(80);
DNSServer dnsServer;
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

bool portalMode = false;
unsigned long lastPriceUpdate = 0;
unsigned long lastWifiAttempt = 0;
uint16_t price95 = 1301;
uint16_t price100 = 1388;
uint16_t priceDiesel = 1190;

String htmlEscape(String s) {
  s.replace("&", "&amp;"); s.replace("<", "&lt;"); s.replace(">", "&gt;"); s.replace("\"", "&quot;");
  return s;
}


String langCode() {
  if (strcmp(cfg.language, "GB") == 0) return "GB";
  if (strcmp(cfg.language, "D") == 0) return "D";
  return "DK";
}

String txt(const char* key) {
  String l = langCode();
  if (l == "GB") {
    if (!strcmp(key,"status")) return "Status";
    if (!strcmp(key,"prices")) return "Prices";
    if (!strcmp(key,"wifi")) return "WiFi";
    if (!strcmp(key,"system")) return "System";
    if (!strcmp(key,"firmware")) return "Firmware";
    if (!strcmp(key,"info")) return "Info";
    if (!strcmp(key,"station")) return "Station name";
    if (!strcmp(key,"random")) return "Random variation";
    if (!strcmp(key,"yesrandom")) return "Yes - like the old program";
    if (!strcmp(key,"norandom")) return "No - fixed 95 octane price";
    if (!strcmp(key,"base95")) return "Fixed 95 octane price";
    if (!strcmp(key,"interval")) return "Change interval, sec.";
    if (!strcmp(key,"min95")) return "Min. 95 price with variation";
    if (!strcmp(key,"max95")) return "Max. 95 price with variation";
    if (!strcmp(key,"plus100")) return "100 octane supplement";
    if (!strcmp(key,"diesel")) return "Diesel difference";
    if (!strcmp(key,"saveprices")) return "Save prices";
  }
  if (l == "D") {
    if (!strcmp(key,"status")) return "Status";
    if (!strcmp(key,"prices")) return "Preise";
    if (!strcmp(key,"wifi")) return "WLAN";
    if (!strcmp(key,"system")) return "System";
    if (!strcmp(key,"firmware")) return "Firmware";
    if (!strcmp(key,"info")) return "Info";
    if (!strcmp(key,"station")) return "Stationsname";
    if (!strcmp(key,"random")) return "Zufällige Variation";
    if (!strcmp(key,"yesrandom")) return "Ja - wie im alten Programm";
    if (!strcmp(key,"norandom")) return "Nein - fester 95-Oktan-Preis";
    if (!strcmp(key,"base95")) return "Fester 95-Oktan-Preis";
    if (!strcmp(key,"interval")) return "Wechselintervall, Sek.";
    if (!strcmp(key,"min95")) return "Min. 95-Preis bei Variation";
    if (!strcmp(key,"max95")) return "Max. 95-Preis bei Variation";
    if (!strcmp(key,"plus100")) return "100-Oktan-Zuschlag";
    if (!strcmp(key,"diesel")) return "Diesel-Differenz";
    if (!strcmp(key,"saveprices")) return "Preise speichern";
  }
  if (!strcmp(key,"status")) return "Status";
  if (!strcmp(key,"prices")) return "Priser";
  if (!strcmp(key,"wifi")) return "WiFi";
  if (!strcmp(key,"system")) return "System";
  if (!strcmp(key,"firmware")) return "Firmware";
  if (!strcmp(key,"info")) return "Info";
  if (!strcmp(key,"station")) return "Stationsnavn";
  if (!strcmp(key,"random")) return "Tilfældig variation";
  if (!strcmp(key,"yesrandom")) return "Ja - som det gamle program";
  if (!strcmp(key,"norandom")) return "Nej - fast 95 oktan pris";
  if (!strcmp(key,"base95")) return "Fast 95 oktan pris";
  if (!strcmp(key,"interval")) return "Skifteinterval, sek.";
  if (!strcmp(key,"min95")) return "Min 95 pris ved variation";
  if (!strcmp(key,"max95")) return "Max 95 pris ved variation";
  if (!strcmp(key,"plus100")) return "100 oktan tillæg";
  if (!strcmp(key,"diesel")) return "Diesel forskel";
  if (!strcmp(key,"saveprices")) return "Gem priser";
  return key;
}


String tStatusTitle() { return txt("status"); }
String tr(const char* dk, const char* gb, const char* de) {
  String l = langCode();
  if (l == "GB") return gb;
  if (l == "D") return de;
  return dk;
}

String flagButton(const String& code) {
  String s = F("<a class='flag f");
  s += code;
  if (langCode() == code) s += F(" current");
  s += F("' href='/language?lang=");
  s += code;
  s += F("'><b>");
  s += code;
  s += F("</b><span></span></a>");
  return s;
}

String navItem(const String& href, const String& icon, const String& text) {
  String cls = (server.uri() == href || (href == "/" && server.uri() == "/")) ? "nav active" : "nav";
  String s = F("<a class='"); s += cls; s += F("' href='"); s += href; s += F("'><span>"); s += icon; s += F("</span>"); s += text; s += F("</a>");
  return s;
}

String fmtOffset(int16_t value) {
  bool neg = value < 0;
  uint16_t v = neg ? -value : value;
  String out = neg ? "-" : "";
  out += String(v / 100);
  out += ",";
  if ((v % 100) < 10) out += "0";
  out += String(v % 100);
  return out;
}

String fmtPrice(uint16_t value) {
  uint16_t kr = value / 100;
  uint8_t ore = value % 100;
  String s = String(kr) + ",";
  if (ore < 10) s += "0";
  s += String(ore);
  return s;
}

uint16_t parsePriceOre(String s, uint16_t fallback) {
  s.trim();
  s.replace("kr", "");
  s.replace(" ", "");
  s.replace(",", ".");
  float v = s.toFloat();
  if (v < 1.0 || v > 99.0) return fallback;
  return (uint16_t)(v * 100.0 + 0.5);
}

int16_t parseOffsetOre(String s, int16_t fallback) {
  s.trim();
  s.replace("kr", "");
  s.replace(" ", "");
  s.replace(",", ".");
  float v = s.toFloat();
  if (v < -20.0 || v > 20.0) return fallback;
  return (int16_t)(v * 100.0 + (v >= 0 ? 0.5 : -0.5));
}

void setDefaults() {
  memset(&cfg, 0, sizeof(cfg));
  cfg.magic = CONFIG_MAGIC;
  strncpy(cfg.hostname, "benzinskilt", sizeof(cfg.hostname)-1);
  strncpy(cfg.stationName, "ARAL", sizeof(cfg.stationName)-1);
  cfg.base95 = 1301;
  cfg.plus100 = 87;
  cfg.dieselOffset = -111;
  cfg.min95 = 1301;
  cfg.max95 = 1599;
  cfg.intervalSec = 30;
  cfg.randomMode = true;
  strncpy(cfg.language, "DK", sizeof(cfg.language)-1);
}

void loadConfig() {
  EEPROM.begin(EEPROM_SIZE);
  EEPROM.get(0, cfg);
  if (cfg.magic != CONFIG_MAGIC || cfg.intervalSec == 0 || cfg.intervalSec > 3600) {
    setDefaults();
    EEPROM.put(0, cfg);
    EEPROM.commit();
  }
  if (strcmp(cfg.language, "DK") != 0 && strcmp(cfg.language, "GB") != 0 && strcmp(cfg.language, "D") != 0) {
    strncpy(cfg.language, "DK", sizeof(cfg.language)-1);
    saveConfig();
  }
}

void saveConfig() {
  cfg.magic = CONFIG_MAGIC;
  EEPROM.put(0, cfg);
  EEPROM.commit();
}

void drawFrame() {
  tft.fillScreen(BLUE);
  tft.setRotation(2);       // stik nedad, som i originalen
  tft.drawRect(0, 0, tft.width(), tft.height(), WHITE);
  tft.drawRect(0, 0, tft.width(), 40, WHITE);
  tft.drawRect(0, 40, tft.width(), 80, WHITE);
  tft.drawRect(0, 80, tft.width(), 120, WHITE);
  tft.drawRect(0, 120, tft.width(), 160, WHITE);
  tft.setTextColor(WHITE);

  tft.setCursor(22, 8);
  tft.setTextSize(3);
  tft.print(String(cfg.stationName).substring(0, 5));

  tft.setCursor(8, 55);
  tft.setTextSize(2);
  tft.print("95");
  tft.setTextSize(1);
  tft.setCursor(45, 62);
  tft.print("oktan");

  tft.setCursor(8, 95);
  tft.setTextSize(2);
  tft.print("100");
  tft.setTextSize(1);
  tft.setCursor(45, 102);
  tft.print("oktan");

  tft.setCursor(8, 135);
  tft.setTextSize(2);
  tft.print("Diesel");
}

void drawPriceLine(uint8_t y, uint16_t price) {
  String s = String(price);
  while (s.length() < 4) s = "0" + s;
  String kr = s.substring(0, s.length() - 2);
  String ore = s.substring(s.length() - 2);

  tft.fillRect(80, y - 10, 45, 30, BLACK);
  tft.setTextColor(YELLOW);
  tft.setTextSize(2);
  tft.setCursor(85, y);
  tft.print(kr);
  tft.setTextSize(1);
  tft.setCursor(110, y);
  tft.print(ore);
}

void drawPrices() {
  drawPriceLine(55, price95);
  drawPriceLine(95, price100);
  drawPriceLine(135, priceDiesel);
}

void calculatePrices() {
  if (cfg.randomMode) {
    uint16_t mn = min(cfg.min95, cfg.max95);
    uint16_t mx = max(cfg.min95, cfg.max95);
    price95 = random(mn, mx + 1);
  } else {
    price95 = cfg.base95;
  }

  int p100 = (int)price95 + cfg.plus100;
  int pdsl = (int)price95 + cfg.dieselOffset;
  price100 = constrain(p100, 0, 9999);
  priceDiesel = constrain(pdsl, 0, 9999);
  drawPrices();
}

void showTftMessage(const String& line1, const String& line2, const String& line3, const String& line4) {
  tft.fillScreen(BLUE);
  tft.setTextColor(WHITE);
  tft.setTextSize(2);
  tft.setCursor(6, 10); tft.print(line1);
  tft.setTextSize(1);
  tft.setCursor(6, 55); tft.print(line2);
  tft.setCursor(6, 75); tft.print(line3);
  tft.setCursor(6, 95); tft.print(line4);
}

String pageStart(const String& title) {
  String s;
  s.reserve(5200);
  s += F("<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>");
  s += F("<title>"); s += title; s += F("</title><style>");
  s += F("*{box-sizing:border-box}body{font-family:Arial,Helvetica,sans-serif;background:#f3f6f9;margin:0;color:#10202c;font-size:16px}.layout{display:flex;min-height:100vh}.side{width:245px;background:linear-gradient(180deg,#06354d,#09202c);color:white;position:fixed;inset:0 auto 0 0}.brand{text-align:center;padding:22px 16px 12px}.logo{width:96px;height:96px;border-radius:16px;background:white;margin:0 auto 12px;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 14px #0006}.langtitle{font-size:13px;font-weight:bold;color:#d9eef7;margin-top:16px;text-transform:uppercase;letter-spacing:.5px}.brand h1{font-size:27px;margin:8px 0 2px}.brand p{margin:0;color:#d9eef7}.flags{display:flex;justify-content:center;gap:18px;margin:20px 0 8px}.flag{color:#fff;text-decoration:none;font-size:12px;text-align:center;font-weight:bold;opacity:.92}.flag.current{opacity:1}.flag.current b{color:#fff}.flag span{display:block;width:42px;height:28px;border-radius:3px;margin-top:5px;box-shadow:0 2px 6px #0007;overflow:hidden;background-size:100% 100%}.fDK span{background:linear-gradient(90deg,transparent 0 31%,#fff 31% 43%,transparent 43%),linear-gradient(0deg,transparent 0 42%,#fff 42% 58%,transparent 58%),#c8102e}.fGB span{background:linear-gradient(146deg,transparent 0 43%,#fff 43% 48%,#c8102e 48% 54%,#fff 54% 59%,transparent 59%),linear-gradient(34deg,transparent 0 43%,#fff 43% 48%,#c8102e 48% 54%,#fff 54% 59%,transparent 59%),linear-gradient(90deg,transparent 0 39%,#fff 39% 45%,#c8102e 45% 55%,#fff 55% 61%,transparent 61%),linear-gradient(0deg,transparent 0 35%,#fff 35% 43%,#c8102e 43% 57%,#fff 57% 65%,transparent 65%),#012169}.fD span{background:linear-gradient(#000 0 33%,#dd0000 33% 66%,#ffce00 66%)}.nav{display:flex;align-items:center;gap:12px;color:white;text-decoration:none;font-size:20px;padding:15px 22px}.nav span{width:24px;text-align:center}.nav:hover,.active{background:#42798a}.foot{position:absolute;bottom:18px;left:18px;right:18px;font-size:13px;color:#d9eef7;line-height:1.5}.foot a{color:#9de0ff}.main{margin-left:245px;flex:1}.top{height:64px;background:white;border-bottom:1px solid #d8e0e5;display:flex;align-items:center;justify-content:space-between;padding:0 28px;box-shadow:0 2px 8px #0001}.content{padding:30px;max-width:1050px}.card{background:white;border:1px solid #e0e6ea;border-radius:9px;padding:22px;margin:0 0 22px;box-shadow:0 2px 13px #0001}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:18px}.val{font-size:32px;font-weight:800;color:#0a63b5}.ok{color:#078b22}.bad{color:#b22626}.tbl{width:100%;border-collapse:collapse}.tbl th,.tbl td{border:1px solid #d9e0e6;padding:10px 12px;text-align:left}.tbl th{background:#eef2f4;width:38%}label{display:block;font-weight:bold;margin-top:14px}input,select{width:100%;padding:11px;border:1px solid #ccd4db;border-radius:7px;font-size:16px;background:#fff}.formgrid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.hint{color:#667780;font-size:13px;margin-top:5px}.pricePreview{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:14px;margin-bottom:18px}.previewBox{background:#f6f9fb;border:1px solid #dde6eb;border-radius:8px;padding:14px}.previewBox b{display:block;margin-bottom:8px}button,.btn{background:#1f7890;color:white;border:0;border-radius:7px;padding:11px 18px;margin-top:16px;text-decoration:none;display:inline-block;font-weight:bold}.redbtn{background:#c13232}.grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px}.small{color:#667780;font-size:13px}@media(max-width:850px){.side{position:relative;width:100%}.main{margin-left:0}.layout{display:block}.top{display:none}.foot{position:static;margin-top:20px}.grid2,.formgrid{grid-template-columns:1fr}.content{padding:18px}.nav{font-size:18px}}");
  s += F("</style></head><body><div class='layout'><aside class='side'><div class='brand'><div class='logo'><svg viewBox='0 0 128 128' width='82' height='82' aria-label='Benzinpumpe'><rect x='24' y='18' width='58' height='92' rx='8' fill='#ef3131'/><rect x='31' y='26' width='44' height='30' rx='5' fill='#79cff4'/><rect x='34' y='32' width='27' height='8' rx='2' fill='#314b56'/><rect x='64' y='32' width='8' height='8' rx='2' fill='#314b56'/><rect x='34' y='44' width='27' height='8' rx='2' fill='#314b56'/><rect x='64' y='44' width='8' height='8' rx='2' fill='#314b56'/><rect x='18' y='108' width='72' height='8' rx='3' fill='#3b3b3b'/><path d='M82 36 C112 42 99 70 103 84 C107 99 120 93 118 78' fill='none' stroke='#707070' stroke-width='10' stroke-linecap='round'/><rect x='100' y='50' width='18' height='44' rx='6' fill='#bdbdbd'/></svg></div><h1>Benzinskilt</h1><p>NodeMCU + TFT + WiFi</p><div class='langtitle'>"); s += tr("Sprog","Language","Sprache"); s += F("</div><div class='flags'>");
  s += flagButton("DK"); s += flagButton("GB"); s += flagButton("D");
  s += F("</div></div>");
  s += navItem("/", "⌂", txt("status"));
  s += navItem("/prices", "⛽", txt("prices"));
  s += navItem("/wifi", "≋", txt("wifi"));
  s += navItem("/system", "⚙", txt("system"));
  s += navItem("/firmware", "↥", txt("firmware"));
  s += navItem("/info", "ⓘ", txt("info"));
  s += F("<div class='foot'>Benzinskilt v"); s += APP_VERSION; s += F("<br>© Jens Krogsgaard<br><a href='https://www.krogsgaardsmodelbane.dk'>krogsgaardsmodelbane.dk</a></div></aside><main class='main'><div class='top'><b>");
  s += htmlEscape(cfg.stationName); s += F("</b><span>"); s += htmlEscape(cfg.hostname); s += F(" | ");
  if (WiFi.isConnected()) s += WiFi.localIP().toString(); else if (portalMode) s += WiFi.softAPIP().toString(); else s += F("offline");
  s += F(" | "); s += langCode();
  s += F("</span></div><div class='content'>");
  return s;
}

String pageEnd() { return F("</div></main></div></body></html>"); }

void handleRoot() {
  String s = pageStart(txt("status"));
  s += F("<h1>"); s += txt("status"); s += F("</h1><div class='cards'>");
  s += F("<div class='card'><h3>95 oktan</h3><div class='val'>"); s += fmtPrice(price95); s += F("</div></div>");
  s += F("<div class='card'><h3>100 oktan</h3><div class='val'>"); s += fmtPrice(price100); s += F("</div></div>");
  s += F("<div class='card'><h3>Diesel</h3><div class='val'>"); s += fmtPrice(priceDiesel); s += F("</div></div></div>");
  s += F("<div class='card'><h2>"); s += tr("Forbindelse","Connection","Verbindung"); s += F("</h2><table class='tbl'>");
  s += F("<tr><th>WiFi</th><td>");
  if (WiFi.isConnected()) { s += F("<span class='ok'>"); s += tr("Forbundet","Connected","Verbunden"); s += F("</span>"); }
  else if (portalMode) { s += F("<span class='bad'>Setup portal</span>"); }
  else { s += F("Offline"); }
  s += F("</td></tr>");
  s += F("<tr><th>IP</th><td>"); if (WiFi.isConnected()) s += WiFi.localIP().toString(); else if (portalMode) s += WiFi.softAPIP().toString(); else s += F("-"); s += F("</td></tr>");
  s += F("<tr><th>Hostname</th><td>"); s += htmlEscape(cfg.hostname); s += F("</td></tr>");
  s += F("<tr><th>"); s += tr("Opdatering","Update","Aktualisierung"); s += F("</th><td>"); s += tr("Hvert","Every","Alle"); s += F(" "); s += cfg.intervalSec; s += F(" sek.</td></tr>");
  s += F("<tr><th>"); s += tr("Fri RAM","Free RAM","Freier RAM"); s += F("</th><td>"); s += ESP.getFreeHeap(); s += F(" bytes</td></tr></table></div>");
  s += pageEnd(); server.send(200, "text/html", s);
}

void handlePrices() {
  String s = pageStart(txt("prices"));
  s += F("<h1>"); s += txt("prices"); s += F("</h1>");
  s += F("<div class='card'><div class='pricePreview'>");
  s += F("<div class='previewBox'><b>95 oktan</b><div class='val'>"); s += fmtPrice(price95); s += F("</div></div>");
  s += F("<div class='previewBox'><b>100 oktan</b><div class='val'>"); s += fmtPrice(price100); s += F("</div></div>");
  s += F("<div class='previewBox'><b>Diesel</b><div class='val'>"); s += fmtPrice(priceDiesel); s += F("</div></div>");
  s += F("</div><form method='POST' action='/saveprices'>");
  s += F("<div class='formgrid'><div><label>"); s += txt("station"); s += F("</label><input name='station' maxlength='15' value='"); s += htmlEscape(cfg.stationName); s += F("'></div>");
  s += F("<div><label>"); s += txt("random"); s += F("</label><select name='random'><option value='1'"); if(cfg.randomMode) s += F(" selected"); s += F(">"); s += txt("yesrandom"); s += F("</option><option value='0'"); if(!cfg.randomMode) s += F(" selected"); s += F(">"); s += txt("norandom"); s += F("</option></select></div></div>");
  s += F("<div class='formgrid'><div><label>"); s += txt("base95"); s += F("</label><input name='base95' value='"); s += fmtPrice(cfg.base95); s += F("'></div><div><label>"); s += txt("interval"); s += F("</label><input name='interval' type='number' min='5' max='3600' value='"); s += cfg.intervalSec; s += F("'></div></div>");
  s += F("<div class='formgrid'><div><label>"); s += txt("min95"); s += F("</label><input name='min95' value='"); s += fmtPrice(cfg.min95); s += F("'></div><div><label>"); s += txt("max95"); s += F("</label><input name='max95' value='"); s += fmtPrice(cfg.max95); s += F("'></div></div>");
  s += F("<div class='formgrid'><div><label>"); s += txt("plus100"); s += F("</label><input name='plus100' value='"); s += fmtOffset(cfg.plus100); s += F("'><div class='hint'>Normalt 0,87</div></div><div><label>"); s += txt("diesel"); s += F("</label><input name='diesel' value='"); s += fmtOffset(cfg.dieselOffset); s += F("'><div class='hint'>Normalt -1,11</div></div></div>");
  s += F("<button>"); s += txt("saveprices"); s += F("</button></form></div>");
  s += pageEnd(); server.send(200, "text/html", s);
}

void handleSavePrices() {
  String station = server.arg("station"); station.trim(); if (station.length() == 0) station = "ARAL";
  station.toUpperCase(); strncpy(cfg.stationName, station.c_str(), sizeof(cfg.stationName)-1); cfg.stationName[sizeof(cfg.stationName)-1] = 0;
  cfg.randomMode = server.arg("random") == "1";
  cfg.base95 = parsePriceOre(server.arg("base95"), cfg.base95);
  cfg.min95 = parsePriceOre(server.arg("min95"), cfg.min95);
  cfg.max95 = parsePriceOre(server.arg("max95"), cfg.max95);
  cfg.plus100 = parseOffsetOre(server.arg("plus100"), cfg.plus100);
  cfg.dieselOffset = parseOffsetOre(server.arg("diesel"), cfg.dieselOffset);
  int iv = server.arg("interval").toInt(); if (iv < 5) iv = 5; if (iv > 3600) iv = 3600; cfg.intervalSec = iv;
  saveConfig(); drawFrame(); calculatePrices();
  server.sendHeader("Location", "/prices?saved=1"); server.send(303);
}

void handleWifi() {
  String s = pageStart(txt("wifi"));
  s += F("<h1>"); s += txt("wifi"); s += F("</h1><div class='card'><form method='POST' action='/savewifi'>");
  s += F("<label>SSID</label><input name='ssid' value='"); s += htmlEscape(cfg.wifiSsid); s += F("'>");
  s += F("<label>"); s += tr("Adgangskode","Password","Passwort"); s += F("</label><input name='pass' type='password' value='"); s += htmlEscape(cfg.wifiPassword); s += F("'>");
  s += F("<button>"); s += tr("Gem WiFi og genstart","Save WiFi and restart","WLAN speichern und neu starten"); s += F("</button></form></div>");
  s += F("<div class='card'><h2>"); s += tr("Scannede netværk","Scanned networks","Gefundene Netzwerke"); s += F("</h2><p>");
  int n = WiFi.scanNetworks();
  if (n <= 0) { s += tr("Ingen netværk fundet.","No networks found.","Keine Netzwerke gefunden."); }
  for (int i=0; i<n; i++) { s += htmlEscape(WiFi.SSID(i)); s += F(" ("); s += WiFi.RSSI(i); s += F(" dBm)<br>"); }
  s += F("</p></div>");
  s += pageEnd(); server.send(200, "text/html", s);
}

void handleSaveWifi() {
  strncpy(cfg.wifiSsid, server.arg("ssid").c_str(), sizeof(cfg.wifiSsid)-1); cfg.wifiSsid[sizeof(cfg.wifiSsid)-1] = 0;
  strncpy(cfg.wifiPassword, server.arg("pass").c_str(), sizeof(cfg.wifiPassword)-1); cfg.wifiPassword[sizeof(cfg.wifiPassword)-1] = 0;
  saveConfig();
  server.send(200, "text/html", pageStart(txt("wifi")) + String("<div class='card'>") + tr("WiFi gemt. Genstarter...","WiFi saved. Restarting...","WLAN gespeichert. Neustart...") + String("</div>") + pageEnd());
  delay(1000); ESP.restart();
}

void handleSystem() {
  String s = pageStart(txt("system"));
  s += F("<h1>"); s += txt("system"); s += F("</h1><div class='card'><form method='POST' action='/savesystem'>");
  s += F("<label>Hostname</label><input name='hostname' maxlength='31' value='"); s += htmlEscape(cfg.hostname); s += F("'>");
  s += F("<button>"); s += tr("Gem og genstart","Save and restart","Speichern und neu starten"); s += F("</button></form></div>");
  s += F("<div class='card'><a class='btn' href='/restart'>"); s += tr("Genstart","Restart","Neustart"); s += F("</a> <a class='btn redbtn' href='/resetconfig' onclick='return confirm(\"Nulstil al konfiguration?\")'>"); s += tr("Nulstil konfiguration","Reset configuration","Konfiguration zurücksetzen"); s += F("</a></div>");
  s += pageEnd(); server.send(200, "text/html", s);
}

void handleSaveSystem() {
  String h = server.arg("hostname"); h.trim(); h.toLowerCase(); if (h.length() == 0) h = "benzinskilt";
  strncpy(cfg.hostname, h.c_str(), sizeof(cfg.hostname)-1); cfg.hostname[sizeof(cfg.hostname)-1] = 0;
  saveConfig();
  server.send(200, "text/html", pageStart(txt("system")) + String("<div class='card'>") + tr("System gemt. Genstarter...","System saved. Restarting...","System gespeichert. Neustart...") + String("</div>") + pageEnd());
  delay(1000); ESP.restart();
}

void handleInfo() {
  String s = pageStart(txt("info"));
  s += F("<h1>"); s += txt("info"); s += F("</h1><div class='card'><h2>NodeMCU Benzinskilt</h2><table class='tbl'>");
  s += F("<tr><th>Version</th><td>v"); s += APP_VERSION; s += F("</td></tr>");
  s += F("<tr><th>Platform</th><td>NodeMCU ESP8266</td></tr>");
  s += F("<tr><th>Skærm</th><td>1.77 inch TFT ST7735 160x128</td></tr>");
  s += F("<tr><th>OTA</th><td>Firmware upload via web</td></tr>");
  s += F("<tr><th>Jens</th><td>Jens Krogsgaard / Krogsgaards Modelbane</td></tr>");
  s += F("</table></div>");
  s += pageEnd(); server.send(200, "text/html", s);
}


void handleLanguage() {
  String l = server.arg("lang");
  if (l == "DK" || l == "GB" || l == "D") {
    strncpy(cfg.language, l.c_str(), sizeof(cfg.language)-1);
    cfg.language[sizeof(cfg.language)-1] = 0;
    saveConfig();
  }
  String back = server.header("Referer");
  if (back.length() == 0) back = "/";
  server.sendHeader("Location", back);
  server.send(303);
}

void handleFirmwareGet() {
  String s = pageStart(txt("firmware"));
  s += F("<h1>"); s += txt("firmware"); s += F("</h1><div class='card'><p>"); s += tr("Vælg en kompileret .bin-fil fra Arduino IDE og upload direkte til NodeMCU.","Choose a compiled .bin file from Arduino IDE and upload directly to the NodeMCU.","Wähle eine kompilierte .bin-Datei aus der Arduino IDE und lade sie direkt auf den NodeMCU."); s += F("</p>");
  s += F("<form method='POST' action='/firmware' enctype='multipart/form-data'><input type='file' name='update'><button>"); s += tr("Upload firmware","Upload firmware","Firmware hochladen"); s += F("</button></form></div>");
  s += pageEnd(); server.send(200, "text/html", s);
}

void startWeb() {
  server.on("/", handleRoot);
  server.on("/prices", handlePrices);
  server.on("/saveprices", HTTP_POST, handleSavePrices);
  server.on("/wifi", handleWifi);
  server.on("/savewifi", HTTP_POST, handleSaveWifi);
  server.on("/system", handleSystem);
  server.on("/savesystem", HTTP_POST, handleSaveSystem);
  server.on("/info", handleInfo);
  server.on("/firmware", HTTP_GET, handleFirmwareGet);
  server.on("/language", handleLanguage);
  server.on("/firmware", HTTP_POST, [](){
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", pageStart(txt("firmware")) + String(Update.hasError() ? "<div class='card bad'>Firmware update fejlede.</div>" : "<div class='card ok'>Firmware opdateret. Genstarter...</div>") + pageEnd());
    delay(1000); ESP.restart();
  }, [](){
    HTTPUpload& upload = server.upload();
    if (upload.status == UPLOAD_FILE_START) {
      WiFiUDP::stopAll();
      uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
      Update.begin(maxSketchSpace);
    } else if (upload.status == UPLOAD_FILE_WRITE) {
      Update.write(upload.buf, upload.currentSize);
    } else if (upload.status == UPLOAD_FILE_END) {
      Update.end(true);
    }
  });
  server.on("/restart", [](){ server.send(200,"text/html", pageStart(txt("system")) + String("<div class='card'>") + tr("Genstarter...","Restarting...","Neustart...") + String("</div>") + pageEnd()); delay(1000); ESP.restart(); });
  server.on("/resetconfig", [](){ setDefaults(); saveConfig(); server.send(200,"text/html", pageStart(txt("system")) + String("<div class='card'>") + tr("Konfiguration nulstillet. Genstarter...","Configuration reset. Restarting...","Konfiguration zurückgesetzt. Neustart...") + String("</div>") + pageEnd()); delay(1000); ESP.restart(); });
  server.onNotFound([](){
    if (portalMode) { server.sendHeader("Location", String("http://") + WiFi.softAPIP().toString(), true); server.send(302, "text/plain", ""); }
    else handleRoot();
  });
  server.begin();
}

bool tryConnectWifi(uint32_t timeoutMs) {
  if (strlen(cfg.wifiSsid) == 0) return false;
  WiFi.mode(WIFI_STA);
  WiFi.hostname(cfg.hostname);
  WiFi.begin(cfg.wifiSsid, cfg.wifiPassword);
  uint32_t start = millis();
  while (millis() - start < timeoutMs) {
    server.handleClient();
    delay(50);
    if (WiFi.status() == WL_CONNECTED) return true;
  }
  return false;
}

void startPortal() {
  portalMode = true;
  WiFi.mode(WIFI_AP_STA);
  String apName = String(cfg.hostname) + "-setup";
  WiFi.softAP(apName.c_str());
  dnsServer.start(DNS_PORT, "*", WiFi.softAPIP());
  showTftMessage("Benzinskilt", "Setup mode", apName, WiFi.softAPIP().toString());
}

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println();
  Serial.println(F("NodeMCU Benzinskilt v1.2 starter"));
  randomSeed(analogRead(A0));

  loadConfig();
  tft.initR(INITR_BLACKTAB);
  drawFrame();
  calculatePrices();

  startWeb();

  if (tryConnectWifi(8000)) {
    portalMode = false;
    MDNS.begin(cfg.hostname);
    MDNS.addService("http", "tcp", 80);
    showTftMessage("Benzinskilt", "WiFi forbundet", WiFi.localIP().toString(), String(cfg.hostname) + ".local");
    delay(2500);
    drawFrame();
    drawPrices();
  } else {
    startPortal();
    delay(2500);
    drawFrame();
    drawPrices();
  }

  lastPriceUpdate = millis();
}

void loop() {
  if (portalMode) dnsServer.processNextRequest();
  server.handleClient();
  if (!portalMode && WiFi.isConnected()) MDNS.update();

  // Hvis gemt WiFi falder ud, forsøges genforbindelse i baggrunden. Skiltet fortsætter uanset hvad.
  if (!portalMode && strlen(cfg.wifiSsid) > 0 && WiFi.status() != WL_CONNECTED && millis() - lastWifiAttempt > 15000) {
    lastWifiAttempt = millis();
    WiFi.begin(cfg.wifiSsid, cfg.wifiPassword);
  }

  if (millis() - lastPriceUpdate >= (unsigned long)cfg.intervalSec * 1000UL) {
    lastPriceUpdate = millis();
    calculatePrices();
  }
}
