import UIKit
import CoreVideo
import Metal

private let lnDefaultFPS = kLNMP4DefaultFPS
private let lnMinFPS = kLNMP4MinFPS
private let lnMaxFPS = LNMP4MaxFPS
private let lnMaxCompatibleVAPVersion = LNVAPMaxCompatibleVersion

@objcMembers
public final class LNVAPPlayerView: UIView {
    private let playbackView = UIView()
    private var core: LNPlayerCore!

    fileprivate var didStart = false

    public weak var delegate: LNVAPPlaybackDelegate?

    public var enterBackgroundOperation: LNEnterBackgroundOperation {
        get { core.enterBackgroundOperation }
        set { core.enterBackgroundOperation = newValue }
    }

    public var renderByOpenGL: Bool {
        get { core.renderByOpenGL }
        set { core.renderByOpenGL = newValue }
    }

    public var fps: Int {
        get { core.fps }
        set { core.fps = newValue }
    }

    public var maskInfo: LNVAPMaskInfo? {
        get { core.maskInfo }
        set { core.maskInfo = newValue }
    }

    public override init(frame: CGRect) {
        super.init(frame: frame)
        setupPlaybackView()
    }

    public required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupPlaybackView()
    }

    public override func layoutSubviews() {
        super.layoutSubviews()
        playbackView.frame = bounds
    }

    deinit {
        core.stop()
    }

    @objc(lnPlayWithFilePath:repeatCount:)
    public func lnPlay(filePath: String, repeatCount: Int) {
        didStart = false
        core.play(filePath: filePath, repeatCount: repeatCount)
    }

    @objc(lnPlayWithFilePath:)
    public func lnPlay(filePath: String) {
        lnPlay(filePath: filePath, repeatCount: 0)
    }

    // Legacy-compatible (LN-prefixed) entry that accepts deprecated parameters.
    // `blendMode` is retained for call-site compatibility and intentionally ignored.
    @objc(lnPlayDeprecatedWithFilePath:fps:blendMode:repeatCount:delegate:)
    public func lnPlayDeprecated(filePath: String,
                                 fps: Int,
                                 blendMode: Int,
                                 repeatCount: Int,
                                 delegate: LNVAPPlaybackDelegate?) {
        _ = blendMode
        self.delegate = delegate
        self.fps = fps
        lnPlay(filePath: filePath, repeatCount: repeatCount)
    }



    @objc(lnStop)
    public func lnStop() {
        core.stop()
    }



    @objc(lnPause)
    public func lnPause() {
        core.pause()
    }



    @objc(lnResume)
    public func lnResume() {
        core.resume()
    }



    @objc(lnSetMute:)
    public func lnSetMute(_ mute: Bool) {
        core.isMute = mute
    }



    @objc(lnEnableOldVersion:)
    public func lnEnableOldVersion(_ enable: Bool) {
        core.enableOldVersion = enable
    }

    @objc(lnCurrentFrame)
    public var lnCurrentFrame: LNMP4AnimatedImageFrame? {
        core.currentFrameSnapshot
    }

    @objc(lnMP4FilePath)
    public var lnMP4FilePath: String {
        core.currentFilePath
    }



    @objc(lnAddTapGestureWithTarget:action:)
    public func lnAddTapGesture(target: Any, action: Selector) {
        let tap = UITapGestureRecognizer(target: target, action: action)
        playbackView.addGestureRecognizer(tap)
        playbackView.isUserInteractionEnabled = true
    }

    @objc(lnAddVapTapGesture:)
    public func lnAddVapTapGesture(_ handler: @escaping LNVAPGestureEventBlock) {
        let tap = UITapGestureRecognizer()
        lnAddVapGesture(tap, callback: handler)
    }

    @objc(lnAddVapGesture:callback:)
    public func lnAddVapGesture(_ gestureRecognizer: UIGestureRecognizer, callback: @escaping LNVAPGestureEventBlock) {
        gestureRecognizer.ln_addActionBlock { [weak self] sender in
            guard let self, let gesture = sender as? UIGestureRecognizer else { return }
            let point = gesture.location(in: self.playbackView)
            if let source = self.core.displayingSource(at: point) {
                callback(gesture, true, source)
            } else {
                callback(gesture, false, nil)
            }
        }
        playbackView.addGestureRecognizer(gestureRecognizer)
        playbackView.isUserInteractionEnabled = true
    }

    fileprivate func notifyShouldStart(_ config: LNVAPConfigModel) -> Bool {
        delegate?.lnPlayerShouldStart?(self, config: config) ?? true
    }

    fileprivate func notifyStart() {
        didStart = true
        delegate?.lnPlayerDidStart?(self)
    }

    fileprivate func notifyPlay(_ frame: LNMP4AnimatedImageFrame) {
        delegate?.lnPlayerDidPlay?(self, frame: frame)
    }

    fileprivate func notifyFinish(_ totalFrameCount: Int) {
        delegate?.lnPlayerDidFinish?(self, totalFrameCount: totalFrameCount)
    }

    fileprivate func notifyFail(_ error: NSError) {
        delegate?.lnPlayerDidFail?(self, error: error)
    }

    fileprivate func contentForTag(_ tag: String, resource: LNVAPSourceInfo) -> String? {
        delegate?.lnPlayerContent?(forTag: tag, resource: resource)
    }

    fileprivate func loadImage(withURL url: String, context: NSDictionary, completion: @escaping LNVAPImageCompletion) {
        if let handler = delegate?.lnPlayerLoadImage {
            handler(url, context, completion)
            return
        }
        let error = NSError(
            domain: NSURLErrorDomain,
            code: -1,
            userInfo: ["msg": "delegate does not implement lnPlayerLoadImage"]
        )
        completion(nil, error, url)
    }

    fileprivate func notifyStopIfNeeded(lastFrameIndex: Int) {
        delegate?.lnPlayerDidStop?(self)
        didStart = false
    }

    private func setupPlaybackView() {
        playbackView.frame = bounds
        playbackView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        addSubview(playbackView)
        core = LNPlayerCore(owner: self, hostView: playbackView)
    }
}

