import AppKit
import Carbon
import ServiceManagement

fileprivate enum Metrics {
    static let contentMinWidth  = 400.0
    static let contentMaxWidth  = /*600.0*/ NSScreen.main!.visibleFrame.size.width * 0.7 // WARNING: Under what circumstances can NSScreen return nil?
    static let contentMinHeight = 160.0
    static let contentMaxHeight = /*800.0*/ NSScreen.main!.visibleFrame.size.height * 0.8 // WARNING: Under what circumstances can NSScreen return nil?
    static let padding          = 10.0
    static let buttonSpacing    = 2.0
}

class CmdViewController: NSViewController {
    private weak var cmdFile: CmdFile?
    private var timer: DispatchSourceTimer?
    private var keyboardEvents: EventMonitor?

    private var maxWidthContraint: NSLayoutConstraint!
    private var maxHeightContraint: NSLayoutConstraint!

    private var visualEffectView: NSView = {
        let visualEffect = NSVisualEffectView()
        visualEffect.blendingMode = .behindWindow
        visualEffect.material = .popover
        visualEffect.state = .active
        visualEffect.translatesAutoresizingMaskIntoConstraints = false
        return visualEffect
    }()

    private var textLabelScrollView: NSScrollView = {
        let scrollView = NSScrollView()
        scrollView.automaticallyAdjustsContentInsets = false
        scrollView.hasVerticalScroller = true
        scrollView.hasHorizontalScroller = true
        scrollView.scrollerStyle = .overlay
        scrollView.drawsBackground = false
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        return scrollView
    }()

    private var textLabel: NSTextField = {
        let textField = NSTextField()
        // NOTE: Setting drawsBackground = false messes with geometry.
        //       Also, setting a custom cell, messes up, too.
        textField.backgroundColor = .clear
        textField.isSelectable = true
        textField.isHidden = false
        textField.isEditable = false
        textField.isBezeled = false
        textField.alignment = .left
        textField.textColor =
            NSColor(name: nil) { getColors(.black, .white, for: $0) }
        textField.font = NSFont
            .monospacedSystemFont(ofSize: NSFontDescriptor
                .preferredFontDescriptor(forTextStyle: .body).pointSize,
                                         weight: .regular)
        textField.translatesAutoresizingMaskIntoConstraints = false
        return textField
    }()

    private var reloadsText: NSTextField = {
        let textField = NSTextField()
        textField.isHidden = false
        textField.stringValue = "--"
        textField.lineBreakMode = .byTruncatingTail
        textField.maximumNumberOfLines = 1
        textField.isEditable = false
        textField.isBezeled = false
        textField.drawsBackground = false
        textField.alignment = .left
        textField.textColor =
            NSColor(name: nil) { getColors(.darkGray, .lightGray, for: $0) }
        textField.font = NSFont
            .systemFont(ofSize: NSFontDescriptor
                .preferredFontDescriptor(forTextStyle: .subheadline).pointSize,
                                         weight: .bold)
        textField.translatesAutoresizingMaskIntoConstraints = false
        return textField
    }()

    private var progressView: CircularProgressView = {
        let progress = CircularProgressView()
        progress.translatesAutoresizingMaskIntoConstraints = false
        return progress
    }()

    private var statusText: NSTextField = {
        let textField = NSTextField()
        textField.isHidden = false
        textField.stringValue = "Loading..."
        textField.isEditable = false
        textField.isBezeled = false
        textField.drawsBackground = false
        textField.alignment = .center
        textField.textColor =
            NSColor(name: nil) { getColors(.darkGray, .lightGray, for: $0) }
        textField.font =
            NSFont(descriptor: NSFontDescriptor
                .preferredFontDescriptor(forTextStyle: .body), size: 0)
        textField.translatesAutoresizingMaskIntoConstraints = false
        return textField
    }()

