import { type APP_COMMAND_TYPES, FRAME_CONSTANTS, type FRAME_HEAD } from './Constants'
import { type ProtocolFrame } from './types'

export class ProtocolManager {
  static calculateChecksum(frameData: Uint8Array): number {
    let sum = 0
    // console.debug(`[ProtocolManager] Calculating checksum for frame count: ${frameData.length}`);
    // Checksum is calculated on all bytes except the last one (which is the checksum itself)
    // Example: 0xA0 03 00 01 01 5B
    // 0xA0 + 0x03 + 0x00 + 0x01 + 0x01 = 0xA5
    // 0 - 0xA5 = 0x5B
    for (let i = 0; i < frameData.length; i++) {
      sum += frameData[i]
    }
    const checksum = (~sum + 1) & 0xff
    // console.debug(`[ProtocolManager] Checksum calculated:  0 - ${sum} = ${checksum.toString(16).padStart(2, '0')}`);
    return checksum
  }

  static verifyChecksum(frameData: Uint8Array, expectedChecksum: number): boolean {
    const calculatedChecksum = this.calculateChecksum(frameData)
    const isValid = calculatedChecksum === expectedChecksum

    if (!isValid) {
      console.warn(
        `[ProtocolManager] Checksum mismatch: calculated=0x${calculatedChecksum.toString(16)}, expected=0x${expectedChecksum.toString(16)}`,
      )
    }

    // console.log('verifyChecksum-----------------', isValid)

    return isValid
  }

  static createFrame(
    type: APP_COMMAND_TYPES,
    data: Uint8Array,
    head: FRAME_HEAD = FRAME_CONSTANTS.HEAD_APP_TO_DEVICE,
    maxDataSize: number = FRAME_CONSTANTS.MAX_DATA_SIZE,
  ): Uint8Array[] {
    // Max pages index is 4 bytes, so we can fit up to 65535 pages of data
    const maxTotalSize = 0xffff * (FRAME_CONSTANTS.HEADER_SIZE + maxDataSize + FRAME_CONSTANTS.FOOTER_SIZE)
    if (data.length > maxTotalSize) {
      throw new Error(`Data size ${data.length} exceeds max size ${maxTotalSize}`)
    }
    if (data.length <= maxDataSize) {
      console.debug(`[ProtocolManager] Frame size ${data.length} is less than max size ${maxDataSize}, not fragmented`)
      return [this.createSingleFrame(type, data, head)]
    } else {
      console.debug(`[ProtocolManager] Frame size ${data.length} is greater than max size ${maxDataSize}, fragmented`)
      return this.createFragmentedFrames(type, data, head, maxDataSize)
    }
  }

  private static createSingleFrame(type: APP_COMMAND_TYPES, data: Uint8Array, head: FRAME_HEAD): Uint8Array {
    const buffer = new Uint8Array(FRAME_CONSTANTS.HEADER_SIZE + data.length + FRAME_CONSTANTS.FOOTER_SIZE)
    let offset = 0

    buffer[offset++] = head
    buffer[offset++] = type

    // subpageTotal = 0
    buffer[offset++] = 0
    buffer[offset++] = 0

    // curPage = 0
    buffer[offset++] = 0
    buffer[offset++] = 0

    // dataLen
    buffer[offset++] = (data.length >> 8) & 0xff
    buffer[offset++] = data.length & 0xff
    const hexHeader = Array.from(buffer.slice(0, offset))
      .map((b) => b.toString(16).padStart(2, '0'))
      .join(' ')
    // console.debug(`chunk length = ${data.length}, buffer 8 header = ${hexHeader}`)

    // data
    buffer.set(data, offset)
    offset += data.length

    // checksum
    // Logic from ProtocolUtilsV2: calculate sum of everything before checksum byte
    const checksum = this.calculateChecksum(buffer.slice(0, offset))
    buffer[offset] = checksum

    return buffer
  }

  private static createFragmentedFrames(
    type: APP_COMMAND_TYPES,
    data: Uint8Array,
    head: FRAME_HEAD,
    maxDataSize: number,
  ): Uint8Array[] {
    const frames: Uint8Array[] = []
    const totalSize = data.length
    const totalPages = Math.ceil(totalSize / maxDataSize)

    for (let i = 0; i < totalPages; i++) {
      const start = i * maxDataSize
      const end = Math.min(start + maxDataSize, totalSize)
      const chunk = data.slice(start, end)

      const buffer = new Uint8Array(FRAME_CONSTANTS.HEADER_SIZE + chunk.length + FRAME_CONSTANTS.FOOTER_SIZE)
      let offset = 0

      buffer[offset++] = head
      buffer[offset++] = type

      // subpageTotal
      buffer[offset++] = (totalPages >> 8) & 0xff
      buffer[offset++] = totalPages & 0xff

      // Protocol specifies: page numbers count down from highest to 0
      const curPageVal = totalPages - 1 - i

      buffer[offset++] = (curPageVal >> 8) & 0xff
      buffer[offset++] = curPageVal & 0xff

      // dataLen
      buffer[offset++] = (chunk.length >> 8) & 0xff
      buffer[offset++] = chunk.length & 0xff
      // const hexHeader = Array.from(buffer.slice(0, offset)).map(b => b.toString(16).padStart(2, '0')).join(' ');
      // console.debug(`chunk length = ${chunk.length}, buffer 8 header = ${hexHeader}`)

      // data
      buffer.set(chunk, offset)
      offset += chunk.length

      buffer[offset] = this.calculateChecksum(buffer.slice(0, offset))

      frames.push(buffer)
    }

    return frames
  }

  static parseFrame(data: ArrayBufferLike): ProtocolFrame | null {
    const bytes = new Uint8Array(data)
    if (bytes.length < FRAME_CONSTANTS.HEADER_SIZE + FRAME_CONSTANTS.FOOTER_SIZE) {
      return null
    }

    const head = bytes[0]
    if (head !== FRAME_CONSTANTS.HEAD_DEVICE_TO_APP && head !== FRAME_CONSTANTS.HEAD_APP_TO_DEVICE) {
      console.warn(`[ProtocolManager] Invalid frame header: 0x${head.toString(16)}`)
      return null
    }

    const type = bytes[1]
    const subpageTotal = (bytes[2] << 8) | bytes[3]
    const curPage = (bytes[4] << 8) | bytes[5]
    const dataLen = (bytes[6] << 8) | bytes[7]

    if (bytes.length < FRAME_CONSTANTS.HEADER_SIZE + dataLen + FRAME_CONSTANTS.FOOTER_SIZE) {
      // Incomplete
      return null
    }

    const frameData = bytes.slice(FRAME_CONSTANTS.HEADER_SIZE, FRAME_CONSTANTS.HEADER_SIZE + dataLen)
    const checksum = bytes[FRAME_CONSTANTS.HEADER_SIZE + dataLen]

    // Verify checksum
    const dataToCheck = bytes.slice(0, FRAME_CONSTANTS.HEADER_SIZE + dataLen)
    if (!this.verifyChecksum(dataToCheck, checksum)) {
      console.warn('Checksum mismatch')
      return null
    }

    return {
      head,
      type,
      subpageTotal,
      curPage,
      dataLen,
      data: frameData.buffer as ArrayBuffer,
      checksum,
    }
  }
}