private final class LNPlayerCore: NSObject {
    weak var owner: LNVAPPlayerView?
    private weak var hostView: UIView?

    var enterBackgroundOperation: LNEnterBackgroundOperation = .stop
    var renderByOpenGL: Bool = false
    var fps: Int = 0
    var enableOldVersion: Bool = false
    var isMute: Bool = false

    var maskInfo: LNVAPMaskInfo? {
        didSet {
            vapMetalView?.maskInfo = maskInfo
        }
    }

    private var currentFrame: LNMP4AnimatedImageFrame?
    private var fileInfo: LNMP4HWDFileInfo?
    private var decodeManager: LNAnimatedImageDecodeManager?
    private var decodeConfig: LNAnimatedImageDecodeConfig = .defaultConfig()
    private var callbackQueue: OperationQueue?

    private var openGLView: LNHWDMP4OpenGLView?
    private var metalView: LNHWDMetalView?
    private var vapMetalView: LNVAPMetalView?

    private var isFinish = true
    private var onPause = false
    private var onSeek = false
    private var repeatCount = 0

    private var configManager: LNVAPConfigManager?
    private let renderQueue = DispatchQueue(label: "com.ln.vap.render")
    private var filePath: String = ""

    var currentFrameSnapshot: LNMP4AnimatedImageFrame? { currentFrame }
    var currentFilePath: String { filePath }

    init(owner: LNVAPPlayerView, hostView: UIView) {
        self.owner = owner
        self.hostView = hostView
        super.init()
        registerNotifications()
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
        stopInternal(triggerDelegate: false)
    }

    func play(filePath: String, repeatCount: Int) {
        if Thread.isMainThread {
            playOnMain(filePath: filePath, repeatCount: repeatCount)
        } else {
            DispatchQueue.main.async { [weak self] in
                self?.playOnMain(filePath: filePath, repeatCount: repeatCount)
            }
        }
    }

    func stop() {
        if Thread.isMainThread {
            stopInternal(triggerDelegate: true)
        } else {
            DispatchQueue.main.async { [weak self] in
                self?.stopInternal(triggerDelegate: true)
            }
        }
    }

    func pause() {
        onPause = true
        decodeManager?.tryToPauseAudioPlay()
    }

    func resume() {
        onPause = false
        openGLView?.pause = false
        decodeManager?.tryToResumeAudioPlay()
    }

    func displayingSource(at point: CGPoint) -> LNVAPSourceDisplayItem? {
        guard let configManager,
              let model = configManager.model.info,
              let currentFrame else {
            return nil
        }

        let mergeInfos = (configManager.model.mergedConfig[NSNumber(value: currentFrame.frameIndex)] ?? [])
            .sorted { $0.renderIndex > $1.renderIndex }

        guard model.size.width > 0, model.size.height > 0 else {
            return nil
        }
        guard let hostView else {
            return nil
        }

        let xRatio = hostView.bounds.width / model.size.width
        let yRatio = hostView.bounds.height / model.size.height

        for mergeInfo in mergeInfos {
            let srcRect = mergeInfo.renderRect
            let frame = CGRect(
                x: srcRect.minX * xRatio,
                y: srcRect.minY * yRatio,
                width: srcRect.width * xRatio,
                height: srcRect.height * yRatio
            )
            if frame.contains(point) {
                let item = LNVAPSourceDisplayItem()
                item.sourceInfo = mergeInfo.source
                item.frame = frame
                return item
            }
        }
        return nil
    }

