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

/*! Set the width and height of Rectangle, TouchArea, ... to 100%,
    the implicit width or aspect ratio preserving for Images.
    Also set the Image.image-fit default depending on the presence of a
    layout parent.

    This pass must be run after lower_layout
*/

use std::rc::Rc;

use crate::diagnostics::{BuildDiagnostics, DiagnosticLevel, SourceLocation, Spanned};
use crate::expression_tree::{
    BindingExpression, BuiltinFunction, Expression, MinMaxOp, NamedReference, Unit,
};
use crate::langtype::{BuiltinElement, DefaultSizeBinding, Type};
use crate::layout::{BuiltinFilter, LayoutConstraints, Orientation, implicit_layout_info_call};
use crate::object_tree::{Component, ElementRc};
use crate::symbol_counters::SymbolCounters;
use smol_str::{SmolStr, format_smolstr};
use std::collections::BTreeMap;

pub fn default_geometry(
    root_component: &Rc<Component>,
    diag: &mut BuildDiagnostics,
    symbol_counters: &SymbolCounters,
) {
    crate::object_tree::recurse_elem_including_sub_components(
        root_component,
        &None,
        &mut |elem: &ElementRc, parent: &Option<ElementRc>| {
            if elem.borrow().repeated.is_some() {
                return None;
            }
            elem.borrow().geometry_props.as_ref()?;

            // whether the width, or height, is filling the parent
            let (mut w100, mut h100) = (false, false);

            w100 |= fix_percent_size(elem, parent, "width", diag, symbol_counters);
            h100 |= fix_percent_size(elem, parent, "height", diag, symbol_counters);

            gen_layout_info_prop(elem, diag, symbol_counters);

            let builtin_type = match elem.borrow().builtin_type() {
                Some(b) => b,
                None => return Some(elem.clone()),
            };

            let is_image = builtin_type.name == "Image";
            if is_image {
                adjust_image_clip_rect(elem, &builtin_type);
            }

            if let Some(parent) = parent {
                match builtin_type.default_size_binding {
                    DefaultSizeBinding::None => {
                        if elem.borrow().default_fill_parent.0 {
                            let e_width =
                                elem.borrow().geometry_props.as_ref().unwrap().width.clone();
                            let p_width =
                                parent.borrow().geometry_props.as_ref().unwrap().width.clone();
                            w100 |= make_default_100(&e_width, &p_width);
                        } else {
                            make_default_implicit(elem, "width");
                        }
                        if elem.borrow().default_fill_parent.1 {
                            let e_height =
                                elem.borrow().geometry_props.as_ref().unwrap().height.clone();
                            let p_height =
                                parent.borrow().geometry_props.as_ref().unwrap().height.clone();
                            h100 |= make_default_100(&e_height, &p_height);
                        } else {
                            make_default_implicit(elem, "height");
                        }
                    }
                    DefaultSizeBinding::ExpandsToParentGeometry => {
                        if !elem.borrow().child_of_layout {
                            let (e_width, e_height) = elem
                                .borrow()
                                .geometry_props
                                .as_ref()
                                .map(|g| (g.width.clone(), g.height.clone()))
                                .unwrap();
                            let (p_width, p_height) = parent
                                .borrow()
                                .geometry_props
                                .as_ref()
                                .map(|g| (g.width.clone(), g.height.clone()))
                                .unwrap();
                            w100 |= make_default_100(&e_width, &p_width);
                            h100 |= make_default_100(&e_height, &p_height);
                        }
                    }
                    DefaultSizeBinding::ImplicitSize => {
                        let has_length_property_binding = |elem: &ElementRc, property: &str| {
                            debug_assert_eq!(
                                elem.borrow().lookup_property(property).property_type,
                                Type::LogicalLength
                            );

                            elem.borrow().is_binding_set(property, true)
                        };

                        let width_specified = has_length_property_binding(elem, "width");
                        let height_specified = has_length_property_binding(elem, "height");

                        if !elem.borrow().child_of_layout {
                            // Add aspect-ratio preserving width or height bindings
                            if is_image && width_specified && !height_specified {
                                make_default_aspect_ratio_preserving_binding(
                                    elem, "height", "width",
                                )
                            } else if is_image && height_specified && !width_specified {
                                make_default_aspect_ratio_preserving_binding(
                                    elem, "width", "height",
                                )
                            } else {
                                make_default_implicit(elem, "width");
                                make_default_implicit(elem, "height");
                            }
                        } else if is_image {
                            // If an image is in a layout and has no explicit width or height specified, change the default for image-fit
                            // to `contain`
                            if !width_specified || !height_specified {
                                let image_fit_lookup = elem.borrow().lookup_property("image-fit");

                                elem.borrow_mut().set_binding_if_not_set(
                                    image_fit_lookup.resolved_name.into(),
                                    || {
                                        Expression::EnumerationValue(
                                            image_fit_lookup
                                                .property_type
                                                .as_enum()
                                                .clone()
                                                .try_value_from_string("contain")
                                                .unwrap(),
                                        )
                                    },
                                );
                            }
                        }
                    }
                }

                if !elem.borrow().child_of_layout
                    && !elem.borrow().is_legacy_syntax
                    && builtin_type.name != "Window"
                {
                    if !w100 {
                        maybe_center_in_parent(elem, parent, "x", "width");
                    }
                    if !h100 {
                        maybe_center_in_parent(elem, parent, "y", "height");
                    }
                }
            }

            Some(elem.clone())
        },
    )
}

