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

//! This pass creates properties that are used but are otherwise not real.
//!
//! Must be run after lower_layout and default_geometry passes

use crate::diagnostics::Spanned;
use crate::expression_tree::{BindingExpression, Expression, Unit};
use crate::langtype::{ElementType, Type};
use crate::layout::Orientation;
use crate::namedreference::NamedReference;
use crate::object_tree::*;
use smol_str::SmolStr;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::rc::Rc;

pub fn materialize_fake_properties(component: &Rc<Component>) {
    let mut to_materialize = std::collections::HashMap::new();

    visit_all_named_references(component, &mut |nr| {
        let elem = nr.element();
        let elem = elem.borrow();
        if !to_materialize.contains_key(nr)
            && let Some(ty) =
                should_materialize(&elem.property_declarations, &elem.base_type, nr.name())
        {
            // This only brings more trouble down the line
            if elem.repeated.is_some() {
                panic!(
                    "Cannot materialize fake property {} on repeated element {}",
                    nr.name(),
                    elem.id
                );
            }
            to_materialize.insert(nr.clone(), ty);
        }
    });

    recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
        for prop in elem.borrow().real_bindings().map(|(name, _)| name) {
            let nr = NamedReference::new(elem, prop.clone());
            if let std::collections::hash_map::Entry::Vacant(entry) = to_materialize.entry(nr) {
                let elem = elem.borrow();
                if let Some(ty) =
                    should_materialize(&elem.property_declarations, &elem.base_type, prop)
                {
                    entry.insert(ty);
                }
            }
        }
    });

    for (nr, ty) in to_materialize {
        let elem = nr.element();

        // Note: a DebugHook binding must materialize like the binding it wraps would.
        // A referenced property whose binding is a *synthetic* hook counts as uninitialized
        // (see `must_initialize`), so `initialize` below upgrades the hook in place with the
        // computed default — e.g. geometry_props references (geometry_props.x → img.x) reach
        // this for unbound geometry, and the Transform element's two-way reference reaches it
        // for the injected `transform-rotation`. Skipping hooked properties here instead would
        // leave a binding on a property that never exists at runtime.
        elem.borrow_mut().property_declarations.insert(
            nr.name().clone(),
            PropertyDeclaration { property_type: ty, ..PropertyDeclaration::default() },
        );

        if !must_initialize(&elem.borrow(), nr.name()) {
            // One must check again if one really need to be initialized, because when
            // we checked the first time, the element's binding were temporarily moved
            // by visit_all_named_references_in_element
            continue;
        }
        if let Some(init_expr) = initialize(&elem, nr.name()) {
            let mut elem_mut = elem.borrow_mut();
            if let Some(cell) = elem_mut.binding_cell_including_synthetic(nr.name()) {
                // A synthetic debug hook may occupy the slot (must_initialize treats it as
                // uninitialized): upgrade it in place, keeping the wrapper and id so the
                // property stays live-editable.
                cell.borrow_mut().set_value_expression(init_expr);
            } else {
                let span = elem_mut.to_source_location();
                let mut binding = BindingExpression::new_with_span(init_expr, span);
                binding.priority = i32::MAX;
                elem_mut.set_binding(nr.name().clone(), binding);
            }
        }
    }
}

// One must initialize if there is no real expression for that binding.
fn must_initialize(elem: &Element, prop: &str) -> bool {
    match elem.binding(prop) {
        None => true,
        Some(b) => {
            matches!(b.value_expression(), Expression::Invalid)
        }
    }
}

/// Returns a type if the property needs to be materialized.
pub(crate) fn should_materialize(
    property_declarations: &BTreeMap<SmolStr, PropertyDeclaration>,
    base_type: &ElementType,
    prop: &str,
) -> Option<Type> {
    if property_declarations.contains_key(prop) {
        return None;
    }
    let has_declared_property = match base_type {
        ElementType::Component(c) => has_declared_property(&c.root_element.borrow(), prop),
        ElementType::Builtin(b) => b.native_class.lookup_property(prop).is_some(),
        ElementType::Native(n) => n.lookup_property(prop).is_some(),
        ElementType::Global | ElementType::Interface | ElementType::Error => false,
    };

    if !has_declared_property {
        let ty = crate::typeregister::reserved_property(Cow::Borrowed(prop)).property_type.clone();
        if ty != Type::Invalid {
            return Some(ty);
        } else if prop == "close-on-click" {
            // PopupWindow::close-on-click
            return Some(Type::Bool);
        } else if prop == "close-policy" {
            // PopupWindow::close-policy
            return Some(Type::Enumeration(
                crate::typeregister::BUILTIN.enums.PopupClosePolicy.clone(),
            ));
        } else {
            let ty = base_type.lookup_property(prop).property_type.clone();
            return (ty != Type::Invalid).then_some(ty);
        }
    }
    None
}

