// 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

//! Compute binding analysis and attempt to find binding loops

use std::collections::HashMap;
use std::collections::HashSet;
use std::rc::Rc;

use by_address::ByAddress;

use crate::diagnostics::{BuildDiagnostics, Spanned};
use crate::expression_tree::{BindingExpression, BuiltinFunction, Expression};
use crate::langtype::ElementType;
use crate::layout::{LayoutItem, Orientation};
use crate::namedreference::NamedReference;
use crate::object_tree::{Document, ElementRc, PropertyAnimation, find_parent_element};
use derive_more as dm;

use crate::CompilerConfiguration;
use crate::expression_tree::Callable;
use smol_str::{SmolStr, ToSmolStr};

/// Represent the kind of property for the DefaultFontSize based on the `default-font-size`` property of every `Window``
#[derive(Debug, Clone, PartialEq, Default)]
pub enum DefaultFontSize {
    /// Not yet known/computed
    #[default]
    Unknown,
    /// The default font size is set to a specific constant value in `px` (logical)
    LogicalValue(f32),
    /// The default font size is set to different, but always constant values
    Const,
    /// All windows are either using const value or unset
    NotSet,
    /// At least one `Window` has a non-constant default-font-size
    Variable,
}
impl DefaultFontSize {
    /// Returns true if the default font size is a constant value
    pub fn is_const(&self) -> bool {
        // Note that NotSet is considered const for now as the renderer won't change the default font size at runtime
        matches!(self, Self::Const | Self::LogicalValue(_))
    }
}

#[derive(Debug, Clone, PartialEq, Default)]
pub struct GlobalAnalysis {
    pub default_font_size: DefaultFontSize,
    pub const_scale_factor: Option<f32>,
    pub const_image_sizes: bool,
}

/// Maps the alias in the other direction than what the BindingExpression::two_way_binding does.
/// So if binding for property A has B in its BindingExpression::two_way_binding, then
/// ReverseAliases maps B to A.
type ReverseAliases = HashMap<NamedReference, Vec<NamedReference>>;

pub fn binding_analysis(
    doc: &Document,
    compiler_config: &CompilerConfiguration,
    diag: &mut BuildDiagnostics,
) -> GlobalAnalysis {
    let mut global_analysis = GlobalAnalysis {
        const_scale_factor: compiler_config.const_scale_factor,
        const_image_sizes: compiler_config.const_image_sizes,
        ..Default::default()
    };
    let mut reverse_aliases = Default::default();
    mark_used_base_properties(doc);
    propagate_is_set_on_aliases(doc, &mut reverse_aliases);
    check_window_properties(doc, &mut global_analysis);
    perform_binding_analysis(
        doc,
        &reverse_aliases,
        &mut global_analysis,
        compiler_config.error_on_binding_loop_with_window_layout,
        diag,
    );
    global_analysis
}
/// A reference to a property which might be deep in a component path.
/// eg: `foo.bar.baz.background`: `baz.background` is the `prop` and `foo` and `bar` are in elements
#[derive(Hash, PartialEq, Eq, Clone)]
struct PropertyPath {
    elements: Vec<ByAddress<ElementRc>>,
    prop: NamedReference,
}

impl std::fmt::Debug for PropertyPath {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for e in &self.elements {
            write!(f, "{}.", e.borrow().id)?;
        }
        self.prop.fmt(f)
    }
}

impl PropertyPath {
    /// Given a namedReference accessed by something on the same leaf component
    /// as self, return a new PropertyPath that represent the property pointer
    /// to by nr in the higher possible element
    fn relative(&self, second: &PropertyPath) -> Self {
        let mut element =
            second.elements.first().map_or_else(|| second.prop.element(), |f| f.0.clone());
        if element.borrow().enclosing_component.upgrade().unwrap().is_global() {
            return second.clone();
        }
        fn check_that_element_is_in_the_component(
            e: &ElementRc,
            c: &Rc<crate::object_tree::Component>,
        ) -> bool {
            let enclosing = e.borrow().enclosing_component.upgrade().unwrap();
            Rc::ptr_eq(c, &enclosing)
                || enclosing
                    .parent_element
                    .borrow()
                    .upgrade()
                    .is_some_and(|e| check_that_element_is_in_the_component(&e, c))
        }
        let mut elements = self.elements.clone();
        loop {
            let enclosing = element.borrow().enclosing_component.upgrade().unwrap();
            if enclosing.parent_element().is_some()
                || !Rc::ptr_eq(&element, &enclosing.root_element)
            {
                break;
            }

            let Some(last) = elements.last() else {
                break;
            };
            let last_component = last.borrow().base_type.as_component().clone();
            if !check_that_element_is_in_the_component(&element, &last_component) {
                // `element` is not inside `last`'s sub-component. The reverse holds
                // instead — `last`'s component is enclosed by `element`'s — meaning
                // `second` is rooted in an enclosing scope (e.g. a repeated cell's
                // input bound to an outer property). There is no descent prefix to
                // lift it through, so return it unchanged. Neither containment
                // holding is a malformed path (asserted in debug builds).
                debug_assert!(
                    check_that_element_is_in_the_component(
                        &last_component.root_element,
                        &enclosing
                    ),
                    "The element is not in the component pointed at by the path ({self:?} / {second:?})"
                );
                return second.clone();
            }
            element = elements.pop().unwrap().0;
        }
        if second.elements.is_empty() {
            debug_assert!(elements.last().is_none_or(|x| *x != ByAddress(second.prop.element())));
            Self { elements, prop: NamedReference::new(&element, second.prop.name().clone()) }
        } else {
            elements.push(ByAddress(element));
            elements.extend(second.elements.iter().skip(1).cloned());
            Self { elements, prop: second.prop.clone() }
        }
    }
}