/// Generate a layout_info_prop based on the children layouts
fn gen_layout_info_prop(
    elem: &ElementRc,
    diag: &mut BuildDiagnostics,
    symbol_counters: &SymbolCounters,
) {
    if elem.borrow().layout_info_prop.is_some() || elem.borrow().is_flickable_content {
        return;
    }

    let child_infos = elem
        .borrow()
        .children
        .iter()
        .filter(|c| {
            !c.borrow().is_binding_set("x", false) && !c.borrow().is_binding_set("y", false)
        })
        .filter_map(|c| {
            gen_layout_info_prop(c, diag, symbol_counters);
            c.borrow()
                .layout_info_prop
                .clone()
                .map(|(h, v)| {
                    (Some(Expression::PropertyReference(h)), Some(Expression::PropertyReference(v)))
                })
                .or_else(|| {
                    if c.borrow().is_legacy_syntax {
                        return None;
                    }
                    if c.borrow().repeated.is_some() {
                        // FIXME: we should ideally add runtime code to merge layout info of all elements that are repeated (same as #407)
                        return None;
                    }
                    let explicit_constraints =
                        LayoutConstraints::new(c, Some((&mut *diag, DiagnosticLevel::Error)));

                    let compute = |orientation| {
                        if explicit_constraints.has_explicit_restrictions(orientation) {
                            Some(explicit_layout_info(c, orientation))
                        } else {
                            implicit_layout_info_call(
                                c,
                                orientation,
                                BuiltinFilter::SkipNonImplicit,
                                None,
                            )
                        }
                    };
                    Some((compute(Orientation::Horizontal), compute(Orientation::Vertical)))
                        .filter(|(a, b)| a.is_some() || b.is_some())
                })
        })
        .collect::<Vec<_>>();

    if child_infos.is_empty() {
        return;
    }

    let li_v = crate::layout::create_new_prop(
        elem,
        SmolStr::new_static("layoutinfo-v"),
        crate::typeregister::layout_info_type().into(),
    );
    let li_h = crate::layout::create_new_prop(
        elem,
        SmolStr::new_static("layoutinfo-h"),
        crate::typeregister::layout_info_type().into(),
    );
    elem.borrow_mut().layout_info_prop = Some((li_h.clone(), li_v.clone()));
    let mut expr_h =
        implicit_layout_info_call(elem, Orientation::Horizontal, BuiltinFilter::All, None).unwrap();
    let mut expr_v =
        implicit_layout_info_call(elem, Orientation::Vertical, BuiltinFilter::All, None).unwrap();

    // The redundant-size-constraint diagnostic of a component root is reported by the lower_layouts
    // pass, so only report here for non-root elements.
    let is_root = elem
        .borrow()
        .enclosing_component
        .upgrade()
        .is_some_and(|c| Rc::ptr_eq(elem, &c.root_element));
    let explicit_constraints =
        LayoutConstraints::new(elem, (!is_root).then_some((&mut *diag, DiagnosticLevel::Warning)));
    if !explicit_constraints.fixed_width {
        merge_explicit_constraints(
            &mut expr_h,
            &explicit_constraints,
            Orientation::Horizontal,
            symbol_counters,
        );
    }
    if !explicit_constraints.fixed_height {
        merge_explicit_constraints(
            &mut expr_v,
            &explicit_constraints,
            Orientation::Vertical,
            symbol_counters,
        );
    }

    for child_info in child_infos {
        if let Some(h) = child_info.0 {
            expr_h = Expression::BinaryExpression {
                lhs: Box::new(std::mem::take(&mut expr_h)),
                rhs: Box::new(h),
                op: '+',
            };
        }
        if let Some(v) = child_info.1 {
            expr_v = Expression::BinaryExpression {
                lhs: Box::new(std::mem::take(&mut expr_v)),
                rhs: Box::new(v),
                op: '+',
            };
        }
    }

    let expr_v = BindingExpression::new_with_span(expr_v, elem.borrow().to_source_location());
    li_v.element().borrow_mut().set_binding(li_v.name().clone(), expr_v);
    let expr_h = BindingExpression::new_with_span(expr_h, elem.borrow().to_source_location());
    li_h.element().borrow_mut().set_binding(li_h.name().clone(), expr_h);
}

