import Foundation
import UIKit
import AVFoundation
import VideoToolbox
import CoreMedia
import CoreVideo

@objcMembers
public final class LNAnimatedImageDecodeConfig: NSObject {
    public var threadCount: Int = 1
    public var bufferCount: Int = 5

    @objc(defaultConfig)
    public static func defaultConfig() -> LNAnimatedImageDecodeConfig {
        let config = LNAnimatedImageDecodeConfig()
        config.threadCount = 1
        config.bufferCount = 5
        return config
    }
}

@objcMembers
public final class LNAnimatedImageDecodeThread: Thread {
    public var occupied: Bool = false

    public var sequenceDec: String {
        #if DEBUG
        return description
        #else
        return description
        #endif
    }
}

@objcMembers
public final class LNAnimatedImageDecodeThreadPool: NSObject {
    public static let shared = LNAnimatedImageDecodeThreadPool()

    private let lock = NSLock()
    private var threads: [LNAnimatedImageDecodeThread] = []

    @objc(sharedPool)
    public static func sharedPool() -> LNAnimatedImageDecodeThreadPool {
        shared
    }

    @objc(getDecodeThread)
    public func getDecodeThread() -> LNAnimatedImageDecodeThread {
        lock.lock()
        defer { lock.unlock() }

        if let freeThread = threads.first(where: { !$0.occupied }) {
            return freeThread
        }

        let thread = LNAnimatedImageDecodeThread(target: self, selector: #selector(run), object: nil)
        thread.start()
        threads.append(thread)
        return thread
    }

    @objc private func run() {
        autoreleasepool {
            let runLoop = RunLoop.current
            runLoop.add(Port(), forMode: .default)
            while !Thread.current.isCancelled {
                runLoop.run(mode: .default, before: .distantFuture)
            }
        }
    }
}

@objcMembers
public final class LNAnimatedImageBufferManager: NSObject {
    public var buffers: NSMutableArray
    private let config: LNAnimatedImageDecodeConfig

    @objc(initWithConfig:)
    public init(config: LNAnimatedImageDecodeConfig) {
        self.config = config
        self.buffers = NSMutableArray(capacity: max(config.bufferCount, 0))
        super.init()
    }

    @objc(getBufferedFrame:)
    public func getBufferedFrame(_ frameIndex: Int) -> LNBaseAnimatedImageFrame? {
        if buffers.count == 0 { return nil }
        let bufferIndex = frameIndex % buffers.count
        if bufferIndex > buffers.count - 1 { return nil }
        guard let frame = buffers.object(at: bufferIndex) as? LNBaseAnimatedImageFrame else { return nil }
        guard frame.frameIndex == frameIndex else { return nil }
        return frame
    }

    @objc(popVideoFrame)
    public func popVideoFrame() -> LNBaseAnimatedImageFrame? {
        guard buffers.count > 0 else { return nil }
        guard let frame = buffers.firstObject as? LNBaseAnimatedImageFrame else { return nil }
        buffers.removeObject(at: 0)
        return frame
    }

    @objc(isBufferFull)
    public func isBufferFull() -> Bool {
        if buffers.count < config.bufferCount { return false }
        for case let obj in buffers {
            if !(obj is LNBaseAnimatedImageFrame) {
                return false
            }
        }
        return true
    }
}

@objc public protocol LNAnimatedImageDecoderDelegate: AnyObject {
    @objc(decoderClassForManager:)
    func decoderClass(for manager: LNAnimatedImageDecodeManager) -> AnyClass

    @objc optional func shouldSetupAudioPlayer() -> Bool
    @objc optional func decoderDidFinishDecode(_ decoder: LNBaseDecoder)
    @objc optional func decoderDidFailDecode(_ decoder: LNBaseDecoder?, error: NSError)
}

public let kLNVAPDecoderSeekStart = "kLNVAPDecoderSeekStart"
public let kLNVAPDecoderSeekFinish = "kLNVAPDecoderSeekFinish"

@objcMembers
open class LNBaseDecoder: NSObject {
    public dynamic var currentDecodeFrame: Int = -1
    public private(set) var fileInfo: LNBaseDFileInfo
    public var initializationError: NSError?

