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

//! Datastructures used to represent layouts in the compiler

use crate::diagnostics::{BuildDiagnostics, DiagnosticLevel, Spanned};
use crate::expression_tree::*;
use crate::langtype::{ElementType, PropertyLookupResult, Type};
use crate::object_tree::{Component, ElementRc};

use smol_str::{SmolStr, ToSmolStr, format_smolstr};

use std::cell::RefCell;
use std::rc::{Rc, Weak};

/// Number of slots a cell occupies in a box layout cache: position and size.
pub const BOX_LAYOUT_CACHE_ENTRIES_PER_CELL: usize = 2;

#[derive(Clone, Debug, Copy, Eq, PartialEq)]
pub enum Orientation {
    Horizontal,
    Vertical,
}

impl Orientation {
    pub fn orthogonal(self) -> Self {
        match self {
            Orientation::Horizontal => Orientation::Vertical,
            Orientation::Vertical => Orientation::Horizontal,
        }
    }
}

#[derive(Clone, Debug, Copy, Eq, PartialEq, Default)]
pub enum FlexboxLayoutDirection {
    /// Items are laid out in rows (horizontal primary axis)
    #[default]
    Row,
    /// Items are laid out in rows in reverse order (horizontal primary axis, right to left)
    RowReverse,
    /// Items are laid out in columns (vertical primary axis)
    Column,
    /// Items are laid out in columns in reverse order (vertical primary axis, bottom to top)
    ColumnReverse,
}

/// Relationship between a queried orientation and a FlexboxLayout's direction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlexboxAxisRelation {
    /// The queried orientation is the main axis (e.g., Horizontal for a Row flex)
    MainAxis,
    /// The queried orientation is the cross axis (e.g., Vertical for a Row flex)
    CrossAxis,
    /// The flex direction is not known at compile time
    Unknown,
}

#[derive(Clone, Debug, derive_more::From)]
pub enum Layout {
    GridLayout(GridLayout),
    BoxLayout(BoxLayout),
    FlexboxLayout(FlexboxLayout),
}

impl Layout {
    /// Call the visitor for each NamedReference stored in the layout
    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
        match self {
            Layout::GridLayout(grid) => grid.visit_named_references(visitor),
            Layout::BoxLayout(l) => l.visit_named_references(visitor),
            Layout::FlexboxLayout(l) => l.visit_named_references(visitor),
        }
    }
}

/// An Item in the layout tree
#[derive(Debug, Default, Clone)]
pub struct LayoutItem {
    pub element: ElementRc,
    pub constraints: LayoutConstraints,
}

/// A FlexboxLayout child item, wrapping a LayoutItem with flex-specific properties.
#[derive(Debug, Clone)]
pub struct FlexboxLayoutItem {
    pub item: LayoutItem,
    pub flex_grow: Option<NamedReference>,
    pub flex_shrink: Option<NamedReference>,
    pub flex_basis: Option<NamedReference>,
    pub align_self: Option<NamedReference>,
    pub order: Option<NamedReference>,
}

/// A child within a repeated Row in a GridLayout.
/// Can be either a static item or a nested repeater (`for y in model: ...`).
#[derive(Debug, Clone)]
pub enum RowChildTemplate {
    Static(LayoutItem),
    Repeated {
        item: LayoutItem,
        /// The repeated element (the `for y in ...` element inside the Row)
        repeated_element: ElementRc,
    },
}

impl RowChildTemplate {
    pub fn layout_item(&self) -> &LayoutItem {
        match self {
            RowChildTemplate::Static(item) => item,
            RowChildTemplate::Repeated { item, .. } => item,
        }
    }

    pub fn layout_item_mut(&mut self) -> &mut LayoutItem {
        match self {
            RowChildTemplate::Static(item) => item,
            RowChildTemplate::Repeated { item, .. } => item,
        }
    }

    pub fn repeated_element(&self) -> Option<&ElementRc> {
        match self {
            RowChildTemplate::Static(_) => None,
            RowChildTemplate::Repeated { repeated_element, .. } => Some(repeated_element),
        }
    }

    pub fn is_repeated(&self) -> bool {
        self.repeated_element().is_some()
    }
}