    private var useVapMetalView: Bool {
        configManager?.hasValidConfig == true
    }

    private func playOnMain(filePath: String, repeatCount: Int) {
        guard !filePath.isEmpty else {
            notifyFail(message: "play failed: empty file path", code: LNMP4HWDErrorCode.fileNotExist.rawValue)
            return
        }
        guard FileManager.default.fileExists(atPath: filePath) else {
            notifyFail(message: "play failed: file does not exist", code: LNMP4HWDErrorCode.fileNotExist.rawValue)
            return
        }

        stopInternal(triggerDelegate: false)

        enterBackgroundOperation = .doNothing
        isFinish = false
        onPause = false
        onSeek = false
        self.filePath = filePath
        self.repeatCount = repeatCount

        if callbackQueue == nil, owner?.delegate != nil {
            let queue = OperationQueue()
            queue.maxConcurrentOperationCount = 1
            callbackQueue = queue
        }

        let info = LNMP4HWDFileInfo()
        info.filePath = filePath
        info.mp4Parser = LNMP4ParserProxy(filePath: filePath)
        info.mp4Parser?.parse()
        fileInfo = info

        let configManager = LNVAPConfigManager(fileInfo: info)
        configManager.delegate = self
        self.configManager = configManager

        if configManager.model.info?.version ?? 0 > lnMaxCompatibleVAPVersion {
            notifyFail(message: "not compatible vap version", code: LNMP4HWDErrorCode.invalidMP4File.rawValue)
            stopInternal(triggerDelegate: true)
            return
        }

        if !configManager.hasValidConfig && !enableOldVersion {
            notifyFail(message: "missing vapc box and oldVersion disabled", code: LNMP4HWDErrorCode.invalidMP4File.rawValue)
            stopInternal(triggerDelegate: true)
            return
        }

#if targetEnvironment(simulator)
        notifyFail(message: "not allowed in simulator", code: LNMP4HWDErrorCode.invalidMP4File.rawValue)
        stopInternal(triggerDelegate: true)
        return
#else
        setupRenderViewIfNeed()

        decodeManager = LNAnimatedImageDecodeManager(fileInfo: info, config: decodeConfig, delegate: self)
        configManager.loadConfigResources()
#endif
    }

    private func setupRenderViewIfNeed() {
        guard let hostView else { return }

        if renderByOpenGL {
            if openGLView == nil {
                let view = LNHWDMP4OpenGLView(frame: hostView.bounds)
                view.displayDelegate = self
                view.translatesAutoresizingMaskIntoConstraints = false
                hostView.addSubview(view)
                NSLayoutConstraint.activate([
                    view.topAnchor.constraint(equalTo: hostView.topAnchor),
                    view.bottomAnchor.constraint(equalTo: hostView.bottomAnchor),
                    view.leadingAnchor.constraint(equalTo: hostView.leadingAnchor),
                    view.trailingAnchor.constraint(equalTo: hostView.trailingAnchor)
                ])
                view.setupGL()
                openGLView = view
            }
            openGLView?.pause = false
            return
        }

        if useVapMetalView {
            if vapMetalView == nil {
                let view = LNVAPMetalView(frame: hostView.bounds)
                view.delegate = self
                view.translatesAutoresizingMaskIntoConstraints = false
                hostView.addSubview(view)
                NSLayoutConstraint.activate([
                    view.topAnchor.constraint(equalTo: hostView.topAnchor),
                    view.bottomAnchor.constraint(equalTo: hostView.bottomAnchor),
                    view.leadingAnchor.constraint(equalTo: hostView.leadingAnchor),
                    view.trailingAnchor.constraint(equalTo: hostView.trailingAnchor)
                ])
                vapMetalView = view
            }
            vapMetalView?.commonInfo = configManager?.model.info
            vapMetalView?.maskInfo = maskInfo
            return
        }

        if metalView == nil {
            let view = LNHWDMetalView(frame: hostView.bounds, blendMode: .alphaLeft)
            view.delegate = self
            view.translatesAutoresizingMaskIntoConstraints = false
            hostView.addSubview(view)
            NSLayoutConstraint.activate([
                view.topAnchor.constraint(equalTo: hostView.topAnchor),
                view.bottomAnchor.constraint(equalTo: hostView.bottomAnchor),
                view.leadingAnchor.constraint(equalTo: hostView.leadingAnchor),
                view.trailingAnchor.constraint(equalTo: hostView.trailingAnchor)
            ])
            metalView = view
        }
        metalView?.blendMode = .alphaLeft
    }

