#include <FastLED.h>
#include <WebServer.h>
#include <WiFi.h>

#define LED_TYPE SK9822HD
#define BRIGHTNESS 25
#define DATA_PIN 33
#define CLK_PIN 32
#define NUM_LEDS 38
#define HALL_PIN 39
#define HALL_THRESHOLD 1.4

#define WIDTH 90
#define HEIGHT 38
#define MAX_FRAMES 7

const char* ssid = "Wifi Name";
const char* password = "Wifi Password";

CRGB leds[NUM_LEDS];
CRGB frames[MAX_FRAMES][WIDTH][HEIGHT];

uint8_t frameCount = 1;
uint8_t currentFrame = 0;
uint16_t frameDelayMs = 80;
int rotationOffset = 0;
bool marqueeEnabled = false;
int marqueeRate = 20;
unsigned long lastMarqueeStep = 0;

WebServer server(80);

unsigned long lastTriggerTime = 0;
unsigned long rotationPeriod = 250;
bool hallTriggered = false;

const uint8_t numFont[10][7] = {
  { 0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110 },
  { 0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110 },
  { 0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111 },
  { 0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110 },
  { 0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010 },
  { 0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110 },
  { 0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110 },
  { 0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000 },
  { 0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110 },
  { 0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100 }
};

const char mainPage[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
  body{font-family:Arial,Helvetica,sans-serif;margin:20px;background:#f4f4f4}
  h1{text-align:center}
  #container{max-width:1200px;margin:0 auto}
  #controls{background:#fff;padding:12px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);text-align:center}
  canvas{display:block;margin:12px auto;border:2px solid #444;image-rendering:pixelated}
  button,input{margin:6px;padding:8px 12px;font-size:15px}
  #status{margin-left:12px;font-weight:bold;color:green}
</style>
</head>
<body>
<div id="container">
  <div id="controls">
    <input type="file" id="imageUpload" accept="image/*">
    <input type="color" id="colorPicker" value="#ff0000">
    <button id="clearCanvas">Clear</button>
    <button id="uploadButton">Upload to ESP32</button>
    <span id="status"></span>
  </div>
  <canvas id="canvas" width="90" height="38"></canvas>
</div>

<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });

function updateScale() {
  const maxWidth = window.innerWidth - 40;
  const scale = Math.min(Math.floor(maxWidth / 90), 14);
  const displayScale = Math.max(scale, 4);
  canvas.style.width = (90 * displayScale) + 'px';
  canvas.style.height = (38 * displayScale) + 'px';
}
updateScale();
window.addEventListener('resize', updateScale);

ctx.fillStyle = '#000';
ctx.fillRect(0,0,90,38);

let drawing = false;
let currentColor = "#ff0000";

function getCanvasPos(clientX, clientY) {
  const rect = canvas.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  return {
    x: Math.floor((clientX - rect.left) * scaleX),
    y: Math.floor((clientY - rect.top) * scaleY)
  };
}

canvas.addEventListener('mousedown', (e) => {
  drawing = true;
  const p = getCanvasPos(e.clientX, e.clientY);
  ctx.fillStyle = currentColor;
  ctx.fillRect(p.x, p.y, 1, 1);
});
canvas.addEventListener('mousemove', (e) => {
  if (!drawing) return;
  const p = getCanvasPos(e.clientX, e.clientY);
  ctx.fillStyle = currentColor;
  ctx.fillRect(p.x, p.y, 1, 1);
});
canvas.addEventListener('mouseup', () => drawing = false);
canvas.addEventListener('mouseleave', () => drawing = false);

canvas.addEventListener('touchstart', (e) => {
  e.preventDefault();
  drawing = true;
  const t = e.touches[0];
  const p = getCanvasPos(t.clientX, t.clientY);
  ctx.fillStyle = currentColor;
  ctx.fillRect(p.x, p.y, 1, 1);
}, { passive:false });

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault();
  if (!drawing) return;
  const t = e.touches[0];
  const p = getCanvasPos(t.clientX, t.clientY);
  ctx.fillStyle = currentColor;
  ctx.fillRect(p.x, p.y, 1, 1);
}, { passive:false });

canvas.addEventListener('touchend', () => drawing = false);

document.getElementById('colorPicker').addEventListener('input', (e)=>{
  currentColor = e.target.value;
});

document.getElementById('imageUpload').addEventListener('change', (e)=>{
  const file = e.target.files[0];
  if (!file) return;
  const img = new Image();
  img.onload = () => { ctx.drawImage(img, 0, 0, 90, 38); };
  img.src = URL.createObjectURL(file);
});