impl LayoutItem {
    pub fn rect(&self) -> LayoutRect {
        let p = |unresolved_name: &str| {
            let PropertyLookupResult { resolved_name, property_type, .. } =
                self.element.borrow().lookup_property(unresolved_name);
            if property_type == Type::LogicalLength {
                Some(NamedReference::new(&self.element, resolved_name.to_smolstr()))
            } else {
                None
            }
        };
        LayoutRect {
            x_reference: p("x"),
            y_reference: p("y"),
            width_reference: if !self.constraints.fixed_width { p("width") } else { None },
            height_reference: if !self.constraints.fixed_height { p("height") } else { None },
        }
    }
}

#[derive(Debug, Clone, Default)]
pub struct LayoutRect {
    pub width_reference: Option<NamedReference>,
    pub height_reference: Option<NamedReference>,
    pub x_reference: Option<NamedReference>,
    pub y_reference: Option<NamedReference>,
}

impl LayoutRect {
    pub fn install_on_element(element: &ElementRc) -> Self {
        let install_prop =
            |name: &'static str| Some(NamedReference::new(element, SmolStr::new_static(name)));

        Self {
            x_reference: install_prop("x"),
            y_reference: install_prop("y"),
            width_reference: install_prop("width"),
            height_reference: install_prop("height"),
        }
    }

    fn visit_named_references(&mut self, mut visitor: &mut impl FnMut(&mut NamedReference)) {
        self.width_reference.as_mut().map(&mut visitor);
        self.height_reference.as_mut().map(&mut visitor);
        self.x_reference.as_mut().map(&mut visitor);
        self.y_reference.as_mut().map(&mut visitor);
    }

    pub fn size_reference(&self, orientation: Orientation) -> Option<&NamedReference> {
        match orientation {
            Orientation::Horizontal => self.width_reference.as_ref(),
            Orientation::Vertical => self.height_reference.as_ref(),
        }
    }
}

#[derive(Debug, Default, Clone)]
pub struct LayoutConstraints {
    pub min_width: Option<NamedReference>,
    pub max_width: Option<NamedReference>,
    pub min_height: Option<NamedReference>,
    pub max_height: Option<NamedReference>,
    pub preferred_width: Option<NamedReference>,
    pub preferred_height: Option<NamedReference>,
    pub horizontal_stretch: Option<NamedReference>,
    pub vertical_stretch: Option<NamedReference>,
    pub fixed_width: bool,
    pub fixed_height: bool,
    /// For each constraint, whether it is set directly on the element (an
    /// override) rather than inherited from a base component. Inherited layout
    /// constraints are already baked into an element's own `layoutinfo-*`, so a
    /// parent layout that measured the cell through its layout-info must not
    /// re-apply them (double-count / height-for-width loop); locally-set ones
    /// must be applied. See [`Self::to_apply`].
    pub local: LayoutConstraintLocality,
}

/// Which [`LayoutConstraints`] are set directly on the element (depth 0) rather
/// than inherited from a base component.
#[derive(Debug, Default, Clone)]
pub struct LayoutConstraintLocality {
    pub min_width: bool,
    pub max_width: bool,
    pub min_height: bool,
    pub max_height: bool,
    pub preferred_width: bool,
    pub preferred_height: bool,
    pub horizontal_stretch: bool,
    pub vertical_stretch: bool,
}

/// The [`LayoutConstraints`] fields along one orientation.
pub struct OrientationConstraints<'a> {
    pub min: &'a Option<NamedReference>,
    pub max: &'a Option<NamedReference>,
    pub preferred: &'a Option<NamedReference>,
    pub stretch: &'a Option<NamedReference>,
    /// The size is set by an explicit `width`/`height` binding.
    pub fixed: bool,
}

