//
// Copyright © 2020 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Carbon.HIToolbox

class VMMetalView: MTKView {
    weak var inputDelegate: VMMetalViewInputDelegate?
    private var wholeTrackingArea: NSTrackingArea?
    private var lastModifiers = NSEvent.ModifierFlags()
    private var lastKeyDown: Int?
    private(set) var isMouseCaptured = false
    private(set) var isFirstResponder = false
    private(set) var isMouseInWindow = false
    
    /// On ISO keyboards we have to switch `kVK_ISO_Section` and `kVK_ANSI_Grave`
    /// from: https://chromium.googlesource.com/chromium/src/+/lkgr/ui/events/keycodes/keyboard_code_conversion_mac.mm
    private func convertToCurrentLayout(for keycode: Int) -> Int {
        guard KBGetLayoutType(Int16(LMGetKbdType())) == kKeyboardISO else {
            return keycode
        }
        switch keycode {
        case kVK_ISO_Section:
            return kVK_ANSI_Grave
        case kVK_ANSI_Grave:
            return kVK_ISO_Section
        default:
            return keycode
        }
    }
    
    /// Returns the scan code for the key code in the `event`, or `0` if scan code is unknown.
    private func getScanCodeForEvent(_ event: NSEvent) -> Int {
        if event.type == .keyDown || event.type == .keyUp {
            let keycode = convertToCurrentLayout(for: Int(event.keyCode))
            /// see KeyCodeMap file for explaination why the .down scan code is used for both key down and up
            return Int(KeyCodeMap.keyCodeToScanCodes[keycode]?.down ?? 0)
        } else {
            return 0
        }
    }
    
    override var acceptsFirstResponder: Bool { true }
    
    override func becomeFirstResponder() -> Bool {
        isFirstResponder = true
        if isMouseInWindow {
            NSCursor.tryHide()
        }
        return super.becomeFirstResponder()
    }
    
    override func resignFirstResponder() -> Bool {
        isFirstResponder = false
        NSCursor.tryUnhide()
        if let lastKeyDown = lastKeyDown {
            inputDelegate?.keyUp(scanCode: lastKeyDown)
        }
        if lastModifiers.containsSpecialKeys {
            sendModifiers(lastModifiers, press: false)
            lastModifiers = []
        }
        return super.resignFirstResponder()
    }
    
    override func updateTrackingAreas() {
        let trackingArea = NSTrackingArea(rect: CGRect(origin: .zero, size: frame.size), options: [.mouseMoved, .mouseEnteredAndExited, .activeInKeyWindow], owner: self, userInfo: nil)
        logger.debug("update tracking area: \(trackingArea.rect)")
        if let oldTrackingArea = wholeTrackingArea {
            logger.debug("remove old tracking area: \(oldTrackingArea.rect)")
            removeTrackingArea(oldTrackingArea)
            NSCursor.tryUnhide()
        }
        wholeTrackingArea = trackingArea
        addTrackingArea(trackingArea)
        super.updateTrackingAreas()
    }
    
    override func mouseEntered(with event: NSEvent) {
        logger.debug("mouse entered (first responder: \(isFirstResponder))")
        isMouseInWindow = true
        if isFirstResponder {
            NSCursor.tryHide()
        }
    }
    
    override func mouseExited(with event: NSEvent) {
        logger.debug("mouse exited")
        isMouseInWindow = false
        NSCursor.tryUnhide()
    }
    
    override func mouseDown(with event: NSEvent) {
        logger.trace("mouse down: \(event.buttonNumber)")
        inputDelegate?.mouseDown(button: .left)
    }
    
    override func rightMouseDown(with event: NSEvent) {
        logger.trace("right mouse down: \(event.buttonNumber)")
        inputDelegate?.mouseDown(button: .right)
    }
    
    override func mouseUp(with event: NSEvent) {
        logger.trace("mouse up: \(event.buttonNumber)")
        inputDelegate?.mouseUp(button: .left)
    }
    
    override func rightMouseUp(with event: NSEvent) {
        logger.trace("right mouse up: \(event.buttonNumber)")
        inputDelegate?.mouseUp(button: .right)
    }
    
    override func keyDown(with event: NSEvent) {
        guard !event.isARepeat else { return }
        logger.trace("key down: \(event.keyCode)")
        lastKeyDown = getScanCodeForEvent(event)
        inputDelegate?.keyDown(scanCode: lastKeyDown!)
    }
    
