import { Device } from "../device.model.js"
import type { IMotherboard } from "../../interfaces/device/motherboard.interface.js"

/**
 * Represents a Griptonite Motherboard device.
 * {@link https://griptonite.io}
 */
export class Motherboard extends Device implements IMotherboard {
  /**
   * Length of the packet received from the device.
   * @type {number}
   * @static
   * @readonly
   * @constant
   */
  private static readonly packetLength: number = 32

  /**
   * Number of samples contained in the data packet.
   * @type {number}
   * @static
   * @readonly
   * @constant
   */
  private static readonly samplesNumber: number = 3

  /**
   * Buffer to store received data from the device.
   * @type {number[]}
   * @private
   */
  private receiveBuffer: number[] = []

  /**
   * Calibration data for each sensor of the device.
   * @type {number[][][]}
   * @private
   */
  private calibrationData: number[][][] = [[], [], [], []]

  /** Per-zone peak and running sum for left/center/right (used for distribution stats). */
  private leftPeak = Number.NEGATIVE_INFINITY
  private leftSum = 0
  private centerPeak = Number.NEGATIVE_INFINITY
  private centerSum = 0
  private rightPeak = Number.NEGATIVE_INFINITY
  private rightSum = 0

  constructor() {
    super({
      filters: [{ name: "Motherboard" }],
      services: [
        {
          name: "Device Information",
          id: "device",
          uuid: "0000180a-0000-1000-8000-00805f9b34fb",
          characteristics: [
            // {
            //     name: 'Serial Number String (Blocked)',
            //     id: 'serial'
            //     uuid: '00002a25-0000-1000-8000-00805f9b34fb'
            // },
            {
              name: "Firmware Revision String",
              id: "firmware",
              uuid: "00002a26-0000-1000-8000-00805f9b34fb",
            },
            {
              name: "Hardware Revision String",
              id: "hardware",
              uuid: "00002a27-0000-1000-8000-00805f9b34fb",
            },
            {
              name: "Manufacturer Name String",
              id: "manufacturer",
              uuid: "00002a29-0000-1000-8000-00805f9b34fb",
            },
          ],
        },
        {
          name: "Battery Service",
          id: "battery",
          uuid: "0000180f-0000-1000-8000-00805f9b34fb",
          characteristics: [
            {
              name: "Battery Level",
              id: "level",
              uuid: "00002a19-0000-1000-8000-00805f9b34fb",
            },
          ],
        },
        {
          name: "LED Service",
          id: "led",
          uuid: "10ababcd-15e1-28ff-de13-725bea03b127",
          characteristics: [
            {
              name: "Red LED",
              id: "red",
              uuid: "10ab1524-15e1-28ff-de13-725bea03b127",
            },
            {
              name: "Green LED",
              id: "green",
              uuid: "10ab1525-15e1-28ff-de13-725bea03b127",
            },
          ],
        },
        {
          name: "UART Nordic Service",
          id: "uart",
          uuid: "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
          characteristics: [
            {
              name: "TX",
              id: "tx",
              uuid: "6e400002-b5a3-f393-e0a9-e50e24dcca9e",
            },
            {
              name: "RX",
              id: "rx",
              uuid: "6e400003-b5a3-f393-e0a9-e50e24dcca9e",
            },
          ],
        },
      ],
      commands: {
        GET_SERIAL: "#",
        START_WEIGHT_MEAS: "S30",
        STOP_WEIGHT_MEAS: "", // All commands will stop the data stream.
        GET_CALIBRATION: "C",
        SLEEP: 0,
        GET_TEXT: "T",
        DEBUG_STREAM: "D",
      },
    })
  }

  /**
   * Applies calibration to a sample value.
   * @param {number} sample - The sample value to calibrate.
   * @param {number[][]} calibration - The calibration data.
   * @returns {number} The calibrated sample value.
   */
  private applyCalibration = (sample: number, calibration: number[][]): number => {
    // Extract the calibrated value for the zero point
    const zeroCalibration: number = calibration[0][2]
    // Initialize sign as positive
    let sign = 1
    // Initialize the final calibrated value
    let final = 0

    // If the sample value is less than the zero calibration point
    if (sample < zeroCalibration) {
      // Change the sign to negative
      sign = -1
      // Reflect the sample around the zero calibration point
      sample = /* 2 * zeroCalibration */ -sample
    }

    // Iterate through the calibration data
    for (let i = 1; i < calibration.length; i++) {
      // Extract the lower and upper bounds of the current calibration range
      const calibrationStart: number = calibration[i - 1][2]
      const calibrationEnd: number = calibration[i][2]

      // If the sample value is within the current calibration range
      if (sample < calibrationEnd) {
        // Interpolate to get the calibrated value within the range
        final =
          calibration[i - 1][1] +
          ((sample - calibrationStart) / (calibrationEnd - calibrationStart)) *
            (calibration[i][1] - calibration[i - 1][1])
        break
      }
    }
    // Return the calibrated value with the appropriate sign (positive/negative)
    return sign * final
  }