impl LayoutConstraints {
    /// Build the constraints for the given element.
    ///
    /// When `diag` is `Some`, a redundant size constraint (e.g. both `width` and `min-width`) is
    /// reported at the given level; pass `None` to compute the constraints without reporting (e.g.
    /// when another pass owns that diagnostic).
    pub fn new(
        element: &ElementRc,
        mut diag: Option<(&mut BuildDiagnostics, DiagnosticLevel)>,
    ) -> Self {
        let mut constraints = Self {
            min_width: binding_reference(element, "min-width"),
            max_width: binding_reference(element, "max-width"),
            min_height: binding_reference(element, "min-height"),
            max_height: binding_reference(element, "max-height"),
            preferred_width: binding_reference(element, "preferred-width"),
            preferred_height: binding_reference(element, "preferred-height"),
            horizontal_stretch: binding_reference(element, "horizontal-stretch"),
            vertical_stretch: binding_reference(element, "vertical-stretch"),
            fixed_width: false,
            fixed_height: false,
            local: LayoutConstraintLocality {
                // min/max-{width,height} may be derived from a local fixed
                // `width`/`height` binding (see below), which is just as local
                // an override as an explicit min/max constraint.
                min_width: is_local_binding(element, "min-width")
                    || is_local_binding(element, "width"),
                max_width: is_local_binding(element, "max-width")
                    || is_local_binding(element, "width"),
                min_height: is_local_binding(element, "min-height")
                    || is_local_binding(element, "height"),
                max_height: is_local_binding(element, "max-height")
                    || is_local_binding(element, "height"),
                preferred_width: is_local_binding(element, "preferred-width"),
                preferred_height: is_local_binding(element, "preferred-height"),
                horizontal_stretch: is_local_binding(element, "horizontal-stretch"),
                vertical_stretch: is_local_binding(element, "vertical-stretch"),
            },
        };
        let mut apply_size_constraint =
            |prop: &'static str,
             binding: &BindingExpression,
             enclosing1: &Weak<Component>,
             depth,
             op: &mut Option<NamedReference>| {
                if let Some(other_prop) = op {
                    find_binding(
                        &other_prop.element(),
                        other_prop.name(),
                        |old, enclosing2, d2| {
                            if let Some((diag, level)) = &mut diag
                                && Weak::ptr_eq(enclosing1, enclosing2)
                                && old.priority.saturating_add(d2)
                                    <= binding.priority.saturating_add(depth)
                            {
                                diag.push_diagnostic_with_span(
                                    format!(
                                        "Cannot specify both '{prop}' and '{}'",
                                        other_prop.name()
                                    ),
                                    binding.to_source_location(),
                                    *level,
                                );
                            }
                        },
                    );
                }
                *op = Some(NamedReference::new(element, SmolStr::new_static(prop)))
            };
        find_binding(element, "height", |s, enclosing, depth| {
            constraints.fixed_height = true;
            apply_size_constraint("height", s, enclosing, depth, &mut constraints.min_height);
            apply_size_constraint("height", s, enclosing, depth, &mut constraints.max_height);
        });
        find_binding(element, "width", |s, enclosing, depth| {
            constraints.fixed_width = true;
            if s.expression.ty() == Type::Percent {
                apply_size_constraint("width", s, enclosing, depth, &mut constraints.min_width);
            } else {
                apply_size_constraint("width", s, enclosing, depth, &mut constraints.min_width);
                apply_size_constraint("width", s, enclosing, depth, &mut constraints.max_width);
            }
        });

        constraints
    }

    pub fn has_explicit_restrictions(&self, orientation: Orientation) -> bool {
        match orientation {
            Orientation::Horizontal => {
                self.min_width.is_some()
                    || self.max_width.is_some()
                    || self.preferred_width.is_some()
                    || self.horizontal_stretch.is_some()
            }
            Orientation::Vertical => {
                self.min_height.is_some()
                    || self.max_height.is_some()
                    || self.preferred_height.is_some()
                    || self.vertical_stretch.is_some()
            }
        }
    }

    /// The constraints a parent layout should apply on top of a cell's measured
    /// layout-info for `orientation`. Native items (whose layout-info doesn't
    /// merge their constraints) keep everything. For elements whose `layoutinfo-*`
    /// already includes their intrinsic constraints, only locally-set overrides
    /// are kept — inherited constraints are already in the measured value, and
    /// re-reading them unconstrained can reintroduce a height-for-width loop.
    pub fn to_apply(&self, element: &ElementRc, orientation: Orientation) -> Self {
        if !element.borrow().layout_info_includes_own_constraints(orientation) {
            return self.clone();
        }
        let mut c = self.clone();
        match orientation {
            Orientation::Horizontal => {
                if !self.local.min_width {
                    c.min_width = None;
                }
                if !self.local.max_width {
                    c.max_width = None;
                }
                if !self.local.preferred_width {
                    c.preferred_width = None;
                }
                if !self.local.horizontal_stretch {
                    c.horizontal_stretch = None;
                }
            }
            Orientation::Vertical => {
                if !self.local.min_height {
                    c.min_height = None;
                }
                if !self.local.max_height {
                    c.max_height = None;
                }
                if !self.local.preferred_height {
                    c.preferred_height = None;
                }
                if !self.local.vertical_stretch {
                    c.vertical_stretch = None;
                }
            }
        }
        c
    }