    override func keyUp(with event: NSEvent) {
        logger.trace("key up: \(event.keyCode)")
        lastKeyDown = nil
        inputDelegate?.keyUp(scanCode: getScanCodeForEvent(event))
    }
    
    override func flagsChanged(with event: NSEvent) {
        let modifiers = event.modifierFlags
        logger.trace("modifers: \(modifiers)")
        if let shouldUseCmdOptForCapture = inputDelegate?.shouldUseCmdOptForCapture {
            let captureKeyPressed: Bool
            if shouldUseCmdOptForCapture {
                captureKeyPressed = modifiers.isSuperset(of: [.command, .option])
            } else {
                captureKeyPressed = modifiers.isSuperset(of: [.control, .option])
            }
            if captureKeyPressed {
                if isMouseCaptured {
                    inputDelegate!.releaseMouse()
                } else {
                    inputDelegate!.captureMouse()
                }
            }
        }
        sendModifiers(lastModifiers.subtracting(modifiers), press: false)
        sendModifiers(modifiers.subtracting(lastModifiers), press: true)
        lastModifiers = modifiers
        if !isMouseCaptured {
            super.flagsChanged(with: event)
        }
    }
    
    private func sendModifiers(_ modifier: NSEvent.ModifierFlags, press: Bool) {
        if modifier.contains(.capsLock) {
            let sc = Int(KeyCodeMap.keyCodeToScanCodes[kVK_CapsLock]!.down)
            if press {
                inputDelegate?.keyDown(scanCode: sc)
            } else {
                inputDelegate?.keyUp(scanCode: sc)
            }
        }
        if !modifier.isDisjoint(with: [.command, .leftCommand, .rightCommand]) {
            let vk = modifier.contains(.rightCommand) ? kVK_RightCommand : kVK_Command
            let sc = Int(KeyCodeMap.keyCodeToScanCodes[vk]!.down)
            if press {
                inputDelegate?.keyDown(scanCode: sc)
            } else {
                inputDelegate?.keyUp(scanCode: sc)
            }
        }
        if !modifier.isDisjoint(with: [.control, .leftControl, .rightControl]) {
            let vk = modifier.contains(.rightControl) ? kVK_RightControl : kVK_Control
            let sc = Int(KeyCodeMap.keyCodeToScanCodes[vk]!.down)
            if press {
                inputDelegate?.keyDown(scanCode: sc)
            } else {
                inputDelegate?.keyUp(scanCode: sc)
            }
        }
        if modifier.contains(.function) {
            let sc = Int(KeyCodeMap.keyCodeToScanCodes[kVK_Function]!.down)
            if press {
                inputDelegate?.keyDown(scanCode: sc)
            } else {
                inputDelegate?.keyUp(scanCode: sc)
            }
        }
        if !modifier.isDisjoint(with: [.option, .leftOption, .rightOption]) {
            let vk = modifier.contains(.rightOption) ? kVK_RightOption : kVK_Option
            let sc = Int(KeyCodeMap.keyCodeToScanCodes[vk]!.down)
            if press {
                inputDelegate?.keyDown(scanCode: sc)
            } else {
                inputDelegate?.keyUp(scanCode: sc)
            }
        }
        if !modifier.isDisjoint(with: [.shift, .leftShift, .rightShift]) {
            let vk = modifier.contains(.rightShift) ? kVK_RightShift : kVK_Shift
            let sc = Int(KeyCodeMap.keyCodeToScanCodes[vk]!.down)
            if press {
                inputDelegate?.keyDown(scanCode: sc)
            } else {
                inputDelegate?.keyUp(scanCode: sc)
            }
        }
    }
    
    override func mouseDragged(with event: NSEvent) {
        mouseMoved(with: event)
    }
    
    override func rightMouseDragged(with event: NSEvent) {
        mouseMoved(with: event)
    }
    
    override func otherMouseDragged(with event: NSEvent) {
        mouseMoved(with: event)
    }
    
    override func mouseMoved(with event: NSEvent) {
        logger.trace("mouse moved: \(event.deltaX), \(event.deltaY)")
        if isMouseCaptured {
            inputDelegate?.mouseMove(relativePoint: CGPoint(x: event.deltaX, y: -event.deltaY),
                                     button: NSEvent.pressedMouseButtons.inputButtons())
        } else {
            let location = event.locationInWindow
            let converted = convert(location, from: nil)
            inputDelegate?.mouseMove(absolutePoint: converted,
                                     button: NSEvent.pressedMouseButtons.inputButtons())
        }
    }
    
