import * as zip from "./zip.min.js";

// Constants adapted from dfu/dfu_transport_serial.py
const DFU_TOUCH_BAUD = 1200;
const SERIAL_PORT_OPEN_WAIT_TIME = 0.1;
const TOUCH_RESET_WAIT_TIME = 1.5;

const DEFAULT_SERIAL_PORT_TIMEOUT = 1.0; // Timeout time on serial port read
const FLASH_PAGE_SIZE = 4096;
const FLASH_PAGE_ERASE_TIME = 0.0897; // nRF52840 max erase time
const FLASH_WORD_WRITE_TIME = 0.000100; // nRF52840 max write time
const FLASH_PAGE_WRITE_TIME = (FLASH_PAGE_SIZE / 4) * FLASH_WORD_WRITE_TIME;
const DFU_PACKET_MAX_SIZE = 512;

const DATA_INTEGRITY_CHECK_PRESENT = 1;
const RELIABLE_PACKET = 1;
const HCI_PACKET_TYPE = 14;

const DFU_INIT_PACKET = 1;
const DFU_START_PACKET = 3;
const DFU_DATA_PACKET = 4;
const DFU_STOP_DATA_PACKET = 5;
const DFU_ERASE_PAGE = 6; // Added for explicit page erase

const DFU_UPDATE_MODE_APP = 4;

// --- Utility Functions (adapted from dfu/util.py) ---

function int32ToBytes(value) {
  const buffer = new ArrayBuffer(4);
  const view = new DataView(buffer);
  view.setUint32(0, value, true); // Little-endian
  return new Uint8Array(buffer);
}

function int16ToBytes(value) {
  const buffer = new ArrayBuffer(2);
  const view = new DataView(buffer);
  view.setUint16(0, value, true); // Little-endian
  return new Uint8Array(buffer);
}

function slipPartsToFourBytes(seq, dip, rp, pktType, pktLen) {
  const ints = new Uint8Array(4);
  ints[0] = seq | (((seq + 1) % 8) << 3) | (dip << 6) | (rp << 7);
  ints[1] = pktType | ((pktLen & 0x000F) << 4);
  ints[2] = (pktLen & 0x0FF0) >> 4;
  ints[3] = (~(ints[0] + ints[1] + ints[2]) + 1) & 0xFF;
  return ints;
}

function slipEncodeEscChars(data) {
  const result = [];
  for (const byte of data) {
    if (byte === 0xC0) {
      result.push(0xDB, 0xDC);
    } else if (byte === 0xDB) {
      result.push(0xDB, 0xDD);
    } else {
      result.push(byte);
    }
  }
  return new Uint8Array(result);
}

// --- CRC16 Calculation (adapted from dfu/crc16.py) ---

function calcCrc16(data, crc = 0xFFFF) {
  if (!(data instanceof Uint8Array)) {
    throw new Error("calcCrc16 requires Uint8Array input");
  }
  for (let i = 0; i < data.length; i++) {
    crc = ((crc >> 8) & 0x00FF) | ((crc << 8) & 0xFF00);
    crc ^= data[i];
    crc ^= (crc & 0x00FF) >> 4;
    crc ^= (crc << 8) << 4;
    crc ^= ((crc & 0x00FF) << 4) << 1;
  }
  return crc & 0xFFFF;
}

function sleep(milliseconds) {
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
}

// --- HciPacket Class (adapted from dfu/dfu_transport_serial.py) ---

class HciPacket {
  static sequenceNumber = 0;

  constructor(data) {
    HciPacket.sequenceNumber = (HciPacket.sequenceNumber + 1) % 8;
    let tempData = [];

    const slipBytes = slipPartsToFourBytes(
      HciPacket.sequenceNumber,
      DATA_INTEGRITY_CHECK_PRESENT,
      RELIABLE_PACKET,
      HCI_PACKET_TYPE,
      data.length
    );
    tempData = tempData.concat(Array.from(slipBytes));

    tempData = tempData.concat(Array.from(data));

    // Add CRC
    const crc = calcCrc16(new Uint8Array(tempData));
    tempData.push(crc & 0xFF);
    tempData.push((crc & 0xFF00) >> 8);

    const encoded = slipEncodeEscChars(new Uint8Array(tempData));
    this.data = new Uint8Array([0xC0, ...encoded, 0xC0]);
  }
}


// --- Main DFU Class ---

export class Dfu {
  /**
   * @param {SerialPort} port - The Web Serial API port object.
   * @param {boolean} [eraseBeforeUpdate=false] - Whether to erase the entire flash before updating.
   */
  constructor(port, eraseBeforeUpdate = false) {
    this.port = port;
    this.transferInProgress = false;
    this.lastAck = -1;
    this.eraseBeforeUpdate = eraseBeforeUpdate; // Store the erase flag
  }

  getReader() {
    const reader = this.port.readable.getReader();

    return {
      read() {
        return new Promise((resolve, reject) => {
          const timeoutHandle = setTimeout(() => {
            reader.releaseLock();
            reject(new Error("Read timeout"));
          }, DEFAULT_SERIAL_PORT_TIMEOUT * 1000 * 5)

          reader.read().then(result => {
            clearTimeout(timeoutHandle);
            resolve(result);
          });
        });
      },
      releaseLock() {
        return reader.releaseLock();
      }
    }
  }