    public required init(fileInfo: LNBaseDFileInfo) {
        self.fileInfo = fileInfo
        super.init()
        self.fileInfo.occupiedCount += 1
    }

    @objc(initWith:error:)
    public convenience init(fileInfo: LNBaseDFileInfo, error: NSErrorPointer) {
        self.init(fileInfo: fileInfo)
        if let err = initializationError {
            error?.pointee = err
        }
    }

    open func decodeFrame(_ frameIndex: Int, buffers: NSMutableArray) {}
    open func shouldStopDecode(_ nextFrameIndex: Int) -> Bool { false }
    open func isFrameIndexBeyondEnd(_ frameIndex: Int) -> Bool { false }
}

@objc public enum LNMP4HWDErrorCode: Int {
    case fileNotExist = 10000
    case invalidMP4File = 10001
    case canNotGetStreamInfo = 10002
    case canNotGetStream = 10003
    case errorCreateVTBDesc = 10004
    case errorCreateVTBSession = 10005
}

@objcMembers
public final class LNMP4FrameHWDecoder: LNBaseDecoder {
    public static let errorDomain = "LNMP4HWDErrorDomain"

    private var buffers: NSMutableArray?
    private var parser: LNMP4ParserProxy?
    private var decodeQueue = DispatchQueue(label: "com.ln.vap.decode")

    private var decodeSession: VTDecompressionSession?
    private var formatDescription: CMFormatDescription?

    private var isFinish = false
    private var finishFrameIndex: Int = -1
    private var invalidRetryCount = 0

    private var spsData: Data?
    private var ppsData: Data?
    private var vpsData: Data?

    private var lastDecodeFrame: Int = -1

    @objc(errorDescriptionForCode:)
    public static func errorDescription(for code: LNMP4HWDErrorCode) -> String {
        switch code {
        case .fileNotExist: return "文件不存在"
        case .invalidMP4File: return "非法文件格式"
        case .canNotGetStreamInfo: return "无法获取视频流信息"
        case .canNotGetStream: return "无法获取视频流"
        case .errorCreateVTBDesc: return "VTB创建desc失败"
        case .errorCreateVTBSession: return "VTB创建session失败"
        }
    }

    public required init(fileInfo: LNBaseDFileInfo) {
        super.init(fileInfo: fileInfo)
        parser = (fileInfo as? LNMP4HWDFileInfo)?.mp4Parser
        if !onInputStart() {
            isFinish = true
        }
    }

    public override func decodeFrame(_ frameIndex: Int, buffers: NSMutableArray) {
        if frameIndex == currentDecodeFrame { return }
        currentDecodeFrame = frameIndex
        self.buffers = buffers

        decodeQueue.async { [weak self] in
            guard let self else { return }
            if frameIndex != self.lastDecodeFrame + 1 { return }
            self.decodeFrameInner(frameIndex, drop: false)
        }
    }

    public override func shouldStopDecode(_ nextFrameIndex: Int) -> Bool {
        isFinish
    }

    public override func isFrameIndexBeyondEnd(_ frameIndex: Int) -> Bool {
        if finishFrameIndex > 0 {
            return frameIndex >= finishFrameIndex
        }
        return false
    }

    deinit {
        onInputEnd()
        fileInfo.occupiedCount -= 1
    }

    private func onInputStart() -> Bool {
        guard !fileInfo.filePath.isEmpty else {
            initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.fileNotExist.rawValue, userInfo: ["location": fileInfo.filePath])
            return false
        }
        guard FileManager.default.fileExists(atPath: fileInfo.filePath) else {
            initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.fileNotExist.rawValue, userInfo: ["location": fileInfo.filePath])
            return false
        }

        isFinish = false
        spsData = nil
        ppsData = nil
        vpsData = nil

