/*
 * This file is part of mpv.
 *
 * mpv is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * mpv is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
 */

import Cocoa

class CocoaCB: Common, EventSubscriber {
    var libmpv: LibmpvHelper
    var layer: GLLayer?

    var isShuttingDown: Bool = false

    enum State {
        case uninitialized
        case needsInit
        case initialized
    }
    var backendState: State = .uninitialized

    init(_ mpv: OpaquePointer) {
        let log = LogHelper(mp_log_new(UnsafeMutablePointer(mpv), mp_client_get_log(mpv), "cocoacb"))
        let option = OptionHelper(UnsafeMutablePointer(mpv), mp_client_get_global(mpv))
        libmpv = LibmpvHelper(mpv, log)
        super.init(option, log)
        layer = GLLayer(cocoaCB: self)
        AppHub.shared.event?.subscribe(self, event: .init(name: "MPV_EVENT_SHUTDOWN"))
    }

    func preinit(_ vo: UnsafeMutablePointer<vo>) {
        eventsLock.withLock { self.vo = vo }
        input = InputHelper(vo.pointee.input_ctx, option)

        if backendState == .uninitialized {
            backendState = .needsInit

            guard let layer = self.layer else {
                log.error("Something went wrong, no GLLayer was initialized")
                exit(1)
            }

            initView(vo, layer)
            initMisc(vo)
        }
    }

    func uninit() {
        eventsLock.withLock { self.vo = nil }
        window?.orderOut(nil)
        window?.close()
    }

    func reconfig(_ vo: UnsafeMutablePointer<vo>) {
        eventsLock.withLock { self.vo = vo }
        if backendState == .needsInit {
            DispatchQueue.main.sync { self.initBackend(vo) }
        } else if option.vo.auto_window_resize {
            DispatchQueue.main.async {
                self.updateWindowSize(vo)
                self.layer?.update(force: true)
                if self.option.vo.focus_on == 2 {
                    NSApp.activate(ignoringOtherApps: true)
                }
            }
        }
    }

    func initBackend(_ vo: UnsafeMutablePointer<vo>) {
        let previousActiveApp = getActiveApp()
        initApp()
        initWindow(vo, previousActiveApp)
        updateICCProfile()
        initWindowState()

        backendState = .initialized
    }

    func updateWindowSize(_ vo: UnsafeMutablePointer<vo>) {
        guard let targetScreen = getTargetScreen(forFullscreen: false) ?? NSScreen.main else {
            log.warning("Couldn't update Window size, no Screen available")
            return
        }

        let (wr, _) = getWindowGeometry(forScreen: targetScreen, videoOut: vo)
        if !(window?.isVisible ?? false) && !(window?.isMiniaturized ?? false) && !NSApp.isHidden {
            window?.makeKeyAndOrderFront(nil)
        }
        layer?.atomicDrawingStart()
        window?.updateSize(wr.size)
    }

    override func displayLinkCallback(_ displayLink: CVDisplayLink,
                                      _ inNow: UnsafePointer<CVTimeStamp>,
                                      _ inOutputTime: UnsafePointer<CVTimeStamp>,
                                      _ flagsIn: CVOptionFlags,
                                      _ flagsOut: UnsafeMutablePointer<CVOptionFlags>) -> CVReturn {
        libmpv.reportRenderFlip()
        return kCVReturnSuccess
    }

    override func updateICCProfile() {
        guard let colorSpace = window?.screen?.colorSpace else {
            log.warning("Couldn't update ICC Profile, no color space available")
            return
        }

        libmpv.setRenderICCProfile(colorSpace)
        let (isEdr, colorspace) = getColorSpace()
        layer?.colorspace = colorspace
        layer?.wantsExtendedDynamicRangeContent = isEdr
    }