fn merge_explicit_constraints(
    expr: &mut Expression,
    constraints: &LayoutConstraints,
    orientation: Orientation,
    symbol_counters: &SymbolCounters,
) {
    if constraints.has_explicit_restrictions(orientation) {
        let unique_name = symbol_counters.generate_name("layout_info_");
        let ty = expr.ty();
        let store = Expression::StoreLocalVariable {
            name: unique_name.clone(),
            value: Box::new(std::mem::take(expr)),
        };
        let Type::Struct(s) = &ty else { unreachable!() };
        let mut values = s
            .fields
            .keys()
            .map(|p| {
                (
                    p.clone(),
                    Expression::StructFieldAccess {
                        base: Expression::ReadLocalVariable {
                            name: unique_name.clone(),
                            ty: ty.clone(),
                        }
                        .into(),
                        name: p.clone(),
                    },
                )
            })
            .collect::<BTreeMap<_, _>>();

        for (nr, s) in constraints.for_each_restrictions(orientation) {
            let e = nr
                .element()
                .borrow()
                .binding(nr.name())
                .expect("constraint must have binding")
                .expression
                .clone();
            debug_assert!(!matches!(e, Expression::Invalid));
            values.insert(s.into(), e);
        }
        *expr = Expression::CodeBlock([store, Expression::Struct { ty: s.clone(), values }].into());
    }
}

fn explicit_layout_info(e: &ElementRc, orientation: Orientation) -> Expression {
    let mut values = BTreeMap::new();
    let (size, orient) = match orientation {
        Orientation::Horizontal => ("width", "horizontal"),
        Orientation::Vertical => ("height", "vertical"),
    };
    for (k, v) in [
        ("min", format_smolstr!("min-{size}")),
        ("max", format_smolstr!("max-{size}")),
        ("preferred", format_smolstr!("preferred-{size}")),
        ("stretch", format_smolstr!("{orient}-stretch")),
    ] {
        values.insert(k.into(), Expression::PropertyReference(NamedReference::new(e, v)));
    }
    values.insert("min_percent".into(), Expression::NumberLiteral(0., Unit::None));
    values.insert("max_percent".into(), Expression::NumberLiteral(100., Unit::None));
    Expression::Struct { ty: crate::typeregister::layout_info_type(), values }
}