impl From<NamedReference> for PropertyPath {
    fn from(prop: NamedReference) -> Self {
        Self { elements: Vec::new(), prop }
    }
}

struct AnalysisContext<'a> {
    visited: HashSet<PropertyPath>,
    /// The stack of properties that depends on each other
    currently_analyzing: linked_hash_set::LinkedHashSet<PropertyPath>,
    /// When set, one of the property in the `currently_analyzing` stack is the window layout property
    /// And we should issue a warning if that's part of a loop instead of an error
    window_layout_property: Option<PropertyPath>,
    error_on_binding_loop_with_window_layout: bool,
    global_analysis: &'a mut GlobalAnalysis,
}

fn perform_binding_analysis(
    doc: &Document,
    reverse_aliases: &ReverseAliases,
    global_analysis: &mut GlobalAnalysis,
    error_on_binding_loop_with_window_layout: bool,
    diag: &mut BuildDiagnostics,
) {
    let mut context = AnalysisContext {
        error_on_binding_loop_with_window_layout,
        visited: HashSet::new(),
        currently_analyzing: Default::default(),
        window_layout_property: None,
        global_analysis,
    };
    doc.visit_all_used_components(|component| {
        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
            component,
            &(),
            &mut |e, _| analyze_element(e, &mut context, reverse_aliases, diag),
        )
    });
}

fn analyze_element(
    elem: &ElementRc,
    context: &mut AnalysisContext,
    reverse_aliases: &ReverseAliases,
    diag: &mut BuildDiagnostics,
) {
    for (name, binding) in elem.borrow().real_bindings() {
        if binding.borrow().analysis.is_some() {
            continue;
        }
        analyze_binding(
            &PropertyPath::from(NamedReference::new(elem, name.clone())),
            context,
            reverse_aliases,
            diag,
        );
    }
    for cb in elem.borrow().change_callbacks.values() {
        for e in cb.borrow().iter() {
            recurse_expression(elem, e, &mut |prop, r| {
                process_property(prop, r, context, reverse_aliases, diag);
            });
        }
    }
    const P: ReadType = ReadType::PropertyRead;
    for nr in elem.borrow().accessibility_props.0.values() {
        process_property(&PropertyPath::from(nr.clone()), P, context, reverse_aliases, diag);
    }
    if let Some(g) = elem.borrow().geometry_props.as_ref() {
        process_property(&g.x.clone().into(), P, context, reverse_aliases, diag);
        process_property(&g.y.clone().into(), P, context, reverse_aliases, diag);
        process_property(&g.width.clone().into(), P, context, reverse_aliases, diag);
        process_property(&g.height.clone().into(), P, context, reverse_aliases, diag);
    }

    if let Some(component) = elem.borrow().enclosing_component.upgrade()
        && Rc::ptr_eq(&component.root_element, elem)
    {
        for e in component.init_code.borrow().iter() {
            recurse_expression(elem, e, &mut |prop, r| {
                process_property(prop, r, context, reverse_aliases, diag);
            });
        }
        component.root_constraints.borrow_mut().visit_named_references(&mut |nr| {
            process_property(&nr.clone().into(), P, context, reverse_aliases, diag);
        });
        component.popup_windows.borrow().iter().for_each(|p| {
            process_property(&p.x.clone().into(), P, context, reverse_aliases, diag);
            process_property(&p.y.clone().into(), P, context, reverse_aliases, diag);
        });
        component.timers.borrow().iter().for_each(|t| {
            process_property(&t.interval.clone().into(), P, context, reverse_aliases, diag);
            process_property(&t.running.clone().into(), P, context, reverse_aliases, diag);
            process_property(&t.triggered.clone().into(), P, context, reverse_aliases, diag);
        });
    }

    if let Some(repeated) = &elem.borrow().repeated {
        recurse_expression(elem, &repeated.model, &mut |prop, r| {
            process_property(prop, r, context, reverse_aliases, diag);
        });
        if let Some(lv) = &repeated.is_listview {
            process_property(&lv.content_y.clone().into(), P, context, reverse_aliases, diag);
            if let Some(content_height) = &lv.content_height {
                process_property(&content_height.clone().into(), P, context, reverse_aliases, diag);
            }
            if let Some(content_width) = &lv.content_width {
                process_property(&content_width.clone().into(), P, context, reverse_aliases, diag);
            }
            process_property(&lv.listview_height.clone().into(), P, context, reverse_aliases, diag);
            process_property(&lv.listview_width.clone().into(), P, context, reverse_aliases, diag);
        }
    }
    if let Some((h, v)) = &elem.borrow().layout_info_prop {
        process_property(&h.clone().into(), P, context, reverse_aliases, diag);
        process_property(&v.clone().into(), P, context, reverse_aliases, diag);
    }

    for info in elem.borrow().debug.iter() {
        if let Some(crate::layout::Layout::GridLayout(grid)) = &info.layout
            && grid.uses_auto
        {
            for rowcol_prop_name in ["row", "col"] {
                for it in grid.elems.iter() {
                    let child = &it.item.element;
                    if child
                        .borrow()
                        .property_analysis
                        .borrow()
                        .get(rowcol_prop_name)
                        .is_some_and(|a| a.is_set || a.is_set_externally)
                    {
                        diag.push_error(
                            format!("Cannot set property '{}' on '{}' because parent GridLayout uses auto-numbering",
                                rowcol_prop_name, child.borrow().id),
                                &child.borrow().to_source_location(), // not ideal, the location of the property being set would be better
                        );
                    }
                }
            }
        }
    }
}

