import AppKit
import ApplicationServices
import Foundation

let canvasRole = "AXWebArea"

struct CanvasDescriptor {
    let role: String
    let rect: CGRect?
    let hidden: HiddenRead
}

enum HiddenRead {
    case value(Bool)
    case absent
    case invalid
}

enum CanvasReadFailure: Error {
    case nonBenign(AXError)
    case traversalLimit
}

func isBenignAbsence(_ error: AXError) -> Bool {
    error == .noValue || error == .attributeUnsupported
}

func validRect(_ rect: CGRect?) -> Bool {
    guard let rect else { return false }
    return [rect.origin.x, rect.origin.y, rect.width, rect.height].allSatisfy(\.isFinite)
        && rect.width > 0 && rect.height > 0
}

func convertAppKitVisibleFramesToAX(
    primaryFrame: CGRect?,
    visibleFrames: [CGRect]
) -> [CGRect]? {
    guard let primaryFrame, validRect(primaryFrame), !visibleFrames.isEmpty,
          visibleFrames.allSatisfy({ validRect($0) }) else { return nil }
    let primaryTop = primaryFrame.maxY
    return visibleFrames.map { visible in
        CGRect(
            x: visible.minX,
            y: primaryTop - visible.maxY,
            width: visible.width,
            height: visible.height
        )
    }
}

func uniqueCanvas(
    _ result: Result<[CanvasDescriptor], CanvasReadFailure>,
    visibleFrames: [CGRect]
) -> CanvasDescriptor? {
    guard let descriptors = try? result.get(), !visibleFrames.isEmpty else { return nil }
    let matches = descriptors.filter { $0.role == canvasRole }
    guard matches.count == 1 else { return nil }
    switch matches[0].hidden {
    case .absent, .value(false): break
    case .invalid, .value(true): return nil
    }
    guard
          let rect = matches[0].rect,
          validRect(rect),
          visibleFrames.contains(where: { validRect($0) && $0.contains(rect) }) else { return nil }
    return matches[0]
}

func selfTest() -> Int32 {
    let display = CGRect(x: 0, y: 0, width: 1920, height: 1040)
    let primaryFrame = CGRect(x: 0, y: 0, width: 1920, height: 1080)
    let appKitVisibleFrames = [
        CGRect(x: 0, y: 0, width: 1920, height: 1055),
        CGRect(x: 0, y: 1080, width: 1600, height: 1200),
        CGRect(x: 0, y: -900, width: 1600, height: 900),
        CGRect(x: -1280, y: 0, width: 1280, height: 1024),
        CGRect(x: 1920, y: 0, width: 1280, height: 1024),
    ]
    guard let convertedFrames = convertAppKitVisibleFramesToAX(
        primaryFrame: primaryFrame,
        visibleFrames: appKitVisibleFrames
    ) else { return 1 }
    let good = CanvasDescriptor(
        role: canvasRole,
        rect: CGRect(x: 100, y: 100, width: 800, height: 600),
        hidden: .value(false)
    )
    let invalidGeometry = [
        CGRect(x: 0, y: 0, width: 0, height: 1),
        CGRect(x: 0, y: 0, width: 1, height: 0),
        CGRect(x: CGFloat.nan, y: 0, width: 1, height: 1),
        CGRect(x: 0, y: CGFloat.infinity, width: 1, height: 1),
    ]
    let outOfBounds = CanvasDescriptor(
        role: good.role,
        rect: CGRect(x: 1800, y: 100, width: 200, height: 200),
        hidden: .value(false)
    )
    do {
        guard case .absent = try hiddenRead(error: .noValue, value: nil),
              case .absent = try hiddenRead(error: .attributeUnsupported, value: nil),
              case .invalid = try hiddenRead(error: .success, value: nil),
              case .invalid = try hiddenRead(error: .success, value: "invalid" as CFString),
              case .value(true) = try hiddenRead(error: .success, value: kCFBooleanTrue),
              case .value(false) = try hiddenRead(error: .success, value: kCFBooleanFalse) else {
            return 1
        }
        do {
            _ = try hiddenRead(error: .cannotComplete, value: nil)
            return 1
        } catch CanvasReadFailure.nonBenign(.cannotComplete) {
        }
    } catch {
        return 1
    }
    guard convertedFrames[0] == CGRect(x: 0, y: 25, width: 1920, height: 1055),
          convertedFrames[1] == CGRect(x: 0, y: -1200, width: 1600, height: 1200),
          convertedFrames[2] == CGRect(x: 0, y: 1080, width: 1600, height: 900),
          convertedFrames[3] == CGRect(x: -1280, y: 56, width: 1280, height: 1024),
          convertedFrames[4] == CGRect(x: 1920, y: 56, width: 1280, height: 1024),
          convertAppKitVisibleFramesToAX(primaryFrame: nil, visibleFrames: appKitVisibleFrames) == nil,
          convertAppKitVisibleFramesToAX(primaryFrame: primaryFrame, visibleFrames: []) == nil,
          uniqueCanvas(.success([good]), visibleFrames: [display]) != nil,
          uniqueCanvas(.success([]), visibleFrames: [display]) == nil,
          uniqueCanvas(.success([good, good]), visibleFrames: [display]) == nil,
          uniqueCanvas(.failure(.nonBenign(.cannotComplete)), visibleFrames: [display]) == nil,
          invalidGeometry.allSatisfy({ rect in
              uniqueCanvas(
                  .success([CanvasDescriptor(role: good.role, rect: rect, hidden: .value(false))]),
                  visibleFrames: [display]
              ) == nil
          }),
          uniqueCanvas(
              .success([CanvasDescriptor(role: good.role, rect: good.rect, hidden: .value(true))]),
              visibleFrames: [display]
          ) == nil,
          uniqueCanvas(
              .success([CanvasDescriptor(role: good.role, rect: good.rect, hidden: .absent)]),
              visibleFrames: [display]
          ) != nil,
          uniqueCanvas(
              .success([CanvasDescriptor(role: good.role, rect: good.rect, hidden: .invalid)]),
              visibleFrames: [display]
          ) == nil,
          uniqueCanvas(
              .success([CanvasDescriptor(role: good.role, rect: nil, hidden: .absent)]),
              visibleFrames: [display]
          ) == nil,
          uniqueCanvas(
              .success([CanvasDescriptor(role: outOfBounds.role, rect: outOfBounds.rect, hidden: .absent)]),
              visibleFrames: [display]
          ) == nil,
          uniqueCanvas(.success([outOfBounds]), visibleFrames: [display]) == nil,
          uniqueCanvas(.success([good]), visibleFrames: []) == nil else { return 1 }
    print("self_test=PASS primary_display_ax_basis=true multi_display_layouts=true unique_web_area=true zero_rejected=true multiple_rejected=true read_error_rejected=true geometry_rejected=true visibility_rejected=true display_bounds_rejected=true")
    return 0
}

