/// EIS4 SysEx Protocol — manufacturer ID 0x7D (non-commercial)
/// Port of cue/src/protocol.rs

import Foundation

// MARK: - Constants

let sysexMfr: UInt8 = 0x7D

// ESP32 -> Cue
let RSP_SWEEP_START:  UInt8 = 0x01
let RSP_DATA_POINT:   UInt8 = 0x02
let RSP_SWEEP_END:    UInt8 = 0x03
let RSP_CONFIG:       UInt8 = 0x04
let RSP_LSV_START:    UInt8 = 0x05
let RSP_LSV_POINT:    UInt8 = 0x06
let RSP_LSV_END:      UInt8 = 0x07
let RSP_AMP_START:    UInt8 = 0x08
let RSP_AMP_POINT:    UInt8 = 0x09
let RSP_AMP_END:      UInt8 = 0x0A
let RSP_CL_START:     UInt8 = 0x0B
let RSP_CL_POINT:     UInt8 = 0x0C
let RSP_CL_RESULT:    UInt8 = 0x0D
let RSP_CL_END:       UInt8 = 0x0E
let RSP_PH_RESULT:    UInt8 = 0x0F
let RSP_TEMP:         UInt8 = 0x10
let RSP_CELL_K:       UInt8 = 0x11
let RSP_REF_FRAME:    UInt8 = 0x20
let RSP_REF_LP_RANGE: UInt8 = 0x21
let RSP_REFS_DONE:    UInt8 = 0x22
let RSP_REF_STATUS:   UInt8 = 0x23
let RSP_CL_FACTOR:    UInt8 = 0x24
let RSP_PH_CAL:       UInt8 = 0x25

// Cue -> ESP32
let CMD_SET_SWEEP:     UInt8 = 0x10
let CMD_SET_RTIA:      UInt8 = 0x11
let CMD_SET_RCAL:      UInt8 = 0x12
let CMD_START_SWEEP:   UInt8 = 0x13
let CMD_GET_CONFIG:    UInt8 = 0x14
let CMD_SET_ELECTRODE: UInt8 = 0x15
let CMD_GET_TEMP:      UInt8 = 0x17
let CMD_START_LSV:     UInt8 = 0x20
let CMD_START_AMP:     UInt8 = 0x21
let CMD_STOP_AMP:      UInt8 = 0x22
let CMD_START_CL:      UInt8 = 0x23
let CMD_START_PH:      UInt8 = 0x24
let CMD_START_CLEAN:   UInt8 = 0x25
let CMD_SET_CELL_K:    UInt8 = 0x28
let CMD_GET_CELL_K:    UInt8 = 0x29
let CMD_SET_CL_FACTOR: UInt8 = 0x33
let CMD_GET_CL_FACTOR: UInt8 = 0x34
let CMD_SET_PH_CAL:    UInt8 = 0x35
let CMD_GET_PH_CAL:    UInt8 = 0x36
let CMD_START_REFS:    UInt8 = 0x30
let CMD_GET_REFS:      UInt8 = 0x31
let CMD_CLEAR_REFS:    UInt8 = 0x32

// MARK: - 7-bit MIDI encoding

/// Encode an IEEE 754 float into 5 MIDI-safe bytes.
/// Byte 0: mask of high bits from each original byte.
/// Bytes 1-4: original bytes with bit 7 stripped.
func encodeFloat(_ val: Float) -> [UInt8] {
    var v = val
    let p = withUnsafeBytes(of: &v) { Array($0) }
    let mask: UInt8 = ((p[0] >> 7) & 1)
                    | ((p[1] >> 6) & 2)
                    | ((p[2] >> 5) & 4)
                    | ((p[3] >> 4) & 8)
    return [mask, p[0] & 0x7F, p[1] & 0x7F, p[2] & 0x7F, p[3] & 0x7F]
}

/// Decode 5 MIDI-safe bytes back into an IEEE 754 float.
func decodeFloat(_ d: [UInt8], at offset: Int = 0) -> Float {
    let m = d[offset]
    let b0 = d[offset + 1] | ((m & 1) << 7)
    let b1 = d[offset + 2] | ((m & 2) << 6)
    let b2 = d[offset + 3] | ((m & 4) << 5)
    let b3 = d[offset + 4] | ((m & 8) << 4)
    var val: Float = 0
    withUnsafeMutableBytes(of: &val) { buf in
        buf[0] = b0; buf[1] = b1; buf[2] = b2; buf[3] = b3
    }
    return val
}