/// Returns true if the property is declared in this element or parent
/// (as opposed to being implicitly declared)
pub fn has_declared_property(elem: &Element, prop: &str) -> bool {
    if elem.property_declarations.contains_key(prop) {
        return true;
    }
    match &elem.base_type {
        ElementType::Component(c) => has_declared_property(&c.root_element.borrow(), prop),
        ElementType::Builtin(b) => b.native_class.lookup_property(prop).is_some(),
        ElementType::Native(n) => n.lookup_property(prop).is_some(),
        ElementType::Global | ElementType::Interface | ElementType::Error => false,
    }
}

/// Initialize a sensible default binding for the now materialized property
pub fn initialize(elem: &ElementRc, name: &str) -> Option<Expression> {
    let mut base_type = elem.borrow().base_type.clone();
    loop {
        base_type = match base_type {
            ElementType::Component(ref c) => c.root_element.borrow().base_type.clone(),
            ElementType::Builtin(b) => {
                match b.properties.get(name).and_then(|prop| prop.default_value.expr(elem)) {
                    Some(expr) => return Some(expr),
                    None => break,
                }
            }
            _ => break,
        };
    }

    // Hardcode properties for images, because this is a very common call, and this allows
    // later optimization steps to eliminate these properties.
    // Note that Rectangles and Empties are similarly optimized in layout_constraint_prop, and
    // we rely on struct field access simplification for those.
    if elem.borrow().builtin_type().is_some_and(|n| n.name == "Image") {
        if elem.borrow().layout_info_prop(Orientation::Horizontal).is_none() {
            match name {
                "min-width" => return Some(Expression::NumberLiteral(0., Unit::Px)),
                "max-width" => return Some(Expression::NumberLiteral(f32::MAX as _, Unit::Px)),
                "horizontal-stretch" => return Some(Expression::NumberLiteral(0., Unit::None)),
                _ => {}
            }
        }

        if elem.borrow().layout_info_prop(Orientation::Vertical).is_none() {
            match name {
                "min-height" => return Some(Expression::NumberLiteral(0., Unit::Px)),
                "max-height" => return Some(Expression::NumberLiteral(f32::MAX as _, Unit::Px)),
                "vertical-stretch" => return Some(Expression::NumberLiteral(0., Unit::None)),
                _ => {}
            }
        }
    }

    let expr = match name {
        "min-height" => layout_constraint_prop(elem, "min", Orientation::Vertical),
        "min-width" => layout_constraint_prop(elem, "min", Orientation::Horizontal),
        "max-height" => layout_constraint_prop(elem, "max", Orientation::Vertical),
        "max-width" => layout_constraint_prop(elem, "max", Orientation::Horizontal),
        "horizontal-stretch" => layout_constraint_prop(elem, "stretch", Orientation::Horizontal),
        "vertical-stretch" => layout_constraint_prop(elem, "stretch", Orientation::Vertical),
        "preferred-height" => layout_constraint_prop(elem, "preferred", Orientation::Vertical),
        "preferred-width" => layout_constraint_prop(elem, "preferred", Orientation::Horizontal),
        "opacity" => Expression::NumberLiteral(1., Unit::None),
        "visible" => Expression::BoolLiteral(true),
        "rowspan" => Expression::NumberLiteral(1., Unit::None),
        "colspan" => Expression::NumberLiteral(1., Unit::None),
        "rotation-origin-x" => size_div_2(elem, "width"),
        "rotation-origin-y" => size_div_2(elem, "height"),
        _ => return None,
    };
    Some(expr)
}

fn layout_constraint_prop(elem: &ElementRc, field: &str, orient: Orientation) -> Expression {
    let expr = match elem.borrow().layout_info_prop(orient) {
        Some(e) => Expression::PropertyReference(e.clone()),
        None => crate::layout::implicit_layout_info_call(
            elem,
            orient,
            crate::layout::BuiltinFilter::All,
            None,
        )
        .unwrap(),
    };
    Expression::StructFieldAccess { base: expr.into(), name: field.into() }
}

fn size_div_2(elem: &ElementRc, field: &str) -> Expression {
    Expression::BinaryExpression {
        lhs: Expression::PropertyReference(NamedReference::new(elem, field.into())).into(),
        op: '/',
        rhs: Expression::NumberLiteral(2., Unit::None).into(),
    }
}
