import SwiftUI
import GhosttyKit

class FullScreenHandler {    var previousTabGroup: NSWindowTabGroup?
    var previousTabGroupIndex: Int?
    var previousContentFrame: NSRect?
    var previousStyleMask: NSWindow.StyleMask? = nil
    
    // We keep track of whether we entered non-native fullscreen in case
    // a user goes to fullscreen, changes the config to disable non-native fullscreen
    // and then wants to toggle it off
    var isInNonNativeFullscreen: Bool = false
    var isInFullscreen: Bool = false
    
    func toggleFullscreen(window: NSWindow, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {
        let useNonNativeFullscreen = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE
        if isInFullscreen {
            if useNonNativeFullscreen || isInNonNativeFullscreen {
                leaveFullscreen(window: window)
                isInNonNativeFullscreen = false
            } else {
                window.toggleFullScreen(nil)
            }
            isInFullscreen = false
        } else {
            if useNonNativeFullscreen {
                let hideMenu = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_VISIBLE_MENU
                enterFullscreen(window: window, hideMenu: hideMenu)
                isInNonNativeFullscreen = true
            } else {
                window.toggleFullScreen(nil)
            }
            isInFullscreen = true
        }
    }
    
    func enterFullscreen(window: NSWindow, hideMenu: Bool) {
        guard let screen = window.screen else { return }
        guard let contentView = window.contentView else { return }
        
        previousTabGroup = window.tabGroup
        previousTabGroupIndex = window.tabGroup?.windows.firstIndex(of: window)
        
        // Save previous contentViewFrame and screen
        previousContentFrame = window.convertToScreen(contentView.frame)
        
        // Change presentation style to hide menu bar and dock if needed
        // It's important to do this in two calls, because setting them in a single call guarantees
        // that the menu bar will also be hidden on any additional displays (why? nobody knows!)
        // When these options are set separately, the menu bar hiding problem will only occur in
        // specific scenarios. More invesitgation is needed to pin these scenarios down precisely,
        // but it seems to have something to do with which app had focus last.
        // Furthermore, it's much easier to figure out which screen the dock is on if the menubar
        // has not yet been hidden, so the order matters here!
        if (shouldHideDock(screen: screen)) {
            self.hideDock()
            
            // Ensure that we always hide the dock bar for this window, but not for non fullscreen ones
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(FullScreenHandler.hideDock),
                name: NSWindow.didBecomeMainNotification,
                object: window)
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(FullScreenHandler.unHideDock),
                name: NSWindow.didResignMainNotification,
                object: window)
        }
        if (hideMenu) {
            self.hideMenu()
            
            // Ensure that we always hide the menu bar for this window, but not for non fullscreen ones
            // This is not the best way to do this, not least because it causes the menu to stay visible
            // for a brief moment before being hidden in some cases (e.g. when switching spaces).
            // If we end up adding a NSWindowDelegate to PrimaryWindow, then we may be better off
            // handling this there.
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(FullScreenHandler.hideMenu),
                name: NSWindow.didBecomeMainNotification,
                object: window)
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(FullScreenHandler.unHideMenu),
                name: NSWindow.didResignMainNotification,
                object: window)
        }
        
        // This is important: it gives us the full screen, including the
        // notch area on MacBooks.
        self.previousStyleMask = window.styleMask
        window.styleMask.remove(.titled)
        
        // Set frame to screen size, accounting for the menu bar if needed
        let frame = calculateFullscreenFrame(screen: screen, subtractMenu: !hideMenu)
        window.setFrame(frame, display: true)
        
        // Focus window
        window.makeKeyAndOrderFront(nil)
    }
    
    @objc func hideMenu() {
        NSApp.presentationOptions.insert(.autoHideMenuBar)
    }
    
    @objc func unHideMenu() {
        NSApp.presentationOptions.remove(.autoHideMenuBar)
    }
    
    @objc func hideDock() {
        NSApp.presentationOptions.insert(.autoHideDock)
    }
    
    @objc func unHideDock() {
        NSApp.presentationOptions.remove(.autoHideDock)
    }
    
    func calculateFullscreenFrame(screen: NSScreen, subtractMenu: Bool)->NSRect {
        if (subtractMenu) {
            if let menuHeight = NSApp.mainMenu?.menuBarHeight {
                var padding: CGFloat = 0
                
                // Detect the notch. If there is a safe area on top it includes the
                // menu height as a safe area so we also subtract that from it.
                if (screen.safeAreaInsets.top > 0) {
                    padding = screen.safeAreaInsets.top - menuHeight;
                }

                return NSMakeRect(
                    screen.frame.minX,
                    screen.frame.minY,
                    screen.frame.width,
                    screen.frame.height - (menuHeight + padding)
                )
            }
        }
        return screen.frame
    }
    
    func leaveFullscreen(window: NSWindow) {
        guard let previousFrame = previousContentFrame else { return }
        
        // Restore the style mask
        window.styleMask = self.previousStyleMask!
        
        // Restore previous presentation options
        NSApp.presentationOptions = []
        
        // Stop handling any window focus notifications
        // that we use to manage menu bar visibility
        NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeMainNotification, object: window)
        NotificationCenter.default.removeObserver(self, name: NSWindow.didResignMainNotification, object: window)
        
        // Restore frame
        window.setFrame(window.frameRect(forContentRect: previousFrame), display: true)
        
        // If the window was previously in a tab group that isn't empty now, we re-add it
        if let group = previousTabGroup, let tabIndex = previousTabGroupIndex, !group.windows.isEmpty {
            var tabWindow: NSWindow?
            var order: NSWindow.OrderingMode = .below
            
            // Index of the window before `window`
            let tabIndexBefore = tabIndex-1
            if tabIndexBefore < 0 {
                // If we were the first tab, we add the window *before* (.below) the first one.
                tabWindow = group.windows.first
            } else if tabIndexBefore < group.windows.count {
                // If we weren't the first tab in the group, we add our window after
                // the tab that was before it.
                tabWindow = group.windows[tabIndexBefore]
                order = .above
            } else {
                // If index is after group, add it after last window
                tabWindow = group.windows.last
            }
            
            // Add the window
            tabWindow?.addTabbedWindow(window, ordered: order)
        }
        
        // Focus window
        window.makeKeyAndOrderFront(nil)
    }
    
    // We only want to hide the dock if it's not already going to be hidden automatically, and if
    // it's on the same display as the ghostty window that we want to make fullscreen.
    func shouldHideDock(screen: NSScreen) -> Bool {
        if let dockAutohide = UserDefaults.standard.persistentDomain(forName: "com.apple.dock")?["autohide"] as? Bool {
            if (dockAutohide) { return false }
        }

        // There is no public API to directly ask about dock visibility, so we have to figure it out
        // by comparing the sizes of visibleFrame (the currently usable area of the screen) and
        // frame (the full screen size). We also need to account for the menubar, any inset caused
        // by the notch on macbooks, and a little extra padding to compensate for the boundary area
        // which triggers showing the dock.
        let frame = screen.frame
        let visibleFrame = screen.visibleFrame
        let menuHeight = NSApp.mainMenu?.menuBarHeight ?? 0
        var notchInset = 0.0
        if #available(macOS 12, *) {
            notchInset = screen.safeAreaInsets.top
        }
        let boundaryAreaPadding = 5.0

        return visibleFrame.height < (frame.height - max(menuHeight, notchInset) - boundaryAreaPadding) || visibleFrame.width < frame.width
    }
}