/// Encode a UInt16 into 3 MIDI-safe bytes.
func encodeU16(_ val: UInt16) -> [UInt8] {
    var v = val
    let p = withUnsafeBytes(of: &v) { Array($0) }
    let mask: UInt8 = ((p[0] >> 7) & 1) | ((p[1] >> 6) & 2)
    return [mask, p[0] & 0x7F, p[1] & 0x7F]
}

/// Decode 3 MIDI-safe bytes back into a UInt16.
func decodeU16(_ d: [UInt8], at offset: Int = 0) -> UInt16 {
    let m = d[offset]
    let b0 = d[offset + 1] | ((m & 1) << 7)
    let b1 = d[offset + 2] | ((m & 2) << 6)
    var val: UInt16 = 0
    withUnsafeMutableBytes(of: &val) { buf in
        buf[0] = b0; buf[1] = b1
    }
    return val
}

// MARK: - Message enum

enum EisMessage {
    case sweepStart(numPoints: UInt16, freqStart: Float, freqStop: Float)
    case dataPoint(index: UInt16, point: EisPoint)
    case sweepEnd
    case config(EisConfig)
    case lsvStart(numPoints: UInt16, vStart: Float, vStop: Float)
    case lsvPoint(index: UInt16, point: LsvPoint)
    case lsvEnd
    case ampStart(vHold: Float)
    case ampPoint(index: UInt16, point: AmpPoint)
    case ampEnd
    case clStart(numPoints: UInt16)
    case clPoint(index: UInt16, point: ClPoint)
    case clResult(ClResult)
    case clEnd
    case phResult(PhResult)
    case temperature(Float)
    case refFrame(mode: UInt8, rtiaIdx: UInt8)
    case refLpRange(mode: UInt8, lowIdx: UInt8, highIdx: UInt8)
    case refsDone
    case refStatus(hasRefs: Bool)
    case cellK(Float)
    case clFactor(Float)
    case phCal(slope: Float, offset: Float)
}

// MARK: - Response parser

