//
//  Property.swift
//  Adwaita
//
//  Created by david-swift on 14.01.24.
//

/// A property.
struct Property: Decodable {

    /// The property's name.
    var name: String
    /// The docs.
    var doc: String?
    /// The property's getter.
    var getter: String?
    /// The property's setter.
    var setter: String?
    /// The property's type.
    var type: GIRType?
    /// This overwrites the prefix provided by the caller of a function.
    var prefix: String?
    // swiftlint:disable discouraged_optional_boolean
    /// This overwrites the cast information provided by the caller of a function.
    var cast: Bool?
    // swiftlint:enable discouraged_optional_boolean
    /// Whether the property is deprecated.
    var deprecated: Bool?

    /// Get the Swift property name.
    /// - Parameter configuration: The generation configuration.
    /// - Returns: The name.
    func convertPropertyName(configuration: GenerationConfiguration) -> String {
        name.convertDelimitedCasingToCamel(delimiter: "-", configuration: configuration, unshorten: true)
    }

    /// Get the property's name and type for parameters or as part of a property definition.
    /// - Parameters:
    ///     - config: The widget configuration.
    ///     - genConfig: The generation configuration.
    ///     - modifier: Whether it is used as a parameter.
    ///     - defaultValue: Whether to set the default value.
    /// - Returns: The code.
    func parameter(
        config: WidgetConfiguration,
        genConfig: GenerationConfiguration,
        modifier: Bool = false,
        defaultValue: Bool = false
    ) -> String {
        var type: String
        if config.bindings.contains(where: { $0.property == name }) {
            type = (try? self.type?.binding(configuration: genConfig)) ?? "Binding<String>"
        } else {
            type = (try? self.type?.generate(configuration: genConfig)) ?? "String"
        }
        if (self.type?.isWidget ?? false) || (self.type?.isMenu ?? false) {
            type = "\(modifier ? "@escaping " : "")(() -> Body)"
        }
        if !config.requiredProperties.contains(name)
        && !(((self.type?.isWidget ?? false) || (self.type?.isMenu ?? false)) && modifier) {
            type += "?"
        }
        if defaultValue, let value = genConfig.defaultModifierValues[type] {
            type += " = \(value)"
        }
        return "\(convertPropertyName(configuration: genConfig)): \(type)"
    }

    /// Generate the property as a Swift property.
    /// - Parameters:
    ///     - config: The widget configuration.
    ///     - genConfig: The generation configuration.
    /// - Returns: The code.
    func generate(config: WidgetConfiguration, genConfig: GenerationConfiguration) -> String {
        """

        \(doc?.docComment(indent: "    ") ?? "/// \(name)")
            var \(parameter(config: config, genConfig: genConfig))
        """
    }

    // swiftlint:disable line_length
    /// Generate the property's modifier.
    /// - Parameters:
    ///     - config: The widget configuration.
    ///     - genConfig: The generation configuration.
    /// - Returns: The code.
    func generateModifier(config: WidgetConfiguration, genConfig: GenerationConfiguration) -> String {
        let property = convertPropertyName(configuration: genConfig)
        let builder = ((type?.isWidget ?? false) || (type?.isMenu ?? false)) ? "@ViewBuilder " : ""
        let mainParameter = parameter(config: config, genConfig: genConfig, modifier: true, defaultValue: true)
        var sideParameters = ""
        var sideAssignments = ""
        return """

        \(doc?.docComment(indent: "    ") ?? "/// \(name)")
            public func \(convertPropertyName(configuration: genConfig))(\(sideParameters)\(builder)_ \(mainParameter)) -> Self {
                var newSelf = self
                newSelf.\(property) = \(property)
                \(sideAssignments)
                return newSelf
            }

        """
    }