        return initPPSAndSPS()
    }

    private func initPPSAndSPS() -> Bool {
        guard let parser else { return false }

        spsData = parser.spsData
        ppsData = parser.ppsData
        vpsData = parser.vpsData

        guard let spsData, let ppsData else {
            initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.canNotGetStreamInfo.rawValue, userInfo: ["location": fileInfo.filePath])
            return false
        }

        if parser.videoCodecID == .h264 {
            guard spsData.count > 0, ppsData.count > 0 else { return false }
            let spsPtr = spsData.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: UInt8.self) }
            let ppsPtr = ppsData.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: UInt8.self) }
            var parameterSetPointers: [UnsafePointer<UInt8>] = [spsPtr, ppsPtr]
            var parameterSetSizes: [Int] = [spsData.count, ppsData.count]

            let status = CMVideoFormatDescriptionCreateFromH264ParameterSets(
                allocator: kCFAllocatorDefault,
                parameterSetCount: 2,
                parameterSetPointers: &parameterSetPointers,
                parameterSetSizes: &parameterSetSizes,
                nalUnitHeaderLength: 4,
                formatDescriptionOut: &formatDescription
            )
            if status != noErr {
                initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.errorCreateVTBDesc.rawValue, userInfo: ["location": fileInfo.filePath])
                return false
            }
        } else if parser.videoCodecID == .h265 {
            guard #available(iOS 11.0, *), let vpsData else {
                initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.canNotGetStreamInfo.rawValue, userInfo: ["location": fileInfo.filePath])
                return false
            }
            guard vpsData.count > 0, spsData.count > 0, ppsData.count > 0 else { return false }
            let vpsPtr = vpsData.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: UInt8.self) }
            let spsPtr = spsData.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: UInt8.self) }
            let ppsPtr = ppsData.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: UInt8.self) }
            var parameterSetPointers: [UnsafePointer<UInt8>] = [vpsPtr, spsPtr, ppsPtr]
            var parameterSetSizes: [Int] = [vpsData.count, spsData.count, ppsData.count]

            let status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(
                allocator: kCFAllocatorDefault,
                parameterSetCount: 3,
                parameterSetPointers: &parameterSetPointers,
                parameterSetSizes: &parameterSetSizes,
                nalUnitHeaderLength: 4,
                extensions: nil,
                formatDescriptionOut: &formatDescription
            )
            if status != noErr {
                initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.errorCreateVTBDesc.rawValue, userInfo: ["location": fileInfo.filePath])
                return false
            }
        }

        return createDecompressionSession()
    }

    private func createDecompressionSession() -> Bool {
        guard let formatDescription else { return false }

        let pixelFormat: UInt32 = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
        let attrs = [
            kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: pixelFormat)
        ] as NSDictionary

        let status = VTDecompressionSessionCreate(
            allocator: kCFAllocatorDefault,
            formatDescription: formatDescription,
            decoderSpecification: nil,
            imageBufferAttributes: attrs,
            outputCallback: nil,
            decompressionSessionOut: &decodeSession
        )

        if status != noErr {
            initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.errorCreateVTBSession.rawValue, userInfo: ["location": fileInfo.filePath])
            return false
        }
        return true
    }

    private func decodeFrameInner(_ frameIndex: Int, drop: Bool) {
        guard !isFinish else { return }
        guard let buffers else { return }
        guard let parser else { return }
        guard let _ = spsData, let _ = ppsData else { return }

        guard let packetData = parser.readPacket(ofSample: frameIndex), !packetData.isEmpty else {
            finishFrameIndex = frameIndex
            onInputEnd()
            return
        }

        let currentPts: UInt64 = {
            guard frameIndex >= 0, frameIndex < parser.videoSamples.count else { return 0 }
            return parser.videoSamples[frameIndex].pts
        }()

        let startDate = Date()

        var blockBuffer: CMBlockBuffer?
        var status = CMBlockBufferCreateWithMemoryBlock(
            allocator: kCFAllocatorDefault,
            memoryBlock: nil,
            blockLength: packetData.count,
            blockAllocator: kCFAllocatorDefault,
            customBlockSource: nil,
            offsetToData: 0,
            dataLength: packetData.count,
            flags: 0,
            blockBufferOut: &blockBuffer
        )
        guard status == kCMBlockBufferNoErr, let blockBuffer else { return }

        packetData.withUnsafeBytes { rawPtr in
            if let base = rawPtr.baseAddress {
                CMBlockBufferReplaceDataBytes(with: base, blockBuffer: blockBuffer, offsetIntoDestination: 0, dataLength: packetData.count)
            }
        }

        var sampleBuffer: CMSampleBuffer?
        var sampleSizeArray = [packetData.count]
        status = CMSampleBufferCreateReady(
            allocator: kCFAllocatorDefault,
            dataBuffer: blockBuffer,
            formatDescription: formatDescription,
            sampleCount: 1,
            sampleTimingEntryCount: 0,
            sampleTimingArray: nil,
            sampleSizeEntryCount: 1,
            sampleSizeArray: &sampleSizeArray,
            sampleBufferOut: &sampleBuffer
        )
        guard status == noErr, let sampleBuffer, let decodeSession else { return }

        var flagOut: VTDecodeInfoFlags = []
        let decodeStatus = VTDecompressionSessionDecodeFrame(
            decodeSession,
            sampleBuffer: sampleBuffer,
            flags: [],
            infoFlagsOut: &flagOut
        ) { [weak self] status, _, imageBuffer, _, _ in
            guard let self else { return }
            self.handleDecodePixelBuffer(
                imageBuffer,
                frameIndex: frameIndex,
                currentPts: currentPts,
                startDate: startDate,
                status: status,
                needDrop: drop,
                buffers: buffers,
                fps: parser.fps
            )
        }

        if decodeStatus == kVTInvalidSessionErr {
            invalidRetryCount += 1
            if invalidRetryCount >= 3 { return }
            resetDecoder()
            findKeyFrameAndDecodeToCurrent(frameIndex)
        } else {
            invalidRetryCount = 0
        }
    }

    private func handleDecodePixelBuffer(
        _ pixelBuffer: CVImageBuffer?,
        frameIndex: Int,
        currentPts: UInt64,
        startDate: Date,
        status: OSStatus,
        needDrop: Bool,
        buffers: NSMutableArray,
        fps: Int
    ) {
        lastDecodeFrame = frameIndex
        if status != noErr { return }
        if needDrop { return }
        guard let pixelBuffer = pixelBuffer else { return }

        let newFrame = LNMP4AnimatedImageFrame()
        newFrame.pixelBuffer = pixelBuffer
        newFrame.frameIndex = frameIndex
        newFrame.decodeTime = Date().timeIntervalSince(startDate) * 1000.0
        newFrame.defaultFps = Int32(fps)
        newFrame.pts = currentPts

        buffers.add(newFrame)
        let sorted = (buffers as? [LNMP4AnimatedImageFrame] ?? []).sorted { $0.pts < $1.pts }
        buffers.removeAllObjects()
        sorted.forEach { buffers.add($0) }
    }

    private func resetDecoder() {
        if let decodeSession {
            VTDecompressionSessionWaitForAsynchronousFrames(decodeSession)
            VTDecompressionSessionInvalidate(decodeSession)
            self.decodeSession = nil
        }
        _ = createDecompressionSession()
    }

    private func findKeyFrameAndDecodeToCurrent(_ frameIndex: Int) {
        NotificationCenter.default.post(name: NSNotification.Name(kLNVAPDecoderSeekStart), object: self)

        guard let parser else { return }
        let keyframeIndexes = parser.videoSyncSampleIndexes
        var index = keyframeIndexes.first?.intValue ?? 0
        for num in keyframeIndexes {
            if num.intValue < frameIndex {
                index = num.intValue
                continue
            }
            break
        }

        while index < frameIndex {
            decodeFrameInner(index, drop: true)
            index += 1
        }
        decodeFrameInner(frameIndex, drop: false)

        NotificationCenter.default.post(name: NSNotification.Name(kLNVAPDecoderSeekFinish), object: self)
    }

    private func onInputEnd() {
        if isFinish { return }
        isFinish = true

        if let decodeSession {
            VTDecompressionSessionWaitForAsynchronousFrames(decodeSession)
            VTDecompressionSessionInvalidate(decodeSession)
            self.decodeSession = nil
        }
        formatDescription = nil
        spsData = nil
        ppsData = nil
        vpsData = nil
    }
}

