//
//  Window.swift
//  Adwaita
//
//  Created by david-swift on 14.09.23.
//

// swiftlint:disable discouraged_optional_collection

import Foundation
import Libadwaita

/// A structure representing an application window type.
///
/// Note that multiple instances of a window can be opened at the same time.
public struct Window: WindowScene {

    /// The window's identifier.
    public var id: String
    /// The window's content.
    var content: (GTUIApplicationWindow) -> Body
    /// Whether an instance of the window type should be opened when the app is starting up.
    public var `open`: Int
    /// The identifier of the window's parent.
    public var parentID: String?
    /// The keyboard shortcuts.
    var shortcuts: [String: (GTUIApplicationWindow) -> Void] = [:]
    /// The keyboard shortcuts on the app level.
    public var appShortcuts: [String: (GTUIApp) -> Void] = [:]
    /// The signal for the file importer.
    var fileImporter: Signal = .init()
    /// The signal for the file exporter.
    var fileExporter: Signal = .init()
    /// The initial folder for the file importer.
    var initialImporterFolder: URL?
    /// The initial folder for the file exporter.
    var initialExporterFolder: URL?
    /// The initial file name for the file exporter.
    var initialName: String?
    /// The accepted extensions for the file importer.
    var extensions: [String]?
    /// Whether folders are accepted in the file importer.
    var folders = false
    /// The closure to run when the import is successful.
    var importerResult: ((URL) -> Void)?
    /// The closure to run when the export is successful.
    var exporterResult: ((URL) -> Void)?
    /// The closure to run when the import is not successful.
    var importerCancel: (() -> Void)?
    /// The closure to run when the export is not successful.
    var exporterCancel: (() -> Void)?

    /// Create a window type with a certain identifier and user interface.
    /// - Parameters:
    ///   - id: The identifier.
    ///   - open: The number of instances of the window type when the app is starting.
    ///   - content: The window's content.
    public init(id: String, `open`: Int = 1, @ViewBuilder content: @escaping (GTUIApplicationWindow) -> Body) {
        self.content = content
        self.id = id
        self.open = open
    }

    /// Get the storage for the window.
    /// - Parameter app: The application.
    /// - Returns: The storage.
    public func createWindow(app: GTUIApp) -> WindowStorage {
        let window = createGTUIWindow(app: app)
        let storage = getViewStorage(window: window)
        let windowStorage = WindowStorage(id: id, window: window, view: storage)
        window.observeHide {
            windowStorage.destroy = true
            return false
        }
        windowStorage.parentID = parentID
        updateFileDialog(storage: windowStorage)
        return windowStorage
    }

    /// Get the window.
    /// - Parameter app: The application.
    /// - Returns: The window.
    func createGTUIWindow(app: GTUIApp) -> GTUIApplicationWindow {
        let window = GTUIApplicationWindow(app: app)
        updateAppShortcuts(app: app)
        window.show()
        return window
    }

    /// Get the storage of the content view.
    /// - Parameter window: The window.
    /// - Returns: The storage of the content of the window.
    func getViewStorage(window: GTUIApplicationWindow) -> ViewStorage {
        let content = content(window)
        let storage = content.widget(modifiers: []).container(modifiers: [])
        window.setChild(storage.view)
        updateShortcuts(window: window)
        return storage
    }

    /// Update a window storage's content.
    /// - Parameter storage: The storage to update.
    public func update(_ storage: WindowStorage, app: GTUIApp) {
        if let window = storage.window as? GTUIApplicationWindow {
            let content = content(window)
            content.widget(modifiers: []).updateStorage(storage.view, modifiers: [])
            updateShortcuts(window: window)
            updateAppShortcuts(app: app)
        }
        updateFileDialog(storage: storage)
    }

    /// Add windows that overlay the last instance of this window if presented.
    /// - Parameter windows: The windows.
    /// - Returns: The new windows and this window.
    public func overlay(@SceneBuilder windows: () -> [WindowSceneGroup]) -> [WindowScene] {
        windows().windows().map { window in
            var newWindow = window
            newWindow.parentID = id
            return newWindow
        } + [self]
    }

    /// Add an importer file dialog to the window.
    /// - Parameters:
    ///     - signal: The signal for opening the dialog.
    ///     - initialFolder: The URL to the folder open when being opened.
    ///     - extensions: The accepted file extensions.
    ///     - folders: Whether folders are accepted.
    ///     - onOpen: Run this when a file for importing has been chosen.
    ///     - onClose: Run this when the user cancelled the action.
    public func fileImporter(
        _ signal: Signal,
        initialFolder: URL? = nil,
        extensions: [String]? = nil,
        folders: Bool = false,
        onOpen: @escaping (URL) -> Void,
        onClose: @escaping () -> Void
    ) -> Self {
        var newSelf = self
        newSelf.fileImporter = signal
        newSelf.initialImporterFolder = initialFolder
        newSelf.extensions = extensions
        newSelf.folders = folders
        newSelf.importerResult = onOpen
        newSelf.importerCancel = onClose
        return newSelf
    }

    /// Add an exporter file dialog to the window.
    /// - Parameters:
    ///     - signal: The signal for opening the dialog.
    ///     - initialFolder: The URL to the folder open when being opened.
    ///     - initialName: The default file name.
    ///     - onSave: Run this when a path for exporting has been chosen.
    ///     - onClose: Run this when the user cancelled the action.
    public func fileExporter(
        _ signal: Signal,
        initialFolder: URL? = nil,
        initialName: String? = nil,
        onSave: @escaping (URL) -> Void,
        onClose: @escaping () -> Void
    ) -> Self {
        var newSelf = self
        newSelf.fileExporter = signal
        newSelf.initialExporterFolder = initialFolder
        newSelf.initialName = initialName
        newSelf.exporterResult = onSave
        newSelf.exporterCancel = onClose
        return newSelf
    }

    /// Add a keyboard shortcut.
    /// - Parameters:
    ///     - shortcut: The keyboard shortcut.
    ///     - action: The closure to execute when the keyboard shortcut is pressed.
    /// - Returns: The window.
    public func keyboardShortcut(_ shortcut: String, action: @escaping (GTUIApplicationWindow) -> Void) -> Self {
        var newSelf = self
        newSelf.shortcuts[shortcut] = action
        return newSelf
    }

    /// Update the keyboard shortcuts.
    /// - Parameter window: The application window.
    func updateShortcuts(window: GTUIApplicationWindow) {
        for shortcut in shortcuts {
            window.addKeyboardShortcut(shortcut.key, id: shortcut.key) { shortcut.value(window) }
        }
    }

    /// Open a file importer or exporter if a signal has been activated and update changes.
    /// - Parameter storage: The window storage.
    func updateFileDialog(storage: WindowStorage) {
        storage.fileDialog.setExtensions(extensions, folders: folders)
        if let initialName {
            storage.fileDialog.setInitialName(initialName)
        }
        if fileImporter.update, let importerResult, let importerCancel {
            storage.fileDialog.open(folder: initialImporterFolder, importerResult, onClose: importerCancel)
        } else if fileExporter.update, let exporterResult, let exporterCancel {
            storage.fileDialog.save(folder: initialExporterFolder, exporterResult, onClose: exporterCancel)
        }
    }

    /// Add the shortcut "<Ctrl>w" which closes the window.
    /// - Returns: The window.
    public func closeShortcut() -> Self {
        keyboardShortcut("w".ctrl()) { $0.close() }
    }

}

// swiftlint:enable discouraged_optional_collection