#[derive(Copy, Clone, dm::BitAnd, dm::BitOr, dm::BitAndAssign, dm::BitOrAssign)]
struct DependsOnExternal(bool);

fn analyze_binding(
    current: &PropertyPath,
    context: &mut AnalysisContext,
    reverse_aliases: &ReverseAliases,
    diag: &mut BuildDiagnostics,
) -> DependsOnExternal {
    let mut depends_on_external = DependsOnExternal(false);
    let element = current.prop.element();
    let name = current.prop.name();
    if (context.currently_analyzing.back() == Some(current))
        && !element
            .borrow()
            .binding_cell_including_synthetic(name)
            .unwrap()
            .borrow()
            .two_way_bindings
            .is_empty()
    {
        let span = element
            .borrow()
            .binding_cell_including_synthetic(name)
            .unwrap()
            .borrow()
            .span
            .clone()
            .unwrap_or_else(|| element.borrow().to_source_location());
        diag.push_error(format!("Property '{name}' cannot refer to itself"), &span);
        return depends_on_external;
    }

    if context.currently_analyzing.contains(current) {
        let mut loop_description = String::new();
        let mut has_window_layout = false;

        fn push_prop(prop: &PropertyPath, out: &mut String) {
            if !out.is_empty() {
                out.push_str(" -> ");
            }
            match prop.prop.element().borrow().id.as_str() {
                "" => out.push_str(prop.prop.name()),
                id => {
                    out.push_str(id);
                    out.push('.');
                    out.push_str(prop.prop.name());
                }
            }
        }

        // Build description by iterating in reverse (trigger direction: "A triggers B")
        // and close the loop by prepending `current` at the start.
        push_prop(current, &mut loop_description);
        for it in context.currently_analyzing.iter().rev() {
            if context.window_layout_property.as_ref().is_some_and(|p| p == it) {
                has_window_layout = true;
            }
            push_prop(it, &mut loop_description);
            if it == current {
                break;
            }
        }

        for it in context.currently_analyzing.iter().rev() {
            let p = &it.prop;
            let elem = p.element();
            let elem = elem.borrow();
            let binding = elem.binding_cell_including_synthetic(p.name()).unwrap().borrow();
            if binding.analysis.as_ref().unwrap().is_in_binding_loop.replace(true) {
                break;
            }

            let span = binding.span.clone().unwrap_or_else(|| elem.to_source_location());
            if !context.error_on_binding_loop_with_window_layout && has_window_layout {
                diag.push_warning(format!("The binding for the property '{}' is part of a binding loop ({loop_description}).\nThis was allowed in previous version of Slint, but is deprecated and may cause panic at runtime", p.name()), &span);
            } else {
                diag.push_error(format!("The binding for the property '{}' is part of a binding loop ({loop_description})", p.name()), &span);
            }
            if it == current {
                break;
            }
        }
        return depends_on_external;
    }

    let element_borrow = element.borrow();
    let binding = element_borrow.binding_cell_including_synthetic(name).unwrap();
    if binding.borrow().analysis.as_ref().is_some_and(|a| a.no_external_dependencies) {
        return depends_on_external;
    } else if !context.visited.insert(current.clone()) {
        return DependsOnExternal(true);
    }

    if let Ok(mut b) = binding.try_borrow_mut() {
        b.analysis = Some(Default::default());
    };
    context.currently_analyzing.insert(current.clone());

    let b = binding.borrow();
    for twb in &b.two_way_bindings {
        if let Some(p) = twb.property()
            && p != &current.prop
        {
            depends_on_external |= process_property(
                &current.relative(&p.clone().into()),
                ReadType::PropertyRead,
                context,
                reverse_aliases,
                diag,
            );
        }
    }

    let mut process_prop = |prop: &PropertyPath, r, context: &mut AnalysisContext| {
        depends_on_external |=
            process_property(&current.relative(prop), r, context, reverse_aliases, diag);
        for x in find_alias_targets(prop, reverse_aliases) {
            // Unlike `x == prop.prop` (a plain duplicate, skipped below), `x == current.prop`
            // is kept: it re-enters the binding being analyzed through its own alias, which is
            // how a loop like `foo <=> bar` plus `foo: bar` gets caught.
            if x.prop != prop.prop {
                depends_on_external |= process_property(
                    &current.relative(&x),
                    ReadType::PropertyRead,
                    context,
                    reverse_aliases,
                    diag,
                );
            }
        }
    };

    recurse_expression(&current.prop.element(), &b.expression, &mut |p, r| {
        process_prop(p, r, context)
    });

    let mut is_const = b.expression.is_constant(Some(context.global_analysis))
        && b.two_way_bindings.iter().all(|n| n.is_constant());

    if is_const && matches!(b.expression, Expression::Invalid) {
        // check the base
        if let Some(base) = element.borrow().sub_component() {
            is_const = NamedReference::new(&base.root_element, name.clone()).is_constant();
        }
    }
    drop(b);

    if let Ok(mut b) = binding.try_borrow_mut() {
        // We have a loop (through different component so we're still borrowed)
        b.analysis.as_mut().unwrap().is_const = is_const;
    }

    match &binding.borrow().animation {
        Some(PropertyAnimation::Static(e)) => analyze_element(e, context, reverse_aliases, diag),
        Some(PropertyAnimation::Transition { animations, state_ref }) => {
            recurse_expression(&current.prop.element(), state_ref, &mut |p, r| {
                process_prop(p, r, context)
            });
            for a in animations {
                analyze_element(&a.animation, context, reverse_aliases, diag);
            }
        }
        None => (),
    }

    let o = context.currently_analyzing.pop_back();
    assert_eq!(&o.unwrap(), current);

    depends_on_external
}