@objcMembers
public final class LNAnimatedImageDecodeManager: NSObject {
    public weak var decoderDelegate: LNAnimatedImageDecoderDelegate?

    private let config: LNAnimatedImageDecodeConfig
    private let fileInfo: LNBaseDFileInfo
    private var decoders: [LNBaseDecoder] = []
    private var bufferManager: LNAnimatedImageBufferManager
    private var audioPlayer: AVAudioPlayer?

    public init(fileInfo: LNBaseDFileInfo, config: LNAnimatedImageDecodeConfig, delegate: LNAnimatedImageDecoderDelegate?) {
        self.fileInfo = fileInfo
        self.config = config
        self.decoderDelegate = delegate
        self.bufferManager = LNAnimatedImageBufferManager(config: config)
        super.init()

        createDecoders(by: config)
        initializeBuffers(fromIndex: 0)
        setupAudioPlayerIfNeed()
    }

    @objc(initWith:config:delegate:)
    public convenience init(_ fileInfo: LNBaseDFileInfo, config: LNAnimatedImageDecodeConfig, delegate: LNAnimatedImageDecoderDelegate?) {
        self.init(fileInfo: fileInfo, config: config, delegate: delegate)
    }

    @objc(consumeDecodedFrame:)
    public func consumeDecodedFrame(_ frameIndex: Int) -> LNBaseAnimatedImageFrame? {
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }

