// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.2 OR LicenseRef-Slint-commercial

// cSpell: ignore imum

use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::rc::Rc;

use crate::expression_tree::BuiltinFunction;
use crate::langtype::{
    BuiltinElement, BuiltinPropertyInfo, ElementType, Enumeration, PropertyLookupResult, Type,
};
use crate::object_tree::{Component, PropertyVisibility};

pub const RESERVED_GEOMETRY_PROPERTIES: &[(&str, Type)] = &[
    ("x", Type::LogicalLength),
    ("y", Type::LogicalLength),
    ("width", Type::LogicalLength),
    ("height", Type::LogicalLength),
    ("z", Type::Float32),
];

pub const RESERVED_LAYOUT_PROPERTIES: &[(&str, Type)] = &[
    ("min-width", Type::LogicalLength),
    ("min-height", Type::LogicalLength),
    ("max-width", Type::LogicalLength),
    ("max-height", Type::LogicalLength),
    ("padding", Type::LogicalLength),
    ("padding-left", Type::LogicalLength),
    ("padding-right", Type::LogicalLength),
    ("padding-top", Type::LogicalLength),
    ("padding-bottom", Type::LogicalLength),
    ("preferred-width", Type::LogicalLength),
    ("preferred-height", Type::LogicalLength),
    ("horizontal-stretch", Type::Float32),
    ("vertical-stretch", Type::Float32),
];

pub const RESERVED_GRIDLAYOUT_PROPERTIES: &[(&str, Type)] = &[
    ("col", Type::Int32),
    ("row", Type::Int32),
    ("colspan", Type::Int32),
    ("rowspan", Type::Int32),
];