/// Find properties two-way-bound (via `<=>`) to `prop`, ascending through base components
/// when the alias was declared there rather than on `prop`'s own element.
fn find_alias_targets(prop: &PropertyPath, reverse_aliases: &ReverseAliases) -> Vec<PropertyPath> {
    // Alias declared on prop's own element, so return the target(s) verbatim without rebasing
    if let Some(v) = reverse_aliases.get(&prop.prop) {
        return v
            .iter()
            .map(|x| PropertyPath { elements: prop.elements.clone(), prop: x.clone() })
            .collect();
    }

    let start_element = prop.elements.first().map_or_else(|| prop.prop.element(), |e| e.0.clone());
    let mut cur = prop.prop.clone();
    loop {
        let element = cur.element();
        if element.borrow().binding(cur.name()).is_some() {
            return Vec::new();
        }
        let next = match &element.borrow().base_type {
            ElementType::Component(base) => {
                if element.borrow().property_declarations.contains_key(cur.name()) {
                    return Vec::new();
                }
                base.root_element.clone()
            }
            _ => return Vec::new(),
        };
        cur = NamedReference::new(&next, cur.name().clone());
        if let Some(v) = reverse_aliases.get(&cur) {
            return v
                .iter()
                .map(|x| PropertyPath::from(NamedReference::new(&start_element, x.name().clone())))
                .collect();
        }
    }
}

#[derive(Copy, Clone, Eq, PartialEq)]
enum ReadType {
    // Read from the native code
    NativeRead,
    // Read from another property binding in Slint
    PropertyRead,
}

/// Process the property `prop`
///
/// This will visit all the bindings from that property
fn process_property(
    prop: &PropertyPath,
    read_type: ReadType,
    context: &mut AnalysisContext,
    reverse_aliases: &ReverseAliases,
    diag: &mut BuildDiagnostics,
) -> DependsOnExternal {
    #[allow(clippy::match_single_binding)]
    let depends_on_external = match prop
        .prop
        .element()
        .borrow()
        .property_analysis
        .borrow_mut()
        .entry(prop.prop.name().clone())
        .or_default()
    {
        a => {
            if read_type == ReadType::PropertyRead {
                a.is_read = true;
            }
            DependsOnExternal(prop.elements.is_empty() && a.is_set_externally)
        }
    };

    let mut prop = prop.clone();

    loop {
        let element = prop.prop.element();
        if element.borrow().binding(prop.prop.name()).is_some() {
            analyze_binding(&prop, context, reverse_aliases, diag);
            break;
        }
        let next = match &element.borrow().base_type {
            ElementType::Component(base) => {
                if element.borrow().property_declarations.contains_key(prop.prop.name()) {
                    break;
                }
                base.root_element.clone()
            }
            ElementType::Builtin(builtin) => {
                if builtin.properties.contains_key(prop.prop.name()) {
                    visit_builtin_property(builtin, &prop, context, reverse_aliases, diag);
                }
                break;
            }
            _ => break,
        };
        next.borrow()
            .property_analysis
            .borrow_mut()
            .entry(prop.prop.name().clone())
            .or_default()
            .is_read_externally = true;
        prop.elements.push(element.into());
        prop.prop = NamedReference::new(&next, prop.prop.name().clone());
    }
    depends_on_external
}