    // swiftlint:disable function_body_length
    /// Generate the property's modification when being updated.
    /// - Parameters:
    ///     - config: The widget configuration.
    ///     - genConfig: The generation configuration.
    ///     - prefix: The widget's prefix.
    /// - Returns: The code.
    func generateModification(
        config: WidgetConfiguration,
        genConfig: GenerationConfiguration,
        prefix: String
    ) -> String {
        let name = convertPropertyName(configuration: genConfig)
        guard !(type?.isWidget ?? false) else {
            return """
                        if let widget = storage.content["\(name)"]?.first {
                            \(name)?().updateStorage(widget, data: data, updateProperties: updateProperties, type: type)
                        }

            """
        }
        guard !(type?.isMenu ?? false) else {
            return """
                        if let menu = storage.content["\(name)"]?.first {
                            MenuCollection { \(name)?() ?? [] }
                                .updateStorage(menu, data: data.noModifiers, updateProperties: updateProperties, type: MenuContext.self)
                        }

            """
        }
        guard var setter else {
            return ""
        }
        setter = (self.prefix ?? prefix) + "_" + setter
        let type = try? type?.generate(configuration: genConfig)
        let cProperty = genConfig.cTypeProperties.first { $0.key == type }
        let propertyString = cProperty == nil ? "" : ".\(cProperty?.value ?? "")"
        let widget = (self.cast ?? config.cast) ? "widget?.cast()" : "widget"
        var setConditions = ""
        var onlySetConditions = ""
        var onlySetConditionsIndentation = ""
        var onlySetConditionsEnd = ""
        if let conditions = config.setConditions[name] {
            setConditions = ", \(conditions)"
            onlySetConditions = "if \(conditions) {\n            "
            onlySetConditionsIndentation = "    "
            onlySetConditionsEnd = "\n            }"
        }
        if config.bindings.contains(where: { $0.property == self.name }) {
            var check = ""
            let widget = (self.cast ?? config.cast) ? "storage.opaquePointer?.cast()" : "storage.opaquePointer"
            if var getter {
                getter = "\((self.prefix ?? prefix) + "_" + getter)(\(widget))"
                check = ", (" + (genConfig.getterTypeConversions[self.type?.name ?? ""]?(getter) ?? getter) + ") != \(name).wrappedValue"
            }
            return """
                        if let \(name)\(setConditions), updateProperties\(check) {
                            \(setter)(\(widget), \(name).wrappedValue\(propertyString))
                        }

            """
        } else if config.requiredProperties.contains(self.name) {
            return """
                        if updateProperties, (storage.previousState as? Self)?.\(name) != \(name) {
                            \(onlySetConditions)\(onlySetConditionsIndentation)\(setter)(\(widget), \(name)\(propertyString))\(onlySetConditionsEnd)
                        }

            """
        } else {
            return """
                        if let \(name)\(setConditions), updateProperties, (storage.previousState as? Self)?.\(name) != \(name) {
                            \(setter)(\(widget), \(name)\(propertyString))
                        }

            """
        }
    }
    // swiftlint:enable line_length function_body_length

    /// Generate the widget assignments.
    /// - Parameters:
    ///     - prefix: The widget's prefix.
    ///     - config: The widget configuration.
    ///     - genConfig: The generation configuration.
    /// - Returns: The code.
    func generateWidgetAssignment(
        prefix: String,
        config: WidgetConfiguration,
        genConfig: GenerationConfiguration
    ) -> String {
        guard var setter else {
            return ""
        }
        setter = (self.prefix ?? prefix) + "_" + setter
        let name = convertPropertyName(configuration: genConfig)
        let view = (self.cast ?? config.cast) ? "storage.opaquePointer?.cast()" : "storage.opaquePointer"
        return """
                if let \(name)Storage = \(name)?().storage(data: data, type: type) {
                    storage.content["\(name)"] = [\(name)Storage]
                    \(setter)(\(view), \(name)Storage.opaquePointer?.cast())
                }

        """
    }

    /// Generate the menu assignments.
    /// - Parameters:
    ///     - prefix: The widget's prefix.
    ///     - config: The widget configuration.
    ///     - genConfig: The generation configuration.
    /// - Returns: The code.
    func generateMenuAssignment(
        prefix: String,
        config: WidgetConfiguration,
        genConfig: GenerationConfiguration
    ) -> String {
        guard var setter else {
            return ""
        }
        setter = (self.prefix ?? prefix) + "_" + setter
        let name = convertPropertyName(configuration: genConfig)
        let view = (self.cast ?? config.cast) ? "storage.opaquePointer?.cast()" : "storage.opaquePointer"
        return """
                if let menu = \(name)?() {
                    let childStorage = MenuCollection { menu }.getMenu(data: data)
                    storage.content["\(name)"] = [childStorage]
                    \(setter)(\(view), childStorage.opaquePointer?.cast())
                }

        """
    }

    /// Generate a binding assignment.
    /// - Parameters:
    ///     - prefix: The widget's prefix.
    ///     - signal: The signal's name, if there is one.
    ///     - config: The widget configuration.
    ///     - genConfig: The generation configuration.
    /// - Returns: The code.
    func generateBindingAssignment(
        prefix: String,
        signal: String?,
        config: WidgetConfiguration,
        genConfig: GenerationConfiguration
    ) -> String {
        guard var getter else {
            return ""
        }
        let widget = (self.cast ?? config.cast) ? "storage.opaquePointer?.cast()" : "storage.opaquePointer"
        getter = "\((self.prefix ?? prefix) + "_" + getter)(\(widget))"
        let name = convertPropertyName(configuration: genConfig)
        let finalGetter = genConfig.getterTypeConversions[type?.name ?? ""]?(getter) ?? getter
        let setter = """
        let newValue = \(finalGetter)
        if let \(name), newValue != \(name).wrappedValue {
            \(name).wrappedValue = newValue
        }
        """
        if let signal {
            return """

                    storage.connectSignal(name: "\(signal)", id: "\(self.name)") {
                        \(setter)
                    }
            """
        } else {
            return """

                    storage.notify(name: "\(self.name)") {
                        \(setter)
                    }
            """
        }
    }

}