        if frameIndex == 0 && bufferManager.buffers.count < config.bufferCount {
            return nil
        }

        let decodeFinished = checkIfDecodeFinish(frameIndex)
        let frame = bufferManager.popVideoFrame()
        if let frame {
            frame.frameIndex = frameIndex
            decodeFrame(frameIndex + config.bufferCount)
            return frame
        }

        if !decodeFinished {
            guard !decoders.isEmpty else { return nil }
            let decoderIndex = decoders.count == 1 ? 0 : frameIndex % decoders.count
            let decoder = decoders[decoderIndex]
            if decoder.shouldStopDecode(frameIndex) {
                decoderDelegate?.decoderDidFinishDecode?(decoder)
                return nil
            }
            initializeBuffers(fromIndex: frameIndex)
        }
        return nil
    }

    @objc(tryToStartAudioPlay)
    public func tryToStartAudioPlay() {
        audioPlayer?.play()
    }

    @objc(tryToStopAudioPlay)
    public func tryToStopAudioPlay() {
        audioPlayer?.stop()
    }

    @objc(tryToPauseAudioPlay)
    public func tryToPauseAudioPlay() {
        audioPlayer?.pause()
    }

    @objc(tryToResumeAudioPlay)
    public func tryToResumeAudioPlay() {
        audioPlayer?.play()
    }

    @objc(containsThisDeocder:)
    public func containsThisDeocder(_ decoder: Any) -> Bool {
        decoders.contains { $0 === (decoder as AnyObject) }
    }

    private func checkIfDecodeFinish(_ frameIndex: Int) -> Bool {
        guard !decoders.isEmpty else { return true }
        let decoderIndex = decoders.count == 1 ? 0 : frameIndex % decoders.count
        let decoder = decoders[decoderIndex]
        if decoder.isFrameIndexBeyondEnd(frameIndex) {
            decoderDelegate?.decoderDidFinishDecode?(decoder)
            return true
        }
        return false
    }

    private func decodeFrame(_ frameIndex: Int) {
        guard !decoders.isEmpty else { return }
        let decoderIndex = decoders.count == 1 ? 0 : frameIndex % decoders.count
        let decoder = decoders[decoderIndex]
        if decoder.shouldStopDecode(frameIndex) { return }
        decoder.decodeFrame(frameIndex, buffers: bufferManager.buffers)
    }

    private func createDecoders(by config: LNAnimatedImageDecodeConfig) {
        guard let decoderDelegate else { return }
        for _ in 0..<max(config.threadCount, 1) {
            guard let cls = decoderDelegate.decoderClass(for: self) as? LNBaseDecoder.Type else { continue }
            let decoder = cls.init(fileInfo: fileInfo)
            if let error = decoder.initializationError {
                decoderDelegate.decoderDidFailDecode?(nil, error: error)
                break
            }
            decoders.append(decoder)
        }
    }

    private func initializeBuffers(fromIndex start: Int) {
        for i in 0..<max(config.bufferCount, 0) {
            decodeFrame(start + i)
        }
    }

    private func setupAudioPlayerIfNeed() {
        if decoderDelegate?.shouldSetupAudioPlayer?() == false {
            return
        }

        guard let fileInfo = fileInfo as? LNMP4HWDFileInfo,
              let mp4Parser = fileInfo.mp4Parser,
              mp4Parser.audioTrackBox != nil else {
            audioPlayer = nil
            return
        }

        let url = URL(fileURLWithPath: self.fileInfo.filePath)
        audioPlayer = try? AVAudioPlayer(contentsOf: url)
    }
}