    pub fn for_orientation(&self, orientation: Orientation) -> OrientationConstraints<'_> {
        match orientation {
            Orientation::Horizontal => OrientationConstraints {
                min: &self.min_width,
                max: &self.max_width,
                preferred: &self.preferred_width,
                stretch: &self.horizontal_stretch,
                fixed: self.fixed_width,
            },
            Orientation::Vertical => OrientationConstraints {
                min: &self.min_height,
                max: &self.max_height,
                preferred: &self.preferred_height,
                stretch: &self.vertical_stretch,
                fixed: self.fixed_height,
            },
        }
    }

    // Iterate over the constraint with a reference to a property, and the corresponding member in the i_slint_core::layout::LayoutInfo struct
    pub fn for_each_restrictions(
        &self,
        orientation: Orientation,
    ) -> impl Iterator<Item = (&NamedReference, &'static str)> {
        let c = self.for_orientation(orientation);
        std::iter::empty()
            .chain(c.min.as_ref().map(|x| {
                if Expression::PropertyReference(x.clone()).ty() != Type::Percent {
                    (x, "min")
                } else {
                    (x, "min_percent")
                }
            }))
            .chain(c.max.as_ref().map(|x| {
                if Expression::PropertyReference(x.clone()).ty() != Type::Percent {
                    (x, "max")
                } else {
                    (x, "max_percent")
                }
            }))
            .chain(c.preferred.as_ref().map(|x| (x, "preferred")))
            .chain(c.stretch.as_ref().map(|x| (x, "stretch")))
    }

    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
        if let Some(e) = self.max_width.as_mut() {
            visitor(&mut *e);
        }
        if let Some(e) = self.min_width.as_mut() {
            visitor(&mut *e);
        }
        if let Some(e) = self.max_height.as_mut() {
            visitor(&mut *e);
        }
        if let Some(e) = self.min_height.as_mut() {
            visitor(&mut *e);
        }
        if let Some(e) = self.preferred_width.as_mut() {
            visitor(&mut *e);
        }
        if let Some(e) = self.preferred_height.as_mut() {
            visitor(&mut *e);
        }
        if let Some(e) = self.horizontal_stretch.as_mut() {
            visitor(&mut *e);
        }
        if let Some(e) = self.vertical_stretch.as_mut() {
            visitor(&mut *e);
        }
    }
}

#[derive(Debug, Clone)]
pub enum RowColExpr {
    Named(NamedReference),
    Literal(u16),
    Auto,
}

#[derive(Debug, Clone)]
pub struct GridLayoutCell {
    pub new_row: bool,
    pub col_expr: RowColExpr,
    pub row_expr: RowColExpr,
    pub colspan_expr: RowColExpr,
    pub rowspan_expr: RowColExpr,
    pub child_items: Option<Vec<RowChildTemplate>>, // for repeated rows
}

impl GridLayoutCell {
    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
        if let RowColExpr::Named(ref mut e) = self.col_expr {
            visitor(e);
        }
        if let RowColExpr::Named(ref mut e) = self.row_expr {
            visitor(e);
        }
        if let RowColExpr::Named(ref mut e) = self.colspan_expr {
            visitor(e);
        }
        if let RowColExpr::Named(ref mut e) = self.rowspan_expr {
            visitor(e);
        }
        if let Some(children) = &mut self.child_items {
            for child in children {
                child.layout_item_mut().constraints.visit_named_references(visitor);
            }
        }
    }
}

/// An element in a GridLayout
#[derive(Debug, Clone)]
pub struct GridLayoutElement {
    /// `Rc<RefCell<GridLayoutCell>>` because shared with the repeated component's element
    pub cell: Rc<RefCell<GridLayoutCell>>,
    pub item: LayoutItem,
}