if CommandLine.arguments == [CommandLine.arguments[0], "--self-test"] {
    exit(selfTest())
}

guard CommandLine.arguments.count == 2,
      let candidatePID = pid_t(CommandLine.arguments[1]),
      candidatePID > 0 else {
    fputs("usage: CanvasTargetInspect candidatePID\n", stderr)
    exit(64)
}

func attribute(_ element: AXUIElement, _ name: String) throws -> CFTypeRef? {
    var value: CFTypeRef?
    let error = AXUIElementCopyAttributeValue(element, name as CFString, &value)
    if error == .success { return value }
    if isBenignAbsence(error) { return nil }
    throw CanvasReadFailure.nonBenign(error)
}

func stringValue(_ element: AXUIElement, _ name: String) throws -> String? {
    guard let value = try attribute(element, name), CFGetTypeID(value) == CFStringGetTypeID() else { return nil }
    return value as? String
}

func hiddenRead(error: AXError, value: CFTypeRef?) throws -> HiddenRead {
    if isBenignAbsence(error) { return .absent }
    guard error == .success else { throw CanvasReadFailure.nonBenign(error) }
    guard let value, CFGetTypeID(value) == CFBooleanGetTypeID() else { return .invalid }
    return .value(CFBooleanGetValue((value as! CFBoolean)))
}

func hiddenValue(_ element: AXUIElement) throws -> HiddenRead {
    var value: CFTypeRef?
    let error = AXUIElementCopyAttributeValue(element, "AXHidden" as CFString, &value)
    return try hiddenRead(error: error, value: value)
}

func rect(_ element: AXUIElement) throws -> CGRect? {
    guard let position = try attribute(element, kAXPositionAttribute),
          CFGetTypeID(position) == AXValueGetTypeID(),
          let size = try attribute(element, kAXSizeAttribute),
          CFGetTypeID(size) == AXValueGetTypeID() else { return nil }
    var point = CGPoint.zero
    var dimensions = CGSize.zero
    guard AXValueGetValue(position as! AXValue, .cgPoint, &point),
          AXValueGetValue(size as! AXValue, .cgSize, &dimensions) else { return nil }
    return CGRect(origin: point, size: dimensions)
}

func collect(
    _ element: AXUIElement,
    depth: Int = 0,
    visited: inout Int,
    into result: inout [CanvasDescriptor]
) throws {
    guard depth <= 32, visited < 20_000 else { throw CanvasReadFailure.traversalLimit }
    visited += 1
    let role = try stringValue(element, kAXRoleAttribute)
    if role == canvasRole {
        result.append(CanvasDescriptor(
            role: role!,
            rect: try rect(element),
            hidden: try hiddenValue(element)
        ))
    }
    guard let childrenValue = try attribute(element, kAXChildrenAttribute) else { return }
    guard CFGetTypeID(childrenValue) == CFArrayGetTypeID(),
          let children = childrenValue as? [AXUIElement] else {
        throw CanvasReadFailure.nonBenign(.illegalArgument)
    }
    for child in children {
        try collect(child, depth: depth + 1, visited: &visited, into: &result)
    }
}

func axVisibleFrames() -> [CGRect]? {
    let screens = NSScreen.screens
    guard let primaryScreen = screens.first else { return nil }
    return convertAppKitVisibleFramesToAX(
        primaryFrame: primaryScreen.frame,
        visibleFrames: screens.map(\.visibleFrame)
    )
}

let app = AXUIElementCreateApplication(candidatePID)
var descriptors: [CanvasDescriptor] = []
var visited = 0
let traversal: Result<[CanvasDescriptor], CanvasReadFailure>
do {
    try collect(app, visited: &visited, into: &descriptors)
    traversal = .success(descriptors)
} catch let error as CanvasReadFailure {
    traversal = .failure(error)
}

guard let visibleFrames = axVisibleFrames(),
      let target = uniqueCanvas(traversal, visibleFrames: visibleFrames),
      let targetRect = target.rect else {
    fputs("candidate canvas binding failed closed\n", stderr)
    exit(67)
}

print("status=PASS role=AXWebArea")
print("geometry=\(targetRect.origin.x),\(targetRect.origin.y),\(targetRect.width),\(targetRect.height)")