/// Replace expression such as  `"width: 30%;` with `width: 0.3 * parent.width;`
///
/// Returns true if the expression was 100%
fn fix_percent_size(
    elem: &ElementRc,
    parent: &Option<ElementRc>,
    property: &'static str,
    diag: &mut BuildDiagnostics,
    symbol_counters: &SymbolCounters,
) -> bool {
    fn inner(
        expression: &mut Expression,
        span: &Option<SourceLocation>,
        parent: &Option<ElementRc>,
        property: &'static str,
        diag: &mut BuildDiagnostics,
        symbol_counters: &SymbolCounters,
    ) -> bool {
        if let Expression::DebugHook { expression, .. } = expression {
            // If the Condition is inside a DebugHook we still need to visit it to fix the
            // percentages, but ignore the result because the debug hook may override the
            // expression.
            inner(expression, span, parent, property, diag, symbol_counters);

            false
        } else if let Expression::Condition { true_expr, false_expr, .. } = expression
            && true_expr.ty() != false_expr.ty()
        {
            // The lower_states pass can generate a Condition with mixed types of percents and
            // lengths. The conversion of percents to lengths here can't happen before states
            // lowering because it depends on inlining and states before lowering can't easily be
            // inlined because they effectively form a single enum per component.
            inner(true_expr, span, parent, property, diag, symbol_counters)
                && inner(false_expr, span, parent, property, diag, symbol_counters)
        } else {
            if expression.ty() != Type::Percent {
                let Some(parent) = parent.as_ref() else { return false };
                // Pattern match to check it was already parent.<property>
                //
                // Note: do not ignore debug hooks here, the debug hook may overwrite the
                // expression so it may not fill after all.
                return matches!(
                    expression,
                    Expression::PropertyReference(nr)
                        if *nr.name() == property && Rc::ptr_eq(&nr.element(), parent),
                );
            }
            if let Some(mut parent) = parent.clone() {
                let flickable = parent.borrow().is_flickable_content;
                if parent.borrow().is_flickable_content {
                    // the `%` in a flickable need to refer to the size of the flickable, not
                    // the size of the content element
                    parent = crate::object_tree::find_parent_element(&parent).unwrap_or(parent)
                }
                debug_assert_eq!(
                    parent.borrow().lookup_property(property).property_type,
                    Type::LogicalLength
                );
                // do not ignore debug hooks here, the debug hook may overwrite the expression
                // so it may not fill after all.
                let fill = matches!(
                    *expression,
                    Expression::NumberLiteral(x, _) if (x - 100.).abs() < 0.001,
                );

                *expression = Expression::BinaryExpression {
                    lhs: Box::new(std::mem::take(expression).maybe_convert_to(
                        Type::Float32,
                        span,
                        diag,
                        symbol_counters,
                    )),
                    rhs: Box::new(Expression::PropertyReference(NamedReference::new(
                        &parent,
                        SmolStr::new_static(property),
                    ))),
                    op: '*',
                };

                // 100% of the outer size of the flickable does not mean the flickable is
                // filled. We don't want to trigger the elimination of centering in this case.
                fill && !flickable
            } else {
                diag.push_error(
                    "Cannot find parent property to apply relative length".into(),
                    span,
                );
                false
            }
        }
    }

    let elem = elem.borrow();
    let Some(mut binding) = elem.binding_mut(property) else {
        return false;
    };

    let binding = &mut *binding;

    inner(&mut binding.expression, &binding.span, parent, property, diag, symbol_counters)
}