macro_rules! declare_enums {
    ($( $(#[$enum_doc:meta])* enum $Name:ident { $( $(#[$value_doc:meta])* $Value:ident,)* })*) => {
        pub struct BuiltinEnums {
            $(pub $Name : Rc<Enumeration>),*
        }
        impl BuiltinEnums {
            fn new() -> Self {
                Self {
                    $($Name : Rc::new(Enumeration {
                        name: stringify!($Name).replace('_', "-"),
                        values: vec![$(crate::generator::to_kebab_case(stringify!($Value).trim_start_matches("r#"))),*],
                        default_value: 0,
                        node: None,
                    })),*
                }
            }
            fn fill_register(&self, register: &mut TypeRegister) {
                $(if stringify!($Name) != "PathEvent" {
                    register.insert_type_with_name(
                        Type::Enumeration(self.$Name.clone()),
                        stringify!($Name).replace('_', "-")
                    );
                })*
            }
        }
    };
}

i_slint_common::for_each_enums!(declare_enums);

thread_local! {
    pub static BUILTIN_ENUMS: BuiltinEnums = BuiltinEnums::new();
}

const RESERVED_OTHER_PROPERTIES: &[(&str, Type)] = &[
    ("clip", Type::Bool),
    ("opacity", Type::Float32),
    ("cache-rendering-hint", Type::Bool),
    ("visible", Type::Bool), // ("enabled", Type::Bool),
];

pub const RESERVED_DROP_SHADOW_PROPERTIES: &[(&str, Type)] = &[
    ("drop-shadow-offset-x", Type::LogicalLength),
    ("drop-shadow-offset-y", Type::LogicalLength),
    ("drop-shadow-blur", Type::LogicalLength),
    ("drop-shadow-color", Type::Color),
];

pub const RESERVED_ROTATION_PROPERTIES: &[(&str, Type)] = &[
    ("rotation-angle", Type::Angle),
    ("rotation-origin-x", Type::LogicalLength),
    ("rotation-origin-y", Type::LogicalLength),
];

pub fn reserved_accessibility_properties() -> impl Iterator<Item = (&'static str, Type)> {
    [
        //("accessible-role", ...)
        ("accessible-checkable", Type::Bool),
        ("accessible-checked", Type::Bool),
        ("accessible-delegate-focus", Type::Int32),
        ("accessible-description", Type::String),
        ("accessible-label", Type::String),
        ("accessible-value", Type::String),
        ("accessible-value-maximum", Type::Float32),
        ("accessible-value-minimum", Type::Float32),
        ("accessible-value-step", Type::Float32),
        ("accessible-action-default", Type::Callback { return_type: None, args: vec![] }),
        ("accessible-action-increment", Type::Callback { return_type: None, args: vec![] }),
        ("accessible-action-decrement", Type::Callback { return_type: None, args: vec![] }),
        (
            "accessible-action-set-value",
            Type::Callback { return_type: None, args: vec![Type::String] },
        ),
    ]
    .into_iter()
}

/// list of reserved property injected in every item
pub fn reserved_properties() -> impl Iterator<Item = (&'static str, Type, PropertyVisibility)> {
    RESERVED_GEOMETRY_PROPERTIES
        .iter()
        .chain(RESERVED_LAYOUT_PROPERTIES.iter())
        .chain(RESERVED_OTHER_PROPERTIES.iter())
        .chain(RESERVED_DROP_SHADOW_PROPERTIES.iter())
        .chain(RESERVED_ROTATION_PROPERTIES.iter())
        .map(|(k, v)| (*k, v.clone(), PropertyVisibility::InOut))
        .chain(reserved_accessibility_properties().map(|(k, v)| (k, v, PropertyVisibility::InOut)))
        .chain(
            RESERVED_GRIDLAYOUT_PROPERTIES
                .iter()
                .map(|(k, v)| (*k, v.clone(), PropertyVisibility::Constexpr)),
        )
        .chain(IntoIterator::into_iter([
            ("absolute-position", logical_point_type(), PropertyVisibility::Output),
            ("forward-focus", Type::ElementReference, PropertyVisibility::Constexpr),
            ("focus", BuiltinFunction::SetFocusItem.ty(), PropertyVisibility::Public),
            ("clear-focus", BuiltinFunction::ClearFocusItem.ty(), PropertyVisibility::Public),
            (
                "dialog-button-role",
                Type::Enumeration(BUILTIN_ENUMS.with(|e| e.DialogButtonRole.clone())),
                PropertyVisibility::Constexpr,
            ),
            (
                "accessible-role",
                Type::Enumeration(BUILTIN_ENUMS.with(|e| e.AccessibleRole.clone())),
                PropertyVisibility::Constexpr,
            ),
        ]))
        .chain(std::iter::once((
            "init",
            Type::Callback { return_type: None, args: vec![] },
            PropertyVisibility::Private,
        )))
}

/// lookup reserved property injected in every item
pub fn reserved_property(name: &str) -> PropertyLookupResult {
    for (p, t, property_visibility) in reserved_properties() {
        if p == name {
            return PropertyLookupResult {
                property_type: t,
                resolved_name: name.into(),
                is_local_to_component: false,
                is_in_direct_base: false,
                property_visibility,
                declared_pure: None,
            };
        }
    }

    // Report deprecated known reserved properties (maximum_width, minimum_height, ...)
    for pre in &["min", "max"] {
        if let Some(a) = name.strip_prefix(pre) {
            for suf in &["width", "height"] {
                if let Some(b) = a.strip_suffix(suf) {
                    if b == "imum-" {
                        return PropertyLookupResult {
                            property_type: Type::LogicalLength,
                            resolved_name: format!("{}-{}", pre, suf).into(),
                            is_local_to_component: false,
                            is_in_direct_base: false,
                            property_visibility: crate::object_tree::PropertyVisibility::InOut,
                            declared_pure: None,
                        };
                    }
                }
            }
        }
    }
    PropertyLookupResult {
        resolved_name: name.into(),
        property_type: Type::Invalid,
        is_local_to_component: false,
        is_in_direct_base: false,
        property_visibility: crate::object_tree::PropertyVisibility::Private,
        declared_pure: None,
    }
}

/// These member functions are injected in every time
pub fn reserved_member_function(name: &str) -> Option<BuiltinFunction> {
    for (m, e) in [
        ("focus", BuiltinFunction::SetFocusItem), // match for callable "focus" property
        ("clear-focus", BuiltinFunction::ClearFocusItem), // match for callable "clear-focus" property
    ] {
        if m == name {
            return Some(e);
        }
    }
    None
}

#[derive(Debug, Default)]
pub struct TypeRegister {
    /// The set of property types.
    types: HashMap<String, Type>,
    /// The set of element types
    elements: HashMap<String, ElementType>,
    supported_property_animation_types: HashSet<String>,
    pub(crate) property_animation_type: ElementType,
    pub(crate) empty_type: ElementType,
    /// Map from a context restricted type to the list of contexts (parent type) it is allowed in. This is
    /// used to construct helpful error messages, such as "Row can only be within a GridLayout element".
    context_restricted_types: HashMap<String, HashSet<String>>,
    parent_registry: Option<Rc<RefCell<TypeRegister>>>,
    /// If the lookup function should return types that are marked as internal
    pub(crate) expose_internal_types: bool,
}

impl TypeRegister {
    /// FIXME: same as 'add' ?
    pub fn insert_type(&mut self, t: Type) {
        self.types.insert(t.to_string(), t);
    }
    pub fn insert_type_with_name(&mut self, t: Type, name: String) {
        self.types.insert(name, t);
    }

    fn builtin_internal() -> Self {
        let mut register = TypeRegister::default();

        register.insert_type(Type::Float32);
        register.insert_type(Type::Int32);
        register.insert_type(Type::String);
        register.insert_type(Type::PhysicalLength);
        register.insert_type(Type::LogicalLength);
        register.insert_type(Type::Color);
        register.insert_type(Type::ComponentFactory);
        register.insert_type(Type::Duration);
        register.insert_type(Type::Image);
        register.insert_type(Type::Bool);
        register.insert_type(Type::Model);
        register.insert_type(Type::Percent);
        register.insert_type(Type::Easing);
        register.insert_type(Type::Angle);
        register.insert_type(Type::Brush);
        register.insert_type(Type::Rem);
        register.types.insert("Point".into(), logical_point_type());

        BUILTIN_ENUMS.with(|e| e.fill_register(&mut register));

        register.supported_property_animation_types.insert(Type::Float32.to_string());
        register.supported_property_animation_types.insert(Type::Int32.to_string());
        register.supported_property_animation_types.insert(Type::Color.to_string());
        register.supported_property_animation_types.insert(Type::PhysicalLength.to_string());
        register.supported_property_animation_types.insert(Type::LogicalLength.to_string());
        register.supported_property_animation_types.insert(Type::Brush.to_string());
        register.supported_property_animation_types.insert(Type::Angle.to_string());

        #[rustfmt::skip]
        macro_rules! map_type {
            ($pub_type:ident, bool) => { Type::Bool };
            ($pub_type:ident, i32) => { Type::Int32 };
            ($pub_type:ident, f32) => { Type::Float32 };
            ($pub_type:ident, SharedString) => { Type::String };
            ($pub_type:ident, Coord) => { Type::LogicalLength };
            ($pub_type:ident, KeyboardModifiers) => { $pub_type.clone() };
            ($pub_type:ident, $_:ident) => {
                BUILTIN_ENUMS.with(|e| Type::Enumeration(e.$pub_type.clone()))
            };
        }
        #[rustfmt::skip]
        macro_rules! maybe_clone {
            ($pub_type:ident, KeyboardModifiers) => { $pub_type.clone() };
            ($pub_type:ident, $_:ident) => { $pub_type };
        }
        macro_rules! register_builtin_structs {
            ($(
                $(#[$attr:meta])*
                struct $Name:ident {
                    @name = $inner_name:literal
                    export {
                        $( $(#[$pub_attr:meta])* $pub_field:ident : $pub_type:ident, )*
                    }
                    private {
                        $( $(#[$pri_attr:meta])* $pri_field:ident : $pri_type:ty, )*
                    }
                }
            )*) => { $(
                let $Name = Type::Struct {
                    fields: BTreeMap::from([
                        $((stringify!($pub_field).replace('_', "-"), map_type!($pub_type, $pub_type))),*
                    ]),
                    name: Some(format!("{}", $inner_name)),
                    node: None,
                    rust_attributes: None,
                };
                register.insert_type_with_name(maybe_clone!($Name, $Name), stringify!($Name).to_string());
            )* };
        }
        i_slint_common::for_each_builtin_structs!(register_builtin_structs);

        crate::load_builtins::load_builtins(&mut register);

        let mut context_restricted_types = HashMap::new();
        register
            .elements
            .values()
            .for_each(|ty| ty.collect_contextual_types(&mut context_restricted_types));
        register.context_restricted_types = context_restricted_types;

        match &mut register.elements.get_mut("PopupWindow").unwrap() {
            ElementType::Builtin(ref mut b) => {
                let popup = Rc::get_mut(b).unwrap();
                popup.properties.insert(
                    "show".into(),
                    BuiltinPropertyInfo::new(BuiltinFunction::ShowPopupWindow.ty()),
                );
                popup.member_functions.insert("show".into(), BuiltinFunction::ShowPopupWindow);

                popup.properties.insert(
                    "close".into(),
                    BuiltinPropertyInfo::new(BuiltinFunction::ClosePopupWindow.ty()),
                );
                popup.member_functions.insert("close".into(), BuiltinFunction::ClosePopupWindow);

                popup.properties.get_mut("close-on-click").unwrap().property_visibility =
                    PropertyVisibility::Constexpr;
            }

            _ => unreachable!(),
        };

        match &mut register.elements.get_mut("TextInput").unwrap() {
            ElementType::Builtin(ref mut b) => {
                let text_input = Rc::get_mut(b).unwrap();
                text_input.properties.insert(
                    "set-selection-offsets".into(),
                    BuiltinPropertyInfo::new(BuiltinFunction::SetSelectionOffsets.ty()),
                );
                text_input
                    .member_functions
                    .insert("set-selection-offsets".into(), BuiltinFunction::SetSelectionOffsets);
            }

            _ => unreachable!(),
        };

        register
    }

    #[doc(hidden)]
    /// All builtins incl. experimental ones! Do not use in production code!
    pub fn builtin_experimental() -> Rc<RefCell<Self>> {
        let register = Self::builtin_internal();
        Rc::new(RefCell::new(register))
    }

    pub fn builtin() -> Rc<RefCell<Self>> {
        let mut register = Self::builtin_internal();

        register.elements.remove("ComponentContainer");
        register.types.remove("component-factory");

        Rc::new(RefCell::new(register))
    }

    pub fn new(parent: &Rc<RefCell<TypeRegister>>) -> Self {
        Self {
            parent_registry: Some(parent.clone()),
            expose_internal_types: parent.borrow().expose_internal_types,
            ..Default::default()
        }
    }

    pub fn lookup(&self, name: &str) -> Type {
        self.types
            .get(name)
            .cloned()
            .or_else(|| self.parent_registry.as_ref().map(|r| r.borrow().lookup(name)))
            .unwrap_or_default()
    }

    fn lookup_element_as_result(
        &self,
        name: &str,
    ) -> Result<ElementType, HashMap<String, HashSet<String>>> {
        match self.elements.get(name).cloned() {
            Some(ty) => Ok(ty),
            None => match &self.parent_registry {
                Some(r) => r.borrow().lookup_element_as_result(name),
                None => Err(self.context_restricted_types.clone()),
            },
        }
    }

    pub fn lookup_element(&self, name: &str) -> Result<ElementType, String> {
        self.lookup_element_as_result(name).map_err(|context_restricted_types| {
            if let Some(permitted_parent_types) = context_restricted_types.get(name) {
                if permitted_parent_types.len() == 1 {
                    format!(
                        "{} can only be within a {} element",
                        name,
                        permitted_parent_types.iter().next().unwrap()
                    )
                } else {
                    let mut elements = permitted_parent_types.iter().cloned().collect::<Vec<_>>();
                    elements.sort();
                    format!(
                        "{} can only be within the following elements: {}",
                        name,
                        elements.join(", ")
                    )
                }
            } else if let Some(ty) = self.types.get(name) {
                format!("'{}' cannot be used as an element", ty)
            } else {
                format!("Unknown type {}", name)
            }
        })
    }

    pub fn lookup_builtin_element(&self, name: &str) -> Option<ElementType> {
        self.parent_registry.as_ref().map_or_else(
            || self.elements.get(name).cloned(),
            |p| p.borrow().lookup_builtin_element(name),
        )
    }

    pub fn lookup_qualified<Member: AsRef<str>>(&self, qualified: &[Member]) -> Type {
        if qualified.len() != 1 {
            return Type::Invalid;
        }
        self.lookup(qualified[0].as_ref())
    }

    pub fn add(&mut self, comp: Rc<Component>) {
        self.add_with_name(comp.id.clone(), comp);
    }

    pub fn add_with_name(&mut self, name: String, comp: Rc<Component>) {
        self.elements.insert(name, ElementType::Component(comp));
    }

    pub fn add_builtin(&mut self, builtin: Rc<BuiltinElement>) {
        self.elements.insert(builtin.name.clone(), ElementType::Builtin(builtin));
    }

    pub fn property_animation_type_for_property(&self, property_type: Type) -> ElementType {
        if self.supported_property_animation_types.contains(&property_type.to_string()) {
            self.property_animation_type.clone()
        } else {
            self.parent_registry
                .as_ref()
                .map(|registry| {
                    registry.borrow().property_animation_type_for_property(property_type)
                })
                .unwrap_or_default()
        }
    }

    /// Return a hashmap with all the registered type
    pub fn all_types(&self) -> HashMap<String, Type> {
        let mut all =
            self.parent_registry.as_ref().map(|r| r.borrow().all_types()).unwrap_or_default();
        for (k, v) in &self.types {
            all.insert(k.clone(), v.clone());
        }
        all
    }

    /// Return a hashmap with all the registered element type
    pub fn all_elements(&self) -> HashMap<String, ElementType> {
        let mut all =
            self.parent_registry.as_ref().map(|r| r.borrow().all_elements()).unwrap_or_default();
        for (k, v) in &self.elements {
            all.insert(k.clone(), v.clone());
        }
        all
    }

    pub fn empty_type(&self) -> ElementType {
        match self.parent_registry.as_ref() {
            Some(parent) => parent.borrow().empty_type(),
            None => self.empty_type.clone(),
        }
    }
}

pub fn logical_point_type() -> Type {
    Type::Struct {
        fields: IntoIterator::into_iter([
            ("x".to_owned(), Type::LogicalLength),
            ("y".to_owned(), Type::LogicalLength),
        ])
        .collect(),
        name: Some("slint::LogicalPosition".into()),
        node: None,
        rust_attributes: None,
    }
}
