//
//  CanonPTPHelper.swift
//  otg_swift
//

import Foundation
@preconcurrency import ImageCaptureCore

/// 佳能 EOS PTP：远程会话、GetEvent 解析、Probe 与 GetObject 下载。
enum CanonPTPHelper {

    /// 会话打开且 catalog 就绪后进入 Canon 远程拍摄模式。
    static func initializeRemoteSession(
        on camera: ICCameraDevice,
        startingTransactionID: UInt32
    ) async -> CanonRemoteSessionResult {
        var transactionID = startingTransactionID

        OTGLog.info(.canon, "Canon EOS remote session initializing")

        let remoteMode = await sendCommand(
            on: camera,
            code: CanonPTPCommand.setRemoteMode,
            transactionID: &transactionID,
            parameters: [CanonPTPCommand.remoteModeEnabled],
            label: "EOS SetRemoteMode"
        )
        guard remoteMode.success else {
            OTGLog.error(.canon, "SetRemoteMode failed")
            return CanonRemoteSessionResult(success: false, nextTransactionID: transactionID)
        }

        let eventMode = await sendCommand(
            on: camera,
            code: CanonPTPCommand.setEventMode,
            transactionID: &transactionID,
            parameters: [CanonPTPCommand.eventModeEnabled],
            label: "EOS SetEventMode"
        )
        guard eventMode.success else {
            OTGLog.error(.canon, "SetEventMode failed")
            return CanonRemoteSessionResult(success: false, nextTransactionID: transactionID)
        }

        _ = await sendCommand(
            on: camera,
            code: CanonPTPCommand.pchddCapacity,
            transactionID: &transactionID,
            parameters: [0x0FFF_FFF8, 0x1000, 1],
            label: "EOS PCHDDCapacity"
        )

        _ = await sendCommand(
            on: camera,
            code: CanonPTPCommand.keepDeviceOn,
            transactionID: &transactionID,
            parameters: [1],
            label: "EOS KeepDeviceOn"
        )

        OTGLog.info(.canon, "Canon EOS remote session ready, transactionID=\(transactionID)")
        return CanonRemoteSessionResult(success: true, nextTransactionID: transactionID)
    }

    /// 轮询 GetEvent 并解析 EOS 事件列表。
    static func pollEvents(
        on camera: ICCameraDevice,
        transactionID: inout UInt32
    ) async -> [CanonEOSEvent] {
        let result = await sendCommand(
            on: camera,
            code: CanonPTPCommand.getEvent,
            transactionID: &transactionID,
            parameters: [],
            label: "EOS GetEvent"
        )
        guard result.success, !result.inData.isEmpty else { return [] }

        let events = parseEOSEvents(result.inData)
        if events.isEmpty, result.inData.count > 8 {
            let hex = result.inData.prefix(64).map { String(format: "%02X", $0) }.joined(separator: " ")
            OTGLog.debug(.canon, "GetEvent inData=\(result.inData.count)B, no events, raw=\(hex)")
        }
        for event in events where isTransferEvent(event) {
            OTGLog.info(
                .canon,
                "transfer event 0x\(String(format: "%04X", event.code)) handle=0x\(String(format: "%08X", event.objectHandle ?? 0)) file=\(event.filename ?? "?") size=\(event.objectSize ?? 0)"
            )
        }
        return events
    }

    /// 解析 GetEvent 返回的 EOS 事件（布局参考 libgphoto2 `ptp_unpack_EOS_events`）。
    static func parseEOSEvents(_ data: Data) -> [CanonEOSEvent] {
        var events: [CanonEOSEvent] = []
        var offset = 0

        while offset + 8 <= data.count {
            let recordLen = Int(PTPHelper.readUInt32LE(data, offset: offset))
            let code = PTPHelper.readUInt32LE(data, offset: offset + CanonEOSEventOffset.recordCode)

            if recordLen == 8, code == 0 { break }
            guard recordLen >= 8, offset + recordLen <= data.count else { break }

            events.append(
                parseSingleEOSEvent(
                    data: data,
                    recordOffset: offset,
                    recordLen: recordLen,
                    code: code
                )
            )

            offset += recordLen
        }

        return events
    }

    /// 是否为应触发下载的传图事件。
    static func isTransferEvent(_ event: CanonEOSEvent) -> Bool {
        guard let handle = event.objectHandle, handle != 0 else { return false }
        switch event.code {
        case UInt32(CanonPTPCommand.eosRequestObjectTransfer),
             UInt32(CanonPTPCommand.eosRequestObjectTransferTS),
             UInt32(CanonPTPCommand.eosRequestObjectTransferDT),
             UInt32(CanonPTPCommand.eosRequestObjectTransferEx),
             UInt32(CanonPTPCommand.eosObjectAddedEx),
             UInt32(CanonPTPCommand.eosObjectInfoChangedEx),
             UInt32(CanonPTPCommand.eosObjectContentChanged),
             UInt32(CanonPTPCommand.objectAdded),
             UInt32(CanonPTPCommand.requestObjectTransfer):
            return true
        default:
            return false
        }
    }

