import AppKit
import AXSwift
import Foundation
import Carbon.HIToolbox

public typealias CString = UnsafePointer<CChar>
public typealias LaunchAppCB = @convention(c) (_ error: CString) -> Void
public typealias CreateNewWindowCB = @convention(c) (_ error: CString) -> Void
public typealias FocusAppCB = @convention(c) (_ error: CString) -> Void

// Note - raising errors from @_cdecl function is not supported.

enum AppError: Error {
    case NoAppWithBundleID(_ bundleID: String)
}

var delegate: AppDelegate!
var application: NSApplication!

private func makeCString(_ s: String) -> CString {
    // TODO: Check if optional
    return (s as NSString).utf8String!
}


class AppDelegate: NSObject, NSApplicationDelegate {
    var observer: Observer!
    var bundleID: String!
    var cb: CreateNewWindowCB!

    init(_ bundleID: String) {
        self.bundleID = bundleID
    }

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Start observer
        // TODO: Check optional
        let app = Application.allForBundleID(self.bundleID).first!

        print("Application did finish launching")
        self.startWatcher(app)
    }

    func startWatcher(_ app: Application) {
        print("Start watcher")
        observer = app.createObserver { (observer: Observer, element: UIElement, event: AXNotification, info: [String: AnyObject]?) in
            var elementDesc: String!
            if let role = try? element.role()!, role == .window {
                elementDesc = "\(element) \"\(try! (element.attribute(.title) as String?)!)\""
            } else {
                elementDesc = "\(element)"
            }
            print("\(event) on \(elementDesc); info: \(info ?? [:])")
            // self.cb(makeCString(""))
            if (event == .applicationActivated) {
                pressKey(CGKeyCode(kVK_ANSI_N), useCommandFlag: true)
                DispatchQueue.main.async {
                    NSApplication.shared.stop(nil)
                    // For stop() to take effect we have to manually send a fake application event to the main thread.
                    // Look - https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop.
                    let event = NSEvent.otherEvent(with: NSEvent.EventType.applicationDefined,
                                                   location: NSMakePoint(0.0, 0.0),
                                                   modifierFlags: NSEvent.ModifierFlags(rawValue: 0),
                                                   timestamp: 0,
                                                   windowNumber: 0,
                                                   context: nil,
                                                   subtype: 0,
                                                   data1: 0,
                                                   data2: 0)
                    NSApplication.shared.postEvent(event!, atStart: true)
                }
            }
        }
        print("Observer registered")
        /*
        try! observer.addNotification(.windowCreated, forElement: app)
        try! observer.addNotification(.focusedWindowChanged, forElement: app)
        try! observer.addNotification(.mainWindowChanged, forElement: app)
        */
        try! observer.addNotification(.applicationActivated, forElement: app)

        try! app.setAttribute(.frontmost, value: true)
    }

    func applicationWillTerminate(_ aNotification: Notification) {
            print("APPLICATION WILL TERMINATE!")
    }

    public func focusAndCreateWindow(_ callback: @escaping CreateNewWindowCB) {
        print("Focus and create window")
        self.cb = callback
        // TODO: Add observer notification registration here
    }
}

@_cdecl("swift_Run") // TODO: Implement in C++.
public func Run(c_bundleID: CString) {
    let bundleID = String(cString: c_bundleID)
    delegate = AppDelegate(bundleID)
    application = NSApplication.shared
    application.setActivationPolicy(NSApplication.ActivationPolicy.accessory)
    application.delegate = delegate
    application.run()
    print("AFTER RUN")
}

@_cdecl("swift_IsProcessTrusted")
public func IsProcessTrusted() -> Bool {
    return UIElement.isProcessTrusted(withPrompt: true)
}

@_cdecl("swift_IsAppRunning") // TODO: Implement in C++.
public func IsAppRuning(c_bundleID: CString) -> CBool {
    let bundleID = String(cString: c_bundleID)
    let running = NSWorkspace.shared.runningApplications
    return running
        .filter({ $0.bundleIdentifier == bundleID })
        .count > 0
}


@_cdecl("swift_LaunchApp")
public func LaunchApp(c_bundleID: CString, callback: @escaping LaunchAppCB)  {
    let bundleID = String(cString: c_bundleID)
    guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) else {
        let error = makeCString("File URL for the given bunlde ID '\(bundleID)' not found");
        callback(error)
        return
    }
    if #available(macOS 10.15, *) {
        NSWorkspace.shared.openApplication(at: url, configuration: NSWorkspace.OpenConfiguration()) { (app, error) in
            callback(makeCString(""))
        }
    } else {
        let success = NSWorkspace.shared.launchApplication(url.absoluteString)
        if (success) {
            callback(makeCString(""))
        } else {
            callback(makeCString("Failed to open application with bundle ID '\(bundleID)'"))
        }
    }
}

@_cdecl("swift_FocusApp")
public func FocusApp(c_bundleID: CString, callback: @escaping FocusAppCB) {
    let bundleID = String(cString: c_bundleID)
    // Check if an app with this bundle ID is running.
    guard let app = Application.allForBundleID(bundleID).first else {
        callback(makeCString("Can't focus the app. There is no app with bundle ID '\(bundleID)' is running"))
        return
    }

    let frontmost: Bool = try! app.attribute(.frontmost)!
    print(frontmost)

    try! app.setAttribute(.frontmost, value: true)

    /*
    guard let windows = try! app.windows() else {
        print("The app has no windows")
        return
    }
    print("Windows for app:")
    for w in windows {
        // print("Window:")
        // print(w)
        // let atrs = try! w.attributes()
        // print(atrs)
        // let atr: NSURL = try! w.attribute(.url)!
        // print(atr)
    }
    */
}

func pressKey(_ keyCode: CGKeyCode, useCommandFlag: Bool) {
    let sourceRef = CGEventSource(stateID: .combinedSessionState)

    if sourceRef == nil {
        // TODO: error
        return
    }

    let keyDownEvent = CGEvent(keyboardEventSource: sourceRef,
                               virtualKey: keyCode,
                               keyDown: true)
    if useCommandFlag {
        keyDownEvent?.flags = .maskCommand
    }

    let keyUpEvent = CGEvent(keyboardEventSource: sourceRef,
                             virtualKey: keyCode,
                             keyDown: false)

    // TODO: Check optionals
    keyDownEvent?.post(tap: .cghidEventTap)
    keyUpEvent?.post(tap: .cghidEventTap)
}

@_cdecl("swift_CreateNewWindow")
public func CreateNewWindow(c_bundleID: CString, callback: @escaping CreateNewWindowCB) {
    let bundleID = String(cString: c_bundleID)
    guard let app = Application.allForBundleID(bundleID).first else {
        callback(makeCString("Can't create new window. There is no app with bundle ID '\(bundleID)' is running"))
        return
    }

    do {
        try app.setAttribute(.frontmost, value: true)
    } catch {
        print("error \(error)")
        callback(makeCString("\(error)"))
        return
    }

    // TODO: Instead of a continuous polling, try to do this through AXSwift observer?
    var quit = false;
    while !quit {
        let frontmost: Bool = try! app.attribute(.frontmost)!
        quit = frontmost
        usleep(100)
    }
    pressKey(CGKeyCode(kVK_ANSI_N), useCommandFlag: true)
    callback(makeCString(""))
}