// Same as in crate::visit_all_named_references_in_element, but not mut
fn recurse_expression(
    elem: &ElementRc,
    expr: &Expression,
    vis: &mut impl FnMut(&PropertyPath, ReadType),
) {
    const P: ReadType = ReadType::PropertyRead;
    expr.visit(|sub| recurse_expression(elem, sub, vis));
    match expr {
        Expression::PropertyReference(r) => vis(&r.clone().into(), P),
        Expression::LayoutCacheAccess { layout_cache_prop, .. } => {
            vis(&layout_cache_prop.clone().into(), P)
        }
        Expression::GridRepeaterCacheAccess { layout_cache_prop, .. } => {
            vis(&layout_cache_prop.clone().into(), P)
        }
        Expression::SolveBoxLayout(l, o)
        | Expression::ComputeBoxLayoutInfo { layout: l, orientation: o, .. } => {
            // we should only visit the layout geometry for the orientation
            if matches!(expr, Expression::SolveBoxLayout(..))
                && let Some(nr) = l.geometry.rect.size_reference(*o)
            {
                vis(&nr.clone().into(), P);
            }
            visit_layout_items_dependencies(l.elems.iter(), *o, vis);

            // The orthogonal solve depends on `cross-axis-alignment`.
            if matches!(expr, Expression::SolveBoxLayout(..))
                && *o != l.orientation
                && let Some(nr) = l.cross_alignment.as_ref()
            {
                vis(&nr.clone().into(), P);
            }

            let mut g = l.geometry.clone();
            g.rect = Default::default(); // already visited;
            g.visit_named_references(&mut |nr| vis(&nr.clone().into(), P))
        }
        Expression::SolveFlexboxLayout(layout)
        | Expression::ComputeFlexboxLayoutInfo { layout, .. } => {
            if let Some(nr) = layout.direction.as_ref() {
                vis(&nr.clone().into(), P);
            }
            // Visit layout geometry dependencies
            if matches!(expr, Expression::SolveFlexboxLayout(..)) {
                // The solve needs the main-axis dimension (width for row,
                // height for column). On the cross axis, *builtin* items
                // receive the perpendicular size through the item VTable's
                // `cross_axis_constraint` parameter and never read `self.width`
                // at runtime, so no edge is declared for them here. *Component*
                // items don't have that shortcut: their compiled
                // `layoutinfo-<cross>` binding is evaluated as an ordinary
                // property and really does depend on the cross-axis dimension
                // (typically via inner height-for-width items). That edge is
                // added by `visit_layout_items_layoutinfo_cross_axis_dependencies`
                // so the binding-loop pass can detect the cycle.
                use crate::layout::FlexboxAxisRelation;
                match layout.axis_relation(Orientation::Horizontal) {
                    FlexboxAxisRelation::MainAxis => {
                        if let Some(nr) = layout.geometry.rect.width_reference.as_ref() {
                            vis(&nr.clone().into(), P);
                        }
                        visit_layout_items_layoutinfo_cross_axis_dependencies(
                            layout.elems.iter().map(|fi| &fi.item),
                            Orientation::Vertical,
                            vis,
                        );
                    }
                    FlexboxAxisRelation::CrossAxis => {
                        if let Some(nr) = layout.geometry.rect.height_reference.as_ref() {
                            vis(&nr.clone().into(), P);
                        }
                        visit_layout_items_layoutinfo_cross_axis_dependencies(
                            layout.elems.iter().map(|fi| &fi.item),
                            Orientation::Horizontal,
                            vis,
                        );
                    }
                    FlexboxAxisRelation::Unknown => {
                        // Runtime direction: conservatively depend on both
                        if let Some(nr) = layout.geometry.rect.width_reference.as_ref() {
                            vis(&nr.clone().into(), P);
                        }
                        if let Some(nr) = layout.geometry.rect.height_reference.as_ref() {
                            vis(&nr.clone().into(), P);
                        }
                        visit_layout_items_layoutinfo_cross_axis_dependencies(
                            layout.elems.iter().map(|fi| &fi.item),
                            Orientation::Horizontal,
                            vis,
                        );
                        visit_layout_items_layoutinfo_cross_axis_dependencies(
                            layout.elems.iter().map(|fi| &fi.item),
                            Orientation::Vertical,
                            vis,
                        );
                    }
                }
            } else if let Expression::ComputeFlexboxLayoutInfo { orientation, .. } = expr {
                let orientation = *orientation;
                use crate::layout::FlexboxAxisRelation;
                match layout.axis_relation(orientation) {
                    FlexboxAxisRelation::MainAxis => {
                        // Main axis: only visit same-axis item dependencies
                        visit_layout_items_dependencies(
                            layout.elems.iter().map(|fi| &fi.item),
                            orientation,
                            vis,
                        );
                    }
                    FlexboxAxisRelation::CrossAxis => {
                        // Cross axis: depends on the perpendicular (main-axis)
                        // dimension for accurate wrapping. Skip that edge
                        // when the element has a parametrized layout-info
                        // function — callers that would otherwise cycle go
                        // through it instead, so the bare binding's read of
                        // `self.{w,h}` is a fallback only.
                        if orientation == Orientation::Vertical
                            && let Some(nr) = layout.geometry.rect.width_reference.as_ref()
                            && nr.element().borrow().layout_info_v_with_constraint.is_none()
                        {
                            vis(&nr.clone().into(), P);
                        }
                        if orientation == Orientation::Horizontal
                            && let Some(nr) = layout.geometry.rect.height_reference.as_ref()
                            && nr.element().borrow().layout_info_h_with_constraint.is_none()
                        {
                            vis(&nr.clone().into(), P);
                        }
                        visit_layout_items_dependencies(
                            layout.elems.iter().map(|fi| &fi.item),
                            Orientation::Horizontal,
                            vis,
                        );
                        visit_layout_items_dependencies(
                            layout.elems.iter().map(|fi| &fi.item),
                            Orientation::Vertical,
                            vis,
                        );
                    }
                    FlexboxAxisRelation::Unknown => {
                        // Unknown direction: visit both orientations' item
                        // dependencies but NOT perpendicular dimensions (adding
                        // those leads to binding loops for runtime direction).
                        visit_layout_items_dependencies(
                            layout.elems.iter().map(|fi| &fi.item),
                            Orientation::Horizontal,
                            vis,
                        );
                        visit_layout_items_dependencies(
                            layout.elems.iter().map(|fi| &fi.item),
                            Orientation::Vertical,
                            vis,
                        );
                    }
                }
            }
            let mut g = layout.geometry.clone();
            g.rect = Default::default(); // already visited;
            g.visit_named_references(&mut |nr| vis(&nr.clone().into(), P))
        }
        Expression::OrganizeGridLayout(layout) => {
            let mut layout = layout.clone();
            layout.visit_rowcol_named_references(&mut |nr: &mut NamedReference| {
                vis(&nr.clone().into(), P)
            });
        }
        Expression::SolveGridLayout { layout_organized_data_prop, layout, orientation }
        | Expression::ComputeGridLayoutInfo {
            layout_organized_data_prop,
            layout,
            orientation,
            ..
        } => {
            // we should only visit the layout geometry for the orientation
            if matches!(expr, Expression::SolveGridLayout { .. })
                && let Some(nr) = layout.geometry.rect.size_reference(*orientation)
            {
                vis(&nr.clone().into(), P);
            }
            vis(&layout_organized_data_prop.clone().into(), P);
            visit_layout_items_dependencies(
                layout.elems.iter().map(|it| &it.item),
                *orientation,
                vis,
            );
            let mut g = layout.geometry.clone();
            g.rect = Default::default(); // already visited;
            g.visit_named_references(&mut |nr| vis(&nr.clone().into(), P))
        }
        Expression::FunctionCall {
            function: Callable::Callback(nr) | Callable::Function(nr),
            ..
        } => vis(&nr.clone().into(), P),
        Expression::FunctionCall { function: Callable::Builtin(b), arguments, .. } => match b {
            BuiltinFunction::ImplicitLayoutInfo(orientation) => {
                if let [Expression::ElementReference(item), ..] = arguments.as_slice() {
                    visit_implicit_layout_info_dependencies(
                        *orientation,
                        &item.upgrade().unwrap(),
                        vis,
                    );
                }
            }
            BuiltinFunction::ItemAbsolutePosition => {
                if let Some(Expression::ElementReference(item)) = arguments.first() {
                    // The result depends on the element's own geometry origin as well as every
                    // ancestor's (map_to_window walks the whole ancestor chain).
                    let mut item = item.upgrade().unwrap();
                    loop {
                        vis(
                            &NamedReference::new(&item, SmolStr::new_static("x")).into(),
                            ReadType::NativeRead,
                        );
                        vis(
                            &NamedReference::new(&item, SmolStr::new_static("y")).into(),
                            ReadType::NativeRead,
                        );
                        let Some(parent) = find_parent_element(&item) else { break };
                        item = parent;
                    }
                }
            }
            BuiltinFunction::ItemFontMetrics => {
                if let Some(Expression::ElementReference(item)) = arguments.first() {
                    let item = item.upgrade().unwrap();
                    vis(
                        &NamedReference::new(&item, SmolStr::new_static("font-size")).into(),
                        ReadType::NativeRead,
                    );
                    vis(
                        &NamedReference::new(&item, SmolStr::new_static("font-weight")).into(),
                        ReadType::NativeRead,
                    );
                    vis(
                        &NamedReference::new(&item, SmolStr::new_static("font-family")).into(),
                        ReadType::NativeRead,
                    );
                    vis(
                        &NamedReference::new(&item, SmolStr::new_static("font-italic")).into(),
                        ReadType::NativeRead,
                    );
                }
            }
            BuiltinFunction::GetWindowDefaultFontSize => {
                let root =
                    elem.borrow().enclosing_component.upgrade().unwrap().root_element.clone();
                if root.borrow().builtin_type().is_some_and(|bt| bt.name == "Window") {
                    vis(
                        &NamedReference::new(&root, SmolStr::new_static("default-font-size"))
                            .into(),
                        ReadType::PropertyRead,
                    );
                }
            }
            _ => {}
        },
        _ => {}
    }
}

