import AppKit
import ApplicationServices
import Foundation

enum ProbeFailure: Error {
    case read
    case dataInvalid
    case limit
}

enum ProbeValue<Value> {
    case value(Value)
    case absent
    case invalid
}

enum SourceDiagnosis: String, CaseIterable {
    case sourceZero = "SOURCE_ZERO"
    case sourceMultiple = "SOURCE_MULTIPLE"
    case traversalLimit = "SOURCE_TRAVERSAL_LIMIT"
    case traversalDataInvalid = "SOURCE_TRAVERSAL_DATA_INVALID"
    case traversalReadError = "SOURCE_TRAVERSAL_READ_ERROR"
    case sourceWindowIDAbsentOrInvalid = "SOURCE_WINDOW_ID_ABSENT_OR_INVALID"
    case otherWindowIDAbsentOrInvalid = "OTHER_WINDOW_ID_ABSENT_OR_INVALID"
    case duplicateWindowIdentity = "DUPLICATE_WINDOW_IDENTITY"
    case sourcePositionAbsentOrInvalid = "SOURCE_POSITION_ABSENT_OR_INVALID"
    case sourceSizeAbsentOrInvalid = "SOURCE_SIZE_ABSENT_OR_INVALID"
    case sourceGeometryNonfiniteOrNonpositive = "SOURCE_GEOMETRY_NONFINITE_OR_NONPOSITIVE"
    case sourceMinimizedAbsentOrInvalid = "SOURCE_MINIMIZED_ABSENT_OR_INVALID"
    case sourceWindowReadError = "SOURCE_WINDOW_READ_ERROR"
    case sourceWindowReady = "SOURCE_WINDOW_READY"

    var line: String { "finder_source=\(rawValue)" }
}

func classifySourceWindow(
    matchCounts: Result<[Int], ProbeFailure>,
    windowIDs: [() -> Result<ProbeValue<Int64>, ProbeFailure>],
    position: () -> Result<ProbeValue<CGPoint>, ProbeFailure>,
    size: () -> Result<ProbeValue<CGSize>, ProbeFailure>,
    minimized: () -> Result<ProbeValue<Bool>, ProbeFailure>
) -> SourceDiagnosis {
    let counts: [Int]
    switch matchCounts {
    case .failure(.limit): return .traversalLimit
    case .failure(.dataInvalid): return .traversalDataInvalid
    case .failure(.read): return .traversalReadError
    case .success(let value): counts = value
    }
    guard counts.count == windowIDs.count, counts.allSatisfy({ $0 >= 0 }) else {
        return .traversalDataInvalid
    }
    let total = counts.reduce(0, +)
    guard total > 0 else { return .sourceZero }
    guard total == 1, let sourceIndex = counts.firstIndex(of: 1) else { return .sourceMultiple }

    let sourceID: Int64
    switch windowIDs[sourceIndex]() {
    case .failure: return .sourceWindowReadError
    case .success(.absent), .success(.invalid): return .sourceWindowIDAbsentOrInvalid
    case .success(.value(let value)): sourceID = value
    }
    let point: CGPoint
    switch position() {
    case .failure: return .sourceWindowReadError
    case .success(.absent), .success(.invalid): return .sourcePositionAbsentOrInvalid
    case .success(.value(let value)): point = value
    }
    let dimensions: CGSize
    switch size() {
    case .failure: return .sourceWindowReadError
    case .success(.absent), .success(.invalid): return .sourceSizeAbsentOrInvalid
    case .success(.value(let value)): dimensions = value
    }
    guard [point.x, point.y, dimensions.width, dimensions.height].allSatisfy(\.isFinite),
          dimensions.width > 0, dimensions.height > 0 else {
        return .sourceGeometryNonfiniteOrNonpositive
    }
    switch minimized() {
    case .failure: return .sourceWindowReadError
    case .success(.absent), .success(.invalid): return .sourceMinimizedAbsentOrInvalid
    case .success(.value): break
    }
    for (index, readID) in windowIDs.enumerated() where index != sourceIndex {
        switch readID() {
        case .failure: return .sourceWindowReadError
        case .success(.absent), .success(.invalid): return .otherWindowIDAbsentOrInvalid
        case .success(.value(let value)) where value == sourceID: return .duplicateWindowIdentity
        case .success(.value): break
        }
    }
    return .sourceWindowReady
}