    private func runRenderLoop() {
        let waitFrame: TimeInterval = 16.0 / 1000.0
        let minLoop: TimeInterval = 1.0 / 1000.0

        renderQueue.async { [weak self] in
            guard let self else { return }
            var lastRenderingInterval: TimeInterval = 0
            var lastRenderingDuration: TimeInterval = 0

            while true {
                var shouldBreak = false
                autoreleasepool {
                    if self.isFinish {
                        shouldBreak = true
                        return
                    }
                    if self.onPause || self.onSeek {
                        lastRenderingInterval = Date.timeIntervalSinceReferenceDate
                        Thread.sleep(forTimeInterval: waitFrame)
                        return
                    }

                    var nextFrame: LNMP4AnimatedImageFrame?
                    DispatchQueue.main.sync {
                        nextFrame = self.displayNextFrame()
                    }

                    var duration = (nextFrame?.duration ?? 0) / 1000.0
                    if duration <= 0 {
                        duration = waitFrame
                    }

                    let currentInterval = Date.timeIntervalSinceReferenceDate
                    if let frame = nextFrame, frame.frameIndex != 0 {
                        duration -= ((currentInterval - lastRenderingInterval) - lastRenderingDuration)
                    }
                    duration = max(minLoop, duration)

                    lastRenderingInterval = currentInterval
                    lastRenderingDuration = duration
                    Thread.sleep(forTimeInterval: duration)
                }
                if shouldBreak {
                    break
                }
            }
        }
    }

    private func displayNextFrame() -> LNMP4AnimatedImageFrame? {
        guard !onPause, !isFinish else { return nil }
        guard let decodeManager else { return nil }

        let nextIndex = (currentFrame?.frameIndex ?? -1) + 1
        guard let nextFrame = decodeManager.consumeDecodedFrame(nextIndex) as? LNMP4AnimatedImageFrame,
              nextFrame.frameIndex == nextIndex else {
            return nil
        }

        if nextIndex == 0 {
            decodeManager.tryToStartAudioPlay()
        }

        nextFrame.duration = appropriateDuration(for: nextFrame)

        if renderByOpenGL {
            openGLView?.displayPixelBuffer(nextFrame.pixelBuffer)
        } else if useVapMetalView {
            let mergeInfos = configManager?.model.mergedConfig[NSNumber(value: nextFrame.frameIndex)] ?? []
            vapMetalView?.display(nextFrame.pixelBuffer, mergeInfos: mergeInfos)
        } else {
            metalView?.display(nextFrame.pixelBuffer)
        }

        currentFrame = nextFrame

        callbackQueue?.addOperation { [weak self] in
            guard let self, let owner = self.owner else { return }
            if nextIndex == 0 {
                owner.notifyStart()
            }
            owner.notifyPlay(nextFrame)
        }

        return nextFrame
    }

    private func appropriateDuration(for frame: LNMP4AnimatedImageFrame) -> TimeInterval {
        var preferredFPS = fps
        if preferredFPS < lnMinFPS || preferredFPS > lnMaxFPS {
            let defaultFPS = Int(frame.defaultFps)
            if defaultFPS >= lnMinFPS && defaultFPS <= lnMaxFPS {
                preferredFPS = defaultFPS
            } else {
                preferredFPS = lnDefaultFPS
            }
        }
        return 1000.0 / Double(preferredFPS)
    }

    private func stopInternal(triggerDelegate: Bool) {
        repeatCount = 0
        if isFinish {
            return
        }

        isFinish = true
        onPause = true

        openGLView?.pause = true
        openGLView?.dispose()
        openGLView?.removeFromSuperview()
        openGLView = nil

        metalView?.dispose()
        metalView?.removeFromSuperview()
        metalView = nil

        vapMetalView?.dispose()
        vapMetalView?.removeFromSuperview()
        vapMetalView = nil

        decodeManager?.tryToStopAudioPlay()

        if triggerDelegate {
            let lastFrameIndex = currentFrame?.frameIndex ?? 0
            performCallback { [weak self] in
                guard let self, let owner = self.owner else { return }
                owner.notifyStopIfNeeded(lastFrameIndex: lastFrameIndex)
            }
        } else {
            owner?.didStart = false
        }

        decodeManager = nil
        currentFrame = nil
        fileInfo = nil
        configManager = nil
    }

