//
//  ViewController.swift
//  MacTerminal
//
//  Created by Miguel de Icaza on 3/11/20.
//  Copyright © 2020 Miguel de Icaza. All rights reserved.
//

import Cocoa
import SwiftTerm
import UniformTypeIdentifiers
class ViewController: NSViewController, LocalProcessTerminalViewDelegate, NSUserInterfaceValidations {
    @IBOutlet var loggingMenuItem: NSMenuItem?

    private struct ReverseVideoTestState {
        let foregroundColor: NSColor
        let backgroundColor: NSColor
        let windowIsOpaque: Bool
        let windowBackgroundColor: NSColor
    }

    private var reverseVideoTestState: ReverseVideoTestState?

    var changingSize = false
    var logging: Bool = false
    var zoomGesture: NSMagnificationGestureRecognizer?
    var postedTitle: String = ""
    var postedDirectory: String? = nil
    
    func sizeChanged(source: LocalProcessTerminalView, newCols: Int, newRows: Int) {
        if changingSize {
            return
        }
        changingSize = true
        //var border = view.window!.frame - view.frame
        var newFrame = terminal.getOptimalFrameSize ()
        let windowFrame = view.window!.frame
        
        newFrame = CGRect (x: windowFrame.minX, y: windowFrame.minY, width: newFrame.width, height: windowFrame.height - view.frame.height + newFrame.height)

        view.window?.setFrame(newFrame, display: true, animate: true)
        changingSize = false
    }
    
    func updateWindowTitle ()
    {
        var newTitle: String
        if let dir = postedDirectory {
            if let uri = URL(string: dir) {
                if postedTitle == "" {
                    newTitle = uri.path
                } else {
                    newTitle = "\(postedTitle) - \(uri.path)"
                }
            } else {
                newTitle = postedTitle
            }
        } else {
            newTitle = postedTitle
        }
        view.window?.title = newTitle
    }
    
    func setTerminalTitle(source: LocalProcessTerminalView, title: String) {
        postedTitle = title
        updateWindowTitle ()
    }
    
    func hostCurrentDirectoryUpdate (source: TerminalView, directory: String?) {
        self.postedDirectory = directory
        updateWindowTitle()
    }
    
    func processTerminated(source: TerminalView, exitCode: Int32?) {
        view.window?.close()
        if let e = exitCode {
            print ("Process terminated with code: \(e)")
        } else {
            print ("Process vanished")
        }
    }
    var terminal: LocalProcessTerminalView!

    static weak var lastTerminal: LocalProcessTerminalView!
    
    func getBufferAsData () -> Data
    {
        return terminal.getTerminal().getBufferAsData ()
    }
    
    func updateLogging ()
    {
//        let path = logging ? "/Users/miguel/Downloads/Logs" : nil
//        terminal.setHostLogging (directory: path)
        NSUserDefaultsController.shared.defaults.set (logging, forKey: "LogHostOutput")
    }
    
    // Returns the shell associated with the current account
    func getShell () -> String
    {
        let bufsize = sysconf(_SC_GETPW_R_SIZE_MAX)
        guard bufsize != -1 else {
            return "/bin/bash"
        }
        let buffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufsize)
        defer {
            buffer.deallocate()
        }
        var pwd = passwd()
        var result: UnsafeMutablePointer<passwd>? = UnsafeMutablePointer<passwd>.allocate(capacity: 1)
        