func selfTest() -> Int32 {
    typealias IDRead = () -> Result<ProbeValue<Int64>, ProbeFailure>
    let goodPosition = CGPoint(x: 10, y: 20)
    let goodSize = CGSize(width: 500, height: 320)
    func classify(
        counts: Result<[Int], ProbeFailure> = .success([1]),
        ids: [IDRead] = [{ .success(.value(7)) }],
        position: @escaping () -> Result<ProbeValue<CGPoint>, ProbeFailure> = { .success(.value(CGPoint(x: 10, y: 20))) },
        size: @escaping () -> Result<ProbeValue<CGSize>, ProbeFailure> = { .success(.value(CGSize(width: 500, height: 320))) },
        minimized: @escaping () -> Result<ProbeValue<Bool>, ProbeFailure> = { .success(.value(false)) }
    ) -> SourceDiagnosis {
        classifySourceWindow(matchCounts: counts, windowIDs: ids, position: position, size: size, minimized: minimized)
    }
    let cases: [(SourceDiagnosis, SourceDiagnosis)] = [
        (.sourceWindowReady, classify()),
        (.sourceZero, classify(counts: .success([0]))),
        (.sourceMultiple, classify(counts: .success([1, 1]), ids: [{ .success(.value(7)) }, { .success(.value(8)) }])),
        (.sourceMultiple, classify(counts: .success([2]))),
        (.traversalLimit, classify(counts: .failure(.limit))),
        (.traversalDataInvalid, classify(counts: .failure(.dataInvalid))),
        (.traversalReadError, classify(counts: .failure(.read))),
        (.traversalDataInvalid, classify(counts: .success([1, 0]), ids: [{ .success(.value(7)) }])),
        (.sourceWindowIDAbsentOrInvalid, classify(ids: [{ .success(.absent) }])),
        (.sourceWindowIDAbsentOrInvalid, classify(ids: [{ .success(.invalid) }])),
        (.sourceWindowReadError, classify(ids: [{ .failure(.read) }])),
        (.sourcePositionAbsentOrInvalid, classify(position: { .success(.absent) })),
        (.sourcePositionAbsentOrInvalid, classify(position: { .success(.invalid) })),
        (.sourceWindowReadError, classify(position: { .failure(.read) })),
        (.sourceSizeAbsentOrInvalid, classify(size: { .success(.absent) })),
        (.sourceSizeAbsentOrInvalid, classify(size: { .success(.invalid) })),
        (.sourceWindowReadError, classify(size: { .failure(.read) })),
        (.sourceGeometryNonfiniteOrNonpositive, classify(position: { .success(.value(CGPoint(x: CGFloat.nan, y: 0))) })),
        (.sourceGeometryNonfiniteOrNonpositive, classify(size: { .success(.value(CGSize(width: 0, height: 1))) })),
        (.sourceGeometryNonfiniteOrNonpositive, classify(size: { .success(.value(CGSize(width: 1, height: CGFloat.infinity))) })),
        (.sourceMinimizedAbsentOrInvalid, classify(minimized: { .success(.absent) })),
        (.sourceMinimizedAbsentOrInvalid, classify(minimized: { .success(.invalid) })),
        (.sourceWindowReadError, classify(minimized: { .failure(.read) })),
        (.duplicateWindowIdentity, classify(
            counts: .success([1, 0]),
            ids: [{ .success(.value(7)) }, { .success(.value(7)) }]
        )),
        (.sourceWindowReadError, classify(
            counts: .success([1, 0]),
            ids: [{ .success(.value(7)) }, { .failure(.read) }]
        )),
        (.otherWindowIDAbsentOrInvalid, classify(
            counts: .success([1, 0]),
            ids: [{ .success(.value(7)) }, { .success(.absent) }]
        )),
        (.otherWindowIDAbsentOrInvalid, classify(
            counts: .success([1, 0]),
            ids: [{ .success(.value(7)) }, { .success(.invalid) }]
        )),
    ]
    var terminalReads = 0
    let terminal = classifySourceWindow(
        matchCounts: .success([1]),
        windowIDs: [{ terminalReads += 1; return .success(.absent) }],
        position: { terminalReads += 1; return .success(.value(goodPosition)) },
        size: { terminalReads += 1; return .success(.value(goodSize)) },
        minimized: { terminalReads += 1; return .success(.value(false)) }
    )
    var otherIDReads = 0
    let otherIDTerminal = classify(
        counts: .success([1, 0, 0]),
        ids: [
            { otherIDReads += 1; return .success(.value(7)) },
            { otherIDReads += 1; return .success(.absent) },
            { otherIDReads += 1; return .success(.value(9)) },
        ]
    )
    guard cases.allSatisfy({ $0.0 == $0.1 }),
          terminal == .sourceWindowIDAbsentOrInvalid,
          terminalReads == 1,
          otherIDTerminal == .otherWindowIDAbsentOrInvalid,
          otherIDReads == 2,
          SourceDiagnosis.allCases.allSatisfy({
              $0.line == "finder_source=\($0.rawValue)" && $0.line.split(separator: "\n").count == 1
          }) else { return 1 }
    print("self_test=PASS finder_source_diagnostics=true")
    return 0
}