    private func notifyFail(message: String, code: Int) {
        let error = NSError(domain: LNMP4FrameHWDecoder.errorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: message])
        performCallback { [weak self] in
            self?.owner?.notifyFail(error)
        }
    }

    private func performCallback(_ block: @escaping () -> Void) {
        if let callbackQueue {
            callbackQueue.addOperation(block)
        } else {
            block()
        }
    }

    private func registerNotifications() {
        NotificationCenter.default.addObserver(self, selector: #selector(onEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(onEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(onSeekStart(_:)), name: NSNotification.Name(kLNVAPDecoderSeekStart), object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(onSeekFinish(_:)), name: NSNotification.Name(kLNVAPDecoderSeekFinish), object: nil)
    }

    @objc private func onEnterBackground() {
        switch enterBackgroundOperation {
        case .pauseAndResume:
            pause()
        case .doNothing:
            break
        default:
            stop()
        }
    }

    @objc private func onEnterForeground() {
        if enterBackgroundOperation == .pauseAndResume {
            resume()
        }
    }

    @objc private func onSeekStart(_ notification: Notification) {
        guard let object = notification.object, decodeManager?.containsThisDeocder(object) == true else { return }
        onSeek = true
    }

    @objc private func onSeekFinish(_ notification: Notification) {
        guard let object = notification.object, decodeManager?.containsThisDeocder(object) == true else { return }
        onSeek = false
    }
}

extension LNPlayerCore: LNAnimatedImageDecoderDelegate {
    @objc(decoderClassForManager:)
    func decoderClass(for manager: LNAnimatedImageDecodeManager) -> AnyClass {
        LNMP4FrameHWDecoder.self
    }

    func shouldSetupAudioPlayer() -> Bool {
        !isMute
    }

    func decoderDidFinishDecode(_ decoder: LNBaseDecoder) {
        performCallback { [weak self] in
            guard let self, let owner = self.owner else { return }
            let totalFrameCount = (self.currentFrame?.frameIndex ?? -1) + 1
            owner.notifyFinish(totalFrameCount)
        }

        var nextRepeatCount = repeatCount
        let shouldRepeat: Bool
        if nextRepeatCount == -1 {
            shouldRepeat = true
        } else if nextRepeatCount > 0 {
            nextRepeatCount -= 1
            shouldRepeat = true
        } else {
            shouldRepeat = false
        }

        if shouldRepeat {
            let path = filePath
            DispatchQueue.main.async { [weak self] in
                self?.playOnMain(filePath: path, repeatCount: nextRepeatCount)
            }
            return
        }
        stopInternal(triggerDelegate: true)
    }

    func decoderDidFailDecode(_ decoder: LNBaseDecoder?, error: NSError) {
        // Stop first, then emit the failure callback to keep playback state consistent.
        stopInternal(triggerDelegate: true)
        performCallback { [weak self] in
            self?.owner?.notifyFail(error)
        }
    }
}

extension LNPlayerCore: LNHWDMP4OpenGLViewDelegate, LNHWDMetalViewDelegate, LNVAPMetalViewDelegate {
    func onViewUnavailableStatus() {
        stopInternal(triggerDelegate: true)
    }

    func onMetalViewUnavailable() {
        // Keep parity with OC: metal unavailable callback does not force-stop by default.
    }
}

extension LNPlayerCore: LNVAPConfigDelegate {
    func onVAPConfigResourcesLoaded(_ config: LNVAPConfigModel, error: NSError?) {
        guard let configManager else { return }

        if let device = MTLCreateSystemDefaultDevice() {
            configManager.loadMTLTextures(device)
            configManager.loadMTLBuffers(device)
        }

        if let owner, !owner.notifyShouldStart(config) {
            stopInternal(triggerDelegate: true)
            return
        }

        runRenderLoop()
    }

    func vap_contentForTag(_ tag: String, resource: LNVAPSourceInfo) -> String? {
        owner?.contentForTag(tag, resource: resource)
    }

    func vap_loadImageWithURL(_ urlStr: String, context: [AnyHashable: Any], completion: @escaping (UIImage?, NSError?, String) -> Void) {
        owner?.loadImage(withURL: urlStr, context: context as NSDictionary, completion: completion)
    }
}