@objc public protocol LNVAPConfigDelegate: AnyObject {
    @objc(onVAPConfigResourcesLoaded:error:)
    func onVAPConfigResourcesLoaded(_ config: LNVAPConfigModel, error: NSError?)

    @objc optional func vap_contentForTag(_ tag: String, resource: LNVAPSourceInfo) -> String?
    @objc optional func vap_loadImageWithURL(_ urlStr: String, context: [AnyHashable: Any], completion: @escaping (UIImage?, NSError?, String) -> Void)
}

@objcMembers
public final class LNVAPConfigManager: NSObject {
    public weak var delegate: LNVAPConfigDelegate?
    public var hasValidConfig: Bool = false
    public var model: LNVAPConfigModel = .init()

    private let fileInfo: LNMP4HWDFileInfo

    public init(fileInfo: LNMP4HWDFileInfo) {
        self.fileInfo = fileInfo
        super.init()
        setupConfig()
    }

    @objc(initWith:)
    public convenience init(_ fileInfo: LNMP4HWDFileInfo) {
        self.init(fileInfo: fileInfo)
    }

    @objc(loadConfigResources)
    public func loadConfigResources() {
        if model.resources.isEmpty {
            delegate?.onVAPConfigResourcesLoaded(model, error: nil)
            return
        }

        if let contentProvider = delegate?.vap_contentForTag {
            model.resources.forEach { resource in
                resource.contentTagValue = contentProvider(resource.contentTag ?? "", resource)
            }
        }

        guard let loadImage = delegate?.vap_loadImageWithURL else {
            // Keep parity with OC: if no image loader delegate, return directly.
            return
        }

        let group = DispatchGroup()
        var loadError: NSError?
        for resource in model.resources {
            guard resource.type == LNVAPAttachmentConstants.sourceTypeImg,
                  resource.loadType == LNVAPAttachmentConstants.sourceLoadTypeNet,
                  let url = resource.contentTagValue else {
                if resource.type == LNVAPAttachmentConstants.sourceTypeText,
                   resource.loadType == LNVAPAttachmentConstants.sourceLoadTypeLocal {
                    resource.sourceImage = LNVAPTextureLoader.drawingImage(
                        forText: resource.contentTagValue,
                        color: resource.color,
                        size: resource.size,
                        bold: resource.style == LNVAPAttachmentConstants.sourceStyleBoldText
                    )
                }
                continue
            }

            group.enter()
            loadImage(url, ["resource": resource]) { image, error, imageURL in
                if image == nil || error != nil {
                    loadError = loadError ?? error ?? NSError(domain: "loadImageError:\(imageURL)", code: -1)
                }
                resource.sourceImage = image
                group.leave()
            }
        }

        group.notify(queue: .main) { [weak self] in
            guard let self else { return }
            self.delegate?.onVAPConfigResourcesLoaded(self.model, error: loadError)
        }
    }