if CommandLine.arguments == [CommandLine.arguments[0], "--self-test"] { exit(selfTest()) }
guard CommandLine.arguments.count == 3,
      CommandLine.arguments[1] == "diagnose",
      !CommandLine.arguments[2].isEmpty else { exit(64) }
let exactSourceIdentifier = CommandLine.arguments[2]

func attribute(_ element: AXUIElement, _ name: String) -> Result<ProbeValue<CFTypeRef>, ProbeFailure> {
    var value: CFTypeRef?
    let error = AXUIElementCopyAttributeValue(element, name as CFString, &value)
    if error == .noValue || error == .attributeUnsupported { return .success(.absent) }
    guard error == .success else { return .failure(.read) }
    guard let value else { return .success(.invalid) }
    return .success(.value(value))
}

func stringValue(_ element: AXUIElement, _ name: String) -> Result<ProbeValue<String>, ProbeFailure> {
    switch attribute(element, name) {
    case .failure(let failure): return .failure(failure)
    case .success(.absent): return .success(.absent)
    case .success(.invalid): return .success(.invalid)
    case .success(.value(let value)):
        guard CFGetTypeID(value) == CFStringGetTypeID(), let result = value as? String else {
            return .success(.invalid)
        }
        return .success(.value(result))
    }
}

func windowID(_ element: AXUIElement) -> Result<ProbeValue<Int64>, ProbeFailure> {
    switch attribute(element, "AXWindowNumber") {
    case .failure(let failure): return .failure(failure)
    case .success(.absent): return .success(.absent)
    case .success(.invalid): return .success(.invalid)
    case .success(.value(let value)):
        guard CFGetTypeID(value) == CFNumberGetTypeID() else { return .success(.invalid) }
        var result: Int64 = 0
        guard CFNumberGetValue((value as! CFNumber), .sInt64Type, &result) else { return .success(.invalid) }
        return .success(.value(result))
    }
}

func pointValue(_ element: AXUIElement) -> Result<ProbeValue<CGPoint>, ProbeFailure> {
    switch attribute(element, kAXPositionAttribute) {
    case .failure(let failure): return .failure(failure)
    case .success(.absent): return .success(.absent)
    case .success(.invalid): return .success(.invalid)
    case .success(.value(let value)):
        guard CFGetTypeID(value) == AXValueGetTypeID() else { return .success(.invalid) }
        var point = CGPoint.zero
        guard AXValueGetValue(value as! AXValue, .cgPoint, &point) else { return .success(.invalid) }
        return .success(.value(point))
    }
}

func sizeValue(_ element: AXUIElement) -> Result<ProbeValue<CGSize>, ProbeFailure> {
    switch attribute(element, kAXSizeAttribute) {
    case .failure(let failure): return .failure(failure)
    case .success(.absent): return .success(.absent)
    case .success(.invalid): return .success(.invalid)
    case .success(.value(let value)):
        guard CFGetTypeID(value) == AXValueGetTypeID() else { return .success(.invalid) }
        var size = CGSize.zero
        guard AXValueGetValue(value as! AXValue, .cgSize, &size) else { return .success(.invalid) }
        return .success(.value(size))
    }
}