fn visit_layout_items_dependencies<'a>(
    items: impl Iterator<Item = &'a LayoutItem>,
    orientation: Orientation,
    vis: &mut impl FnMut(&PropertyPath, ReadType),
) {
    for it in items {
        let mut element = it.element.clone();
        if element
            .borrow()
            .repeated
            .as_ref()
            .map(|r| recurse_expression(&element, &r.model, vis))
            .is_some()
        {
            element = it.element.borrow().base_type.as_component().root_element.clone();
        }

        if let Some(nr) = element.borrow().layout_info_prop(orientation) {
            vis(&nr.clone().into(), ReadType::PropertyRead);
        } else {
            if let ElementType::Component(base) = &element.borrow().base_type
                && let Some(nr) = base.root_element.borrow().layout_info_prop(orientation)
            {
                vis(
                    &PropertyPath { elements: vec![ByAddress(element.clone())], prop: nr.clone() },
                    ReadType::PropertyRead,
                );
            }
            visit_implicit_layout_info_dependencies(orientation, &element, vis);
        }

        for (nr, _) in it.constraints.for_each_restrictions(orientation) {
            vis(&nr.clone().into(), ReadType::PropertyRead)
        }
    }
}

/// Visit cross-axis `layoutinfo-<cross>` dependencies for child elements that
/// have a compiled `layoutinfo-<cross>` binding (i.e. an inlined component
/// root, or a nested layout) and no parametrized variant that bypasses it.
///
/// Pure builtins (`Image`, `Text`, `Rectangle`, …) do not set `layout_info_prop`
/// — their cross-axis size is computed through the item VTable, which accepts a
/// `cross_axis_constraint` argument, so they never read `self.{w,h}` at
/// runtime and the parent's `SolveFlexboxLayout` has no real dependency on
/// them. Elements that *do* set `layout_info_prop` run an ordinary property
/// binding that may transitively depend on the cross-axis dimension.
/// `implicit_layout_info_call` dispatches via the parametrized
/// `layoutinfo-{v,h}-with-constraint` function when the child carries one, so
/// the property dependency only exists at runtime for cells without that
/// function — mirror that here.
fn visit_layout_items_layoutinfo_cross_axis_dependencies<'a>(
    items: impl Iterator<Item = &'a LayoutItem>,
    cross_axis: Orientation,
    vis: &mut impl FnMut(&PropertyPath, ReadType),
) {
    for it in items {
        let element = it.element.clone();
        // Parent dispatches via the parametrized function, not the property.
        let bypassed = match cross_axis {
            Orientation::Vertical => {
                element.borrow().inherited_layout_info_v_with_constraint().is_some()
            }
            Orientation::Horizontal => {
                element.borrow().inherited_layout_info_h_with_constraint().is_some()
            }
        };
        if bypassed {
            continue;
        }
        if let Some(nr) = element.borrow().layout_info_prop(cross_axis) {
            vis(&nr.clone().into(), ReadType::PropertyRead);
        } else if let ElementType::Component(base) = &element.borrow().base_type
            && let Some(nr) = base.root_element.borrow().layout_info_prop(cross_axis)
        {
            vis(
                &PropertyPath { elements: vec![ByAddress(element.clone())], prop: nr.clone() },
                ReadType::PropertyRead,
            );
        } else {
            visit_cell_cross_axis_implicit_dependency(cross_axis, &element, vis);
        }
    }
}