    override func scrollWheel(with event: NSEvent) {
        guard event.deltaY != 0 else { return }
        logger.trace("scroll: \(event.deltaY)")
        inputDelegate?.mouseScroll(dy: event.deltaY,
                                   button: NSEvent.pressedMouseButtons.inputButtons())
    }
}

extension VMMetalView {
    private var screenCenter: CGPoint? {
        guard let window = self.window else { return nil }
        guard let screen = window.screen else { return nil }
        let centerView = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
        let centerWindow = convert(centerView, to: nil)
        var centerScreen = window.convertPoint(toScreen: centerWindow)
        let screenHeight = screen.frame.height
        centerScreen.y = screenHeight - centerScreen.y
        logger.debug("screen \(centerScreen.x), \(centerScreen.y)")
        return centerScreen
    }
    
    func captureMouse() {
        logger.trace("capture cursor")
        CGAssociateMouseAndMouseCursorPosition(0)
        CGWarpMouseCursorPosition(screenCenter ?? .zero)
        isMouseCaptured = true
        NSCursor.tryHide()
        CGSSetGlobalHotKeyOperatingMode(CGSMainConnectionID(), .disable)
    }
    
    func releaseMouse() {
        logger.trace("release cursor")
        CGAssociateMouseAndMouseCursorPosition(1)
        isMouseCaptured = false
        if !isMouseInWindow {
            NSCursor.tryUnhide()
        }
        CGSSetGlobalHotKeyOperatingMode(CGSMainConnectionID(), .enable)
    }
}

extension VMMetalView: NSAccessibilityGroup {
    override func accessibilityRole() -> NSAccessibility.Role? {
        .group
    }
    
    override func accessibilityRoleDescription() -> String? {
        NSLocalizedString("Capture Input", comment: "VMMetalView")
    }
    
    override func accessibilityLabel() -> String? {
        NSLocalizedString("Virtual Machine", comment: "VMMetalView")
    }
    
    override func accessibilityHelp() -> String? {
        NSLocalizedString("To capture input or to release the capture, press Command and Option at the same time.", comment: "VMMetalView")
    }
    
    override func isAccessibilityElement() -> Bool {
        true
    }
    
    override func isAccessibilityEnabled() -> Bool {
        true
    }
}

private extension NSCursor {
    private static var isCursorHidden: Bool = false
    
    static func tryHide() {
        if !NSCursor.isCursorHidden {
            NSCursor.hide()
            NSCursor.isCursorHidden = true
        }
    }
    
    static func tryUnhide() {
        if NSCursor.isCursorHidden {
            NSCursor.unhide()
            NSCursor.isCursorHidden = false
        }
    }
}

private extension NSEvent.ModifierFlags {
    var containsSpecialKeys: Bool {
        !self.isDisjoint(with: [.capsLock, .command, .control, .function, .option, .shift])
    }
}

private extension Int {
    func inputButtons() -> CSInputButton {
        var pressed = CSInputButton()
        if self & (1 << 0) != 0 {
            pressed.formUnion(.left)
        }
        if self & (1 << 1) != 0 {
            pressed.formUnion(.right)
        }
        if self & (1 << 2) != 0 {
            pressed.formUnion(.middle)
        }
        return pressed
    }
}

private extension NSEvent.ModifierFlags {
    static var leftCommand: NSEvent.ModifierFlags {
        NSEvent.ModifierFlags(rawValue: 0x8)
    }
    
    static var rightCommand: NSEvent.ModifierFlags {
        NSEvent.ModifierFlags(rawValue: 0x10)
    }
    
    static var leftControl: NSEvent.ModifierFlags {
        NSEvent.ModifierFlags(rawValue: 0x1)
    }
    
    static var rightControl: NSEvent.ModifierFlags {
        NSEvent.ModifierFlags(rawValue: 0x2000)
    }
    
    static var leftOption: NSEvent.ModifierFlags {
        NSEvent.ModifierFlags(rawValue: 0x20)
    }
    
    static var rightOption: NSEvent.ModifierFlags {
        NSEvent.ModifierFlags(rawValue: 0x40)
    }
    
    static var leftShift: NSEvent.ModifierFlags {
        NSEvent.ModifierFlags(rawValue: 0x2)
    }
    
    static var rightShift: NSEvent.ModifierFlags {
        NSEvent.ModifierFlags(rawValue: 0x4)
    }
}
