import AppKit

extension NSWindow {
    /// Get the CGWindowID type for the window (used for low level CoreGraphics APIs).
    var cgWindowId: CGWindowID? {
        // "If the window doesn’t have a window device, the value of this
        // property is equal to or less than 0." - Docs. In practice I've
        // found this is true if a window is not visible.
        guard windowNumber > 0 else { return nil }
        return CGWindowID(windowNumber)
    }

    /// Adjusts the window frame if necessary to ensure the window remains visible on screen.
    /// This constrains both the size (to not exceed the screen) and the origin (to keep the window on screen).
    func constrainToScreen() {
        guard let screen = screen ?? NSScreen.main else { return }
        let visibleFrame = screen.visibleFrame
        var windowFrame = frame

        windowFrame.size.width = min(windowFrame.size.width, visibleFrame.size.width)
        windowFrame.size.height = min(windowFrame.size.height, visibleFrame.size.height)

        windowFrame.origin.x = max(visibleFrame.minX,
            min(windowFrame.origin.x, visibleFrame.maxX - windowFrame.width))
        windowFrame.origin.y = max(visibleFrame.minY,
            min(windowFrame.origin.y, visibleFrame.maxY - windowFrame.height))

        if windowFrame != frame {
            setFrame(windowFrame, display: true)
        }
    }
}

// MARK: Native Tabbing

extension NSWindow {
    /// True if this is the first window in the tab group.
    var isFirstWindowInTabGroup: Bool {
        guard let firstWindow = tabGroup?.windows.first else { return true }
        return firstWindow === self
    }

    /// Wraps `addTabbedWindow` with an Objective-C exception catcher because AppKit can
    /// throw NSExceptions in visual tab picker flows. Swift cannot safely recover from
    /// those exceptions, so we route through Obj-C and log a recoverable failure.
    @discardableResult
    func addTabbedWindowSafely(
        _ child: NSWindow,
        ordered: NSWindow.OrderingMode
    ) -> Bool {
        var error: NSError?
        let success = GhosttyAddTabbedWindowSafely(self, child, ordered.rawValue, &error)
        if let error {
            Ghostty.logger.error("addTabbedWindow failed: \(error.localizedDescription, privacy: .public)")
        }

        return success
    }
}

/// Native tabbing private API usage. :(
extension NSWindow {
    var titlebarView: NSView? {
        // In normal window, `NSTabBar` typically appears as a subview of `NSTitlebarView` within `NSThemeFrame`.
        // In fullscreen, the system creates a dedicated fullscreen window and the view hierarchy changes;
        // in that case, the `titlebarView` is only accessible via a reference on `NSThemeFrame`.
        // ref: https://github.com/mozilla-firefox/firefox/blob/054e2b072785984455b3b59acad9444ba1eeffb4/widget/cocoa/nsCocoaWindow.mm#L7205
        guard let themeFrameView = contentView?.rootView else { return nil }
        guard themeFrameView.responds(to: Selector(("titlebarView"))) else { return nil }
        return themeFrameView.value(forKey: "titlebarView") as? NSView
    }

    /// Returns the [private] NSTabBar view, if it exists.
    var tabBarView: NSView? {
        titlebarView?.firstDescendant(withClassName: "NSTabBar")
    }

    /// Returns tab button views in visual order from left to right.
    func tabButtonsInVisualOrder() -> [NSView] {
        guard let tabBarView else { return [] }
        return tabBarView
            .descendants(withClassName: "NSTabButton")
            .sorted { $0.frame.minX < $1.frame.minX }
    }

    /// Returns the visual tab index and matching tab button at the given screen point.
    func tabButtonHit(atScreenPoint screenPoint: NSPoint) -> (index: Int, tabButton: NSView)? {
        guard let tabBarView, let tabBarWindow = tabBarView.window else { return nil }

        // In fullscreen, AppKit can host the titlebar and tab bar in a separate
        // NSToolbarFullScreenWindow. Hit testing has to use that window's base
        // coordinate space or content clicks can be misinterpreted as tab clicks.
        let locationInTabBarWindow = tabBarWindow.convertPoint(fromScreen: screenPoint)
        let locationInTabBar = tabBarView.convert(locationInTabBarWindow, from: nil)
        guard tabBarView.bounds.contains(locationInTabBar) else { return nil }

        for (index, tabButton) in tabButtonsInVisualOrder().enumerated() {
            let locationInTabButton = tabButton.convert(locationInTabBarWindow, from: nil)
            if tabButton.bounds.contains(locationInTabButton) {
                return (index, tabButton)
            }
        }

        return nil
    }

    /// Returns the index of the tab button at the given screen point, if any.
    func tabIndex(atScreenPoint screenPoint: NSPoint) -> Int? {
        tabButtonHit(atScreenPoint: screenPoint)?.index
    }
}
