import UIKit
import ObjectiveC.runtime

// MARK: - IOKit

@objc private protocol IOHIDEvent: NSObjectProtocol {}

private struct IOHIDDigitizerEventMask: OptionSet {
    let rawValue: UInt32
    init(rawValue: UInt32) { self.rawValue = rawValue }

    static let range = IOHIDDigitizerEventMask(rawValue: 1 << 0)
    static let touch = IOHIDDigitizerEventMask(rawValue: 1 << 1)
    static let position = IOHIDDigitizerEventMask(rawValue: 1 << 2)
    static let cancel = IOHIDDigitizerEventMask(rawValue: 1 << 7)
}

private enum IOHIDEventField: UInt32 {
    case digitizerX = 0xB0000
    case digitizerY = 0xB0001
    case digitizerMajorRadius = 0xB0014
    case digitizerMinorRadius = 0xB0015
    case digitizerIsDisplayIntegrated = 0xB0019
}

private enum IOHIDDigitizerTransducerType: UInt32 {
    case finger = 2
}

private struct IOKit {

    typealias CHIDEventCreateDigitizerEvent = @convention(c) (_ allocator: CFAllocator?, _ timestamp: UInt64, _ transducer_type: IOHIDDigitizerTransducerType.RawValue, _ index: UInt32, _ identifier: UInt32, _ eventMask: IOHIDDigitizerEventMask.RawValue, _ buttonEvent: UInt32, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat, _ pressure: CGFloat, _ twist: CGFloat, _ isRange: DarwinBoolean, _ isTouch: DarwinBoolean, _ options: CFOptionFlags) -> IOHIDEvent
    typealias CHIDEventCreateDigitizerFingerEvent = @convention(c) (_ allocator: CFAllocator?, _ timestamp: UInt64, _ identifier: UInt32, _ fingerIndex: UInt32, _ eventMask: IOHIDDigitizerEventMask.RawValue, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat, _ pressure: CGFloat, _ twist: CGFloat, _ isRange: DarwinBoolean, _ isTouch: DarwinBoolean, _ options: CFOptionFlags) -> IOHIDEvent
    typealias CHIDEventGetIntegerValue = @convention(c) (_ event: IOHIDEvent, _ field: IOHIDEventField.RawValue) -> Int
    typealias CHIDEventSetIntegerValue = @convention(c) (_ event: IOHIDEvent, _ field: IOHIDEventField.RawValue, _ value: Int) -> Void
    typealias CHIDEventGetFloatValue = @convention(c) (_ event: IOHIDEvent, _ field: IOHIDEventField.RawValue) -> CGFloat
    typealias CHIDEventSetFloatValue = @convention(c) (_ event: IOHIDEvent, _ field: IOHIDEventField.RawValue, _ value: CGFloat) -> Void
    typealias CHIDEventAppendEvent = @convention(c) (_ event: IOHIDEvent, _ subevent: IOHIDEvent, _ options: CFOptionFlags) -> Void

    let hidCreateDigitizerEvent: CHIDEventCreateDigitizerEvent
    let hidCreateDigitizerFingerEvent: CHIDEventCreateDigitizerFingerEvent
    let hidEventGetIntegerValue: CHIDEventGetIntegerValue
    let hidEventSetIntegerValue: CHIDEventSetIntegerValue
    let hidEventGetFloatValue: CHIDEventGetFloatValue
    let hidEventSetFloatValue: CHIDEventSetFloatValue
    let hidEventAppend: CHIDEventAppendEvent

    static let shared: IOKit = {
        let handle = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", RTLD_NOW)
        return IOKit(
            hidCreateDigitizerEvent: unsafeBitCast(dlsym(handle, "IOHIDEventCreateDigitizerEvent"), to: CHIDEventCreateDigitizerEvent.self),
            hidCreateDigitizerFingerEvent: unsafeBitCast(dlsym(handle, "IOHIDEventCreateDigitizerFingerEvent"), to: CHIDEventCreateDigitizerFingerEvent.self),
            hidEventGetIntegerValue: unsafeBitCast(dlsym(handle, "IOHIDEventGetIntegerValue"), to: CHIDEventGetIntegerValue.self),
            hidEventSetIntegerValue: unsafeBitCast(dlsym(handle, "IOHIDEventSetIntegerValue"), to: CHIDEventSetIntegerValue.self),
            hidEventGetFloatValue: unsafeBitCast(dlsym(handle, "IOHIDEventGetFloatValue"), to: CHIDEventGetFloatValue.self),
            hidEventSetFloatValue: unsafeBitCast(dlsym(handle, "IOHIDEventSetFloatValue"), to: CHIDEventSetFloatValue.self),
            hidEventAppend: unsafeBitCast(dlsym(handle, "IOHIDEventAppendEvent"), to: CHIDEventAppendEvent.self))
    }()

}

// MARK: -

private struct BackBoardServices {

    typealias CHIDEventSetDigitizerInfo = @convention(c) (_ digitizerEvent: IOHIDEvent, _ contextID: UInt32, _ systemGestureIsPossible: DarwinBoolean, _ isSystemGestureStateChangeEvent: DarwinBoolean, _ displayUUID: CFString?, _ initialTouchTimestamp: CFTimeInterval, _ maxForce: Float) -> Void

    let hidEventSetDigitizerInfo: CHIDEventSetDigitizerInfo