  /**
   * Retrieves battery or voltage information from the device.
   * @returns {Promise<string | undefined>} A Promise that resolves with the battery or voltage information,
   */
  battery = async (): Promise<string | undefined> => {
    return await this.read("battery", "level", 250)
  }

  /**
   * Writes a command to get calibration data from the device.
   * @returns {Promise<void>} A Promise that resolves when the command is successfully sent.
   */
  calibration = async (): Promise<void> => {
    await this.write("uart", "tx", this.commands.GET_CALIBRATION, 2500, (data) => {
      console.log(data)
    })
  }

  /**
   * Retrieves firmware version from the device.
   * @returns {Promise<string>} A Promise that resolves with the firmware version,
   */
  firmware = async (): Promise<string | undefined> => {
    return await this.read("device", "firmware", 250)
  }

  /**
   * Handles data received from the Motherboard device. Processes hex-encoded streaming packets
   * to extract samples, calibrate masses, and update running averages of mass data.
   * If the received data is not a valid hex packet, it returns the unprocessed data.
   *
   * @param {DataView} value - The notification event.
   */
  override handleNotifications = (value: DataView): void => {
    if (value) {
      // Update timestamp
      this.updateTimestamp()
      if (value.buffer) {
        for (let i = 0; i < value.byteLength; i++) {
          this.receiveBuffer.push(value.getUint8(i))
        }

        let idx: number
        while ((idx = this.receiveBuffer.indexOf(10)) >= 0) {
          const line: number[] = this.receiveBuffer.splice(0, idx + 1).slice(0, -1) // Combine and remove LF
          if (line.length > 0 && line[line.length - 1] === 13) line.pop() // Remove CR
          const decoder: TextDecoder = new TextDecoder("utf-8")
          const receivedData: string = decoder.decode(new Uint8Array(line))

          const receivedTime: number = Date.now()

          // Check if the line is entirely hex characters
          const isAllHex: boolean = /^[0-9A-Fa-f]+$/g.test(receivedData)

          // Handle streaming packet
          if (isAllHex && receivedData.length === Motherboard.packetLength) {
            this.currentSamplesPerPacket = Motherboard.samplesNumber
            this.recordPacketReceived()
            // Base-16 decode the string: convert hex pairs to byte values
            const bytes: number[] = Array.from({ length: receivedData.length / 2 }, (_, i) =>
              Number(`0x${receivedData.substring(i * 2, i * 2 + 2)}`),
            )

            // Translate header into packet, number of samples from the packet length
            const sampleIndex = new DataView(new Uint8Array(bytes).buffer).getUint16(0, true)
            const battRaw = new DataView(new Uint8Array(bytes).buffer).getUint16(2, true)
            const packet = {
              timestamp: receivedTime,
              sampleIndex,
              battRaw,
              samples: [] as number[],
              forces: [] as number[],
            }

            const dataView = new DataView(new Uint8Array(bytes).buffer)

            for (let i = 0; i < Motherboard.samplesNumber; i++) {
              const sampleStart: number = 4 + 3 * i
              // Use DataView to read the 24-bit unsigned integer
              const rawValue =
                dataView.getUint8(sampleStart) |
                (dataView.getUint8(sampleStart + 1) << 8) |
                (dataView.getUint8(sampleStart + 2) << 16)

              // Ensure unsigned 32-bit integer
              packet.samples[i] = rawValue >>> 0

              if (packet.samples[i] >= 0x7fffff) {
                packet.samples[i] -= 0x1000000
              }
              packet.forces[i] = this.applyCalibration(packet.samples[i], this.calibrationData[i])
            }
            // invert center and right values
            packet.forces[1] *= -1
            packet.forces[2] *= -1

            let left: number = packet.forces[0]
            let center: number = packet.forces[1]
            let right: number = packet.forces[2]

            // Tare correction
            left -= this.applyTare(left)
            center -= this.applyTare(center)
            right -= this.applyTare(right)

            const totalCurrent = Math.max(-1000, left + center + right)
            const leftClamped = Math.max(-1000, left)
            const centerClamped = Math.max(-1000, center)
            const rightClamped = Math.max(-1000, right)

            // Update session stats before building packet
            this.peak = Math.max(this.peak, totalCurrent)
            this.min = Math.min(this.min, totalCurrent)
            this.sum += totalCurrent
            this.dataPointCount++
            this.mean = this.sum / this.dataPointCount

            // Per-zone peak and sum for distribution
            this.leftPeak = Math.max(this.leftPeak, leftClamped)
            this.leftSum += leftClamped
            this.centerPeak = Math.max(this.centerPeak, centerClamped)
            this.centerSum += centerClamped
            this.rightPeak = Math.max(this.rightPeak, rightClamped)
            this.rightSum += rightClamped

            // Add data to downloadable Array (distribution = per-zone measurements)
            this.downloadPackets.push(
              this.buildDownloadPacket(totalCurrent, [...packet.samples], {
                timestamp: packet.timestamp,
                battRaw: packet.battRaw,
                sampleIndex: packet.sampleIndex,
                distribution: {
                  left: this.buildZoneMeasurement(leftClamped, this.leftPeak, this.leftSum / this.dataPointCount),
                  center: this.buildZoneMeasurement(
                    centerClamped,
                    this.centerPeak,
                    this.centerSum / this.dataPointCount,
                  ),
                  right: this.buildZoneMeasurement(rightClamped, this.rightPeak, this.rightSum / this.dataPointCount),
                },
              }),
            )

            // Check if device is being used
            this.activityCheck(center)

            // Notify with weight data (distribution zones have proper peak/mean per zone)
            this.notifyCallback(
              this.buildForceMeasurement(totalCurrent, {
                left: this.buildZoneMeasurement(leftClamped, this.leftPeak, this.leftSum / this.dataPointCount),
                center: this.buildZoneMeasurement(centerClamped, this.centerPeak, this.centerSum / this.dataPointCount),
                right: this.buildZoneMeasurement(rightClamped, this.rightPeak, this.rightSum / this.dataPointCount),
              }),
            )
          } else if (this.writeLast === this.commands.GET_CALIBRATION) {
            // check data integrity
            if ((receivedData.match(/,/g) || []).length === 3) {
              const parts: string[] = receivedData.split(",")
              const numericParts: number[] = parts.map((x) => parseFloat(x))
              ;(this.calibrationData[numericParts[0]] as number[][]).push(numericParts.slice(1))
            }
          } else {
            // unhandled data
            this.writeCallback(receivedData)
          }
        }
      }
    }
  }