/// Generate a size property that covers the parent.
/// Return true if it was changed
fn make_default_100(prop: &NamedReference, parent_prop: &NamedReference) -> bool {
    prop.element().borrow_mut().set_binding_if_not_set(prop.name().clone(), || {
        Expression::PropertyReference(parent_prop.clone())
    })
}

fn make_default_implicit(elem: &ElementRc, property: &str) {
    let e = crate::builtin_macros::min_max_expression(
        Expression::PropertyReference(NamedReference::new(
            elem,
            format_smolstr!("preferred-{}", property),
        )),
        Expression::PropertyReference(NamedReference::new(
            elem,
            format_smolstr!("min-{}", property),
        )),
        MinMaxOp::Max,
    );
    elem.borrow_mut().set_binding_if_not_set(property.into(), || e);
}

// For an element with `width`, `height`, `preferred-width` and `preferred-height`, make an aspect
// ratio preserving binding. This is currently only called for Image elements. For example when for an
// image the `width` is specified and there is no `height` binding, it is called with `missing_size_property = height`
// and `given_size_property = width` and install a binding like this:
//
//    height: self.width * self.preferred_height / self.preferred_width;
//
fn make_default_aspect_ratio_preserving_binding(
    elem: &ElementRc,
    missing_size_property: &'static str,
    given_size_property: &'static str,
) {
    if elem.borrow().is_binding_set(missing_size_property, false) {
        return;
    }

    debug_assert_eq!(elem.borrow().lookup_property("source").property_type, Type::Image);

    let missing_size_property = SmolStr::new_static(missing_size_property);
    let given_size_property = SmolStr::new_static(given_size_property);

    let ratio = if elem.borrow().is_binding_set("source-clip-height", false) {
        Expression::BinaryExpression {
            lhs: Box::new(Expression::PropertyReference(NamedReference::new(
                elem,
                format_smolstr!("source-clip-{missing_size_property}"),
            ))),
            rhs: Box::new(Expression::PropertyReference(NamedReference::new(
                elem,
                format_smolstr!("source-clip-{given_size_property}"),
            ))),
            op: '/',
        }
    } else {
        let implicit_size_var = Box::new(Expression::ReadLocalVariable {
            name: "image_implicit_size".into(),
            ty: BuiltinFunction::ImageSize.ty().return_type.clone(),
        });

        Expression::CodeBlock(vec![
            Expression::StoreLocalVariable {
                name: "image_implicit_size".into(),
                value: Box::new(Expression::FunctionCall {
                    function: BuiltinFunction::ImageSize.into(),
                    arguments: vec![Expression::PropertyReference(NamedReference::new(
                        elem,
                        SmolStr::new_static("source"),
                    ))],
                    source_location: None,
                }),
            },
            Expression::BinaryExpression {
                lhs: Box::new(Expression::StructFieldAccess {
                    base: implicit_size_var.clone(),
                    name: missing_size_property.clone(),
                }),
                rhs: Box::new(Expression::StructFieldAccess {
                    base: implicit_size_var,
                    name: given_size_property.clone(),
                }),
                op: '/',
            },
        ])
    };
    let binding = Expression::BinaryExpression {
        lhs: Box::new(ratio),
        rhs: Expression::PropertyReference(NamedReference::new(elem, given_size_property)).into(),
        op: '*',
    };

    let binding_expr = binding;
    elem.borrow_mut().set_binding_if_not_set(missing_size_property, || binding_expr);
}

fn maybe_center_in_parent(
    elem: &ElementRc,
    parent: &ElementRc,
    pos_prop: &'static str,
    size_prop: &'static str,
) {
    if elem.borrow().is_binding_set(pos_prop, false) {
        return;
    }

    let size_prop = SmolStr::new_static(size_prop);
    let diff = Expression::BinaryExpression {
        lhs: Expression::PropertyReference(NamedReference::new(parent, size_prop.clone())).into(),
        op: '-',
        rhs: Expression::PropertyReference(NamedReference::new(elem, size_prop)).into(),
    };

    let pos_prop = SmolStr::new_static(pos_prop);
    elem.borrow_mut().set_binding_if_not_set(pos_prop, || Expression::BinaryExpression {
        lhs: diff.into(),
        op: '/',
        rhs: Expression::NumberLiteral(2., Unit::None).into(),
    });
}

