import AppKit

fileprivate enum UpdateStatus: String {
    case check         = "Check for updates..."
    case latest        = "You're up-to-date!"
    case checking      = "Checking for udpates..."
    case available     = "A new version is available!"
    case downloading   = "Downloading update..."
    case extracting    = "Extracting update..."
    case install       = "Ready to Install"
    case checkFailed   = "Failed to Check for Updates"
    case installFailed = "Failed to Install Update"
}

class UpdateViewController: NSViewController, UpdateManagerDelegate {
    private weak var updaterDelegate: UpdateManagerDelegate?

    private var appIconImage: NSImageView = {
        let image = NSImageView()
        image.image = NSWorkspace.shared
                                     .icon(forFile: Bundle.main.bundlePath)
        image.imageScaling = .scaleAxesIndependently
        image.translatesAutoresizingMaskIntoConstraints = false
        return image
    }()

    private var statusLabel: NSTextField = {
        let textField = NSTextField()
        textField.isEditable = false
        textField.isBezeled = false
        textField.drawsBackground = false
        textField.alignment = .left
        textField.textColor = NSColor(name: nil) { getColors(.black, .white, for: $0) }
        textField.font = NSFont.systemFont(ofSize: NSFontDescriptor
                   .preferredFontDescriptor(forTextStyle: .body).pointSize,
                                           weight: .bold)
        textField.translatesAutoresizingMaskIntoConstraints = false
        return textField
    }()

    private var subStatusLabel: NSTextField = {
        let textField = NSTextField()
        textField.stringValue = "CHANGELOG"
        textField.isHidden = true
        textField.isEditable = false
        textField.isBezeled = false
        textField.drawsBackground = false
        textField.alignment = .left
        textField.textColor = .gray
        textField.font = NSFont.systemFont(ofSize: NSFontDescriptor
                   .preferredFontDescriptor(forTextStyle: .body).pointSize,
                                           weight: .bold)
        textField.translatesAutoresizingMaskIntoConstraints = false
        return textField
    }()

    // TODO: Improve some more. Maybe make it scrollable.
    private var changelogLabel: NSScrollView = {
        let scrollableTextView = NSTextView.scrollableTextView()
        scrollableTextView.isHidden = true
        (scrollableTextView.documentView as? NSTextView)?.isEditable = false
        (scrollableTextView.documentView as? NSTextView)?.textColor = .gray
        (scrollableTextView.documentView as? NSTextView)?.font =
            NSFont.systemFont(ofSize: NSFontDescriptor
                   .preferredFontDescriptor(forTextStyle: .body).pointSize,
                              weight: .regular)
        scrollableTextView.translatesAutoresizingMaskIntoConstraints = false
        return scrollableTextView
    }()

    private var progressIndicator: NSProgressIndicator = {
        let progress = NSProgressIndicator()
        progress.maxValue = 1.0
        progress.isIndeterminate = false
        progress.translatesAutoresizingMaskIntoConstraints = false
        return progress
    }()

    private var updateButton: NSButton = {
        let button = NSButton()
        button.title = "Check for Updates"
        button.sizeToFit()
        button.bezelStyle = .rounded
        button.action = #selector(checkForUpdate)
        button.isEnabled = false
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    private var changelogButton: NSButton = {
        let button = NSButton()
        button.title = "Changelog"
        button.sizeToFit()
        button.bezelStyle = .rounded
        button.action = #selector(openChangelog)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    private var stackView: NSStackView = {
        let stack = NSStackView()
        stack.orientation = .vertical
        stack.alignment = .trailing
        stack.spacing = ViewConstants.spacing10
        stack.translatesAutoresizingMaskIntoConstraints = false
        return stack
    }()

    private var buttonsStackView: NSStackView = {
        let stack = NSStackView()
        stack.orientation = .horizontal
        stack.translatesAutoresizingMaskIntoConstraints = false
        return stack
    }()

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

        view.addSubview(statusLabel)
        view.addSubview(subStatusLabel)

        view.addSubview(stackView)
        stackView.addArrangedSubview(changelogLabel)
        stackView.addArrangedSubview(progressIndicator)
        stackView.addArrangedSubview(buttonsStackView)
        buttonsStackView.addArrangedSubview(changelogButton)
        buttonsStackView.addArrangedSubview(updateButton)
    }

    private func setConstraints() {
        stackView.setHuggingPriority(.required, for: .vertical)

        NSLayoutConstraint.activate([
            appIconImage.widthAnchor.constraint(equalToConstant: 70),
            appIconImage.heightAnchor
                             .constraint(equalTo: appIconImage.widthAnchor,
                                         multiplier: 1),
            appIconImage.topAnchor.constraint(equalTo: view.topAnchor,
                                        constant: ViewConstants.spacing10),
            appIconImage.leadingAnchor
                            .constraint(equalTo: view.leadingAnchor,
                                        constant: ViewConstants.spacing10),

            statusLabel.topAnchor
                            .constraint(equalTo: appIconImage.topAnchor,
                                        constant: ViewConstants.spacing10),
            statusLabel.leadingAnchor
                          .constraint(equalTo: appIconImage.trailingAnchor,
                                      constant: ViewConstants.spacing10),

            subStatusLabel.topAnchor
                            .constraint(equalTo: statusLabel.bottomAnchor,
                                        constant: ViewConstants.spacing10),
            subStatusLabel.leadingAnchor
                           .constraint(equalTo: statusLabel.leadingAnchor),

            changelogLabel.widthAnchor
                               .constraint(equalTo: stackView.widthAnchor),
            changelogLabel.heightAnchor.constraint(equalToConstant: 150),

            stackView.topAnchor
                          .constraint(equalTo: subStatusLabel.bottomAnchor,
                                      constant: ViewConstants.spacing2),
            stackView.leadingAnchor
                           .constraint(equalTo: statusLabel.leadingAnchor),
            view.trailingAnchor
                            .constraint(equalTo: stackView.trailingAnchor,
                                        constant: ViewConstants.spacing20),
            view.bottomAnchor
                            .constraint(equalTo: stackView.bottomAnchor,
                                        constant: ViewConstants.spacing20),

            buttonsStackView.heightAnchor
                           .constraint(equalTo: updateButton.heightAnchor),
        ])
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        addSubviews()
        setConstraints()
    }