    private var quitButton: NSButton = {
        let button = NSButton()
        button.image = systemImage("xmark.circle.fill", .title2, .large,
                                .init(paletteColors: [.white, .systemRed]))
        button.isBordered = false
        button.action = #selector(terminateApp)
        button.sizeToFit()
        button.toolTip = "Quit"
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    private var reloadButton: NSButton = {
        let button = NSButton()
        button.image = systemImage("arrow.clockwise.circle.fill", .title2,
                                   .large,
                               .init(paletteColors: [.white, .systemBlue]))
        button.isBordered = false
        button.action = #selector(reloadWidget)
        button.sizeToFit()
        button.toolTip = "Reload Current"
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    private var startButton: NSButton = {
        let button = NSButton()
        button.image = systemImage("sun.max.circle.fill", .title2, .large,
                             .init(paletteColors: [.white, .systemOrange]))
        button.isBordered = false
        button.action = #selector(openAtLogin)
        button.sizeToFit()
        button.toolTip = "Open at Login — OFF"
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    private var aboutButton: NSButton = {
        let button = NSButton()
        button.image = systemImage("info.circle.fill", .title2, .large,
                               .init(paletteColors: [.white, .systemGray]))
        button.isBordered = false
        button.action = #selector(showAbout)
        button.sizeToFit()
        button.toolTip = "About"
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    private var buttonsBackground: NSView = {
        let blurView = NSVisualEffectView()
        blurView.blendingMode = .behindWindow
        blurView.material = .sheet
        blurView.state = .active
        blurView.wantsLayer = true
        blurView.layer?.borderColor =
            NSColor.systemGray.withAlphaComponent(0.05).cgColor
        blurView.layer?.borderWidth = 1.5
        blurView.translatesAutoresizingMaskIntoConstraints = false
        return blurView
    }()

    private var buttonsContainer: NSLayoutGuide = {
        let container = NSLayoutGuide()
        return container
    }()

    private func addSubviews() {
        view.addSubview(visualEffectView)

        view.addSubview(statusText)

        textLabelScrollView.documentView = textLabel
        view.addSubview(textLabelScrollView)

        view.addLayoutGuide(buttonsContainer)
        view.addSubview(buttonsBackground)
        view.addSubview(quitButton)
        view.addSubview(reloadButton)
        view.addSubview(startButton)
        view.addSubview(aboutButton)
        view.addSubview(reloadsText)
        view.addSubview(progressView)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        addSubviews()
        setup()
        setContraints()
    }

    override func loadView() {
        self.view = QuietView()
    }

    override func viewWillAppear() {
        maxWidthContraint.constant =
            NSScreen.main!.visibleFrame.size.width * 0.7
        maxHeightContraint.constant =
            NSScreen.main!.visibleFrame.size.height * 0.8
    }

    override func viewDidAppear() {
        super.viewDidAppear()

        textLabelScrollView.flashScrollers()

        startReloadTextTimer()
        keyboardEvents?.start()

        resetReloadButton()
    }

    override func viewDidDisappear() {
        super.viewDidDisappear()

        timer?.cancel()
        timer = nil

        keyboardEvents?.stop()

        if let editor = textLabel.currentEditor()
            { textLabel.endEditing(editor) }
    }

    private func setContraints() {
        setViewConstraints()
        setTextConstraints()
        setReloadConstrains()
        setButtonConstraints()
    }

    func setup() {
        openAtLoginToggle()

        buttonsBackground.layer?.cornerRadius =
            quitButton.bounds.height / Metrics.buttonSpacing +
                Metrics.buttonSpacing

        setupKeyEvents()
    }

    private func setReloadConstrains() {
        NSLayoutConstraint.activate([
            progressView.widthAnchor.constraint(equalToConstant: 16),
            progressView.heightAnchor.constraint(equalToConstant: 16),

            progressView.leadingAnchor
                .constraint(equalTo: view.leadingAnchor,
                            constant: Metrics.padding),
            progressView.centerYAnchor
                .constraint(equalTo: reloadsText.centerYAnchor),
        ])

        NSLayoutConstraint.activate([
            reloadsText.leadingAnchor
                .constraint(equalTo: progressView.trailingAnchor,
                            constant: Metrics.buttonSpacing),
            reloadsText.centerYAnchor
                .constraint(equalTo: buttonsBackground.centerYAnchor),
            reloadsText.trailingAnchor
                .constraint(equalTo: buttonsBackground.leadingAnchor,
                            constant: -Metrics.buttonSpacing),
        ])
    }

    private func setViewConstraints() {
        NSLayoutConstraint.activate([
            visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
            visualEffectView.bottomAnchor
                .constraint(equalTo: view.bottomAnchor),
            visualEffectView.leadingAnchor
                .constraint(equalTo: view.leadingAnchor),
            visualEffectView.trailingAnchor
                .constraint(equalTo: view.trailingAnchor),
        ])
    }

    private func setButtonConstraints() {
        NSLayoutConstraint.activate([
            quitButton.widthAnchor
                .constraint(equalToConstant: quitButton.bounds.width),
            quitButton.heightAnchor
                .constraint(equalToConstant: quitButton.bounds.height),
            reloadButton.widthAnchor
                .constraint(equalToConstant: reloadButton.bounds.width),
            reloadButton.heightAnchor
                .constraint(equalToConstant: reloadButton.bounds.height),
            startButton.widthAnchor
                .constraint(equalToConstant: startButton.bounds.width),
            startButton.heightAnchor
                .constraint(equalToConstant: startButton.bounds.height),
            aboutButton.widthAnchor
                .constraint(equalToConstant: aboutButton.bounds.width),
            aboutButton.heightAnchor
                .constraint(equalToConstant: aboutButton.bounds.height),

            buttonsContainer.trailingAnchor
                .constraint(equalTo: view.trailingAnchor,
                            constant: -Metrics.padding),
            buttonsContainer.bottomAnchor
                .constraint(equalTo: view.bottomAnchor,
                            constant: -Metrics.padding),

            buttonsBackground.leadingAnchor
                .constraint(equalTo: buttonsContainer.leadingAnchor),
            buttonsBackground.trailingAnchor
                .constraint(equalTo: buttonsContainer.trailingAnchor),
            buttonsBackground.topAnchor
                .constraint(equalTo: buttonsContainer.topAnchor),
            buttonsBackground.bottomAnchor
                .constraint(equalTo: buttonsContainer.bottomAnchor),

            quitButton.trailingAnchor
                .constraint(equalTo: buttonsContainer.trailingAnchor,
                            constant: -Metrics.buttonSpacing),
            quitButton.topAnchor
                .constraint(equalTo: buttonsContainer.topAnchor,
                            constant: Metrics.buttonSpacing),
            quitButton.bottomAnchor
                .constraint(equalTo: buttonsContainer.bottomAnchor,
                            constant: -Metrics.buttonSpacing),

            reloadButton.firstBaselineAnchor
                .constraint(equalTo: quitButton.firstBaselineAnchor),
            reloadButton.trailingAnchor
                .constraint(equalTo: quitButton.leadingAnchor,
                            constant: -Metrics.buttonSpacing),

            startButton.firstBaselineAnchor
                .constraint(equalTo: reloadButton.firstBaselineAnchor),
            startButton.trailingAnchor
                .constraint(equalTo: reloadButton.leadingAnchor,
                            constant: -Metrics.buttonSpacing),

            aboutButton.firstBaselineAnchor
                .constraint(equalTo: startButton.firstBaselineAnchor),
            aboutButton.trailingAnchor
                .constraint(equalTo: startButton.leadingAnchor,
                            constant: -Metrics.buttonSpacing),
            aboutButton.leadingAnchor
                .constraint(equalTo: buttonsContainer.leadingAnchor,
                            constant: Metrics.buttonSpacing),
        ])
    }

    private func setTextConstraints() {
        NSLayoutConstraint.activate([
            statusText.centerXAnchor
                .constraint(equalTo: view.centerXAnchor),
            statusText
                .centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])

        let trailing =
            textLabel.trailingAnchor
                .constraint(equalTo: textLabelScrollView.trailingAnchor)
        trailing.priority = .init(600)

        let bottom =
            textLabel.bottomAnchor
                .constraint(equalTo: textLabelScrollView.bottomAnchor)
        bottom.priority = .init(600)

        maxWidthContraint =
            textLabelScrollView.widthAnchor
                .constraint(lessThanOrEqualToConstant: Metrics.contentMaxWidth)
        maxHeightContraint =
            textLabelScrollView.heightAnchor
                .constraint(lessThanOrEqualToConstant: Metrics.contentMaxHeight)

        textLabel.setContentHuggingPriority(.required, for: .horizontal)
        NSLayoutConstraint.activate([
            textLabel.widthAnchor
                .constraint(
                    greaterThanOrEqualToConstant: Metrics.contentMinWidth
                ),
            // textLabelScrollView.widthAnchor
            //     .constraint(
            //         lessThanOrEqualToConstant: Metrics.contentMaxWidth
            //     ),
            maxWidthContraint,

            textLabel.heightAnchor
                .constraint(
                    greaterThanOrEqualToConstant: Metrics.contentMinHeight
                ),
            // textLabelScrollView.heightAnchor
            //     .constraint(
            //         lessThanOrEqualToConstant: Metrics.contentMaxHeight
            //     ),
            maxHeightContraint,

            textLabel.topAnchor
                .constraint(equalTo: textLabelScrollView.topAnchor),
            bottom,
            textLabel.leadingAnchor
                .constraint(equalTo: textLabelScrollView.leadingAnchor),
            trailing,

            textLabelScrollView.topAnchor
                .constraint(equalTo: view.topAnchor, constant: 10),
            textLabelScrollView.bottomAnchor
                .constraint(equalTo: buttonsContainer.topAnchor,
                            constant: -10),
            textLabelScrollView.leadingAnchor
                .constraint(equalTo: view.leadingAnchor, constant: 10),
            textLabelScrollView.trailingAnchor
                .constraint(equalTo: view.trailingAnchor, constant: -10),
        ])
    }

    private func resetReloadButton() {
        reloadButton.image =
            systemImage("arrow.clockwise.circle.fill", .title2, .large,
                        .init(paletteColors: [.white, .systemBlue]))
        reloadButton.action = #selector(reloadWidget)
        reloadButton.toolTip = "Reload Current"
    }

    private func startReloadTextTimer() {
        timer =
            DispatchSource.makeTimerSource(queue: DispatchQueue.global())
        timer?.schedule(deadline: .now(), repeating: .milliseconds(500))
        timer?.setEventHandler { [weak self] in
            DispatchQueue.main.async {
                guard let controller = self,
                    let file = controller.cmdFile
                else { return }
                controller.setUntil("\(stringifySeconds(file.untilNextExec))")
                controller.progressView.value = Double(file.untilNextExec)
            }
        }
        timer?.resume()
    }

    private func setupKeyEvents() {
        keyboardEvents = LocalEventMonitor(mask: [.flagsChanged, .keyDown],
            handler: { [weak self] event in
                let modifiers = event.modifierFlags.rawValue
                let key = event.keyCode

                if modsContains(keys: OSShift, in: modifiers) {
                    self?.reloadButton.image =
                        systemImage("arrow.clockwise.circle.fill", .title2,
                                    .large,
                               .init(paletteColors: [.white, .systemCyan]))
                    self?.reloadButton.action =
                        #selector(self?.reloadWidgets)
                    self?.reloadButton.toolTip = "Reload All"
                } else {
                    self?.reloadButton.image =
                        systemImage("arrow.clockwise.circle.fill", .title2,
                                    .large,
                               .init(paletteColors: [.white, .systemBlue]))
                    self?.reloadButton.action =
                        #selector(self?.reloadWidget)
                    self?.reloadButton.toolTip = "Reload Current"
                }

                if modsContains(keys: OSCmd, in: modifiers) {
                    if key == kVK_ANSI_Q {
                        self?.terminateApp()
                    } else if key == kVK_ANSI_W {
                        self?.view.superview?.window?.resignKey()
                    } else if key == kVK_ANSI_R {
                        self?.reloadWidget()
                    } else if key == kVK_ANSI_C {
                        self?.textLabel.currentEditor()?.copy(nil)
                    }
                } else
                if modsContains(keys: OSCmd | OSShift, in: modifiers) {
                    if key == kVK_ANSI_R {
                        // NOTE: We need to resign the window in order to
                        //       prevent the program from intercepting the
                        //       events, which result in sounds beign made
                        //       for missing performKeyEquivalent.
                        self?.view.window?.resignKey()
                        self?.reloadWidgets()
                    }
                } else if modsContainsNone(in: modifiers) {
                    if key == kVK_ANSI_Q || key == kVK_Escape {
                        self?.view.superview?.window?.resignKey()
                    }
                }

                return event
            })
    }

    func setFile(_ file: CmdFile) {
        self.cmdFile = file
        progressView.minValue = 0.0
        progressView.maxValue = Double(file.reloadTime)
        progressView.value = Double(file.reloadTime)
    }

    func setText(_ text: String) {
        if text.isEmpty {
            statusText.stringValue = "Empty"
            textLabel.stringValue = ""
            statusText.isHidden = false
            textLabel.isHidden = true
        } else {
            textLabel.stringValue = text
            statusText.isHidden = true
            textLabel.isHidden = false
        }
    }

    func setUntil(_ text: String) {
        reloadsText.stringValue = text
    }

    @objc private func reloadWidgets() {
        delegate.reloadWidgets()
    }

    @objc private func reloadWidget() {
        cmdFile?.restartTimer()
        statusText.stringValue = "Loading..."
        textLabel.isHidden = true
        statusText.isHidden = false
    }

    @objc private func showAbout() {
        delegate.showAbout()
    }

    @objc private func openAtLogin() {
        delegate.toggleLaunchAtLogin()
        openAtLoginToggle()
    }

    @objc func terminateApp() {
        delegate.terminateApp()
    }

    private func openAtLoginToggle() {
        if SMAppService.mainApp.status == .enabled {
            startButton.toolTip = "Open at Login — ON"
            startButton.image =
                systemImage("sunrise.circle.fill", .title2, .large,
                            .init(paletteColors: [.white, .systemOrange]))
        } else {
            startButton.toolTip = "Open at Login — OFF"
            startButton.image =
                systemImage("sun.max.circle.fill", .title2, .large,
                            .init(paletteColors: [.white, .systemOrange]))
        }
    }

    deinit {
        if timer != nil {
            timer!.cancel()
            timer = nil
        }
        keyboardEvents?.stop()
    }
}