document.getElementById('clearCanvas').addEventListener('click', ()=>{
  ctx.fillStyle = '#000';
  ctx.fillRect(0,0,90,38);
});

function buildPayload(){
  const d = ctx.getImageData(0,0,90,38).data;
  const out = new Uint8Array(90*38*3);
  let p = 0;
  for (let x=0; x<90; x++) {
    for (let y=0; y<38; y++) {
      const i = (y*90 + x)*4;
      out[p++] = d[i];
      out[p++] = d[i+1];
      out[p++] = d[i+2];
    }
  }
  return out;
}

document.getElementById('uploadButton').addEventListener('click', async ()=>{
  const status = document.getElementById('status');
  status.style.color = 'black';
  status.textContent = 'Uploading...';

  try {
    const payload = buildPayload();
    const blob = new Blob([payload], { type:"application/octet-stream" });
    const fd = new FormData();
    fd.append("file", blob, "image.raw");

    const resp = await fetch("/upload", { method:"POST", body:fd });
    const txt = await resp.text();

    if (resp.ok) {
      status.style.color = "green";
      status.textContent = "OK: " + txt;
    } else {
      status.style.color = "red";
      status.textContent = "ERR: " + txt;
    }
    setTimeout(() => status.textContent="", 2000);

  } catch (err) {
    status.style.color = 'red';
    status.textContent = 'Upload failed';
    setTimeout(() => status.textContent="", 2000);
  }
});
</script>
</body>
</html>
)rawliteral";

void drawNum(int xOff, int yOff, int digit) {
  if (digit < 0 || digit > 9) return;
  for (int y = 0; y < 7; y++)
    for (int x = 0; x < 5; x++)
      if (numFont[digit][y] & (1 << (4 - x))) {
        int xx = xOff + x, yy = yOff + y;
        if (xx >= 0 && xx < WIDTH && yy >= 0 && yy < HEIGHT)
          frames[0][xx][yy] = CRGB(0, 180, 255);
      }
}

unsigned long uploadByteCounter = 0;
uint8_t uploadFrameIndex = 0;

void handleUploadData() {
  HTTPUpload& up = server.upload();
  if (up.status == UPLOAD_FILE_START) {
    uploadByteCounter = 0;
    uploadFrameIndex = server.hasArg("frame") ? server.arg("frame").toInt() : 0;
    if (uploadFrameIndex >= MAX_FRAMES) uploadFrameIndex = 0;
  } else if (up.status == UPLOAD_FILE_WRITE) {
    for (int i = 0; i < up.currentSize; i++) {
      unsigned long bc = uploadByteCounter++;
      unsigned long pixel = bc / 3;
      if (pixel >= WIDTH * HEIGHT) continue;
      int ch = bc % 3;
      int x = pixel / HEIGHT, y = pixel % HEIGHT;
      CRGB& px = frames[uploadFrameIndex][x][y];
      if (ch == 0) px.r = up.buf[i];
      else if (ch == 1) px.g = up.buf[i];
      else px.b = up.buf[i];
    }
  }
}