  async sendPacket(pkt) {
    if (!this.port || !this.port.writable) {
      throw new Error("Serial port not open or not writable.");
    }

    const writer = this.port.writable.getWriter();
    try {
      await writer.write(pkt.data);
      console.debug("Sent packet:", pkt.data.length);
    } finally {
      writer.releaseLock();
    }

    await this.getAck(); // Wait for ACK after sending
  }

  async getAck() {
    if (!this.port || !this.port.readable) {
      throw new Error("Serial port not open or not readable.");
    }

    const reader = this.getReader();
    let buffer = [];
    let c0Count = 0;

    try {
      const startTime = Date.now();
      while (c0Count < 2) {
        const { value, done } = await reader.read();
        if (done) {
          throw new Error("Stream closed before receiving full ACK.");
        }

        if (value) {
          for (const byte of value) {
            buffer.push(byte);
            if (byte === 0xC0) {
              c0Count++;
            }
          }
        }
      }
    }
    catch(e) {
      HciPacket.sequenceNumber = 0;
      throw e;
    }
    finally {
      reader.releaseLock();
    }

    // Extract the SLIP frame between the two 0xC0 delimiters, ignoring any
    // stale bytes that arrived before the opening delimiter.
    const firstC0 = buffer.indexOf(0xC0);
    const secondC0 = buffer.indexOf(0xC0, firstC0 + 1);
    if (firstC0 === -1 || secondC0 === -1) {
      throw new Error("Received incomplete ACK.");
    }
    const decodedData = this.decodeSlip(buffer.slice(firstC0 + 1, secondC0));

    if (decodedData.length < 2) {
      throw new Error("Received incomplete ACK.");
    }
    const ack = (decodedData[0] >> 3) & 0x07;

    // Check for valid ACK sequence
    if (this.lastAck !== -1 && ack !== (this.lastAck + 1) % 8) {
      HciPacket.sequenceNumber = 0; // Reset on bad ack
      throw new Error(`Invalid ACK sequence. Expected ${(this.lastAck + 1) % 8}, got ${ack}`);
    }
    this.lastAck = ack;

    return ack;
  }

  decodeSlip(data) {
    const result = [];
    let i = 0;
    while (i < data.length) {
      if (data[i] === 0xDB) {
        i++;
        if (i >= data.length) {
          throw new Error("Invalid SLIP escape sequence: incomplete.");
        }
        if (data[i] === 0xDC) {
          result.push(0xC0);
        } else if (data[i] === 0xDD) {
          result.push(0xDB);
        } else {
          throw new Error(`Invalid SLIP escape sequence: DB followed by ${data[i].toString(16)}`);
        }
      } else if (data[i] === 0xC0) {
        // Ignore 0xC0 (start/end of packet)
      }
      else {
        result.push(data[i]);
      }
      i++;
    }
    return new Uint8Array(result);
  }

  async sendInitPacket(initPacket) {
    const frame = new Uint8Array([
      ...int32ToBytes(DFU_INIT_PACKET),
      ...initPacket,
      ...int16ToBytes(0x0000), // Padding
    ]);
    const packet = new HciPacket(frame);
    await this.sendPacket(packet);
  }

  // THANKS Liam!!!
  static async forceDfuMode(port) {
    // open port
    await port.open({
      baudRate: DFU_TOUCH_BAUD,
    });

    // wait SERIAL_PORT_OPEN_WAIT_TIME before closing port
    await sleep(SERIAL_PORT_OPEN_WAIT_TIME * 1000);

    // close port
    await port.close();

    // wait TOUCH_RESET_WAIT_TIME for device to enter into DFU mode
    await sleep(TOUCH_RESET_WAIT_TIME * 1000);
  }

  async sendStartDfu(mode, softdeviceSize = 0, bootloaderSize = 0, appSize = 0) {
    const frame = new Uint8Array([
      ...int32ToBytes(DFU_START_PACKET),
      ...int32ToBytes(mode),
      ...int32ToBytes(softdeviceSize),
      ...int32ToBytes(bootloaderSize),
      ...int32ToBytes(appSize),
    ]);

    const packet = new HciPacket(frame);
    await this.sendPacket(packet);

    // Calculate and apply erase wait time.
    const totalSize = softdeviceSize + bootloaderSize + appSize;
    const eraseWaitTime = Math.max(0.5, ((totalSize / FLASH_PAGE_SIZE) + 1) * FLASH_PAGE_ERASE_TIME);
    await sleep(eraseWaitTime * 1000);
  }


  async sendErasePage(pageAddress) {
    const frame = new Uint8Array([
      ...int32ToBytes(DFU_ERASE_PAGE),
      ...int32ToBytes(pageAddress),
    ]);
    const packet = new HciPacket(frame);
    await this.sendPacket(packet);
    await sleep(FLASH_PAGE_ERASE_TIME * 1000); // Wait for page erase
  }