    /// 读取对象元数据（文件名、压缩大小）。
    static func probeObject(
        handle: UInt32,
        storageID: UInt32?,
        on camera: ICCameraDevice,
        transactionID: inout UInt32
    ) async -> (filename: String, size: UInt32)? {
        if let storageID, storageID != 0 {
            let infoResult = await sendCommand(
                on: camera,
                code: CanonPTPCommand.eosGetObjectInfoEx,
                transactionID: &transactionID,
                parameters: [storageID, handle, 0],
                label: "EOS GetObjectInfoEx(0x\(String(format: "%08X", handle)))"
            )
            if infoResult.success,
               let size = PTPHelper.parseObjectCompressedSize(infoResult.inData), size > 0 {
                let filename = PTPHelper.parseObjectInfoFilename(infoResult.inData) ?? defaultFilename()
                return (filename, size)
            }
        }

        let infoResult = await sendCommand(
            on: camera,
            code: CanonPTPCommand.getObjectInfo,
            transactionID: &transactionID,
            parameters: [handle],
            label: "GetObjectInfo(0x\(String(format: "%08X", handle)))"
        )
        guard infoResult.success,
              let size = PTPHelper.parseObjectCompressedSize(infoResult.inData), size > 0 else {
            return nil
        }

        let filename = PTPHelper.parseObjectInfoFilename(infoResult.inData) ?? defaultFilename()
        return (filename, size)
    }

    /// 下载对象二进制；先试标准 GetObject，再试 EOS GetObject。
    static func downloadObject(
        handle: UInt32,
        storageID: UInt32?,
        preferredFilename: String?,
        on camera: ICCameraDevice,
        transactionID: inout UInt32
    ) async -> (filename: String, data: Data)? {
        var filename = preferredFilename ?? defaultFilename()

        if preferredFilename == nil,
           let probe = await probeObject(
               handle: handle,
               storageID: storageID,
               on: camera,
               transactionID: &transactionID
           ) {
            filename = probe.filename
        }

        let standardResult = await sendCommand(
            on: camera,
            code: CanonPTPCommand.getObject,
            transactionID: &transactionID,
            parameters: [handle],
            label: "GetObject(0x\(String(format: "%08X", handle)))"
        )
        if standardResult.success, PTPHelper.isLikelyImageData(standardResult.inData) {
            return (filename, standardResult.inData)
        }

        if let storageID, storageID != 0 {
            let eosResult = await sendCommand(
                on: camera,
                code: CanonPTPCommand.eosGetObject,
                transactionID: &transactionID,
                parameters: [storageID, handle, 0],
                label: "EOS GetObject(0x\(String(format: "%08X", handle)))"
            )
            if eosResult.success, PTPHelper.isLikelyImageData(eosResult.inData) {
                return (filename, eosResult.inData)
            }
        }

        return nil
    }

    /// 传图完成后 ACK，避免后续事件卡住。
    static func acknowledgeTransfer(
        handle: UInt32,
        on camera: ICCameraDevice,
        transactionID: inout UInt32
    ) async {
        _ = await sendCommand(
            on: camera,
            code: CanonPTPCommand.transferComplete,
            transactionID: &transactionID,
            parameters: [handle],
            label: "EOS TransferComplete(0x\(String(format: "%08X", handle)))"
        )
    }

    /// 从被动 PTP Event Container 提取 object handle（ObjectAdded 等）。
    static func parsePTPEventHandle(_ eventData: Data) -> UInt32? {
        guard let container = try? PTPHelper.parseContainer(eventData) else { return nil }
        switch container.code {
        case CanonPTPCommand.objectAdded, CanonPTPCommand.requestObjectTransfer:
            return container.params.first
        default:
            return nil
        }
    }

    static func objectSignature(filename: String, size: UInt32) -> String {
        PTPHelper.objectSignature(filename: filename, size: size)
    }

    // MARK: - Private