    @objc(loadMTLTextures:)
    public func loadMTLTextures(_ device: MTLDevice) {
        model.resources.forEach { resource in
            resource.texture = LNVAPTextureLoader.loadTexture(with: resource.sourceImage, device: device)
            resource.sourceImage = nil
        }
    }

    @objc(loadMTLBuffers:)
    public func loadMTLBuffers(_ device: MTLDevice) {
        model.resources.forEach { resource in
            resource.colorParamsBuffer = LNVAPTextureLoader.lnLoadVAPColorFillBuffer(with: resource.color, device: device)
        }
    }

    private func setupConfig() {
        guard let vapc = fileInfo.mp4Parser?.rootBox?.subBox(ofType: .vapc) else {
            hasValidConfig = false
            LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "config can not find vapc box")
            return
        }

        hasValidConfig = true
        guard let vapcData = fileInfo.mp4Parser?.readData(of: vapc, length: Int(vapc.length - 8), offset: 8),
              let dict = (try? JSONSerialization.jsonObject(with: vapcData)) as? [String: Any] else {
            LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "fail to parse config as dictionary")
            return
        }
        parseConfigDictionary(dict)
    }

    private func parseConfigDictionary(_ configDic: [String: Any]) {
        guard let commonInfoDic = lnDictionary(configDic["info"]) else {
            LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "has no commonInfoDic")
            return
        }
        let sourcesArr = lnArray(configDic["src"])
        let framesArr = lnArray(configDic["frame"])

        let configModel = LNVAPConfigModel()
        self.model = configModel

        let commonInfo = LNVAPCommonInfo()
        commonInfo.version = lnInt(commonInfoDic["v"])
        commonInfo.framesCount = lnInt(commonInfoDic["f"])
        commonInfo.size = CGSize(width: lnFloat(commonInfoDic["w"]), height: lnFloat(commonInfoDic["h"]))
        commonInfo.videoSize = CGSize(width: lnFloat(commonInfoDic["videoW"]), height: lnFloat(commonInfoDic["videoH"]))
        commonInfo.targetOrientaion = LNVAPOrientation(rawValue: lnInt(commonInfoDic["orien"])) ?? .none
        commonInfo.fps = lnInt(commonInfoDic["fps"])
        commonInfo.isMerged = lnInt(commonInfoDic["isVapx"]) == 1
        commonInfo.alphaAreaRect = lnRect(commonInfoDic["aFrame"])
        commonInfo.rgbAreaRect = lnRect(commonInfoDic["rgbFrame"])
        configModel.info = commonInfo

        fileInfo.mp4Parser?.fps = commonInfo.fps

        var sources: [String: LNVAPSourceInfo] = [:]
        for sourceObj in sourcesArr {
            guard let sourceDic = lnDictionary(sourceObj) else {
                LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "sourceDic is not dic")
                continue
            }
            let sourceID = lnString(sourceDic["srcId"])
            guard !sourceID.isEmpty else {
                LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "has no sourceID")
                continue
            }
            let sourceInfo = LNVAPSourceInfo()
            sourceInfo.type = lnString(sourceDic["srcType"])
            sourceInfo.loadType = lnString(sourceDic["loadType"])
            sourceInfo.contentTag = lnString(sourceDic["srcTag"])
            sourceInfo.style = lnString(sourceDic["style"])
            sourceInfo.fitType = lnString(sourceDic["fitType"])
            sourceInfo.size = CGSize(width: lnFloat(sourceDic["w"]), height: lnFloat(sourceDic["h"]))
            let colorHex = lnString(sourceDic["color"])
            if !colorHex.isEmpty {
                sourceInfo.color = UIColor.lnColor(hex: colorHex)
            }
            sources[sourceID] = sourceInfo
        }
        configModel.resources = Array(sources.values)

        var mergedConfig: [NSNumber: [LNVAPMergedInfo]] = [:]
        for frameObj in framesArr {
            guard let frameMergedDic = lnDictionary(frameObj) else {
                LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "frameMergedDic is not dic")
                continue
            }
            let frameIndex = lnInt(frameMergedDic["i"])
            let mergedObjs = lnArray(frameMergedDic["obj"])
            var mergedInfos: [LNVAPMergedInfo] = []

            for mergeObj in mergedObjs {
                guard let mergeInfoDic = lnDictionary(mergeObj) else {
                    LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "mergeInfoDic is not dic")
                    continue
                }
                let sourceID = lnString(mergeInfoDic["srcId"])
                guard let sourceInfo = sources[sourceID] else {
                    LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "sourceInfo is nil")
                    continue
                }

                let mergeInfo = LNVAPMergedInfo()
                mergeInfo.source = sourceInfo
                mergeInfo.renderIndex = lnInt(mergeInfoDic["z"])
                mergeInfo.needMask = mergeInfoDic["mFrame"] != nil
                mergeInfo.renderRect = lnRect(mergeInfoDic["frame"])
                mergeInfo.maskRect = lnRect(mergeInfoDic["mFrame"])
                mergeInfo.maskRotation = lnInt(mergeInfoDic["mt"])
                mergedInfos.append(mergeInfo)
            }
            mergedConfig[NSNumber(value: frameIndex)] = mergedInfos.sorted(by: { $0.renderIndex < $1.renderIndex })
        }

        configModel.mergedConfig = mergedConfig
    }
}