/// Parse a SysEx payload (after BLE MIDI unwrapping).
/// Input: [0x7D, RSP_ID, ...payload_bytes...]
func parseSysex(_ data: [UInt8]) -> EisMessage? {
    guard data.count >= 2, data[0] == sysexMfr else { return nil }
    let p = Array(data.dropFirst(2))

    switch data[1] {

    case RSP_SWEEP_START where p.count >= 13:
        return .sweepStart(
            numPoints: decodeU16(p, at: 0),
            freqStart: decodeFloat(p, at: 3),
            freqStop:  decodeFloat(p, at: 8)
        )

    case RSP_DATA_POINT where p.count >= 28:
        let ext = p.count >= 53
        return .dataPoint(
            index: decodeU16(p, at: 0),
            point: EisPoint(
                freqHz:        decodeFloat(p, at: 3),
                magOhms:       decodeFloat(p, at: 8),
                phaseDeg:      decodeFloat(p, at: 13),
                zReal:         decodeFloat(p, at: 18),
                zImag:         decodeFloat(p, at: 23),
                rtiaMagBefore: ext ? decodeFloat(p, at: 28) : 0,
                rtiaMagAfter:  ext ? decodeFloat(p, at: 33) : 0,
                revMag:        ext ? decodeFloat(p, at: 38) : 0,
                revPhase:      ext ? decodeFloat(p, at: 43) : 0,
                pctErr:        ext ? decodeFloat(p, at: 48) : 0
            )
        )

    case RSP_SWEEP_END:
        return .sweepEnd

    case RSP_CONFIG where p.count >= 16:
        return .config(EisConfig(
            freqStart: decodeFloat(p, at: 0),
            freqStop:  decodeFloat(p, at: 5),
            ppd:       decodeU16(p, at: 10),
            rtia:      Rtia(rawValue: p[13]) ?? .r5K,
            rcal:      Rcal(rawValue: p[14]) ?? .r3K,
            electrode: Electrode(rawValue: p[15]) ?? .fourWire
        ))

    case RSP_LSV_START where p.count >= 13:
        return .lsvStart(
            numPoints: decodeU16(p, at: 0),
            vStart:    decodeFloat(p, at: 3),
            vStop:     decodeFloat(p, at: 8)
        )

    case RSP_LSV_POINT where p.count >= 13:
        return .lsvPoint(
            index: decodeU16(p, at: 0),
            point: LsvPoint(
                vMv: decodeFloat(p, at: 3),
                iUa: decodeFloat(p, at: 8)
            )
        )

    case RSP_LSV_END:
        return .lsvEnd

    case RSP_AMP_START where p.count >= 5:
        return .ampStart(vHold: decodeFloat(p, at: 0))

    case RSP_AMP_POINT where p.count >= 13:
        return .ampPoint(
            index: decodeU16(p, at: 0),
            point: AmpPoint(
                tMs: decodeFloat(p, at: 3),
                iUa: decodeFloat(p, at: 8)
            )
        )

    case RSP_AMP_END:
        return .ampEnd

    case RSP_CL_START where p.count >= 3:
        return .clStart(numPoints: decodeU16(p, at: 0))

    case RSP_CL_POINT where p.count >= 14:
        return .clPoint(
            index: decodeU16(p, at: 0),
            point: ClPoint(
                tMs:   decodeFloat(p, at: 3),
                iUa:   decodeFloat(p, at: 8),
                phase: p[13]
            )
        )

    case RSP_CL_RESULT where p.count >= 10:
        return .clResult(ClResult(
            iFreeUa:  decodeFloat(p, at: 0),
            iTotalUa: decodeFloat(p, at: 5)
        ))

    case RSP_CL_END:
        return .clEnd

    case RSP_PH_RESULT where p.count >= 15:
        return .phResult(PhResult(
            vOcpMv: decodeFloat(p, at: 0),
            ph:     decodeFloat(p, at: 5),
            tempC:  decodeFloat(p, at: 10)
        ))

    case RSP_TEMP where p.count >= 5:
        return .temperature(decodeFloat(p, at: 0))

    case RSP_REF_FRAME where p.count >= 2:
        return .refFrame(mode: p[0], rtiaIdx: p[1])

    case RSP_REF_LP_RANGE where p.count >= 3:
        return .refLpRange(mode: p[0], lowIdx: p[1], highIdx: p[2])

    case RSP_REFS_DONE:
        return .refsDone

    case RSP_REF_STATUS where p.count >= 1:
        return .refStatus(hasRefs: p[0] != 0)

    case RSP_CELL_K where p.count >= 5:
        return .cellK(decodeFloat(p, at: 0))

    case RSP_CL_FACTOR where p.count >= 5:
        return .clFactor(decodeFloat(p, at: 0))

    case RSP_PH_CAL where p.count >= 10:
        return .phCal(
            slope:  decodeFloat(p, at: 0),
            offset: decodeFloat(p, at: 5)
        )

    default:
        return nil
    }
}

// MARK: - Command builders

func buildSysexSetSweep(freqStart: Float, freqStop: Float, ppd: UInt16) -> [UInt8] {
    var sx: [UInt8] = [0xF0, sysexMfr, CMD_SET_SWEEP]
    sx.append(contentsOf: encodeFloat(freqStart))
    sx.append(contentsOf: encodeFloat(freqStop))
    sx.append(contentsOf: encodeU16(ppd))
    sx.append(0xF7)
    return sx
}

func buildSysexSetRtia(_ rtia: Rtia) -> [UInt8] {
    [0xF0, sysexMfr, CMD_SET_RTIA, rtia.rawValue, 0xF7]
}

func buildSysexSetRcal(_ rcal: Rcal) -> [UInt8] {
    [0xF0, sysexMfr, CMD_SET_RCAL, rcal.rawValue, 0xF7]
}

func buildSysexSetElectrode(_ e: Electrode) -> [UInt8] {
    [0xF0, sysexMfr, CMD_SET_ELECTRODE, e.rawValue, 0xF7]
}

func buildSysexStartSweep() -> [UInt8] {
    [0xF0, sysexMfr, CMD_START_SWEEP, 0xF7]
}

func buildSysexGetConfig() -> [UInt8] {
    [0xF0, sysexMfr, CMD_GET_CONFIG, 0xF7]
}