/// Cross-axis variant of [`visit_implicit_layout_info_dependencies`]: only
/// declare deps that actually exist on the cross-axis path. Image/Text and
/// other h-for-w builtins receive the cross-axis size via the item VTable's
/// `cross_axis_constraint`, so they don't read `self.{w,h}` here. For other
/// items (user components, plain builtins), the native `ImplicitLayoutInfo`
/// reads `preferred-{w,h}` — declare it when the user has bound it to read
/// the opposite-axis dim on the same element. That catches cycles like
/// `preferred-height: self.width` at compile time instead of panicking at
/// runtime.
fn visit_cell_cross_axis_implicit_dependency(
    cross_axis: Orientation,
    item: &ElementRc,
    vis: &mut impl FnMut(&PropertyPath, ReadType),
) {
    let base_type = item.borrow().base_type.to_smolstr();
    if matches!(base_type.as_str(), "Image" | "ClippedImage" | "Text" | "TextInput" | "StyledText")
    {
        return;
    }
    let (prop, opposite_dim) = match cross_axis {
        Orientation::Horizontal => ("preferred-width", "height"),
        Orientation::Vertical => ("preferred-height", "width"),
    };
    if !item.borrow().is_binding_set(prop, false) {
        return;
    }
    let reads_opposite = item
        .borrow()
        .binding(prop)
        .map(|b| {
            let mut seen = false;
            b.expression.visit_recursive(&mut |sub| {
                if let Expression::PropertyReference(nr) = sub
                    && nr.name() == opposite_dim
                    && Rc::ptr_eq(&nr.element(), item)
                {
                    seen = true;
                }
            });
            seen
        })
        .unwrap_or(false);
    if reads_opposite {
        vis(&NamedReference::new(item, SmolStr::new_static(prop)).into(), ReadType::NativeRead);
    }
}

/// The builtin function can call native code, and we need to visit the properties that are accessed by it
fn visit_implicit_layout_info_dependencies(
    orientation: crate::layout::Orientation,
    item: &ElementRc,
    vis: &mut impl FnMut(&PropertyPath, ReadType),
) {
    let base_type = item.borrow().base_type.to_smolstr();
    const N: ReadType = ReadType::NativeRead;
    match base_type.as_str() {
        "Image" => {
            vis(&NamedReference::new(item, SmolStr::new_static("source")).into(), N);
            vis(&NamedReference::new(item, SmolStr::new_static("source-clip-width")).into(), N);
            if orientation == Orientation::Vertical {
                vis(&NamedReference::new(item, SmolStr::new_static("width")).into(), N);
                vis(
                    &NamedReference::new(item, SmolStr::new_static("source-clip-height")).into(),
                    N,
                );
            }
        }
        "Text" | "TextInput" => {
            vis(&NamedReference::new(item, SmolStr::new_static("text")).into(), N);
            vis(&NamedReference::new(item, SmolStr::new_static("font-family")).into(), N);
            vis(&NamedReference::new(item, SmolStr::new_static("font-size")).into(), N);
            vis(&NamedReference::new(item, SmolStr::new_static("font-weight")).into(), N);
            vis(&NamedReference::new(item, SmolStr::new_static("letter-spacing")).into(), N);
            vis(&NamedReference::new(item, SmolStr::new_static("wrap")).into(), N);
            let wrap_set = item.borrow().is_binding_set("wrap", false)
                || item
                    .borrow()
                    .property_analysis
                    .borrow()
                    .get("wrap")
                    .is_some_and(|a| a.is_set || a.is_set_externally);
            if wrap_set && orientation == Orientation::Vertical {
                vis(&NamedReference::new(item, SmolStr::new_static("width")).into(), N);
            }
            if base_type.as_str() == "TextInput" {
                vis(&NamedReference::new(item, SmolStr::new_static("single-line")).into(), N);
            } else {
                vis(&NamedReference::new(item, SmolStr::new_static("overflow")).into(), N);
            }
        }

        _ => (),
    }
}

fn visit_builtin_property(
    builtin: &crate::langtype::BuiltinElement,
    prop: &PropertyPath,
    context: &mut AnalysisContext,
    reverse_aliases: &ReverseAliases,
    diag: &mut BuildDiagnostics,
) {
    let name = prop.prop.name();
    if builtin.name == "Window" {
        for (p, orientation) in
            [("width", Orientation::Horizontal), ("height", Orientation::Vertical)]
        {
            if name == p {
                // find the actual root component
                let is_root = |e: &ElementRc| -> bool {
                    ElementRc::ptr_eq(
                        e,
                        &e.borrow().enclosing_component.upgrade().unwrap().root_element,
                    )
                };
                let mut root = prop.prop.element();
                if !is_root(&root) {
                    return;
                };
                for e in prop.elements.iter().rev() {
                    if !is_root(&e.0) {
                        return;
                    }
                    root = e.0.clone();
                }
                if let Some(p) = root.borrow().layout_info_prop(orientation) {
                    let path = PropertyPath::from(p.clone());
                    let old_layout = context.window_layout_property.replace(path.clone());
                    process_property(&path, ReadType::NativeRead, context, reverse_aliases, diag);
                    context.window_layout_property = old_layout;
                };
            }
        }
    }
}