impl GridLayoutElement {
    pub fn span(&self, orientation: Orientation) -> RowColExpr {
        let cell = self.cell.borrow();
        match orientation {
            Orientation::Horizontal => cell.colspan_expr.clone(),
            Orientation::Vertical => cell.rowspan_expr.clone(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct Padding {
    pub left: Option<NamedReference>,
    pub right: Option<NamedReference>,
    pub top: Option<NamedReference>,
    pub bottom: Option<NamedReference>,
}

impl Padding {
    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
        if let Some(e) = self.left.as_mut() {
            visitor(&mut *e)
        }
        if let Some(e) = self.right.as_mut() {
            visitor(&mut *e)
        }
        if let Some(e) = self.top.as_mut() {
            visitor(&mut *e)
        }
        if let Some(e) = self.bottom.as_mut() {
            visitor(&mut *e)
        }
    }

    // Return reference to the begin and end padding for a given orientation
    pub fn begin_end(&self, o: Orientation) -> (Option<&NamedReference>, Option<&NamedReference>) {
        match o {
            Orientation::Horizontal => (self.left.as_ref(), self.right.as_ref()),
            Orientation::Vertical => (self.top.as_ref(), self.bottom.as_ref()),
        }
    }
}

#[derive(Debug, Clone)]
pub struct Spacing {
    pub horizontal: Option<NamedReference>,
    pub vertical: Option<NamedReference>,
}

impl Spacing {
    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
        if let Some(e) = self.horizontal.as_mut() {
            visitor(&mut *e);
        }
        if let Some(e) = self.vertical.as_mut() {
            visitor(&mut *e);
        }
    }

    pub fn orientation(&self, o: Orientation) -> Option<&NamedReference> {
        match o {
            Orientation::Horizontal => self.horizontal.as_ref(),
            Orientation::Vertical => self.vertical.as_ref(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct LayoutGeometry {
    pub rect: LayoutRect,
    pub spacing: Spacing,
    pub alignment: Option<NamedReference>,
    pub padding: Padding,
}

impl LayoutGeometry {
    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
        self.rect.visit_named_references(visitor);
        if let Some(e) = self.alignment.as_mut() {
            visitor(&mut *e)
        }
        self.spacing.visit_named_references(visitor);
        self.padding.visit_named_references(visitor);
    }

    pub fn new(layout_element: &ElementRc) -> Self {
        let spacing = || binding_reference(layout_element, "spacing");
        init_fake_property(layout_element, "spacing-horizontal", spacing);
        init_fake_property(layout_element, "spacing-vertical", spacing);

        let alignment = binding_reference(layout_element, "alignment");

        let padding = || binding_reference(layout_element, "padding");
        init_fake_property(layout_element, "padding-left", padding);
        init_fake_property(layout_element, "padding-right", padding);
        init_fake_property(layout_element, "padding-top", padding);
        init_fake_property(layout_element, "padding-bottom", padding);

        let padding = Padding {
            left: binding_reference(layout_element, "padding-left").or_else(padding),
            right: binding_reference(layout_element, "padding-right").or_else(padding),
            top: binding_reference(layout_element, "padding-top").or_else(padding),
            bottom: binding_reference(layout_element, "padding-bottom").or_else(padding),
        };

        let spacing = Spacing {
            horizontal: binding_reference(layout_element, "spacing-horizontal").or_else(spacing),
            vertical: binding_reference(layout_element, "spacing-vertical").or_else(spacing),
        };

        let rect = LayoutRect::install_on_element(layout_element);

        Self { rect, spacing, padding, alignment }
    }
}

/// If this element or any of the parent has a binding to the property, call the functor with that binding, and the depth.
/// Return None if the binding does not exist in any of the sub component, or Some with the result of the functor otherwise
fn find_binding<R>(
    element: &ElementRc,
    name: &str,
    f: impl FnOnce(&BindingExpression, &Weak<Component>, i32) -> R,
) -> Option<R> {
    let mut element = element.clone();
    let mut depth = 0;
    loop {
        if let Some(b) = element.borrow().binding(name)
            && b.has_binding()
        {
            return Some(f(&b, &element.borrow().enclosing_component, depth));
        }
        let e = match &element.borrow().base_type {
            ElementType::Component(base) => base.root_element.clone(),
            _ => return None,
        };
        element = e;
        depth += 1;
    }
}

/// Return a named reference to a property if a binding is set on that property
pub fn binding_reference(element: &ElementRc, name: &'static str) -> Option<NamedReference> {
    find_binding(element, name, |_, _, _| NamedReference::new(element, SmolStr::new_static(name)))
}

/// Whether `name`'s binding is set directly on `element` (depth 0) rather than
/// inherited from a base component. Must be evaluated while the binding is still
/// present (i.e. when building [`LayoutConstraints`]); later passes may move it.
fn is_local_binding(element: &ElementRc, name: &str) -> bool {
    find_binding(element, name, |_, _, depth| depth == 0) == Some(true)
}

fn init_fake_property(
    grid_layout_element: &ElementRc,
    name: &str,
    lazy_default: impl Fn() -> Option<NamedReference>,
) {
    if grid_layout_element.borrow().property_declarations.contains_key(name)
        && grid_layout_element.borrow().binding(name).is_none()
        && let Some(e) = lazy_default()
    {
        if e.name() == name && Rc::ptr_eq(&e.element(), grid_layout_element) {
            // Don't reference self
            return;
        }
        grid_layout_element
            .borrow_mut()
            .set_binding(name.into(), Expression::PropertyReference(e).into());
    }
}

/// Internal representation of a grid layout
#[derive(Debug, Clone)]
pub struct GridLayout {
    /// All the elements which will be laid out within that element.
    pub elems: Vec<GridLayoutElement>,

    pub geometry: LayoutGeometry,

    /// When this GridLayout is actually the layout of a Dialog, then the cells start with all the buttons,
    /// and this variable contains their roles. The string is actually one of the values from the i_slint_core::layout::DialogButtonRole
    pub dialog_button_roles: Option<Vec<SmolStr>>,

    /// Whether any of the row/column expressions use 'auto'
    pub uses_auto: bool,
}

impl GridLayout {
    /// Clone each element's cell into a new Rc, breaking any Rc sharing with the original.
    pub fn clone_cells(&mut self) {
        for e in &mut self.elems {
            let cloned = Rc::new(RefCell::new(e.cell.borrow().clone()));
            e.cell = cloned;
        }
    }

    pub fn visit_rowcol_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
        for elem in &mut self.elems {
            let mut cell = elem.cell.borrow_mut();
            if let RowColExpr::Named(ref mut e) = cell.col_expr {
                visitor(e);
            }
            if let RowColExpr::Named(ref mut e) = cell.row_expr {
                visitor(e);
            }
            if let RowColExpr::Named(ref mut e) = cell.colspan_expr {
                visitor(e);
            }
            if let RowColExpr::Named(ref mut e) = cell.rowspan_expr {
                visitor(e);
            }
        }
    }

    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
        self.visit_rowcol_named_references(visitor);
        for layout_elem in &mut self.elems {
            layout_elem.item.constraints.visit_named_references(visitor);
            if let Some(child_items) = &mut layout_elem.cell.borrow_mut().child_items {
                for child in child_items {
                    child.layout_item_mut().constraints.visit_named_references(visitor);
                }
            }
        }
        self.geometry.visit_named_references(visitor);
    }
}

/// Internal representation of a BoxLayout
#[derive(Debug, Clone)]
pub struct BoxLayout {
    /// Whether this is a HorizontalLayout or a VerticalLayout
    pub orientation: Orientation,
    pub elems: Vec<LayoutItem>,
    pub geometry: LayoutGeometry,
    /// The `cross-axis-alignment` property, if set.
    pub cross_alignment: Option<NamedReference>,
}

impl BoxLayout {
    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
        for cell in &mut self.elems {
            cell.constraints.visit_named_references(visitor);
        }
        self.geometry.visit_named_references(visitor);
        if let Some(e) = self.cross_alignment.as_mut() {
            visitor(&mut *e);
        }
    }
}

/// Internal representation of a FlexboxLayout (row or column direction with wrapping)
#[derive(Debug, Clone)]
pub struct FlexboxLayout {
    pub elems: Vec<FlexboxLayoutItem>,
    pub geometry: LayoutGeometry,
    pub direction: Option<NamedReference>,
    pub cross_axis_line_alignment: Option<NamedReference>,
    pub cross_axis_alignment: Option<NamedReference>,
    pub flex_wrap: Option<NamedReference>,
}

impl FlexboxLayout {
    /// If `elem` is a (lowered, inline) FlexboxLayout, return its layout
    /// description. The struct is embedded in the synthesized
    /// `layoutinfo-{h,v}` / `layout-cache` bindings on the element.
    pub fn from_element(elem: &ElementRc) -> Option<FlexboxLayout> {
        use crate::expression_tree::Expression;
        // The `layoutinfo-{h,v}` property's binding (on this element or its
        // base component's root) holds a `ComputeFlexboxLayoutInfo` with the
        // layout when the element is a FlexboxLayout.
        let nr = {
            let eb = elem.borrow();
            eb.layout_info_prop(Orientation::Vertical)
                .or_else(|| eb.layout_info_prop(Orientation::Horizontal))
                .cloned()
        }?;
        let target = nr.element();
        let target = target.borrow();
        let binding = target.binding(nr.name())?;
        match binding.value_expression() {
            Expression::ComputeFlexboxLayoutInfo { layout, .. } => Some(layout.clone()),
            _ => None,
        }
    }

    /// Try to determine the flex direction at compile time from a constant binding.
    /// Returns None if the direction is set at runtime.
    fn compile_time_direction(&self) -> Option<FlexboxLayoutDirection> {
        match self.direction.as_ref() {
            None => Some(FlexboxLayoutDirection::Row),
            Some(nr) => nr.element().borrow().binding(nr.name()).and_then(|binding| {
                if let crate::expression_tree::Expression::EnumerationValue(ev) =
                    binding.value_expression()
                {
                    match ev.enumeration.values[ev.value].as_str() {
                        "row" => Some(FlexboxLayoutDirection::Row),
                        "row-reverse" => Some(FlexboxLayoutDirection::RowReverse),
                        "column" => Some(FlexboxLayoutDirection::Column),
                        "column-reverse" => Some(FlexboxLayoutDirection::ColumnReverse),
                        _ => None,
                    }
                } else {
                    None
                }
            }),
        }
    }

    /// Determine the relationship between a queried orientation and this flex's direction.
    pub fn axis_relation(&self, orientation: Orientation) -> FlexboxAxisRelation {
        match self.compile_time_direction() {
            None => FlexboxAxisRelation::Unknown,
            Some(dir) => {
                let is_main = matches!(
                    (dir, orientation),
                    (
                        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse,
                        Orientation::Horizontal
                    ) | (
                        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse,
                        Orientation::Vertical
                    )
                );
                if is_main { FlexboxAxisRelation::MainAxis } else { FlexboxAxisRelation::CrossAxis }
            }
        }
    }

    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
        for cell in &mut self.elems {
            cell.item.constraints.visit_named_references(visitor);
            if let Some(e) = cell.flex_grow.as_mut() {
                visitor(&mut *e)
            }
            if let Some(e) = cell.flex_shrink.as_mut() {
                visitor(&mut *e)
            }
            if let Some(e) = cell.flex_basis.as_mut() {
                visitor(&mut *e)
            }
            if let Some(e) = cell.align_self.as_mut() {
                visitor(&mut *e)
            }
            if let Some(e) = cell.order.as_mut() {
                visitor(&mut *e)
            }
        }
        self.geometry.visit_named_references(visitor);
        if let Some(e) = self.direction.as_mut() {
            visitor(&mut *e)
        }
        if let Some(e) = self.cross_axis_line_alignment.as_mut() {
            visitor(&mut *e)
        }
        if let Some(e) = self.cross_axis_alignment.as_mut() {
            visitor(&mut *e)
        }
        if let Some(e) = self.flex_wrap.as_mut() {
            visitor(&mut *e)
        }
    }
}

/// Controls whether `implicit_layout_info_call` returns layout info for builtins
/// that don't have an intrinsic size (Rectangle, Empty, TouchArea, etc.).
#[derive(Clone, Copy, PartialEq)]
pub enum BuiltinFilter {
    /// Return layout info for all builtins (existing behavior).
    All,
    /// Skip builtins whose `default_size_binding` is not `ImplicitSize`.
    SkipNonImplicit,
}

/// Get the implicit layout info of a particular element.
/// When `constraint` is `Some`, it's passed as the `cross_axis_constraint`
/// parameter to `Item::layout_info` for height-for-width support.
pub fn implicit_layout_info_call(
    elem: &ElementRc,
    orientation: Orientation,
    filter: BuiltinFilter,
    constraint: Option<Expression>,
) -> Option<Expression> {
    let mut elem_it = elem.clone();
    loop {
        return match &elem_it.clone().borrow().base_type {
            ElementType::Component(base_comp) => {
                // Flexbox supplies a cross-axis constraint to break its
                // h/v cache cycle; call the base component's parametrized
                // layout-info function when present.
                let parametrized_nr = constraint.as_ref().and_then(|_| match orientation {
                    Orientation::Vertical => {
                        base_comp.root_element.borrow().layout_info_v_with_constraint.clone()
                    }
                    Orientation::Horizontal => {
                        base_comp.root_element.borrow().layout_info_h_with_constraint.clone()
                    }
                });
                if let Some(nr) = parametrized_nr
                    && let Some(c) = &constraint
                {
                    debug_assert!(Rc::ptr_eq(&nr.element(), &base_comp.root_element));
                    return Some(Expression::FunctionCall {
                        function: crate::expression_tree::Callable::Function(NamedReference::new(
                            elem,
                            nr.name().clone(),
                        )),
                        arguments: vec![c.clone()],
                        source_location: None,
                    });
                }
                match base_comp.root_element.borrow().layout_info_prop(orientation) {
                    Some(nr) => {
                        // We cannot take nr as is because it is relative to the elem's component. We therefore need to
                        // use `elem` as an element for the PropertyReference, not `root` within the base of elem
                        debug_assert!(Rc::ptr_eq(&nr.element(), &base_comp.root_element));
                        Some(Expression::PropertyReference(NamedReference::new(
                            elem,
                            nr.name().clone(),
                        )))
                    }
                    None => {
                        elem_it = base_comp.root_element.clone();
                        continue;
                    }
                }
            }
            ElementType::Builtin(base_type)
                if matches!(
                    base_type.name.as_str(),
                    "Rectangle"
                        | "Empty"
                        | "TouchArea"
                        | "FocusScope"
                        | "Opacity"
                        | "Layer"
                        | "BoxShadow"
                        | "Clip"
                ) =>
            {
                if filter == BuiltinFilter::SkipNonImplicit {
                    return None;
                }
                // hard-code the value for rectangle because many rectangle end up optimized away and we
                // don't want to depend on the element.
                Some(Expression::Struct {
                    ty: crate::typeregister::layout_info_type(),
                    values: [("min", 0.), ("max", f32::MAX), ("preferred", 0.)]
                        .iter()
                        .map(|(s, v)| {
                            (SmolStr::new_static(s), Expression::NumberLiteral(*v as _, Unit::Px))
                        })
                        .chain(
                            [("min_percent", 0.), ("max_percent", 100.), ("stretch", 1.)]
                                .iter()
                                .map(|(s, v)| {
                                    (
                                        SmolStr::new_static(s),
                                        Expression::NumberLiteral(*v, Unit::None),
                                    )
                                }),
                        )
                        .collect(),
                })
            }
            ElementType::Builtin(base_type)
                if filter == BuiltinFilter::SkipNonImplicit
                    && base_type.default_size_binding
                        != crate::langtype::DefaultSizeBinding::ImplicitSize =>
            {
                None
            }
            _ => Some(Expression::FunctionCall {
                function: BuiltinFunction::ImplicitLayoutInfo(orientation).into(),
                arguments: vec![
                    Expression::ElementReference(Rc::downgrade(elem)),
                    constraint.unwrap_or(Expression::NumberLiteral(-1., Unit::None)),
                ],
                source_location: None,
            }),
        };
    }
}

/// The stretch factor of elements based on text or image items, which never
/// stretch: their `layout_info` always reports stretch 0, and a layoutinfo
/// property synthesized later can only merge it with smaller values.
pub fn static_native_stretch(elem: &ElementRc) -> Option<Expression> {
    elem.borrow()
        .builtin_type()
        .filter(|b| matches!(b.name.as_str(), "Text" | "StyledText" | "TextInput" | "Image"))
        .map(|_| Expression::NumberLiteral(0., Unit::None))
}

/// Create a new property based on the name. (it might get a different name if that property exist)
pub fn create_new_prop(elem: &ElementRc, tentative_name: SmolStr, ty: Type) -> NamedReference {
    let mut e = elem.borrow_mut();
    if !e.lookup_property(&tentative_name).is_valid() {
        e.property_declarations.insert(tentative_name.clone(), ty.into());
        drop(e);
        NamedReference::new(elem, tentative_name)
    } else {
        let mut counter = 0;
        loop {
            counter += 1;
            let name = format_smolstr!("{}{}", tentative_name, counter);
            if !e.lookup_property(&name).is_valid() {
                e.property_declarations.insert(name.clone(), ty.into());
                drop(e);
                return NamedReference::new(elem, name);
            }
        }
    }
}

/// Return true if this type is a layout that has constraints
pub fn is_layout(base_type: &ElementType) -> bool {
    match base_type {
        ElementType::Component(c) => is_layout(&c.root_element.borrow().base_type),
        ElementType::Builtin(be) => {
            matches!(
                be.name.as_str(),
                "GridLayout" | "HorizontalLayout" | "VerticalLayout" | "FlexboxLayout"
            )
        }
        _ => false,
    }
}