void handleUploadFinish() {
  server.send(200, "text/plain", "OK");
}
void handleSetMeta() {
  frameCount = server.arg("frames").toInt();
  frameDelayMs = server.arg("delay").toInt();

  marqueeEnabled = server.hasArg("marquee") && server.arg("marquee").toInt() == 1;

  if (server.hasArg("offset"))
    rotationOffset = server.arg("offset").toInt() % WIDTH;

  if (server.hasArg("marqueeRate"))
    marqueeRate = max(1, (int)server.arg("marqueeRate").toInt());

  if (frameCount < 1) frameCount = 1;
  if (frameCount > MAX_FRAMES) frameCount = MAX_FRAMES;
  if (frameDelayMs < 10) frameDelayMs = 10;

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

/* ===== SETUP ===== */
void setup() {
  FastLED.addLeds<LED_TYPE, DATA_PIN, CLK_PIN, BGR, DATA_RATE_MHZ(24)>(leds, NUM_LEDS);
  FastLED.setBrightness(BRIGHTNESS);
  pinMode(HALL_PIN, INPUT);
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  Serial.println("Connecting to Wifi...");
  unsigned long wifiStart = millis();
  int dotPos = 0;
  int dotDir = 1;
  unsigned long lastAnim = 0;
  int timeouts = 0;

  while (WiFi.status() != WL_CONNECTED && timeouts <= 5) {
    unsigned long now = millis();
    if (now - lastAnim > 40) {
      lastAnim = now;

      for (int i = 0; i < NUM_LEDS; i++)
        leds[i] = CRGB::Black;

      leds[dotPos] = CRGB(255, 255, 255);
      FastLED.show();

      dotPos += dotDir;
      if (dotPos >= NUM_LEDS - 1) dotDir = -1;
      if (dotPos <= 0) dotDir = 1;
    }

    if (millis() - wifiStart > 10000) {
      Serial.println("Wifi timeout, retrying...");
      WiFi.disconnect(true);
      delay(500);
      WiFi.begin(ssid, password);
      wifiStart = millis();
      timeouts++;
    }

    delay(10);
  }

  String ip;

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("Wifi connected!");
    for (int f = 0; f < MAX_FRAMES; f++)
      for (int x = 0; x < WIDTH; x++)
        for (int y = 0; y < HEIGHT; y++)
          frames[f][x][y] = CRGB::Green;
    for (int i = 0; i < NUM_LEDS; i++)
      leds[i] = CRGB::Green;
    FastLED.show();
    delay(5000);
    ip = WiFi.localIP().toString();
  } else {
    WiFi.disconnect(true);
    WiFi.mode(WIFI_AP);
    WiFi.softAP("POV Globe", "12345678");
    Serial.println("Fallback to access point mode");
    for (int f = 0; f < MAX_FRAMES; f++)
      for (int x = 0; x < WIDTH; x++)
        for (int y = 0; y < HEIGHT; y++)
          frames[f][x][y] = CRGB::Black;
    for (int i = 0; i < NUM_LEDS; i++)
      leds[i] = CRGB::Blue;
    FastLED.show();
    delay(5000);
    ip = WiFi.softAPIP().toString();
  }

  for (int x = 0; x < WIDTH; x++)
    for (int y = 0; y < HEIGHT; y++)
      frames[0][x][y] = CRGB::Black;

  int width = 0;
  for (unsigned int i = 0; i < ip.length(); i++) {
    if (ip[i] == '.') width += 2;
    else width += 6;
  }

  int xStart = (WIDTH - width) / 2;
  int yStart = (HEIGHT - 7) / 2;
  int x = xStart;

  for (unsigned int i = 0; i < ip.length(); i++) {
    char c = ip[i];
    if (c == '.') {
      if (x >= 0 && x < WIDTH && yStart + 6 < HEIGHT)
        frames[0][x][yStart + 6] = CRGB(0, 180, 255);
      x += 2;
    } else if (c >= '0' && c <= '9') {
      drawNum(x, yStart, c - '0');
      x += 6;
    }
    if (x > WIDTH - 3) break;
  }
  frameCount = 1;
  currentFrame = 0;

  server.on("/", HTTP_GET, []() {
    server.send_P(200, "text/html", mainPage);
  });
  server.on("/upload", HTTP_POST, handleUploadFinish, handleUploadData);
  server.on("/setMeta", HTTP_GET, handleSetMeta);
  server.begin();

  lastTriggerTime = millis();
}

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

  float hallValue = analogRead(HALL_PIN) * (3.3 / 4095.0);
  if (hallValue < HALL_THRESHOLD && !hallTriggered) {
    hallTriggered = true;
    unsigned long now = millis();
    rotationPeriod = now - lastTriggerTime;
    lastTriggerTime = now;
  }
  if (hallValue > HALL_THRESHOLD) hallTriggered = false;

  if (marqueeEnabled) {
    unsigned long now = millis();
    if (now - lastMarqueeStep >= marqueeRate) {
      lastMarqueeStep = now;
      rotationOffset++;
      if (rotationOffset >= WIDTH)
        rotationOffset = 0;
    }
  }

  static unsigned long lastFrameSwitch = 0;
  if (millis() - lastFrameSwitch > frameDelayMs) {
    currentFrame = (currentFrame + 1) % frameCount;
    lastFrameSwitch = millis();
  }

  unsigned long elapsed = millis() - lastTriggerTime;
  unsigned long rp = rotationPeriod ? rotationPeriod : 1;
  int section = (elapsed * WIDTH) / rp;
  section = (section + rotationOffset) % WIDTH;

  for (int i = 0; i < NUM_LEDS; i++)
    leds[i] = frames[currentFrame][section][NUM_LEDS - 1 - i];

  FastLED.show();
}
