import { ESPLoader, Transport } from 'esptool-js';

const FIRMWARE_URL = '/firmware/ESP32_GENERIC_S3-20251209-v1.27.0.bin';

function arrayBufferToBinaryString(buffer) {
  const bytes = new Uint8Array(buffer);
  const chunks = [];
  const chunkSize = 8192;
  for (let i = 0; i < bytes.length; i += chunkSize) {
    const slice = bytes.subarray(i, i + chunkSize);
    chunks.push(String.fromCharCode.apply(null, slice));
  }
  return chunks.join('');
}

export async function flashFirmware(onLog, onProgress) {
  const port = await navigator.serial.requestPort();

  // Buffer to track message state for proper newline insertion
  let messageBuffer = '';

  function normalizeAndLog(data) {
    messageBuffer += data;
    
    // Detect patterns that indicate message boundaries and insert newlines
    // Pattern 1: "Writing at" should always start on a new line (unless already at start/newline)
    messageBuffer = messageBuffer.replace(/([^\n])(Writing at)/g, '$1\n$2');
    
    // Pattern 2: Messages ending with "%)" followed by "Writing at" need a newline
    messageBuffer = messageBuffer.replace(/(%\))(Writing at)/g, '$1\n$2');
    
    // Pattern 3: Messages ending with "." or "..." followed by capital letters (like "Leaving...")
    messageBuffer = messageBuffer.replace(/(\.\.?)([A-Z])/g, '$1\n$2');
    
    // Pattern 4: "Wrote" messages should be on their own line
    messageBuffer = messageBuffer.replace(/([^\n])(Wrote \d+ bytes)/g, '$1\n$2');
    
    // Split on newlines and process complete lines
    const lines = messageBuffer.split('\n');
    // Keep the last potentially incomplete line in buffer
    messageBuffer = lines.pop() || '';
    
    // Log all complete lines
    for (const line of lines) {
      if (line.trim()) {
        onLog?.(line + '\n');
      }
    }
  }
  
  function flushBuffer() {
    if (messageBuffer.trim()) {
      onLog?.(messageBuffer + '\n');
      messageBuffer = '';
    }
  }

  const transport = new Transport(port);
  const esploader = new ESPLoader({
    transport,
    baudrate: 115200,
    romBaudrate: 115200,
    terminal: {
      clean() {},
      writeLine(data) { 
        // Flush any buffered content first
        flushBuffer();
        onLog?.(data + (data.endsWith('\n') ? '' : '\n')); 
      },
      write(data) {
        normalizeAndLog(data);
      },
    },
  });

  onLog?.('Connecting to ESP32-S3...\n');
  const chip = await esploader.main();
  flushBuffer();
  onLog?.(`Detected: ${chip}\n`);

  onLog?.('Fetching firmware...\n');
  const resp = await fetch(FIRMWARE_URL);
  if (!resp.ok) throw new Error(`Failed to fetch firmware: ${resp.status}`);
  const firmwareBuf = await resp.arrayBuffer();
  const firmwareBin = arrayBufferToBinaryString(firmwareBuf);
  onLog?.(`Firmware size: ${(firmwareBuf.byteLength / 1024).toFixed(0)} KB\n`);

  onLog?.('Erasing flash...\n');
  await esploader.eraseFlash();
  flushBuffer();
  onLog?.('Erase complete.\n');

  onLog?.('Writing firmware at 0x0...\n');
  await esploader.writeFlash({
    fileArray: [{ data: firmwareBin, address: 0x0 }],
    flashSize: 'keep',
    flashMode: 'keep',
    flashFreq: 'keep',
    eraseAll: false,
    compress: true,
    reportProgress(fileIndex, written, total) {
      const pct = Math.round((written / total) * 100);
      onProgress?.(pct);
    },
  });
  flushBuffer();

  onLog?.('\nFirmware written successfully!\n');
  onLog?.('Hard resetting device...\n');
  await esploader.after('hard_reset');
  flushBuffer();

  try {
    await transport.disconnect();
  } catch (_) {
    // already closed
  }

  onLog?.('Done! Device is ready.\n');
}
