/*
 * 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
import IOKit.pwr_mgt

class Common: NSObject {
    var option: OptionHelper
    var input: InputHelper?
    var log: LogHelper
    let queue: DispatchQueue = DispatchQueue(label: "io.mpv.queue")

    @objc var window: Window?
    var view: View?
    var titleBar: TitleBar?

    var link: CVDisplayLink?

    let eventsLock = NSLock()
    var vo: UnsafeMutablePointer<vo>?
    var events: Int = 0

    var lightSensor: io_connect_t = 0
    var lastLmu: Double = 0
    var lightSensorIOPort: IONotificationPortRef?

    var displaySleepAssertion: IOPMAssertionID = IOPMAssertionID(0)

    var appNotificationObservers: [NSObjectProtocol] = []

    var cursorVisibilityWanted: Bool = true
    var needsInitialDraw: Bool = true

    var title: String = "mpv" {
        didSet { if let window = window { window.title = title } }
    }

    init(_ option: OptionHelper, _ log: LogHelper) {
        self.option = option
        self.log = log
    }

    func initMisc(_ vo: UnsafeMutablePointer<vo>) {
        startDisplayLink(vo)
        initLightSensor()
        addDisplayReconfigureObserver()
        addAppNotifications()
        option.setMacOptionCallback(macOptsWakeupCallback, context: self)
    }

    func initApp() {
        var policy: NSApplication.ActivationPolicy = .regular
        switch option.mac.macos_app_activation_policy {
        case 0: policy = .regular
        case 1: policy = .accessory
        case 2: policy = .prohibited
        default: break
        }

        NSApp.setActivationPolicy(policy)
        setAppIcon()
    }

    func initWindow(_ vo: UnsafeMutablePointer<vo>, _ previousActiveApp: NSRunningApplication?) {
        let (targetScreen, wr, _) = getInitProperties(vo)

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

        window = Window(contentRect: wr, screen: targetScreen, view: view, common: self)
        guard let window = self.window else {
            log.error("Something went wrong, no Window was initialized")
            exit(1)
        }

        window.setOnTop(Bool(option.vo.ontop), Int(option.vo.ontop_level))
        window.setOnAllWorkspaces(Bool(option.vo.all_workspaces))
        window.keepAspect = Bool(option.vo.keepaspect_window)
        window.title = title
        window.border = Bool(option.vo.border)

        titleBar = TitleBar(frame: wr, window: window, common: self)

        let maximized = Bool(option.vo.window_maximized)
        let minimized = Bool(option.vo.window_minimized)
        window.isRestorable = false
        window.isReleasedWhenClosed = false
        window.setMaximized((minimized || !maximized) ? window.isZoomed : maximized)
        window.setMinimized(minimized)
        window.makeMain()
        window.makeKey()

        view.layer?.contentsScale = window.backingScaleFactor

        if !minimized {
            window.orderFront(nil)
        }

        NSApp.activate(ignoringOtherApps: option.vo.focus_on >= 1)

        // workaround for macOS 10.15 to refocus the previous App
        if option.vo.focus_on == 0 {
            previousActiveApp?.activate()
        }
    }

    func initView(_ vo: UnsafeMutablePointer<vo>, _ layer: CALayer) {
        let (_, wr, _) = getInitProperties(vo)

        view = View(frame: wr, common: self)
        guard let view = self.view else {
            log.error("Something went wrong, no View was initialized")
            exit(1)
        }

        view.layer = layer
        view.wantsLayer = true
        view.layerContentsPlacement = .scaleProportionallyToFit
        layer.delegate = view
    }

    func initWindowState() {
        if option.vo.fullscreen {
            DispatchQueue.main.async {
                self.window?.toggleFullScreen(nil)
            }
        }
    }

    func uninitCommon() {
        eventsLock.withLock { self.vo = nil }
        setCursorVisibility(true)
        stopDisplaylink()
        uninitLightSensor()
        removeDisplayReconfigureObserver()
        removeAppNotifications()
        enableDisplaySleep()
        window?.orderOut(nil)

        titleBar?.removeFromSuperview()
        view?.removeFromSuperview()
    }

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

    func startDisplayLink(_ vo: UnsafeMutablePointer<vo>) {
        CVDisplayLinkCreateWithActiveCGDisplays(&link)

        guard let screen = getTargetScreen(forFullscreen: false) ?? NSScreen.main,
              let link = self.link else {
            log.warning("Couldn't start DisplayLink, no Screen or DisplayLink available")
            return
        }

        CVDisplayLinkSetCurrentCGDisplay(link, screen.displayID)
        CVDisplayLinkSetOutputHandler(link) { link, now, out, inFlags, outFlags -> CVReturn in
            return self.displayLinkCallback(link, now, out, inFlags, outFlags)
        }
        CVDisplayLinkStart(link)
    }

    func stopDisplaylink() {
        if let link = self.link, CVDisplayLinkIsRunning(link) {
            CVDisplayLinkStop(link)
        }
    }

    func updateDisplaylink() {
        guard let screen = window?.screen, let link = self.link else {
            log.warning("Couldn't update DisplayLink, no Screen or DisplayLink available")
            return
        }

        CVDisplayLinkSetCurrentCGDisplay(link, screen.displayID)
        queue.asyncAfter(deadline: DispatchTime.now() + 0.1) {
            self.flagEvents(VO_EVENT_WIN_STATE)
        }
    }

    func currentFps() -> Double {
        if let link = self.link {
            var actualFps = CVDisplayLinkGetActualOutputVideoRefreshPeriod(link)
            let nominalData = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link)

            if (nominalData.flags & Int32(CVTimeFlags.isIndefinite.rawValue)) < 1 {
                let nominalFps = Double(nominalData.timeScale) / Double(nominalData.timeValue)

                if actualFps > 0 {
                    actualFps = 1/actualFps
                }

                if fabs(actualFps - nominalFps) > 0.1 {
                    log.verbose("Falling back to nominal display refresh rate: \(nominalFps)")
                    return nominalFps
                } else {
                    return actualFps
                }
            }
        } else {
            log.warning("No DisplayLink available")
        }

        log.warning("Falling back to standard display refresh rate: 60Hz")
        return 60.0
    }

    func enableDisplaySleep() {
        IOPMAssertionRelease(displaySleepAssertion)
        displaySleepAssertion = IOPMAssertionID(0)
    }

    func disableDisplaySleep() {
        if displaySleepAssertion != IOPMAssertionID(0) { return }
        IOPMAssertionCreateWithName(
            kIOPMAssertionTypePreventUserIdleDisplaySleep as CFString,
            IOPMAssertionLevel(kIOPMAssertionLevelOn),
            "io.mpv.video_playing_back" as CFString,
            &displaySleepAssertion)
    }

    func lmuToLux(_ lmu: Double) -> Double {
        // the polynomial approximation for apple lmu value -> lux was empirically
        // derived by firefox developers (Apple provides no documentation).
        // https://bugzilla.mozilla.org/show_bug.cgi?id=793728
        let power_c4: Double = 1 / pow(10, 27)
        let power_c3: Double = 1 / pow(10, 19)
        let power_c2: Double = 1 / pow(10, 12)
        let power_c1: Double = 1 / pow(10, 5)

        let term4: Double = -3.0 * power_c4 * pow(lmu, 4.0)
        let term3: Double = 2.6 * power_c3 * pow(lmu, 3.0)
        let term2: Double = -3.4 * power_c2 * pow(lmu, 2.0)
        let term1: Double = 3.9 * power_c1 * lmu

        let lux = ceil(term4 + term3 + term2 + term1 - 0.19)
        return lux > 0 ? lux : 0
    }

    var lightSensorCallback: IOServiceInterestCallback = { (ctx, _ service, _ messageType, _ messageArgument) in
        let com = unsafeBitCast(ctx, to: Common.self)

        var outputs: UInt32 = 2
        var values: [UInt64] = [0, 0]

        var kr = IOConnectCallMethod(com.lightSensor, 0, nil, 0, nil, 0, &values, &outputs, nil, nil)
        if kr == KERN_SUCCESS {
            var mean: Double = (Double(values[0]) + Double(values[1])) / 2
            if com.lastLmu != mean {
                com.lastLmu = mean
                com.lightSensorUpdate()
            }
        }
    }

    func lightSensorUpdate() {
        flagEvents(VO_EVENT_AMBIENT_LIGHTING_CHANGED)
    }

    func initLightSensor() {
        let mainPort: mach_port_t
        if #available(macOS 12.0, *) { mainPort = kIOMainPortDefault } else { mainPort = kIOMasterPortDefault }

        let srv = IOServiceGetMatchingService(mainPort, IOServiceMatching("AppleLMUController"))
        if srv == IO_OBJECT_NULL {
            log.verbose("Can't find an ambient light sensor")
            return
        }

        lightSensorIOPort = IONotificationPortCreate(mainPort)
        IONotificationPortSetDispatchQueue(lightSensorIOPort, queue)
        var n = io_object_t()
        IOServiceAddInterestNotification(lightSensorIOPort, srv, kIOGeneralInterest, lightSensorCallback,
                                         TypeHelper.bridge(obj: self), &n)
        let kr = IOServiceOpen(srv, mach_task_self_, 0, &lightSensor)
        IOObjectRelease(srv)

        if kr != KERN_SUCCESS {
            log.verbose("Can't start ambient light sensor connection")
            return
        }
        lightSensorCallback(TypeHelper.bridge(obj: self), 0, 0, nil)
    }

    func uninitLightSensor() {
        if lightSensorIOPort != nil {
            IONotificationPortDestroy(lightSensorIOPort)
            IOObjectRelease(lightSensor)
        }
    }

    var reconfigureCallback: CGDisplayReconfigurationCallBack = { (display, flags, userInfo) in
        if flags.contains(.setModeFlag) {
            let com = unsafeBitCast(userInfo, to: Common.self)
            let displayID = com.window?.screen?.displayID ?? display

            if displayID == display {
                com.log.verbose("Detected display mode change, updating screen refresh rate")
                com.flagEvents(VO_EVENT_WIN_STATE)
            }
        }
    }

    func addDisplayReconfigureObserver() {
        CGDisplayRegisterReconfigurationCallback(reconfigureCallback, TypeHelper.bridge(obj: self))
    }

    func removeDisplayReconfigureObserver() {
        CGDisplayRemoveReconfigurationCallback(reconfigureCallback, TypeHelper.bridge(obj: self))
    }

    func addAppNotifications() {
        appNotificationObservers.append(NotificationCenter.default.addObserver(
            forName: NSApplication.didBecomeActiveNotification,
            object: nil,
            queue: .main,
            using: { [weak self] (_) in self?.appDidBecomeActive() }
        ))
        appNotificationObservers.append(NotificationCenter.default.addObserver(
            forName: NSApplication.didResignActiveNotification,
            object: nil,
            queue: .main,
            using: { [weak self] (_) in self?.appDidResignActive() }
        ))
    }

    func removeAppNotifications() {
        appNotificationObservers.forEach { NotificationCenter.default.removeObserver($0) }
        appNotificationObservers.removeAll()
    }

    func appDidBecomeActive() {
        flagEvents(VO_EVENT_FOCUS)
    }

    func appDidResignActive() {
        flagEvents(VO_EVENT_FOCUS)
    }

    func setAppIcon() {
        if !AppHub.shared.isBundle {
            NSApp.applicationIconImage = AppHub.shared.getIcon()
        }
    }

    func updateCursorVisibility() {
        setCursorVisibility(cursorVisibilityWanted)
    }

    func setCursorVisibility(_ visible: Bool) {
        NSCursor.setHiddenUntilMouseMoves(!visible && (view?.canHideCursor() ?? false))
    }

    func updateICCProfile() {
        log.warning("updateICCProfile not implemented")
    }

    func getScreenBy(id screenID: Int) -> NSScreen? {
        if screenID >= NSScreen.screens.count {
            log.info("Screen ID \(screenID) does not exist, falling back to current device")
            return nil
        } else if screenID < 0 {
            return nil
        }
        return NSScreen.screens[screenID]
    }

    func getScreenBy(name screenName: String?) -> NSScreen? {
        for screen in NSScreen.screens
            where [screen.localizedName, screen.name, screen.uniqueName, screen.serialNumber].contains(screenName) {
            return screen
        }
        return nil
    }

    func getTargetScreen(forFullscreen fs: Bool) -> NSScreen? {
        let screenID = fs ? option.vo.fsscreen_id : option.vo.screen_id
        var name: String?
        if let screenName = fs ? option.vo.fsscreen_name : option.vo.screen_name {
            name = String(cString: screenName)
        }
        return getScreenBy(id: Int(screenID)) ?? getScreenBy(name: name)
    }

    func getCurrentScreen() -> NSScreen? {
         return window != nil ? window?.screen :
                                    getTargetScreen(forFullscreen: false) ??
                                    NSScreen.main
    }

    func getWindowGeometry(forScreen screen: NSScreen,
                           videoOut vo: UnsafeMutablePointer<vo>) -> (NSRect, Bool) {
        let r = screen.convertRectToBacking(screen.frame)
        let targetFrame = option.mac.macos_geometry_calculation == FRAME_VISIBLE
            ? screen.visibleFrame : screen.frame
        let rv = screen.convertRectToBacking(targetFrame)

        // convert origin to be relative to target screen
        var originY = rv.origin.y - r.origin.y
        let originX = rv.origin.x - r.origin.x
        // flip the y origin, mp_rect expects the origin at the top-left
        // macOS' windowing system operates from the bottom-left
        originY = -(originY + rv.size.height)
        var screenRC: mp_rect = mp_rect(x0: Int32(originX),
                                        y0: Int32(originY),
                                        x1: Int32(originX + rv.size.width),
                                        y1: Int32(originY + rv.size.height))

        var geo: vo_win_geometry = vo_win_geometry()
        vo_calc_window_geometry(vo, vo.pointee.opts, &screenRC, &screenRC, Double(screen.backingScaleFactor), false, &geo, nil)
        vo_apply_window_geometry(vo, &geo)

        let height = CGFloat(geo.win.y1 - geo.win.y0)
        let width = CGFloat(geo.win.x1 - geo.win.x0)
        // flip the y origin again
        let y = CGFloat(-geo.win.y1)
        let x = CGFloat(geo.win.x0)
        let wr = screen.convertRectFromBacking(NSRect(x: x, y: y, width: width, height: height))
        return (wr, Bool(geo.flags & Int32(VO_WIN_FORCE_POS)))
    }

    func getInitProperties(_ vo: UnsafeMutablePointer<vo>) -> (NSScreen, NSRect, Bool) {
        guard let targetScreen = getTargetScreen(forFullscreen: false) ?? NSScreen.main else {
            log.error("Something went wrong, no Screen was found")
            exit(1)
        }

        let (wr, forcePosition) = getWindowGeometry(forScreen: targetScreen, videoOut: vo)

        return (targetScreen, wr, forcePosition)
    }

    // call before initApp, because on macOS +10.15 it changes the active App
    func getActiveApp() -> NSRunningApplication? {
        return NSWorkspace.shared.runningApplications.first(where: {$0.isActive})
    }

    func flagEvents(_ ev: Int) {
        eventsLock.withLock {
            events |= ev
            guard let vo = vo else { return }
            vo_wakeup(vo)
        }
    }

    func checkEvents() -> Int {
        eventsLock.withLock {
            let ev = events
            events = 0
            return ev
        }
    }

    func windowDidMiniaturize() {
        needsInitialDraw = false
    }

    func windowDidBecomeMain() {
        needsInitialDraw = false
    }

    func windowDidEndAnimation() {}
    func windowSetToFullScreen() {}
    func windowSetToWindow() {}
    func windowDidUpdateFrame() {}
    func windowDidChangeScreen() {}
    func windowDidChangeScreenProfile() {}
    func windowDidChangeBackingProperties() {}
    func windowWillStartLiveResize() {}
    func windowDidEndLiveResize() {}
    func windowDidResize() {}
    func windowDidChangeOcclusionState() {}

    @objc func control(_ vo: UnsafeMutablePointer<vo>,
                       events: UnsafeMutablePointer<Int32>,
                       request: UInt32,
                       data: UnsafeMutableRawPointer?) -> Int32 {
        switch mp_voctrl(request) {
        case VOCTRL_CHECK_EVENTS:
            events.pointee |= Int32(checkEvents())
            return VO_TRUE
        case VOCTRL_VO_OPTS_CHANGED:
            var opt: UnsafeMutableRawPointer?
            while option.nextChangedOption(property: &opt) {
                switch opt {
                case TypeHelper.toPointer(&option.voPtr.pointee.border):
                    DispatchQueue.main.async {
                        self.window?.border = Bool(self.option.vo.border)
                    }
                case TypeHelper.toPointer(&option.voPtr.pointee.fullscreen):
                    DispatchQueue.main.async {
                        self.window?.toggleFullScreen(nil)
                    }
                case TypeHelper.toPointer(&option.voPtr.pointee.ontop),
                     TypeHelper.toPointer(&option.voPtr.pointee.ontop_level):
                    DispatchQueue.main.async {
                        self.window?.setOnTop(Bool(self.option.vo.ontop), Int(self.option.vo.ontop_level))
                    }
                case TypeHelper.toPointer(&option.voPtr.pointee.all_workspaces):
                    DispatchQueue.main.async {
                        self.window?.setOnAllWorkspaces(Bool(self.option.vo.all_workspaces))
                    }
                case TypeHelper.toPointer(&option.voPtr.pointee.keepaspect_window):
                    DispatchQueue.main.async {
                        self.window?.keepAspect = Bool(self.option.vo.keepaspect_window)
                    }
                case TypeHelper.toPointer(&option.voPtr.pointee.window_minimized):
                    DispatchQueue.main.async {
                        self.window?.setMinimized(Bool(self.option.vo.window_minimized))
                    }
                case TypeHelper.toPointer(&option.voPtr.pointee.window_maximized):
                    DispatchQueue.main.async {
                        self.window?.setMaximized(Bool(self.option.vo.window_maximized))
                    }
                case TypeHelper.toPointer(&option.voPtr.pointee.cursor_passthrough):
                    DispatchQueue.main.async {
                        self.window?.ignoresMouseEvents = self.option.vo.cursor_passthrough
                    }
                case TypeHelper.toPointer(&option.voPtr.pointee.geometry),
                     TypeHelper.toPointer(&option.voPtr.pointee.autofit),
                     TypeHelper.toPointer(&option.voPtr.pointee.autofit_smaller),
                     TypeHelper.toPointer(&option.voPtr.pointee.autofit_larger):
                    DispatchQueue.main.async {
                        let (_, wr, forcePosition) = self.getInitProperties(vo)
                        forcePosition ? self.window?.updateFrame(wr) : self.window?.updateSize(wr.size)
                    }
                default:
                    break
                }
            }
            return VO_TRUE
        case VOCTRL_GET_DISPLAY_FPS:
            let fps = data!.assumingMemoryBound(to: CDouble.self)
            fps.pointee = currentFps()
            return VO_TRUE
        case VOCTRL_GET_WINDOW_ID:
            guard let window = window else {
                return VO_NOTAVAIL
            }
            let wid = data!.assumingMemoryBound(to: Int64.self)
            wid.pointee = unsafeBitCast(window, to: Int64.self)
            return VO_TRUE
        case VOCTRL_GET_HIDPI_SCALE:
            let scaleFactor = data!.assumingMemoryBound(to: CDouble.self)
            let screen = getCurrentScreen()
            let factor = window?.backingScaleFactor ??
                         screen?.backingScaleFactor ?? 1.0
            scaleFactor.pointee = Double(factor)
            return VO_TRUE
        case VOCTRL_RESTORE_SCREENSAVER:
            enableDisplaySleep()
            return VO_TRUE
        case VOCTRL_KILL_SCREENSAVER:
            disableDisplaySleep()
            return VO_TRUE
        case VOCTRL_SET_CURSOR_VISIBILITY:
            let cursorVisibility = data!.assumingMemoryBound(to: CBool.self)
            cursorVisibilityWanted = cursorVisibility.pointee
            DispatchQueue.main.async {
                self.setCursorVisibility(self.cursorVisibilityWanted)
            }
            return VO_TRUE
        case VOCTRL_GET_ICC_PROFILE:
            let screen = getCurrentScreen()
            guard var iccData = screen?.colorSpace?.iccProfileData else {
                log.warning("No Screen available to retrieve ICC profile")
                return VO_TRUE
            }

            let icc = data!.assumingMemoryBound(to: bstr.self)
            iccData.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) in
                guard let baseAddress = ptr.baseAddress, ptr.count > 0 else { return }
                let u8Ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
                icc.pointee = bstrdup(nil, bstr(start: u8Ptr, len: ptr.count))
            }
            return VO_TRUE
        case VOCTRL_GET_AMBIENT_LUX:
            if lightSensor != 0 {
                let lux = data!.assumingMemoryBound(to: CDouble.self)
                lux.pointee = lmuToLux(lastLmu)
                return VO_TRUE
            }
            return VO_NOTIMPL
        case VOCTRL_GET_UNFS_WINDOW_SIZE:
            let sizeData = data!.assumingMemoryBound(to: Int32.self)
            let size = UnsafeMutableBufferPointer(start: sizeData, count: 2)
            let rect = (Bool(option.vo.hidpi_window_scale) ? window?.unfsContentFrame
                : window?.unfsContentFramePixel) ?? NSRect(x: 0, y: 0, width: 1280, height: 720)

            size[0] = Int32(rect.size.width)
            size[1] = Int32(rect.size.height)
            return VO_TRUE
        case VOCTRL_SET_UNFS_WINDOW_SIZE:
            let sizeData = data!.assumingMemoryBound(to: Int32.self)
            let size = UnsafeBufferPointer(start: sizeData, count: 2)
            var rect = NSRect(x: 0, y: 0, width: CGFloat(size[0]), height: CGFloat(size[1]))
            DispatchQueue.main.async {
                if let screen = self.window?.currentScreen, !Bool(self.option.vo.hidpi_window_scale) {
                    rect = screen.convertRectFromBacking(rect)
                }
                self.window?.updateSize(rect.size)
            }
            return VO_TRUE
        case VOCTRL_GET_DISPLAY_NAMES:
            let dnames = data!.assumingMemoryBound(to: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?.self)
            var array: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?
            var count: Int32 = 0
            let displayName = getCurrentScreen()?.uniqueName ?? "Unknown"

            app_bridge_tarray_append(nil, &array, &count, ta_xstrdup(nil, displayName))
            app_bridge_tarray_append(nil, &array, &count, nil)
            dnames.pointee = array
            return VO_TRUE
        case VOCTRL_GET_DISPLAY_RES:
            guard let screen = getCurrentScreen() else {
                log.warning("No Screen available to retrieve frame")
                return VO_NOTAVAIL
            }
            let sizeData = data!.assumingMemoryBound(to: Int32.self)
            let size = UnsafeMutableBufferPointer(start: sizeData, count: 2)
            let frame = screen.convertRectToBacking(screen.frame)
            size[0] = Int32(frame.size.width)
            size[1] = Int32(frame.size.height)
            return VO_TRUE
        case VOCTRL_GET_FOCUSED:
            let focus = data!.assumingMemoryBound(to: CBool.self)
            focus.pointee = NSApp.isActive
            return VO_TRUE
        case VOCTRL_UPDATE_WINDOW_TITLE:
            let title = String(cString: data!.assumingMemoryBound(to: CChar.self))
            DispatchQueue.main.async {
                self.title = title
            }
            return VO_TRUE
        case VOCTRL_BEGIN_DRAGGING:
            self.window?.startDragging()
            return VO_TRUE
        default:
            return VO_NOTIMPL
        }
    }

    let macOptsWakeupCallback: OptionHelper.WakeupCallback = { ( ctx ) in
        let com = unsafeBitCast(ctx, to: Common.self)
        DispatchQueue.main.async {
            com.macOptsUpdate()
        }
    }

    func macOptsUpdate() {
        var opt: UnsafeMutableRawPointer?
        while option.nextChangedMacOption(property: &opt) {
            switch opt {
            case TypeHelper.toPointer(&option.macPtr.pointee.macos_title_bar_appearance):
                titleBar?.set(appearance: option.mac.macos_title_bar_appearance)
            case TypeHelper.toPointer(&option.macPtr.pointee.macos_title_bar_material):
                titleBar?.set(material: option.mac.macos_title_bar_material)
            case TypeHelper.toPointer(&option.macPtr.pointee.macos_title_bar_color):
                titleBar?.set(color: option.mac.macos_title_bar_color)
            case TypeHelper.toPointer(&option.macPtr.pointee.cocoa_cb_output_csp):
                updateICCProfile()
            default:
                break
            }
        }
    }
}