    static let shared: BackBoardServices = {
        let handle = dlopen("/System/Library/PrivateFrameworks/BackBoardServices.framework/BackBoardServices", RTLD_NOW)
        return BackBoardServices(
            hidEventSetDigitizerInfo: unsafeBitCast(dlsym(handle, "BKSHIDEventSetDigitizerInfo"), to: CHIDEventSetDigitizerInfo.self))
    }()

}

// MARK: -

@objc private protocol UIApplicationSPI: NSObjectProtocol {
    @objc(_enqueueHIDEvent:) func enqueue(_ event: IOHIDEvent)
}

@objc private protocol UIWindowSPI: NSObjectProtocol {
    @objc(_contextId) var contextID: UInt32 { get }
}

private struct UIKit {

    init() {}

    static let shared: UIKit = {
        class_addProtocol(UIApplication.self, UIApplicationSPI.self)
        class_addProtocol(UIWindow.self, UIWindowSPI.self)
        return UIKit()
    }()

    @discardableResult
    func send(_ event: IOHIDEvent, in window: UIWindow?) -> Bool {
        guard let window = window as? UIWindow & UIWindowSPI,
            let app = window.target(forAction: #selector(UIApplicationSPI.enqueue), withSender: window) as? UIApplication & UIApplicationSPI else { return false }
        BackBoardServices.shared.hidEventSetDigitizerInfo(event, window.contextID, false, false, nil, 0, 0)
        app.enqueue(event)
        return true
    }

}

// MARK: -

struct EventGenerator {

    struct Touch {
        var point = CGPoint.zero
        var phase = UITouch.Phase.stationary
    }

    struct Hand {
        var touches = [Touch]()
        var phase = UITouch.Phase.stationary
    }

    private let window: UIWindow?

    init(window: UIWindow?) {
        self.window = window
    }

}

// MARK: -

private extension EventGenerator {

    static var callbackID = UInt32(0)

    func nextEventCallbackID() -> UInt32 {
        EventGenerator.callbackID &+= 1
        return EventGenerator.callbackID
    }

    func send(_ event: IOHIDEvent) {
        UIKit.shared.send(event, in: window)
    }

    func eventMask(from info: Hand) -> IOHIDDigitizerEventMask {
        for touch in info.touches {
            switch touch.phase {
            case .began, .ended, .cancelled:
                return .touch
            case .moved, .stationary:
                break
            @unknown default:
                break
            }
        }
        return []
    }

    func isRangeAndTouch(in info: Hand) -> DarwinBoolean {
        for touch in info.touches {
            switch touch.phase {
            case .began, .moved, .stationary:
                return true
            default:
                break
            }
        }
        return false
    }

    func eventMask(from info: Touch) -> IOHIDDigitizerEventMask {
        switch info.phase {
        case .began, .ended:
            return [ .touch, .range ]
        case .cancelled:
            return [ .touch, .range, .cancel ]
        case .moved:
            return .position
        case .stationary:
            return []
        @unknown default:
            return []
        }
    }

    func createEvent(_ info: Hand) -> IOHIDEvent {
        let machTime = mach_absolute_time()
        let touch = isRangeAndTouch(in: info)

        let event = IOKit.shared.hidCreateDigitizerEvent(nil, machTime, IOHIDDigitizerTransducerType.finger.rawValue, 0, 0, eventMask(from: info).rawValue, 0, 0, 0, 0, 0, 0, false, touch, 0)
        IOKit.shared.hidEventSetIntegerValue(event, IOHIDEventField.digitizerIsDisplayIntegrated.rawValue, 1)

        for info in info.touches {
            let subevent = IOKit.shared.hidCreateDigitizerFingerEvent(nil, machTime, nextEventCallbackID(), 2, eventMask(from: info).rawValue, info.point.x, info.point.y, 0, 0, 0, touch, touch, 0)
            IOKit.shared.hidEventAppend(event, subevent, 0)
        }

        return event
    }

}

// MARK: -

extension EventGenerator {

    private enum Constants {
        static let fingerLiftDelay = TimeInterval(0.05)
        static let longPressHoldDelay = TimeInterval(2)
        static let multiTapInterval = TimeInterval(0.15)
    }

    func send(_ info: Hand) {
        let event = createEvent(info)
        send(event)
    }

    func touchDown(at point: CGPoint, count: Int = 1) {
        let touches = (0 ..< count).prefix(5).map { _ in
            Touch(point: point, phase: .began)
        }

        send(Hand(touches: touches, phase: .began))
    }

    func liftUp(at point: CGPoint, count: Int = 1) {
        let touches = (0 ..< count).prefix(5).map { _ in
            Touch(point: point, phase: .ended)
        }

        send(Hand(touches: touches, phase: .ended))
    }

    func tap(at point: CGPoint) {
        touchDown(at: point)
        DispatchQueue.main.asyncAfter(deadline: .now() + Constants.fingerLiftDelay) {
            self.liftUp(at: point)
        }
    }

    func longPress(at point: CGPoint) {
        touchDown(at: point)
        DispatchQueue.main.asyncAfter(deadline: .now() + Constants.longPressHoldDelay) {
            self.liftUp(at: point)
        }
    }

    func sendTaps(_ count: Int, at point: CGPoint, numberOfTouches: Int = 1) {
        func handleNext(_ remaining: Range<Int>) {
            tap(at: point)

            guard !remaining.isEmpty else { return }
            DispatchQueue.main.asyncAfter(deadline: .now() + Constants.multiTapInterval) {
                handleNext(remaining.dropFirst())
            }
        }

        handleNext(0 ..< count)
    }

}