fn adjust_image_clip_rect(elem: &ElementRc, builtin: &Rc<BuiltinElement>) {
    debug_assert_eq!(builtin.native_class.class_name, "ClippedImage");

    if builtin.native_class.properties.keys().any(|p| {
        // Deliberately count synthetic debug hooks here (via binding_cell_including_synthetic): they also count as
        // "used" in resolve_native_classes, so the ClippedImage native class gets selected —
        // and a ClippedImage without the synthesized clip defaults renders/measures as a
        // zero-size clip. This condition must match the class-selection semantics.
        elem.borrow().binding_cell_including_synthetic(p).is_some()
            || elem.borrow().property_analysis.borrow().get(p).is_some_and(|a| a.is_used())
    }) {
        let source = NamedReference::new(elem, SmolStr::new_static("source"));
        let x = NamedReference::new(elem, SmolStr::new_static("source-clip-x"));
        let y = NamedReference::new(elem, SmolStr::new_static("source-clip-y"));
        let make_expr = |dim: &str, prop: NamedReference| Expression::BinaryExpression {
            lhs: Box::new(Expression::StructFieldAccess {
                base: Box::new(Expression::FunctionCall {
                    function: BuiltinFunction::ImageSize.into(),
                    arguments: vec![Expression::PropertyReference(source.clone())],
                    source_location: None,
                }),
                name: dim.into(),
            }),
            rhs: Expression::PropertyReference(prop).into(),
            op: '-',
        };

        elem.borrow_mut()
            .set_binding_if_not_set("source-clip-width".into(), || make_expr("width", x));
        elem.borrow_mut()
            .set_binding_if_not_set("source-clip-height".into(), || make_expr("height", y));
    }
}

#[test]
fn test_no_property_for_100pc() {
    //! Test that we don't generate x or y property to center elements if the size is filling the parent
    let mut compiler_config =
        crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
    compiler_config.style = Some("fluent".into());
    let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
    let doc_node = crate::parser::parse(
        r#"
        export component Foo inherits Window {
            r1 := Rectangle {
                r2 := Rectangle {
                    width: 100%;
                    background: blue;
                }
                r3 := Rectangle {
                    height: parent.height;
                    width: 50%;
                    background: red;
                }
            }

            out property <length> r2x: r2.x;
            out property <length> r2y: r2.y;
            out property <length> r3x: r3.x;
            out property <length> r3y: r3.y;
        }
"#
        .into(),
        Some(std::path::Path::new("HELLO")),
        &mut test_diags,
    );
    let (doc, diag, _) =
        spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
    assert!(!diag.has_errors(), "{:?}", diag.to_string_vec());

    let root_elem = doc.inner_components.last().unwrap().root_element.borrow();

    // const propagation must have seen that the x and y property are literal 0
    assert!(matches!(
        root_elem.binding("r2x").unwrap().value_expression(),
        Expression::NumberLiteral(v, _) if *v == 0.
    ));
    assert!(matches!(
        root_elem.binding("r2y").unwrap().value_expression(),
        Expression::NumberLiteral(v, _) if *v == 0.
    ));
    assert!(matches!(
        root_elem.binding("r3y").unwrap().value_expression(),
        Expression::NumberLiteral(v, _) if *v == 0.
    ));
    // this one is 50% so it should be set to be in the center
    assert!(!matches!(
        root_elem.binding("r3x").unwrap().value_expression(),
        Expression::BinaryExpression { .. }
    ));
}