    override func viewDidAppear() {
        super.viewDidAppear()

        (changelogLabel.documentView as? NSTextView)?.string = ""
        subStatusLabel.isHidden = true
        changelogLabel.isHidden = true
        updateStatus(.checking)
        checkForUpdate()
    }

    private func updateStatus(_ status: UpdateStatus) {
        DispatchQueue.main.async { [weak self] in
            self?.updateButton.isEnabled = true
            self?.updateButton.title = "Check for Update"
            self?.updateButton.keyEquivalent = "\r"
            self?.progressIndicator.isHidden = true
            self?.statusLabel.stringValue = UpdateStatus.check.rawValue
            self?.updateButton.action = #selector(self!.checkForUpdate)

            switch status {
            case .check:
                break
            case .latest:
                self?.statusLabel.stringValue = UpdateStatus.latest
                                                                  .rawValue
                break
            case .checking:
                self?.statusLabel.stringValue = UpdateStatus.checking
                                                                  .rawValue
                self?.updateButton.keyEquivalent = ""
                self?.updateButton.isEnabled = false
            case .available:
                self?.updateButton.title = "Download"
                self?.updateButton.keyEquivalent = ""
                self?.statusLabel.stringValue = UpdateStatus.available
                                                                  .rawValue
                self?.updateButton.action = #selector(self!.downloadUpdate)
            case .downloading:
                self?.updateButton.isEnabled = false
                self?.updateButton.title = "Install"
                self?.progressIndicator.isHidden = false
                self?.statusLabel.stringValue = UpdateStatus.downloading
                                                                  .rawValue
            case .extracting:
                self?.updateButton.isEnabled = false
                self?.updateButton.title = "Install"
                self?.progressIndicator.isHidden = false
                self?.statusLabel.stringValue = UpdateStatus.extracting
                                                                  .rawValue
            case .install:
                self?.updateButton.title = "Install"
                self?.progressIndicator.isHidden = false
                self?.statusLabel.stringValue = UpdateStatus.install
                                                                  .rawValue
                self?.updateButton.action = #selector(self!.installUpdate)
            case .checkFailed:
                self?.statusLabel.stringValue = UpdateStatus.checkFailed
                                                                  .rawValue
            case .installFailed:
                self?.statusLabel.stringValue = UpdateStatus.installFailed
                                                                  .rawValue
                self?.updateButton.action = #selector(self!.checkForUpdate)
            }
        }
    }

    @objc private func checkForUpdate() {
        updateStatus(.checking)
        Task { [weak self] in
            let status = await UpdateManager.shared.isUpdateAvailable()
            if status.available == nil {
                self?.updateStatus(.checkFailed)
            } else {
                if status.available! {
                    self?.updateStatus(.available)
                    if let model = status.model, model.changes.count > 0 {
                        DispatchQueue.main.async {
                            self?.subStatusLabel.isHidden = false
                            self?.changelogLabel.isHidden = false
                            for (i, change) in model.changes.enumerated() {
                                (self?.changelogLabel.documentView as? 
                                                     NSTextView)?.string +=
                                                       "\u{2022} \(change)"
                                if i < model.changes.count-1 {
                                    (self?.changelogLabel.documentView as?
                                                     NSTextView)?.string += "\n"
                                }
                            }
                        }
                    }
                } else {
                    self?.updateStatus(.latest)
                }
            }
        }
    }

    @objc private func downloadUpdate() {
        updateStatus(.downloading)

        UpdateManager.shared.downloadUpdate { [weak self] data in
            switch data {
            case .success:
                self?.updateStatus(.extracting)
                UpdateManager.shared.extractUpdate { [weak self] idata in
                    switch idata {
                    case .success:
                        self?.updateStatus(.install)
                    case .failure:
                        self?.updateStatus(.installFailed)
                    }
                }
            case .failure:
                self?.updateStatus(.installFailed)
            }
        }
    }

    @objc private func installUpdate() {
        updateButton.isEnabled = false
        UpdateManager.shared.installUpdate()
        delegate.terminateApp()
    }

    @objc private func openChangelog() {
        openLink(EndpointConstants.changelog)
    }

    public func setUpdateDelegate() {
        UpdateManager.shared.delegate = self
    }

    public func downloadProgressChanged(_ value: Double) {
        DispatchQueue.main.async { [weak self] in
            self?.progressIndicator.doubleValue = value
        }
    }

    public func unzipProgressChanged(_ value: Double) {
        DispatchQueue.main.async { [weak self] in
            self?.progressIndicator.doubleValue = value
        }
    }
}