func buildSysexStartLsv(vStart: Float, vStop: Float, scanRate: Float, lpRtia: LpRtia) -> [UInt8] {
    var sx: [UInt8] = [0xF0, sysexMfr, CMD_START_LSV]
    sx.append(contentsOf: encodeFloat(vStart))
    sx.append(contentsOf: encodeFloat(vStop))
    sx.append(contentsOf: encodeFloat(scanRate))
    sx.append(lpRtia.rawValue)
    sx.append(0xF7)
    return sx
}

func buildSysexStartAmp(vHold: Float, intervalMs: Float, durationS: Float, lpRtia: LpRtia) -> [UInt8] {
    var sx: [UInt8] = [0xF0, sysexMfr, CMD_START_AMP]
    sx.append(contentsOf: encodeFloat(vHold))
    sx.append(contentsOf: encodeFloat(intervalMs))
    sx.append(contentsOf: encodeFloat(durationS))
    sx.append(lpRtia.rawValue)
    sx.append(0xF7)
    return sx
}

func buildSysexStopAmp() -> [UInt8] {
    [0xF0, sysexMfr, CMD_STOP_AMP, 0xF7]
}

func buildSysexStartCl(
    vCond: Float, tCondMs: Float, vFree: Float, vTotal: Float,
    tDepMs: Float, tMeasMs: Float, lpRtia: LpRtia
) -> [UInt8] {
    var sx: [UInt8] = [0xF0, sysexMfr, CMD_START_CL]
    sx.append(contentsOf: encodeFloat(vCond))
    sx.append(contentsOf: encodeFloat(tCondMs))
    sx.append(contentsOf: encodeFloat(vFree))
    sx.append(contentsOf: encodeFloat(vTotal))
    sx.append(contentsOf: encodeFloat(tDepMs))
    sx.append(contentsOf: encodeFloat(tMeasMs))
    sx.append(lpRtia.rawValue)
    sx.append(0xF7)
    return sx
}

func buildSysexGetTemp() -> [UInt8] {
    [0xF0, sysexMfr, CMD_GET_TEMP, 0xF7]
}

func buildSysexStartPh(stabilizeS: Float) -> [UInt8] {
    var sx: [UInt8] = [0xF0, sysexMfr, CMD_START_PH]
    sx.append(contentsOf: encodeFloat(stabilizeS))
    sx.append(0xF7)
    return sx
}

func buildSysexStartClean(vMv: Float, durationS: Float) -> [UInt8] {
    var sx: [UInt8] = [0xF0, sysexMfr, CMD_START_CLEAN]
    sx.append(contentsOf: encodeFloat(vMv))
    sx.append(contentsOf: encodeFloat(durationS))
    sx.append(0xF7)
    return sx
}

func buildSysexStartRefs() -> [UInt8] {
    [0xF0, sysexMfr, CMD_START_REFS, 0xF7]
}

func buildSysexGetRefs() -> [UInt8] {
    [0xF0, sysexMfr, CMD_GET_REFS, 0xF7]
}

func buildSysexClearRefs() -> [UInt8] {
    [0xF0, sysexMfr, CMD_CLEAR_REFS, 0xF7]
}

func buildSysexSetCellK(_ k: Float) -> [UInt8] {
    var sx: [UInt8] = [0xF0, sysexMfr, CMD_SET_CELL_K]
    sx.append(contentsOf: encodeFloat(k))
    sx.append(0xF7)
    return sx
}

func buildSysexGetCellK() -> [UInt8] {
    [0xF0, sysexMfr, CMD_GET_CELL_K, 0xF7]
}

func buildSysexSetClFactor(_ f: Float) -> [UInt8] {
    var sx: [UInt8] = [0xF0, sysexMfr, CMD_SET_CL_FACTOR]
    sx.append(contentsOf: encodeFloat(f))
    sx.append(0xF7)
    return sx
}

func buildSysexGetClFactor() -> [UInt8] {
    [0xF0, sysexMfr, CMD_GET_CL_FACTOR, 0xF7]
}

func buildSysexSetPhCal(_ slope: Float, _ offset: Float) -> [UInt8] {
    var sx: [UInt8] = [0xF0, sysexMfr, CMD_SET_PH_CAL]
    sx.append(contentsOf: encodeFloat(slope))
    sx.append(contentsOf: encodeFloat(offset))
    sx.append(0xF7)
    return sx
}

func buildSysexGetPhCal() -> [UInt8] {
    [0xF0, sysexMfr, CMD_GET_PH_CAL, 0xF7]
}
