import ApplicationServices
import CoreGraphics
import Foundation

struct PrivateWindowActivationOutcome {
    let getProcessStatus: OSStatus
    let setFrontError: CGError?
    let makeKeyFirstError: CGError?
    let makeKeySecondError: CGError?

    var succeeded: Bool {
        getProcessStatus == 0
            && (setFrontError == nil || setFrontError == .success)
            && (makeKeyFirstError == nil || makeKeyFirstError == .success)
            && (makeKeySecondError == nil || makeKeySecondError == .success)
    }
}

final class PrivateWindowActivationBridge: @unchecked Sendable {
    enum BridgeError: Error {
        case frameworkUnavailable
        case symbolUnavailable(String)
    }

    private typealias SLPSSetFrontProcessWithOptionsFunction = @convention(c) (
        UnsafeMutablePointer<ProcessSerialNumber>,
        CGWindowID,
        UInt32
    ) -> CGError
    private typealias SLPSPostEventRecordToFunction = @convention(c) (
        UnsafeMutablePointer<ProcessSerialNumber>,
        UnsafeMutablePointer<UInt8>
    ) -> CGError

    static let shared = try? PrivateWindowActivationBridge()

    private let handle: UnsafeMutableRawPointer?
    private let setFrontProcessWithOptions: SLPSSetFrontProcessWithOptionsFunction
    private let postEventRecordTo: SLPSPostEventRecordToFunction

    private init() throws {
        guard let handle = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_LAZY) else {
            throw BridgeError.frameworkUnavailable
        }

        self.handle = handle
        self.setFrontProcessWithOptions = try Self.symbol("_SLPSSetFrontProcessWithOptions", in: handle)
        self.postEventRecordTo = try Self.symbol("SLPSPostEventRecordTo", in: handle)
    }

    deinit {
        if let handle {
            dlclose(handle)
        }
    }

    func activate(processIdentifier: Int32, windowID: UInt32) -> PrivateWindowActivationOutcome {
        var psn = ProcessSerialNumber()
        let getProcessStatus = GetProcessForPID(pid_t(processIdentifier), &psn)
        guard getProcessStatus == 0 else {
            return PrivateWindowActivationOutcome(
                getProcessStatus: getProcessStatus,
                setFrontError: nil,
                makeKeyFirstError: nil,
                makeKeySecondError: nil
            )
        }

        let cgWindowID = CGWindowID(windowID)
        let setFrontError = setFrontProcessWithOptions(&psn, cgWindowID, 0x200)
        let makeKeyErrors = makeKeyWindow(processSerialNumber: &psn, windowID: cgWindowID)
        return PrivateWindowActivationOutcome(
            getProcessStatus: getProcessStatus,
            setFrontError: setFrontError,
            makeKeyFirstError: makeKeyErrors.0,
            makeKeySecondError: makeKeyErrors.1
        )
    }

    private func makeKeyWindow(
        processSerialNumber: inout ProcessSerialNumber,
        windowID: CGWindowID
    ) -> (CGError, CGError) {
        var mutableWindowID = windowID
        var bytes = [UInt8](repeating: 0, count: 0xf8)
        bytes[0x04] = 0xf8
        bytes[0x3a] = 0x10
        memcpy(&bytes[0x3c], &mutableWindowID, MemoryLayout<UInt32>.size)
        memset(&bytes[0x20], 0xff, 0x10)

        bytes[0x08] = 0x01
        let first = bytes.withUnsafeMutableBufferPointer { buffer in
            postEventRecordTo(&processSerialNumber, buffer.baseAddress!)
        }

        bytes[0x08] = 0x02
        let second = bytes.withUnsafeMutableBufferPointer { buffer in
            postEventRecordTo(&processSerialNumber, buffer.baseAddress!)
        }

        return (first, second)
    }

    private static func symbol<T>(_ name: String, in handle: UnsafeMutableRawPointer) throws -> T {
        guard let rawSymbol = dlsym(handle, name) else {
            throw BridgeError.symbolUnavailable(name)
        }

        return unsafeBitCast(rawSymbol, to: T.self)
    }
}

@_silgen_name("GetProcessForPID") @discardableResult
private func GetProcessForPID(_ pid: pid_t, _ psn: UnsafeMutablePointer<ProcessSerialNumber>) -> OSStatus

enum PrivateAXWindowIDReader {
    static func windowID(for window: AXUIElement) -> UInt32? {
        var windowID = CGWindowID(0)
        guard _AXUIElementGetWindow(window, &windowID) == .success, windowID != 0 else {
            return nil
        }

        return UInt32(windowID)
    }
}

@_silgen_name("_AXUIElementGetWindow") @discardableResult
private func _AXUIElementGetWindow(_ axUIElement: AXUIElement, _ windowID: UnsafeMutablePointer<CGWindowID>) -> AXError