  /**
   * Retrieves hardware version from the device.
   * @returns {Promise<string>} A Promise that resolves with the hardware version,
   */
  hardware = async (): Promise<string | undefined> => {
    return await this.read("device", "hardware", 250)
  }

  /**
   * Sets the LED color based on a single color option. Defaults to turning the LEDs off if no configuration is provided.
   * @param {"green" | "red" | "orange"} [config] - Optional color or array of climb placements for the LEDs. Ignored if placements are provided.
   * @returns {Promise<number[] | undefined>} A promise that resolves with the payload array for the Kilter Board if LED settings were applied, or `undefined` if no action was taken or for the Motherboard.
   */
  led = async (config?: "green" | "red" | "orange"): Promise<number[] | undefined> => {
    if (this.isConnected()) {
      const colorMapping: Record<string, number[][]> = {
        green: [[0x00], [0x01]],
        red: [[0x01], [0x00]],
        orange: [[0x01], [0x01]],
        off: [[0x00], [0x00]],
      }
      // Default to "off" color if config is not set or not found in colorMapping
      const color = typeof config === "string" && colorMapping[config] ? config : "off"
      const [redValue, greenValue] = colorMapping[color]
      await this.write("led", "red", new Uint8Array(redValue))
      await this.write("led", "green", new Uint8Array(greenValue), 1250)
    }
    return undefined
  }

  /**
   * Retrieves manufacturer information from the device.
   * @returns {Promise<string>} A Promise that resolves with the manufacturer information,
   */
  manufacturer = async (): Promise<string | undefined> => {
    return await this.read("device", "manufacturer", 250)
  }

  /**
   * Retrieves serial number from the device.
   * @returns {Promise<string>} A Promise that resolves with the serial number,
   */
  serial = async (): Promise<string | undefined> => {
    let response: string | undefined = undefined
    await this.write("uart", "tx", this.commands.GET_SERIAL, 250, (data) => {
      response = data
    })
    return response
  }

  /**
   * Stops the data stream on the specified device.
   * @returns {Promise<void>} A promise that resolves when the stream is stopped.
   */
  stop = async (): Promise<void> => {
    await this.write("uart", "tx", this.commands.STOP_WEIGHT_MEAS, 0)
  }

  /**
   * Starts streaming data from the specified device.
   * @param {number} [duration=0] - The duration of the stream in milliseconds. If set to 0, stream will continue indefinitely.
   * @returns {Promise<void>} A promise that resolves when the streaming operation is completed.
   */
  stream = async (duration = 0): Promise<void> => {
    this.resetPacketTracking()
    this.downloadPackets.length = 0
    // Read calibration data if not already available
    if (!this.calibrationData[0].length) {
      await this.calibration()
    }
    // Start streaming data
    await this.write("uart", "tx", this.commands.START_WEIGHT_MEAS, duration)
    // Stop streaming if duration is set
    if (duration !== 0) {
      await this.stop()
    }
  }

  /**
   * Retrieves the entire 320 bytes of non-volatile memory from the device.
   *
   * The memory consists of 10 segments, each 32 bytes long. If any segment was previously written,
   * the corresponding data will appear in the response. Unused portions of the memory are
   * padded with whitespace.
   *
   * @returns {Promise<string>} A Promise that resolves with the 320-byte memory content as a string,
   */
  text = async (): Promise<string | undefined> => {
    let response: string | undefined = undefined
    await this.write("uart", "tx", this.commands.GET_TEXT, 250, (data) => {
      response = data
    })
    return response
  }
}