  async eraseFlash(appSize) {
    console.log("Erasing flash...");
    const numPages = Math.ceil(appSize / FLASH_PAGE_SIZE);

    // Assuming application starts at address 0x00000000
    let startAddress = 0x00000000;

    for (let i = 0; i < numPages; i++) {
      const pageAddress = startAddress + (i * FLASH_PAGE_SIZE);
      console.log(`Erasing page ${i} at address 0x${pageAddress.toString(16)}`);
      await this.sendErasePage(pageAddress);
    }
    console.log("Flash erase complete.");
  }


  async sendFirmware(firmware, progressCallback) {
    const frames = [];
    let totalBytes = firmware.length;

    // Chunk firmware into DFU packets
    for (let i = 0; i < firmware.length; i += DFU_PACKET_MAX_SIZE) {
      const chunk = firmware.subarray(i, i + DFU_PACKET_MAX_SIZE);
      const frame = new Uint8Array([
        ...int32ToBytes(DFU_DATA_PACKET),
        ...chunk,
      ]);
      const dataPacket = new HciPacket(frame);
      frames.push(dataPacket);
    }

    let bytesSent = 0;
    // Brief stabilization pause before starting data transfer (mirrors Python's implicit
    // pause at count=0 — it sleeps FLASH_PAGE_WRITE_TIME after the very first packet).
    await sleep(FLASH_PAGE_WRITE_TIME * 1000);

    // Send firmware packets
    for (const [index, pkt] of frames.entries()) {
      await this.sendPacket(pkt);
      bytesSent += pkt.data.length - 6; // Correctly calculate sent bytes, excluding SLIP overhead

      if (progressCallback) {
        const progress = Math.min(100, Math.round((bytesSent / totalBytes) * 100)); // Ensure progress doesn't exceed 100
        progressCallback(progress);
      }

      // Wait after every 8 frames (one flash page)
      if ((index + 1) % 8 === 0) {
        await sleep(FLASH_PAGE_WRITE_TIME * 1000);
      }
    }

    // Wait for the last page to be written
    await sleep(FLASH_PAGE_WRITE_TIME * 1000);

    // Send stop packet
    const stopPacket = new HciPacket(int32ToBytes(DFU_STOP_DATA_PACKET));
    await this.sendPacket(stopPacket);
  }

  async dfuUpdate(zipFile, progressCallback) {
    if (this.transferInProgress) {
      throw new Error("DFU update already in progress.");
    }
    this.transferInProgress = true;
    this.lastAck = -1; // Reset last ACK
    HciPacket.sequenceNumber = 0; // Reset HCI sequence number
    const decoder = new TextDecoder();
    try {
      await this.port.open({ baudRate: 115200 }); // Open with correct baudrate

      const reader = new zip.ZipReader(new zip.BlobReader(zipFile));
      const entries = await reader.getEntries();

      let manifest = null;
      let firmwareFiles = {};

      for (const entry of entries) {
        const filename = decoder.decode(entry.rawFilename);
        console.debug('Found zip filename: ', filename);
        if (filename === 'manifest.json') {
          const text = await entry.getData(new zip.TextWriter());
          manifest = JSON.parse(text);
        } else if (filename.endsWith('.bin') || filename.endsWith('.dat')) {
          firmwareFiles[filename] = await entry.getData(new zip.Uint8ArrayWriter());
        }
      }

      await reader.close();

      if (!manifest) {
        throw new Error("manifest.json not found in the ZIP file.");
      }
      if (!firmwareFiles[manifest.manifest.application.bin_file] ||
        !firmwareFiles[manifest.manifest.application.dat_file])
      {
        throw new Error("Application .bin or .dat file not found.");
      }

      const appBin = firmwareFiles[manifest.manifest.application.bin_file];
      const initPacket = firmwareFiles[manifest.manifest.application.dat_file];
      const appSize = appBin.length;

      // Erase flash if requested
      if (this.eraseBeforeUpdate) {
        await this.eraseFlash(appSize);
      }

      // Start DFU
      await this.sendStartDfu(DFU_UPDATE_MODE_APP, 0, 0, appSize);

      // Send Init Packet
      await this.sendInitPacket(initPacket);

      // Send Firmware
      await this.sendFirmware(appBin, progressCallback);

      console.log("DFU update complete.");

    } catch (error) {
      console.error("DFU Update failed:", error);
      throw error; // Re-throw the error for handling by the caller
    } finally {
      this.transferInProgress = false;
      if (this.port && this.port.readable) {
        try {
          const reader = this.port.readable.getReader();
          await reader.cancel();
          reader.releaseLock();

        } catch (error) {
          // Ignore errors when trying to cancel the reader
          console.debug(`Error: closing reader: ${error}`);
        }
      }
      if (this.port && this.port.writable) {
        try {
          const writer = this.port.writable.getWriter();
          await writer.close();
          writer.releaseLock();
        } catch(error) {
          // Ignore errors when trying to close the writer
          console.debug(`Error: closing writer: ${error}`);
        }
      }
      if (this.port) {
        try {
          await this.port.close();
        }
        catch (error) {
          // Ignore errors when trying to close the port
          console.debug(`Error: closing port: ${error}`);
        }
      }
    }
  }
}