    func getColorSpace() -> (Bool, CGColorSpace?) {
        guard let colorSpace = window?.screen?.colorSpace?.cgColorSpace else {
            log.warning("Couldn't retrieve ICC Profile, no color space available")
            return (false, nil)
        }

        let outputCsp = Int(option.mac.cocoa_cb_output_csp)

        switch outputCsp {
        case MAC_CSP_AUTO: return (false, colorSpace)
        case MAC_CSP_DISPLAY_P3: return (true, CGColorSpace(name: CGColorSpace.displayP3))
        case MAC_CSP_DISPLAY_P3_HLG: return (true, CGColorSpace(name: CGColorSpace.displayP3_HLG))
        case MAC_CSP_DCI_P3: return (true, CGColorSpace(name: CGColorSpace.dcip3))
        case MAC_CSP_BT_2020: return (true, CGColorSpace(name: CGColorSpace.itur_2020))
        case MAC_CSP_BT_709: return (false, CGColorSpace(name: CGColorSpace.itur_709))
        case MAC_CSP_SRGB: return (false, CGColorSpace(name: CGColorSpace.sRGB))
        case MAC_CSP_SRGB_LINEAR: return (false, CGColorSpace(name: CGColorSpace.linearSRGB))
        case MAC_CSP_RGB_LINEAR: return (false, CGColorSpace(name: CGColorSpace.genericRGBLinear))
        case MAC_CSP_ADOBE: return (false, CGColorSpace(name: CGColorSpace.adobeRGB1998))
        default: break
        }

#if HAVE_MACOS_10_15_4_FEATURES
        if #available(macOS 10.15.4, *) {
            switch outputCsp {
            case MAC_CSP_DISPLAY_P3_PQ: return (true, CGColorSpace(name: CGColorSpace.displayP3_PQ))
            default: break
            }
        }
#endif

#if HAVE_MACOS_11_FEATURES
        if #available(macOS 11.0, *) {
            switch outputCsp {
            case MAC_CSP_BT_2100_HLG: return (true, CGColorSpace(name: CGColorSpace.itur_2100_HLG))
            case MAC_CSP_BT_2100_PQ: return (true, CGColorSpace(name: CGColorSpace.itur_2100_PQ))
            default: break
            }
        }
#endif

#if HAVE_MACOS_12_FEATURES
        if #available(macOS 12.0, *) {
            switch outputCsp {
            case MAC_CSP_DISPLAY_P3_LINEAR: return (true, CGColorSpace(name: CGColorSpace.linearDisplayP3))
            case MAC_CSP_BT_2020_LINEAR: return (true, CGColorSpace(name: CGColorSpace.linearITUR_2020))
            default: break
            }
        }
#endif

        log.warning("Couldn't retrieve configured color space, falling back to auto")

        return (false, colorSpace)
    }

    override func windowDidEndAnimation() {
        layer?.update()
        checkShutdown()
    }

    override func windowSetToFullScreen() {
        layer?.update(force: true)
    }

    override func windowSetToWindow() {
        layer?.update(force: true)
    }

    override func windowDidUpdateFrame() {
        layer?.update(force: true)
    }

    override func windowDidChangeScreen() {
        layer?.update(force: true)
    }

    override func windowDidChangeScreenProfile() {
        layer?.needsICCUpdate = true
    }

    override func windowDidChangeBackingProperties() {
        layer?.contentsScale = window?.backingScaleFactor ?? 1
    }

    override func windowWillStartLiveResize() {
        layer?.inLiveResize = true
    }

    override func windowDidEndLiveResize() {
        layer?.inLiveResize = false
    }

    override func windowDidChangeOcclusionState() {
        layer?.update(force: true)
    }

    var controlCallback: mp_render_cb_control_fn = { ( v, ctx, e, request, data ) -> Int32 in
        let ccb = unsafeBitCast(ctx, to: CocoaCB.self)

        guard let vo = v, let events = e else {
            ccb.log.warning("Unexpected nil value in Control Callback")
            return VO_FALSE
        }

        return ccb.control(vo, events: events, request: request, data: data)
    }

    override func control(_ vo: UnsafeMutablePointer<vo>,
                          events: UnsafeMutablePointer<Int32>,
                          request: UInt32,
                          data: UnsafeMutableRawPointer?) -> Int32 {
        switch mp_voctrl(request) {
        case VOCTRL_PREINIT:
            DispatchQueue.main.sync { self.preinit(vo) }
            return VO_TRUE
        case VOCTRL_UNINIT:
            DispatchQueue.main.async { self.uninit() }
            return VO_TRUE
        case VOCTRL_RECONFIG:
            reconfig(vo)
            return VO_TRUE
        default:
            break
        }

        return super.control(vo, events: events, request: request, data: data)
    }

    func shutdown() {
        isShuttingDown = window?.isAnimating ?? false ||
                         window?.isInFullscreen ?? false && option.vo.native_fs
        if window?.isInFullscreen ?? false && !(window?.isAnimating ?? false) {
            window?.close()
        }
        if isShuttingDown { return }

        uninit()
        uninitCommon()
        window = nil

        layer?.lockCglContext()
        libmpv.uninit()
        layer?.unlockCglContext()
    }

    func checkShutdown() {
        if isShuttingDown {
            shutdown()
        }
    }

    func handle(event: EventHelper.Event) {
        if event.name == String(describing: MPV_EVENT_SHUTDOWN) { shutdown() }
    }
}