/// Analyze the Window default-font-size property
fn check_window_properties(doc: &Document, global_analysis: &mut GlobalAnalysis) {
    doc.visit_all_used_components(|component| {
        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
            component,
            &(),
            &mut |elem, _| {
                if elem.borrow().builtin_type().as_ref().is_some_and(|b| b.name == "Window") {
                    const DEFAULT_FONT_SIZE: &str = "default-font-size";
                    if elem.borrow().is_binding_set(DEFAULT_FONT_SIZE, false)
                        || elem
                            .borrow()
                            .property_analysis
                            .borrow()
                            .get(DEFAULT_FONT_SIZE)
                            .is_some_and(|a| a.is_set)
                    {
                        // Do not ignore debug hooks here. They make the expression variable, so the
                        // const-check would incorrectly mark the font size as const, even if it is
                        // not.
                        let value = elem.borrow().binding(DEFAULT_FONT_SIZE).and_then(|e| match e
                            .expression
                        {
                            Expression::NumberLiteral(v, crate::expression_tree::Unit::Px) => {
                                Some(v as f32)
                            }
                            _ => None,
                        });
                        let is_const = value.is_some()
                            || NamedReference::new(elem, SmolStr::new_static(DEFAULT_FONT_SIZE))
                                .is_constant();
                        global_analysis.default_font_size = match global_analysis.default_font_size
                        {
                            DefaultFontSize::Unknown => match value {
                                Some(v) => DefaultFontSize::LogicalValue(v),
                                None if is_const => DefaultFontSize::Const,
                                None => DefaultFontSize::Variable,
                            },
                            DefaultFontSize::NotSet if is_const => DefaultFontSize::NotSet,
                            DefaultFontSize::LogicalValue(val) => match value {
                                Some(v) if v == val => DefaultFontSize::LogicalValue(val),
                                _ if is_const => DefaultFontSize::Const,
                                _ => DefaultFontSize::Variable,
                            },
                            DefaultFontSize::Const if is_const => DefaultFontSize::Const,
                            _ => DefaultFontSize::Variable,
                        }
                    } else {
                        global_analysis.default_font_size = match global_analysis.default_font_size
                        {
                            DefaultFontSize::Unknown => DefaultFontSize::NotSet,
                            DefaultFontSize::NotSet => DefaultFontSize::NotSet,
                            DefaultFontSize::LogicalValue(_) => DefaultFontSize::NotSet,
                            DefaultFontSize::Const => DefaultFontSize::NotSet,
                            DefaultFontSize::Variable => DefaultFontSize::Variable,
                        }
                    }
                }
            },
        );
    });
}

/// Make sure that the is_set property analysis is set to any property which has a two way binding
/// to a property that is, itself, is set
///
/// Example:
/// ```slint
/// Xx := TouchArea {
///    property <int> bar <=> foo;
///    clicked => { bar+=1; }
///    property <int> foo; // must ensure that this is not considered as const, because the alias with bar
/// }
/// ```
fn propagate_is_set_on_aliases(doc: &Document, reverse_aliases: &mut ReverseAliases) {
    doc.visit_all_used_components(|component| {
        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
            component,
            &(),
            &mut |e, _| visit_element(e, reverse_aliases),
        );
    });

    fn visit_element(e: &ElementRc, reverse_aliases: &mut ReverseAliases) {
        for (name, binding) in e.borrow().real_bindings() {
            if !binding.borrow().two_way_bindings.is_empty() {
                check_alias(e, name, &binding.borrow());

                let nr = NamedReference::new(e, name.clone());
                for a in &binding.borrow().two_way_bindings {
                    if let Some(a) = a.property()
                        && a != &nr
                        && !a.element().borrow().enclosing_component.upgrade().unwrap().is_global()
                    {
                        reverse_aliases.entry(a.clone()).or_default().push(nr.clone())
                    }
                }
            }
        }
        for decl in e.borrow().property_declarations.values() {
            if let Some(alias) = &decl.is_alias {
                mark_alias(alias)
            }
        }
    }

    fn check_alias(e: &ElementRc, name: &SmolStr, binding: &BindingExpression) {
        // Note: since the analysis hasn't been run, any property access will result in a non constant binding. this is slightly non-optimal
        let is_binding_constant =
            binding.is_constant(None) && binding.two_way_bindings.iter().all(|n| n.is_constant());
        if is_binding_constant && !NamedReference::new(e, name.clone()).is_externally_modified() {
            for alias in binding.two_way_bindings.iter().filter_map(|x| x.property()) {
                crate::namedreference::mark_property_set_derived_in_base(
                    alias.element(),
                    alias.name(),
                );
            }
            return;
        }

        propagate_alias(binding);
    }

    fn propagate_alias(binding: &BindingExpression) {
        for alias in binding.two_way_bindings.iter().filter_map(|x| x.property()) {
            mark_alias(alias);
        }
    }

    fn mark_alias(alias: &NamedReference) {
        alias.mark_as_set();
        if !alias.is_externally_modified()
            && let Some(bind) = alias.element().borrow().binding(alias.name())
        {
            propagate_alias(&bind)
        }
    }
}

/// Make sure that the is_set_externally is true for all bindings.
/// And change bindings are used externally
fn mark_used_base_properties(doc: &Document) {
    doc.visit_all_used_components(|component| {
        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
            component,
            &(),
            &mut |element, _| {
                if !matches!(element.borrow().base_type, ElementType::Component(_)) {
                    return;
                }
                for (name, binding) in element.borrow().real_bindings() {
                    if binding.borrow().has_binding() {
                        crate::namedreference::mark_property_set_derived_in_base(
                            element.clone(),
                            name,
                        );
                    }
                }
                for name in element.borrow().change_callbacks.keys() {
                    crate::namedreference::mark_property_read_derived_in_base(
                        element.clone(),
                        name,
                    );
                }
            },
        );
    });
}