private extension LNVAPConfigManager {
    func lnInt(_ value: Any?) -> Int {
        if let n = value as? NSNumber { return n.intValue }
        if let s = value as? NSString { return s.integerValue }
        if let s = value as? String { return Int(s) ?? 0 }
        return 0
    }

    func lnFloat(_ value: Any?) -> CGFloat {
        if let n = value as? NSNumber { return CGFloat(n.doubleValue) }
        if let s = value as? NSString { return CGFloat(s.doubleValue) }
        if let s = value as? String { return CGFloat(Double(s) ?? 0) }
        return 0
    }

    func lnString(_ value: Any?) -> String {
        if let s = value as? String { return s }
        if let s = value as? NSString { return s as String }
        if let n = value as? NSNumber { return n.stringValue }
        return ""
    }

    func lnArray(_ value: Any?) -> [Any] {
        if let arr = value as? [Any] { return arr }
        if let arr = value as? NSArray { return arr.compactMap { $0 } }
        return []
    }

    func lnDictionary(_ value: Any?) -> [String: Any]? {
        if let dict = value as? [String: Any] { return dict }
        if let dict = value as? NSDictionary {
            var out: [String: Any] = [:]
            for (k, v) in dict {
                if let key = k as? String {
                    out[key] = v
                }
            }
            return out
        }
        return nil
    }

    func lnRect(_ value: Any?) -> CGRect {
        if let arr = value as? NSArray {
            return arr.ln_rectValue()
        }
        if let arr = value as? [Any] {
            return (arr as NSArray).ln_rectValue()
        }
        return .zero
    }
}

private extension UIColor {
    static func lnColor(hex: String) -> UIColor? {
        let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
        var int: UInt64 = 0
        guard Scanner(string: cleaned).scanHexInt64(&int) else { return nil }

        let a, r, g, b: UInt64
        switch cleaned.count {
        case 3:
            (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
        case 6:
            (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
        case 8:
            (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
        default:
            return nil
        }

        return UIColor(
            red: CGFloat(r) / 255,
            green: CGFloat(g) / 255,
            blue: CGFloat(b) / 255,
            alpha: CGFloat(a) / 255
        )
    }
}