func boolValue(_ element: AXUIElement, _ name: String) -> Result<ProbeValue<Bool>, ProbeFailure> {
    switch attribute(element, name) {
    case .failure(let failure): return .failure(failure)
    case .success(.absent): return .success(.absent)
    case .success(.invalid): return .success(.invalid)
    case .success(.value(let value)):
        guard CFGetTypeID(value) == CFBooleanGetTypeID() else { return .success(.invalid) }
        return .success(.value(CFBooleanGetValue((value as! CFBoolean))))
    }
}

func children(_ element: AXUIElement) -> Result<ProbeValue<[AXUIElement]>, ProbeFailure> {
    switch attribute(element, kAXChildrenAttribute) {
    case .failure(let failure): return .failure(failure)
    case .success(.absent): return .success(.absent)
    case .success(.invalid): return .success(.invalid)
    case .success(.value(let value)):
        guard CFGetTypeID(value) == CFArrayGetTypeID(), let result = value as? [AXUIElement] else {
            return .success(.invalid)
        }
        return .success(.value(result))
    }
}

func countExactSource(_ element: AXUIElement, depth: Int = 0, visited: inout Int) throws -> Int {
    guard depth <= 14, visited < 20_000 else { throw ProbeFailure.limit }
    visited += 1
    let role: ProbeValue<String>
    switch stringValue(element, kAXRoleAttribute) {
    case .failure(let failure): throw failure
    case .success(let value): role = value
    }
    var count = 0
    if case .value(let roleValue) = role, roleValue == (kAXTextFieldRole as String) {
        switch stringValue(element, kAXValueAttribute) {
        case .failure(let failure): throw failure
        case .success(.invalid): throw ProbeFailure.dataInvalid
        case .success(.value(let value)) where value == exactSourceIdentifier: count += 1
        case .success: break
        }
    } else if case .invalid = role {
        throw ProbeFailure.dataInvalid
    }
    switch children(element) {
    case .failure(let failure): throw failure
    case .success(.invalid): throw ProbeFailure.dataInvalid
    case .success(.absent): break
    case .success(.value(let values)):
        for child in values { count += try countExactSource(child, depth: depth + 1, visited: &visited) }
    }
    return count
}

func emit(_ diagnosis: SourceDiagnosis) -> Never {
    print(diagnosis.line)
    exit(diagnosis == .sourceWindowReady ? 0 : 69)
}

guard let finder = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.finder").first else {
    emit(.traversalReadError)
}
let app = AXUIElementCreateApplication(finder.processIdentifier)
let windows: [AXUIElement]
switch attribute(app, kAXWindowsAttribute) {
case .failure: emit(.traversalReadError)
case .success(.absent), .success(.invalid): emit(.traversalDataInvalid)
case .success(.value(let value)):
    guard CFGetTypeID(value) == CFArrayGetTypeID(), let typed = value as? [AXUIElement] else {
        emit(.traversalDataInvalid)
    }
    windows = typed
}
let matchCounts: Result<[Int], ProbeFailure>
do {
    var values: [Int] = []
    for window in windows {
        var visited = 0
        values.append(try countExactSource(window, visited: &visited))
    }
    matchCounts = .success(values)
} catch let failure as ProbeFailure {
    matchCounts = .failure(failure)
} catch {
    matchCounts = .failure(.read)
}
let sourceIndex: Int? = {
    guard case .success(let counts) = matchCounts, counts.reduce(0, +) == 1 else { return nil }
    return counts.firstIndex(of: 1)
}()
let diagnosis = classifySourceWindow(
    matchCounts: matchCounts,
    windowIDs: windows.map { window in { windowID(window) } },
    position: { sourceIndex.map { pointValue(windows[$0]) } ?? .success(.invalid) },
    size: { sourceIndex.map { sizeValue(windows[$0]) } ?? .success(.invalid) },
    minimized: { sourceIndex.map { boolValue(windows[$0], kAXMinimizedAttribute) } ?? .success(.invalid) }
)
emit(diagnosis)