    private static func parseSingleEOSEvent(
        data: Data,
        recordOffset: Int,
        recordLen: Int,
        code: UInt32
    ) -> CanonEOSEvent {
        func u32(at absoluteOffset: Int) -> UInt32? {
            guard absoluteOffset + 4 <= recordOffset + recordLen, absoluteOffset + 4 <= data.count else { return nil }
            return PTPHelper.readUInt32LE(data, offset: absoluteOffset)
        }

        func string(at absoluteOffset: Int) -> String? {
            guard absoluteOffset < recordOffset + recordLen else { return nil }
            guard let parsed = PTPHelper.parsePTPString(data, offset: absoluteOffset) else { return nil }
            let sanitized = PTPHelper.sanitizeFilename(parsed.string)
            return sanitized.isEmpty ? nil : sanitized
        }

        switch code {
        case UInt32(CanonPTPCommand.eosRequestObjectTransfer),
             UInt32(CanonPTPCommand.eosRequestObjectTransferTS),
             UInt32(CanonPTPCommand.eosRequestObjectTransferDT),
             UInt32(CanonPTPCommand.eosRequestObjectTransferEx):
            guard let handle = u32(at: recordOffset + CanonEOSEventOffset.objectHandle), handle != 0 else {
                return CanonEOSEvent(code: code, storageID: nil, objectHandle: nil, filename: nil, objectSize: nil)
            }
            return CanonEOSEvent(
                code: code,
                storageID: nil,
                objectHandle: handle,
                filename: string(at: recordOffset + CanonEOSEventOffset.transferFilename),
                objectSize: u32(at: recordOffset + CanonEOSEventOffset.transferObjectSize)
            )

        case UInt32(CanonPTPCommand.eosObjectAddedEx),
             UInt32(CanonPTPCommand.eosObjectInfoChangedEx):
            guard let handle = u32(at: recordOffset + CanonEOSEventOffset.objectHandle), handle != 0 else {
                return CanonEOSEvent(code: code, storageID: nil, objectHandle: nil, filename: nil, objectSize: nil)
            }
            return CanonEOSEvent(
                code: code,
                storageID: u32(at: recordOffset + CanonEOSEventOffset.addedStorageID),
                objectHandle: handle,
                filename: string(at: recordOffset + CanonEOSEventOffset.addedFilename),
                objectSize: u32(at: recordOffset + CanonEOSEventOffset.addedObjectSize)
            )

        case UInt32(CanonPTPCommand.eosObjectContentChanged):
            guard let handle = u32(at: recordOffset + CanonEOSEventOffset.objectHandle), handle != 0 else {
                return CanonEOSEvent(code: code, storageID: nil, objectHandle: nil, filename: nil, objectSize: nil)
            }
            return CanonEOSEvent(code: code, storageID: nil, objectHandle: handle, filename: nil, objectSize: nil)

        case UInt32(CanonPTPCommand.objectAdded),
             UInt32(CanonPTPCommand.requestObjectTransfer):
            guard let handle = u32(at: recordOffset + CanonEOSEventOffset.objectHandle), handle != 0 else {
                return CanonEOSEvent(code: code, storageID: nil, objectHandle: nil, filename: nil, objectSize: nil)
            }
            return CanonEOSEvent(code: code, storageID: nil, objectHandle: handle, filename: nil, objectSize: nil)

        default:
            if isNonTransferNoiseCode(code) {
                return CanonEOSEvent(code: code, storageID: nil, objectHandle: nil, filename: nil, objectSize: nil)
            }
            let handle = u32(at: recordOffset + CanonEOSEventOffset.objectHandle)
            if handle != nil {
                OTGLog.debug(
                    .canon,
                    "EOS event 0x\(String(format: "%04X", code)) len=\(recordLen) handle=0x\(String(format: "%08X", handle ?? 0))"
                )
            }
            return CanonEOSEvent(
                code: code,
                storageID: u32(at: recordOffset + CanonEOSEventOffset.addedStorageID),
                objectHandle: handle,
                filename: string(at: recordOffset + CanonEOSEventOffset.addedFilename)
                    ?? string(at: recordOffset + CanonEOSEventOffset.transferFilename),
                objectSize: u32(at: recordOffset + CanonEOSEventOffset.addedObjectSize)
                    ?? u32(at: recordOffset + CanonEOSEventOffset.transferObjectSize)
            )
        }
    }

    private static func isNonTransferNoiseCode(_ code: UInt32) -> Bool {
        switch code {
        case UInt32(CanonPTPCommand.eosDevicePropChanged),
             UInt32(CanonPTPCommand.eosPropValueDescChanged),
             0xC18B, 0xC185, 0xC19D, 0xC1F6, 0xC1A4:
            return true
        default:
            return false
        }
    }

    private static func defaultFilename() -> String {
        PTPHelper.sanitizeFilename("IMG_\(Int(Date().timeIntervalSince1970)).JPG")
    }

    private static func sendCommand(
        on camera: ICCameraDevice,
        code: UInt16,
        transactionID: inout UInt32,
        parameters: [UInt32],
        label: String
    ) async -> PTPHelper.CommandResult {
        await PTPHelper.sendCommand(
            on: camera,
            code: code,
            transactionID: &transactionID,
            parameters: parameters,
            label: label
        )
    }
}
