import Foundation

enum PeakKind {
    case freeCl, totalCl, crossover
}

struct LsvPeak {
    var vMv: Float
    var iUa: Float
    var kind: PeakKind
}

func smoothLsv(_ data: [Float], window: Int) -> [Float] {
    let n = data.count
    if n == 0 || window < 2 { return data }
    let half = window / 2
    var out = [Float](repeating: 0, count: n)
    for i in 0..<n {
        let lo = max(0, i - half)
        let hi = min(n - 1, i + half)
        var sum: Float = 0
        for j in lo...hi { sum += data[j] }
        out[i] = sum / Float(hi - lo + 1)
    }
    return out
}

func findExtrema(_ v: [Float], _ iSmooth: [Float], minProminence: Float) -> [(Int, Bool)] {
    let n = iSmooth.count
    if n < 3 { return [] }

    var candidates: [(Int, Bool)] = []
    for i in 1..<(n - 1) {
        let prev = iSmooth[i - 1]
        let curr = iSmooth[i]
        let next = iSmooth[i + 1]
        if curr > prev && curr > next {
            candidates.append((i, true))
        } else if curr < prev && curr < next {
            candidates.append((i, false))
        }
    }

    var result: [(Int, Bool)] = []
    for (idx, isMax) in candidates {
        let val = iSmooth[idx]
        let leftSlice = iSmooth[..<idx]
        let rightSlice = iSmooth[(idx + 1)...]
        if isMax {
            let lb = leftSlice.min() ?? val
            let rb = rightSlice.min() ?? val
            if val - max(lb, rb) >= minProminence { result.append((idx, isMax)) }
        } else {
            let lb = leftSlice.max() ?? val
            let rb = rightSlice.max() ?? val
            if min(lb, rb) - val >= minProminence { result.append((idx, isMax)) }
        }
    }
    return result
}

/// Detect Q/HQ redox peak in the -100 to +600 mV window.
/// Returns peak voltage in mV if found.
func detectQhqPeak(_ points: [LsvPoint]) -> Float? {
    if points.count < 5 { return nil }

    let iVals = points.map { $0.iUa }
    let vVals = points.map { $0.vMv }

    let window = max(5, points.count / 50)
    let smoothed = smoothLsv(iVals, window: window)

    guard let iMin = smoothed.min(), let iMax = smoothed.max() else { return nil }
    let prominence = (iMax - iMin) * 0.05

    let extrema = findExtrema(vVals, smoothed, minProminence: prominence)

    var bestIdx: Int? = nil
    var bestVal: Float = -.infinity
    for (idx, isMax) in extrema {
        guard isMax, vVals[idx] >= -100, vVals[idx] <= 600 else { continue }
        if smoothed[idx] > bestVal {
            bestVal = smoothed[idx]
            bestIdx = idx
        }
    }
    if let idx = bestIdx { return vVals[idx] }
    return nil
}

struct ClPotentials: Equatable {
    var vFree: Float
    var vFreeDetected: Bool
    var vTotal: Float
    var vTotalDetected: Bool
}

func deriveClPotentials(_ points: [LsvPoint]) -> ClPotentials {
    let dflt = ClPotentials(vFree: 100, vFreeDetected: false, vTotal: -200, vTotalDetected: false)
    guard points.count >= 5 else { return dflt }

    let iVals = points.map { $0.iUa }
    let vVals = points.map { $0.vMv }

    let window = max(5, points.count / 50)
    let smoothed = smoothLsv(iVals, window: window)

    guard let iMin = smoothed.min(), let iMax = smoothed.max() else { return dflt }
    let prominence = (iMax - iMin) * 0.05

    let extrema = findExtrema(vVals, smoothed, minProminence: prominence)

    // v_free: most prominent cathodic peak (isMax==false) in +300 to -300 mV
    var vFree: Float = 100
    var vFreeDetected = false
    var freeIdx: Int? = nil
    var freeBest: Float = .infinity
    for (idx, isMax) in extrema {
        guard !isMax, vVals[idx] >= -300, vVals[idx] <= 300 else { continue }
        if smoothed[idx] < freeBest {
            freeBest = smoothed[idx]
            vFree = vVals[idx]
            vFreeDetected = true
            freeIdx = idx
        }
    }

    // v_total: secondary cathodic peak between (vFree-100) and -500, excluding free peak
    let totalHi = vFree - 100
    let totalLo: Float = -500
    var vTotal: Float = vFree - 300
    var vTotalDetected = false
    var totalBest: Float = .infinity
    for (idx, isMax) in extrema {
        guard !isMax, vVals[idx] >= totalLo, vVals[idx] <= totalHi, idx != freeIdx else { continue }
        if smoothed[idx] < totalBest {
            totalBest = smoothed[idx]
            vTotal = vVals[idx]
            vTotalDetected = true
        }
    }
    vTotal = max(vTotal, -400)

    return ClPotentials(vFree: vFree, vFreeDetected: vFreeDetected, vTotal: vTotal, vTotalDetected: vTotalDetected)
}

func detectLsvPeaks(_ points: [LsvPoint]) -> [LsvPeak] {
    if points.count < 5 { return [] }

    let iVals = points.map { $0.iUa }
    let vVals = points.map { $0.vMv }

    let window = max(5, points.count / 50)
    let smoothed = smoothLsv(iVals, window: window)

    guard let iMin = smoothed.min(), let iMax = smoothed.max() else { return [] }
    let prominence = (iMax - iMin) * 0.05

    let extrema = findExtrema(vVals, smoothed, minProminence: prominence)

    var peaks: [LsvPeak] = []

    // crossover: where current changes sign
    for i in 1..<smoothed.count {
        let prev = smoothed[i - 1]
        let curr = smoothed[i]
        let crossed = (prev > 0 && curr < 0) || (prev < 0 && curr > 0)
        if crossed {
            let aPrev = abs(prev)
            let aCurr = abs(curr)
            let frac = aPrev / (aPrev + aCurr)
            let dv = vVals[i] - vVals[i - 1]
            let vCross = vVals[i - 1] + frac * dv
            peaks.append(LsvPeak(vMv: vCross, iUa: 0, kind: .crossover))
            break
        }
    }

    // largest peak in positive voltage region -> freeCl
    var freeClIdx: Int? = nil
    var freeClVal: Float = -.infinity
    for (idx, isMax) in extrema {
        guard isMax, vVals[idx] >= 0 else { continue }
        if smoothed[idx] > freeClVal {
            freeClVal = smoothed[idx]
            freeClIdx = idx
        }
    }
    if let idx = freeClIdx {
        peaks.append(LsvPeak(vMv: vVals[idx], iUa: smoothed[idx], kind: .freeCl))
    }

    // largest peak in negative voltage region -> totalCl
    var totalClIdx: Int? = nil
    var totalClVal: Float = -.infinity
    for (idx, isMax) in extrema {
        guard isMax, vVals[idx] < 0 else { continue }
        if smoothed[idx] > totalClVal {
            totalClVal = smoothed[idx]
            totalClIdx = idx
        }
    }
    if let idx = totalClIdx {
        peaks.append(LsvPeak(vMv: vVals[idx], iUa: smoothed[idx], kind: .totalCl))
    }

    return peaks
}