        if getpwuid_r(getuid(), &pwd, buffer, bufsize, &result) != 0 {
            return "/bin/bash"
        }
        return String (cString: pwd.pw_shell)
    }
    
    class TD: TerminalDelegate {
        func send(source: Terminal, data: ArraySlice<UInt8>) {
        }
        
        
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        terminal = LocalProcessTerminalView(frame: view.frame)
        terminal.metalBufferingMode = .perFrameAggregated
        do {
            try terminal.setUseMetal(false)
        } catch {
            print("METAL DISABLED: \(error)")
        }
        let defaultForegroundColor = NSColor(
            calibratedRed: 1.0,
            green: 1.0,
            blue: 1.0,
            alpha: 1.0
        )
        let defaultBackgroundColor = NSColor(
            calibratedRed: CGFloat(0x28) / 255.0,
            green: CGFloat(0x2c) / 255.0,
            blue: CGFloat(0x34) / 255.0,
            alpha: 1.0
        )
        terminal.nativeForegroundColor = defaultForegroundColor
        terminal.nativeBackgroundColor = defaultBackgroundColor
        terminal.layer?.backgroundColor = defaultBackgroundColor.cgColor
        terminal.caretColor = .systemGreen
        terminal.getTerminal().setCursorStyle(.steadyBlock)
        zoomGesture = NSMagnificationGestureRecognizer(target: self, action: #selector(zoomGestureHandler))
        terminal.addGestureRecognizer(zoomGesture!)
        ViewController.lastTerminal = terminal
        terminal.processDelegate = self
        terminal.feed(text: "Welcome to SwiftTerm")

        let shell = getShell()
        let shellIdiom = "-" + NSString(string: shell).lastPathComponent
        
        FileManager.default.changeCurrentDirectoryPath (FileManager.default.homeDirectoryForCurrentUser.path)
        terminal.startProcess (executable: shell, execName: shellIdiom)
        view.addSubview(terminal)
        logging = NSUserDefaultsController.shared.defaults.bool(forKey: "LogHostOutput")
        updateLogging ()

        // Support --cmd "command" launch argument for automation/profiling
        let args = ProcessInfo.processInfo.arguments
        if let idx = args.firstIndex(of: "--cmd"), idx + 1 < args.count {
            let command = args[idx + 1]
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
                guard let self else { return }
                let cmdLine = command + "\n"
                let bytes = Array(cmdLine.utf8)
                self.terminal.send(source: self.terminal, data: bytes[...])
            }
        }

        #if DEBUG_MOUSE_FOCUS
        var t = NSTextField(frame: NSRect (x: 0, y: 100, width: 200, height: 30))
        t.backgroundColor = NSColor.white
        t.stringValue = "Hello - here to test focus switching"
        
        view.addSubview(t)
        #endif
    }
    
    override func viewWillDisappear() {
        //terminal = nil
    }
    
    @objc
    func zoomGestureHandler (_ sender: NSMagnificationGestureRecognizer) {
        if sender.magnification > 0 {
            biggerFont (sender)
        } else {
            smallerFont(sender)
        }
    }

    override func viewDidLayout() {
        super.viewDidLayout()
        changingSize = true
        terminal.frame = view.frame
        changingSize = false
        terminal.needsLayout = true
    }

    @objc @IBAction
    func toggleReverseVideoTest(_ source: AnyObject)
    {
        if let savedState = reverseVideoTestState {
            terminal.nativeForegroundColor = savedState.foregroundColor
            terminal.nativeBackgroundColor = savedState.backgroundColor
            view.window?.isOpaque = savedState.windowIsOpaque
            view.window?.backgroundColor = savedState.windowBackgroundColor
            reverseVideoTestState = nil
            terminal.feed(text: "\u{1b}[0m\r\nReverse-video test ended.\r\n")
            return
        }

        guard let window = view.window else {
            return
        }

        reverseVideoTestState = ReverseVideoTestState(
            foregroundColor: terminal.nativeForegroundColor,
            backgroundColor: terminal.nativeBackgroundColor,
            windowIsOpaque: window.isOpaque,
            windowBackgroundColor: window.backgroundColor)

        // This non-complementary pair makes RGB inversion visibly incorrect.
        terminal.nativeForegroundColor = NSColor(
            srgbRed: 0x93 / 255.0,
            green: 0xa1 / 255.0,
            blue: 0xa1 / 255.0,
            alpha: 1)
        terminal.nativeBackgroundColor = NSColor(
            srgbRed: 0x00 / 255.0,
            green: 0x2b / 255.0,
            blue: 0x36 / 255.0,
            alpha: 1)

        window.isOpaque = false
        window.backgroundColor = .clear
        terminal.backgroundOpacity = 0

        let escape = "\u{1b}"
        terminal.feed(text:
            "\(escape)[2J\(escape)[H" +
            "Reverse-video transparency test\r\n" +
            "Default background opacity: 0%\r\n\r\n" +
            "\(escape)[0m Normal default text \r\n" +
            "\(escape)[7m Reversed default text \(escape)[27m  Expected: opaque light block, dark text\r\n" +
            "\(escape)[38;2;220;50;47m Red foreground \(escape)[7m Red/default reversed \(escape)[0m\r\n" +
            "\(escape)[48;2;38;139;210m Default foreground on blue \(escape)[7m Reversed \(escape)[0m\r\n\r\n" +
            "Before PR 623, the default reversed block disappears or uses inverted RGB.\r\n" +
            "With PR 623, the default colors swap and the reversed block stays opaque.\r\n")
    }


    @objc @IBAction
    func set80x25 (_ source: AnyObject)
    {
        terminal.resize(cols: 80, rows: 25)
    }

    var lowerCol = 80
    var lowerRow = 25
    var higherCol = 160
    var higherRow = 60
    
    func queueNextSize ()
    {
        // If they requested a stop
        if resizificating == 0 {
            return
        }
        var next = terminal.getTerminal().getDims ()
        if resizificating > 0 {
            if next.cols < higherCol {
                next.cols += 1
            }
            if next.rows < higherRow {
                next.rows += 1
            }
        } else {
            if next.cols > lowerCol {
                next.cols -= 1
            }
            if next.rows > lowerRow {
                next.rows -= 1
            }
        }
        terminal.resize (cols: next.cols, rows: next.rows)
        var direction = resizificating
        
        if next.rows == higherRow && next.cols == higherCol {
            direction = -1
        }
        if next.rows == lowerRow && next.cols == lowerCol {
            direction = 1
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) {
            self.resizificating = direction
            self.queueNextSize()
        }
    }
    
    var resizificating = 0
    
    @objc @IBAction
    func resizificator (_ source: AnyObject)
    {
        if resizificating != 1 {
            resizificating = 1
            queueNextSize ()
        } else {
            resizificating = 0
        }
    }

    @objc @IBAction
    func resizificatorDown (_ source: AnyObject)
    {
        if resizificating != -1 {
            resizificating = -1
            queueNextSize ()
        } else {
            resizificating = 0
        }
    }

    @objc @IBAction
    func toggleMetalRenderer(_ source: AnyObject) {
        do {
            try terminal.setUseMetal(!terminal.isUsingMetalRenderer)
        } catch {
            print("METAL TOGGLE FAILED: \(error)")
        }
        terminal.setNeedsDisplay(terminal.bounds)
    }

    @objc @IBAction
    func toggleMetalBufferingMode(_ source: AnyObject) {
        let current = terminal.metalBufferingMode
        terminal.metalBufferingMode = (current == .perRowPersistent) ? .perFrameAggregated : .perRowPersistent
        terminal.setNeedsDisplay(terminal.bounds)
    }

    @objc @IBAction
    func allowMouseReporting (_ source: AnyObject)
    {
        terminal.allowMouseReporting.toggle ()
    }

    @objc @IBAction
    func toggleCustomBlockGlyphs (_ source: AnyObject)
    {
        terminal.customBlockGlyphs.toggle()
    }

    @objc @IBAction
    func toggleAnsi256PaletteStrategy (_ source: AnyObject)
    {
        let term = terminal.getTerminal()
        term.ansi256PaletteStrategy = nextAnsi256PaletteStrategy(after: term.ansi256PaletteStrategy)
    }
    
    @objc @IBAction
    func exportBuffer (_ source: AnyObject)
    {
        saveData { self.terminal.getTerminal().getBufferAsData () }
    }

    @objc @IBAction
    func exportSelection (_ source: AnyObject)
    {
        saveData {
            if let str = self.terminal.getSelection () {
                return str.data (using: .utf8) ?? Data ()
            }
            return Data ()
        }
    }

    func saveData (_ getData: @escaping () -> Data)
    {
        let savePanel = NSSavePanel ()
        savePanel.canCreateDirectories = true
        if #available(macOS 12.0, *) {
            savePanel.allowedContentTypes = [UTType.text, UTType.plainText]
        } else {
            savePanel.allowedFileTypes = ["txt"]
        }
        savePanel.title = "Export Buffer Contents As Text"
        savePanel.nameFieldStringValue = "TerminalCapture"
        
        savePanel.begin { (result) in
            if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
                let data = getData ()
                if let url = savePanel.url {
                    do {
                        try data.write(to: url)
                    } catch let error as NSError {
                        let alert = NSAlert (error: error)
                        alert.runModal()
                    }
                }
            }
        }
    }
    
    @objc @IBAction
    func softReset (_ source: AnyObject)
    {
        terminal.getTerminal().softReset ()
        terminal.setNeedsDisplay(terminal.frame)
    }
    
    @objc @IBAction
    func hardReset (_ source: AnyObject)
    {
        terminal.getTerminal().resetToInitialState ()
        terminal.setNeedsDisplay(terminal.frame)
    }
    
    @objc @IBAction
    func toggleOptionAsMetaKey (_ source: AnyObject)
    {
        terminal.optionAsMetaKey.toggle ()
    }
    
    @objc @IBAction
    func biggerFont (_ source: AnyObject)
    {
        let size = terminal.font.pointSize
        guard size < 72 else {
            return
        }
        
        terminal.font = NSFont.monospacedSystemFont(ofSize: size+1, weight: .regular)
    }

    @objc @IBAction
    func smallerFont (_ source: AnyObject)
    {
        let size = terminal.font.pointSize
        guard size > 5 else {
            return
        }
        
        terminal.font = NSFont.monospacedSystemFont(ofSize: size-1, weight: .regular)
    }

    @objc @IBAction
    func defaultFontSize  (_ source: AnyObject)
    {
        terminal.font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
    }
    

    @objc @IBAction
    func addTab (_ source: AnyObject)
    {
        
//        if let win = view.window {
//            win.tabbingMode = .preferred
//            if let wc = win.windowController {
//                if let d = wc.document as? Document {
//                    do {
//                        let x = Document()
//                        x.makeWindowControllers()
//                        
//                        try NSDocumentController.shared.newDocument(self)
//                    } catch {}
//                    print ("\(d.debugDescription)")
//                }
//            }
//        }
//            win.tabbingMode = .preferred
//            win.addTabbedWindow(win, ordered: .above)
//
//            if let wc = win.windowController {
//                wc.newWindowForTab(self()
//                wc.showWindow(source)
//            }
//        }
    }
    
    func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool
    {
        if item.action == #selector(debugToggleHostLogging(_:)) {
            if let m = item as? NSMenuItem {
                m.state = logging ? NSControl.StateValue.on : NSControl.StateValue.off
            }
        }
        if item.action == #selector(resizificator(_:)) {
            if let m = item as? NSMenuItem {
                m.state = resizificating == 1 ? NSControl.StateValue.on : NSControl.StateValue.off
            }
        }
        if item.action == #selector(resizificatorDown(_:)) {
            if let m = item as? NSMenuItem {
                m.state = resizificating == -1 ? NSControl.StateValue.on : NSControl.StateValue.off
            }
        }
        if item.action == #selector(allowMouseReporting(_:)) {
            if let m = item as? NSMenuItem {
                m.state = terminal.allowMouseReporting ? NSControl.StateValue.on : NSControl.StateValue.off
            }
        }
        if item.action == #selector(toggleCustomBlockGlyphs(_:)) {
            if let m = item as? NSMenuItem {
                m.state = terminal.customBlockGlyphs ? NSControl.StateValue.on : NSControl.StateValue.off
            }
        }
        if item.action == #selector(toggleAnsi256PaletteStrategy(_:)) {
            if let m = item as? NSMenuItem {
                let term = terminal.getTerminal()
                let strategy = term.ansi256PaletteStrategy
                m.title = ansi256PaletteMenuTitle(for: strategy)
                m.state = ansi256PaletteMenuState(for: strategy)
            }
        }
        if item.action == #selector(toggleOptionAsMetaKey(_:)) {
            if let m = item as? NSMenuItem {
                m.state = terminal.optionAsMetaKey ? NSControl.StateValue.on : NSControl.StateValue.off
            }
        }
        if item.action == #selector(toggleMetalRenderer(_:)) {
            if let m = item as? NSMenuItem {
                m.state = terminal.isUsingMetalRenderer ? .on : .off
            }
        }
        if item.action == #selector(toggleMetalBufferingMode(_:)) {
            if let m = item as? NSMenuItem {
                m.state = terminal.metalBufferingMode == .perFrameAggregated ? .on : .off
            }
        }
        if item.action == #selector(toggleReverseVideoTest(_:)) {
            if let m = item as? NSMenuItem {
                m.state = reverseVideoTestState == nil ? .off : .on
            }
        }
        
        // Only enable "Export selection" if we have a selection
        if item.action == #selector(exportSelection(_:)) {
            return terminal.selectionActive
        }
        return true
    }

    private func nextAnsi256PaletteStrategy(after strategy: Ansi256PaletteStrategy) -> Ansi256PaletteStrategy {
        switch strategy {
        case .xterm:
            return .base16Lab
        case .base16Lab:
            return .base16LabHarmonious
        case .base16LabHarmonious:
            return .xterm
        }
    }

    private func ansi256PaletteMenuTitle(for strategy: Ansi256PaletteStrategy) -> String {
        switch strategy {
        case .xterm:
            return "ANSI 256 Palette: xterm"
        case .base16Lab:
            return "ANSI 256 Palette: Base16 LAB"
        case .base16LabHarmonious:
            return "ANSI 256 Palette: Base16 LAB Harmonious"
        }
    }

    private func ansi256PaletteMenuState(for strategy: Ansi256PaletteStrategy) -> NSControl.StateValue {
        switch strategy {
        case .xterm:
            return .off
        case .base16Lab:
            return .on
        case .base16LabHarmonious:
            return .mixed
        }
    }
    
    @objc @IBAction
    func debugToggleHostLogging (_ source: AnyObject)
    {
        logging = !logging
        updateLogging()
    }
    
}
