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

use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::{Rc, Weak};
use std::sync::Arc;

use smol_str::SmolStr;

use super::lower_layout_expression::{
    compute_box_layout_info, compute_flexbox_layout_info, compute_grid_layout_info,
    organize_grid_layout, solve_box_layout, solve_flexbox_layout, solve_grid_layout,
};
use super::lower_to_item_tree::{LoweredSubComponentMapping, LoweringState};
use super::{Animation, LocalMemberReference, MemberReference, PropertyIdx};
use crate::expression_tree::{
    BuiltinFunction, Callable, Expression as tree_Expression, MouseCursorInner,
};
use crate::langtype::{BuiltinStruct, ConstantExpression, Struct, StructName, Type};
use crate::llr::ArrayOutput as llr_ArrayOutput;
use crate::llr::Expression as llr_Expression;
use crate::namedreference::NamedReference;
use crate::object_tree::{Component, Element, ElementRc, ElementWeak, PropertyAnimation};
use crate::typeregister::BUILTIN;

pub struct ExpressionLoweringCtxInner<'a> {
    pub component: &'a Rc<crate::object_tree::Component>,
    /// The mapping for the current component
    pub mapping: &'a LoweredSubComponentMapping,
    pub parent: Option<&'a ExpressionLoweringCtxInner<'a>>,
}
#[derive(derive_more::Deref)]
pub struct ExpressionLoweringCtx<'a> {
    pub state: &'a mut LoweringState,
    #[deref]
    pub inner: ExpressionLoweringCtxInner<'a>,
}

impl ExpressionLoweringCtx<'_> {
    /// How many parent contexts up `enclosing` is, together with its lowering context.
    fn find_component(
        &self,
        enclosing: &Rc<Component>,
    ) -> (usize, &ExpressionLoweringCtxInner<'_>) {
        let mut level = 0;
        let mut map = &self.inner;
        while !Rc::ptr_eq(enclosing, map.component) {
            map = map.parent.unwrap_or_else(|| {
                panic!(
                    "Could not find component {:?} from component {:?}",
                    enclosing.id, self.component.id
                )
            });
            level += 1;
        }
        (level, map)
    }

    pub fn map_property_reference(&self, from: &NamedReference) -> MemberReference {
        let element = from.element();
        let enclosing = &element.borrow().enclosing_component.upgrade().unwrap();
        let (level, map) =
            if enclosing.is_global() { (0, &self.inner) } else { self.find_component(enclosing) };
        let mut r = map.mapping.map_property_reference(from, self.state);
        if let MemberReference::Relative { parent_level, .. } = &mut r {
            *parent_level += level;
        }
        r
    }
}

impl super::TypeResolutionContext for ExpressionLoweringCtx<'_> {
    fn property_ty(&self, _: &MemberReference) -> &Type {
        unimplemented!()
    }
}

/// Lower a constant expression, such as the default value of a struct field
/// (see [`crate::langtype::Struct::field_defaults`]),
/// so that the code generators can compile it with their regular expression compilation.
pub fn lower_constant_expression(expression: &ConstantExpression) -> llr_Expression {
    match expression {
        ConstantExpression::StringLiteral(s) => llr_Expression::StringLiteral(s.clone()),
        ConstantExpression::NumberLiteral(n, _unit) => llr_Expression::NumberLiteral(*n),
        ConstantExpression::BoolLiteral(b) => llr_Expression::BoolLiteral(*b),
        ConstantExpression::EnumerationValue(e) => llr_Expression::EnumerationValue(e.clone()),
        ConstantExpression::Cast { from, to } => {
            llr_Expression::Cast { from: Box::new(lower_constant_expression(from)), to: to.clone() }
        }
        ConstantExpression::UnaryOp { sub, op } => {
            llr_Expression::UnaryOp { sub: Box::new(lower_constant_expression(sub)), op: *op }
        }
        ConstantExpression::Struct { ty, values } => llr_Expression::Struct {
            ty: ty.clone(),
            values: values.iter().map(|(k, v)| (k.clone(), lower_constant_expression(v))).collect(),
        },
        ConstantExpression::Array { element_ty, values } => llr_Expression::Array {
            element_ty: element_ty.clone(),
            values: values.iter().map(lower_constant_expression).collect(),
            output: llr_ArrayOutput::Model,
        },
    }
}

/// The body of every non-trivial match arm lives in its own `#[inline(never)]`
/// helper function: this function recurses for nested expressions, and with all
/// arm bodies inlined, its stack frame in unoptimized builds becomes so large
/// that deeply nested expressions overflow the stack.
pub fn lower_expression(
    expression: &tree_Expression,
    ctx: &mut ExpressionLoweringCtx<'_>,
) -> llr_Expression {
    match expression {
        tree_Expression::Invalid => {
            panic!("internal error, encountered invalid expression at code generation time")
        }
        tree_Expression::Uncompiled(_) => panic!(),
        tree_Expression::StringLiteral(s) => llr_Expression::StringLiteral(s.clone()),
        tree_Expression::NumberLiteral(n, _unit) => llr_Expression::NumberLiteral(*n),
        tree_Expression::BoolLiteral(b) => llr_Expression::BoolLiteral(*b),
        tree_Expression::PropertyReference(nr) => {
            llr_Expression::PropertyReference(ctx.map_property_reference(nr))
        }
        tree_Expression::ElementReference(_) => lower_element_reference(expression, ctx),
        tree_Expression::RepeaterIndexReference { element } => llr_Expression::PropertyReference(
            repeater_special_property(element, ctx.component, PropertyIdx::REPEATER_INDEX),
        ),
        tree_Expression::RepeaterModelReference { element } => llr_Expression::PropertyReference(
            repeater_special_property(element, ctx.component, PropertyIdx::REPEATER_DATA),
        ),
        tree_Expression::FunctionParameterReference { index, .. } => {
            llr_Expression::FunctionParameterReference { index: *index }
        }
        tree_Expression::StoreLocalVariable { name, value } => llr_Expression::StoreLocalVariable {
            name: name.clone(),
            value: Box::new(lower_expression(value, ctx)),
        },
        tree_Expression::ReadLocalVariable { name, ty } => {
            llr_Expression::ReadLocalVariable { name: name.clone(), ty: ty.clone() }
        }
        tree_Expression::StructFieldAccess { base, name } => llr_Expression::StructFieldAccess {
            base: Box::new(lower_expression(base, ctx)),
            name: name.clone(),
        },
        tree_Expression::ArrayIndex { array, index } => llr_Expression::ArrayIndex {
            array: Box::new(lower_expression(array, ctx)),
            index: Box::new(lower_expression(index, ctx)),
        },
        tree_Expression::Cast { from, to } => {
            llr_Expression::Cast { from: Box::new(lower_expression(from, ctx)), to: to.clone() }
        }
        tree_Expression::CodeBlock(expr) => {
            llr_Expression::CodeBlock(expr.iter().map(|e| lower_expression(e, ctx)).collect::<_>())
        }
        tree_Expression::FunctionCall { .. } => lower_function_call(expression, ctx),
        tree_Expression::SelfAssignment { lhs, rhs, op, .. } => {
            lower_assignment(lhs, rhs, *op, ctx)
        }
        tree_Expression::BinaryExpression { .. } => lower_binary_expression(expression, ctx),
        tree_Expression::UnaryOp { sub, op } => {
            llr_Expression::UnaryOp { sub: Box::new(lower_expression(sub, ctx)), op: *op }
        }
        tree_Expression::ImageReference { resource_ref, nine_slice, .. } => {
            llr_Expression::ImageReference {
                resource_ref: resource_ref.clone(),
                nine_slice: *nine_slice,
            }
        }
        tree_Expression::Condition { .. } => lower_condition(expression, ctx),
        tree_Expression::Array { element_ty, values } => llr_Expression::Array {
            element_ty: if *element_ty == Type::Void { Type::Int32 } else { element_ty.clone() },
            values: values.iter().map(|e| lower_expression(e, ctx)).collect::<_>(),
            output: llr_ArrayOutput::Model,
        },
        tree_Expression::Struct { ty, values } => llr_Expression::Struct {
            ty: ty.clone(),
            values: values
                .iter()
                .map(|(s, e)| (s.clone(), lower_expression(e, ctx)))
                .collect::<_>(),
        },
        tree_Expression::PathData(data) => compile_path(data, ctx),
        tree_Expression::EasingCurve(x) => llr_Expression::EasingCurve(x.clone()),
        tree_Expression::MouseCursor(_) => lower_mouse_cursor(expression, ctx),
        tree_Expression::LinearGradient { .. } => lower_linear_gradient(expression, ctx),
        tree_Expression::RadialGradient { .. } => lower_radial_gradient(expression, ctx),
        tree_Expression::ConicGradient { .. } => lower_conic_gradient(expression, ctx),
        tree_Expression::EnumerationValue(e) => llr_Expression::EnumerationValue(e.clone()),
        tree_Expression::Keys(ks) => llr_Expression::KeysLiteral(ks.clone()),
        tree_Expression::ReturnStatement(..) => {
            panic!("The remove return pass should have removed all return")
        }
        tree_Expression::LayoutCacheAccess { .. } => lower_layout_cache_access(expression, ctx),
        tree_Expression::GridRepeaterCacheAccess { .. } => {
            lower_grid_repeater_cache_access(expression, ctx)
        }
        tree_Expression::OrganizeGridLayout(l) => organize_grid_layout(l, ctx),
        tree_Expression::ComputeBoxLayoutInfo { layout, orientation, cross_axis_size } => {
            compute_box_layout_info(layout, *orientation, ctx, cross_axis_size.as_deref())
        }
        tree_Expression::ComputeGridLayoutInfo {
            layout_organized_data_prop,
            layout,
            orientation,
            cross_axis_size,
        } => compute_grid_layout_info(
            layout_organized_data_prop,
            layout,
            *orientation,
            ctx,
            cross_axis_size.as_deref(),
        ),
        tree_Expression::SolveBoxLayout(l, o) => solve_box_layout(l, *o, ctx),
        tree_Expression::SolveGridLayout { layout_organized_data_prop, layout, orientation } => {
            solve_grid_layout(layout_organized_data_prop, layout, *orientation, ctx)
        }
        tree_Expression::SolveFlexboxLayout(l) => solve_flexbox_layout(l, ctx),
        tree_Expression::ComputeFlexboxLayoutInfo { layout, orientation, cross_axis_size } => {
            compute_flexbox_layout_info(layout, *orientation, ctx, cross_axis_size.as_deref())
        }
        tree_Expression::MinMax { ty, op, lhs, rhs } => llr_Expression::MinMax {
            ty: ty.clone(),
            op: *op,
            lhs: Box::new(lower_expression(lhs, ctx)),
            rhs: Box::new(lower_expression(rhs, ctx)),
        },
        tree_Expression::EmptyComponentFactory => llr_Expression::EmptyComponentFactory,
        tree_Expression::EmptyDataTransfer => llr_Expression::EmptyDataTransfer,
        tree_Expression::DebugHook { expression, .. } => lower_expression(expression, ctx),
        tree_Expression::Closure { arg_name, expression } => llr_Expression::Closure {
            arg_name: arg_name.clone(),
            expression: Box::new(lower_expression(expression, ctx)),
        },
    }
}

#[inline(never)]
fn lower_binary_expression(
    expression: &tree_Expression,
    ctx: &mut ExpressionLoweringCtx<'_>,
) -> llr_Expression {
    // Long chains of binary operators, such as `a && b && c && ...`, nest on the
    // left side. Iterate that spine instead of recursing into it, so that the
    // stack depth stays bounded no matter how long the chain is.
    let mut spine = Vec::new();
    let mut node = expression;
    while let tree_Expression::BinaryExpression { lhs, rhs, op } = node {
        spine.push((rhs, *op));
        node = lhs;
    }
    let mut result = lower_expression(node, ctx);
    for (rhs, op) in spine.into_iter().rev() {
        result = llr_Expression::BinaryExpression {
            lhs: Box::new(result),
            rhs: Box::new(lower_expression(rhs, ctx)),
            op,
        };
    }
    result
}

#[inline(never)]
fn lower_element_reference(
    expression: &tree_Expression,
    ctx: &mut ExpressionLoweringCtx<'_>,
) -> llr_Expression {
    let tree_Expression::ElementReference(e) = expression else { unreachable!() };
    let elem = e.upgrade().unwrap();
    let enclosing = elem.borrow().enclosing_component.upgrade().unwrap();
    // When within a ShowPopupMenu builtin function, this is a reference to the root of the menu item tree
    if Rc::ptr_eq(&elem, &enclosing.root_element)
        && let Some(idx) =
            ctx.component.menu_item_tree.borrow().iter().position(|c| Rc::ptr_eq(c, &enclosing))
    {
        return llr_Expression::NumberLiteral(idx as _);
    }

    // We map an element reference to a reference to the property "" inside that native item
    llr_Expression::PropertyReference(
        ctx.map_property_reference(&NamedReference::new(&elem, SmolStr::default())),
    )
}

#[inline(never)]
fn lower_function_call(
    expression: &tree_Expression,
    ctx: &mut ExpressionLoweringCtx<'_>,
) -> llr_Expression {
    let tree_Expression::FunctionCall { function, arguments, .. } = expression else {
        unreachable!()
    };
    match function {
        Callable::Builtin(BuiltinFunction::RestartTimer) => lower_restart_timer(arguments, ctx),
        Callable::Builtin(BuiltinFunction::ShowPopupWindow) => {
            lower_show_popup_window(arguments, ctx)
        }
        Callable::Builtin(BuiltinFunction::ClosePopupWindow) => {
            lower_close_popup_window(arguments, ctx)
        }
        Callable::Builtin(f) => {
            let mut arguments =
                arguments.iter().map(|e| lower_expression(e, ctx)).collect::<Vec<_>>();
            // https://github.com/rust-lang/rust-clippy/issues/16191
            #[allow(clippy::collapsible_if)]
            if *f == BuiltinFunction::Translate {
                if let llr_Expression::Array { output, .. } = &mut arguments[3] {
                    *output = llr_ArrayOutput::Slice;
                }
                #[cfg(feature = "bundle-translations")]
                if let Some(translation_builder) = ctx.state.translation_builder.as_mut() {
                    return translation_builder.lower_translate_call(arguments);
                }
            }
            if *f == BuiltinFunction::ParseMarkdown
                && let Some(llr_Expression::Array { output, .. }) = &mut arguments.get_mut(1)
            {
                *output = llr_ArrayOutput::Slice;
            }
            llr_Expression::BuiltinFunctionCall { function: f.clone(), arguments }
        }
        Callable::Callback(nr) => {
            let arguments = arguments.iter().map(|e| lower_expression(e, ctx)).collect::<_>();
            llr_Expression::CallBackCall { callback: ctx.map_property_reference(nr), arguments }
        }
        Callable::Function(nr)
            if nr
                .element()
                .borrow()
                .native_class()
                .is_some_and(|n| n.properties.contains_key(nr.name())) =>
        {
            llr_Expression::ItemMemberFunctionCall { function: ctx.map_property_reference(nr) }
        }
        Callable::Function(nr) => {
            let arguments = arguments.iter().map(|e| lower_expression(e, ctx)).collect::<_>();
            llr_Expression::FunctionCall { function: ctx.map_property_reference(nr), arguments }
        }
    }
}

#[inline(never)]
fn lower_condition(
    expression: &tree_Expression,
    ctx: &mut ExpressionLoweringCtx<'_>,
) -> llr_Expression {
    let tree_Expression::Condition { condition, true_expr, false_expr } = expression else {
        unreachable!()
    };
    let (true_ty, false_ty) = (true_expr.ty(), false_expr.ty());
    llr_Expression::Condition {
        condition: Box::new(lower_expression(condition, ctx)),
        true_expr: Box::new(lower_expression(true_expr, ctx)),
        false_expr: if false_ty == Type::Invalid || false_ty == Type::Void || true_ty == false_ty {
            Box::new(lower_expression(false_expr, ctx))
        } else {
            // Because the type of the Condition is based on the false expression, we need to insert a cast
            Box::new(llr_Expression::Cast {
                from: Box::new(lower_expression(false_expr, ctx)),
                to: Type::Void,
            })
        },
    }
}

#[inline(never)]
fn lower_mouse_cursor(
    expression: &tree_Expression,
    ctx: &mut ExpressionLoweringCtx<'_>,
) -> llr_Expression {
    let tree_Expression::MouseCursor(cursor) = expression else { unreachable!() };
    llr_Expression::MouseCursor(match cursor {
        MouseCursorInner::BuiltIn(expression) => {
            crate::llr::MouseCursorInner::BuiltIn(Box::new(lower_expression(expression, ctx)))
        }
        MouseCursorInner::CustomMouseCursor { image, hotspot_x, hotspot_y } => {
            crate::llr::MouseCursorInner::CustomMouseCursor {
                image: Box::new(lower_expression(image, ctx)),
                hotspot_x: Box::new(lower_expression(hotspot_x, ctx)),
                hotspot_y: Box::new(lower_expression(hotspot_y, ctx)),
            }
        }
    })
}

#[inline(never)]
fn lower_linear_gradient(
    expression: &tree_Expression,
    ctx: &mut ExpressionLoweringCtx<'_>,
) -> llr_Expression {
    let tree_Expression::LinearGradient { angle, stops } = expression else { unreachable!() };
    llr_Expression::LinearGradient {
        angle: Box::new(lower_expression(angle, ctx)),
        stops: stops
            .iter()
            .map(|(a, b)| (lower_expression(a, ctx), lower_expression(b, ctx)))
            .collect::<_>(),
    }
}

#[inline(never)]
fn lower_radial_gradient(
    expression: &tree_Expression,
    ctx: &mut ExpressionLoweringCtx<'_>,
) -> llr_Expression {
    let tree_Expression::RadialGradient { center, radius, stops } = expression else {
        unreachable!()
    };
    llr_Expression::RadialGradient {
        center: center.as_ref().map(|(cx, cy)| {
            (Box::new(lower_expression(cx, ctx)), Box::new(lower_expression(cy, ctx)))
        }),
        radius: radius.as_ref().map(|r| Box::new(lower_expression(r, ctx))),
        stops: stops
            .iter()
            .map(|(a, b)| (lower_expression(a, ctx), lower_expression(b, ctx)))
            .collect::<_>(),
    }
}

#[inline(never)]
fn lower_conic_gradient(
    expression: &tree_Expression,
    ctx: &mut ExpressionLoweringCtx<'_>,
) -> llr_Expression {
    let tree_Expression::ConicGradient { from_angle, center, stops } = expression else {
        unreachable!()
    };
    llr_Expression::ConicGradient {
        from_angle: Box::new(lower_expression(from_angle, ctx)),
        center: center.as_ref().map(|(cx, cy)| {
            (Box::new(lower_expression(cx, ctx)), Box::new(lower_expression(cy, ctx)))
        }),
        stops: stops
            .iter()
            .map(|(a, b)| (lower_expression(a, ctx), lower_expression(b, ctx)))
            .collect::<_>(),
    }
}

#[inline(never)]
fn lower_layout_cache_access(
    expression: &tree_Expression,
    ctx: &mut ExpressionLoweringCtx<'_>,
) -> llr_Expression {
    let tree_Expression::LayoutCacheAccess {
        layout_cache_prop,
        index,
        repeater_index,
        entries_per_item,
    } = expression
    else {
        unreachable!()
    };
    llr_Expression::LayoutCacheAccess {
        layout_cache_prop: ctx.map_property_reference(layout_cache_prop),
        index: *index,
        repeater_index: repeater_index.as_ref().map(|e| lower_expression(e, ctx).into()),
        entries_per_item: *entries_per_item,
    }
}

#[inline(never)]
fn lower_grid_repeater_cache_access(
    expression: &tree_Expression,
    ctx: &mut ExpressionLoweringCtx<'_>,
) -> llr_Expression {
    let tree_Expression::GridRepeaterCacheAccess {
        layout_cache_prop,
        index,
        repeater_index,
        stride,
        child_offset,
        inner_repeater_index,
        entries_per_item,
    } = expression
    else {
        unreachable!()
    };
    llr_Expression::GridRepeaterCacheAccess {
        layout_cache_prop: ctx.map_property_reference(layout_cache_prop),
        index: *index,
        repeater_index: lower_expression(repeater_index, ctx).into(),
        stride: lower_expression(stride, ctx).into(),
        child_offset: *child_offset,
        inner_repeater_index: inner_repeater_index
            .as_ref()
            .map(|e| lower_expression(e, ctx).into()),
        entries_per_item: *entries_per_item,
    }
}

fn lower_assignment(
    lhs: &tree_Expression,
    rhs: &tree_Expression,
    op: char,
    ctx: &mut ExpressionLoweringCtx,
) -> llr_Expression {
    match lhs {
        tree_Expression::PropertyReference(nr) => {
            let rhs = lower_expression(rhs, ctx);
            let property = ctx.map_property_reference(nr);
            let value = if op == '=' {
                rhs
            } else {
                llr_Expression::BinaryExpression {
                    lhs: llr_Expression::PropertyReference(property.clone()).into(),
                    rhs: rhs.into(),
                    op,
                }
            }
            .into();
            llr_Expression::PropertyAssignment { property, value }
        }
        tree_Expression::StructFieldAccess { base, name } => {
            let ty = base.ty();

            let unique_name = ctx.state.unique_struct_assignment_name();
            let s = tree_Expression::StoreLocalVariable {
                name: unique_name.clone(),
                value: base.clone(),
            };
            let lower_base =
                tree_Expression::ReadLocalVariable { name: unique_name, ty: ty.clone() };
            let mut values = BTreeMap::new();
            let Type::Struct(ty) = ty else { unreachable!() };

            for field in ty.fields.keys() {
                let e = if field != name {
                    tree_Expression::StructFieldAccess {
                        base: lower_base.clone().into(),
                        name: field.clone(),
                    }
                } else if op == '=' {
                    rhs.clone()
                } else {
                    tree_Expression::BinaryExpression {
                        lhs: tree_Expression::StructFieldAccess {
                            base: lower_base.clone().into(),
                            name: field.clone(),
                        }
                        .into(),
                        rhs: Box::new(rhs.clone()),
                        op,
                    }
                };
                values.insert(field.clone(), e);
            }

            let new_value =
                tree_Expression::CodeBlock(vec![s, tree_Expression::Struct { ty, values }]);
            lower_assignment(base, &new_value, '=', ctx)
        }
        tree_Expression::RepeaterModelReference { element } => {
            let rhs = lower_expression(rhs, ctx);
            let prop =
                repeater_special_property(element, ctx.component, PropertyIdx::REPEATER_DATA);

            let level = match &prop {
                MemberReference::Relative { parent_level, .. } => *parent_level,
                _ => 0,
            };

            let value = Box::new(if op == '=' {
                rhs
            } else {
                llr_Expression::BinaryExpression {
                    lhs: llr_Expression::PropertyReference(prop).into(),
                    rhs: rhs.into(),
                    op,
                }
            });

            llr_Expression::ModelDataAssignment { level, value }
        }
        tree_Expression::ArrayIndex { array, index } => {
            let rhs = lower_expression(rhs, ctx);
            let array = Box::new(lower_expression(array, ctx));
            let index = Box::new(lower_expression(index, ctx));
            let value = Box::new(if op == '=' {
                rhs
            } else {
                // FIXME: this will compute the index and the array twice:
                // Ideally we should store the index and the array in local variable
                llr_Expression::BinaryExpression {
                    lhs: llr_Expression::ArrayIndex { array: array.clone(), index: index.clone() }
                        .into(),
                    rhs: rhs.into(),
                    op,
                }
            });

            llr_Expression::ArrayIndexAssignment { array, index, value }
        }
        _ => panic!("not a rvalue"),
    }
}

pub fn repeater_special_property(
    element: &Weak<RefCell<Element>>,
    component: &Rc<crate::object_tree::Component>,
    property_index: PropertyIdx,
) -> MemberReference {
    let enclosing = element.upgrade().unwrap().borrow().enclosing_component.upgrade().unwrap();
    let mut parent_level = 0;
    let mut component = component.clone();
    while !Rc::ptr_eq(&enclosing, &component) {
        let parent_elem = component.parent_element().unwrap();
        component = parent_elem.borrow().enclosing_component.upgrade().unwrap();
        parent_level += 1;
    }
    MemberReference::Relative {
        parent_level: parent_level - 1,
        local_reference: LocalMemberReference {
            sub_component_path: Vec::new(),
            reference: property_index.into(),
        },
    }
}

/// Lowers to `RestartTimer(timer_reference)`.
/// The argument is a `PropertyReference` with a [`LocalMemberIndex::Timer`]
/// locating the timer in the component that declares it.
fn lower_restart_timer(args: &[tree_Expression], ctx: &ExpressionLoweringCtx) -> llr_Expression {
    if let [tree_Expression::ElementReference(e)] = args {
        let timer_element = e.upgrade().unwrap();
        let timer_comp = timer_element.borrow().enclosing_component.upgrade().unwrap();
        let (parent_level, _) = ctx.find_component(&timer_comp);

        let timer_list = timer_comp.timers.borrow();
        let timer_index = timer_list
            .iter()
            .position(|t| Rc::ptr_eq(&t.element.upgrade().unwrap(), &timer_element))
            .unwrap();

        llr_Expression::BuiltinFunctionCall {
            function: BuiltinFunction::RestartTimer,
            arguments: vec![llr_Expression::PropertyReference(MemberReference::Relative {
                parent_level,
                local_reference: LocalMemberReference {
                    sub_component_path: Vec::new(),
                    reference: crate::llr::TimerIdx::from(timer_index).into(),
                },
            })],
        }
    } else {
        panic!("invalid arguments to RestartTimer");
    }
}

/// Resolve the component that declares the popup referenced by `e`: a reference to its root,
/// where the `popup_id` and scope live, and the popup's index in that component. The declaring
/// component is the parent item's enclosing component, which is not always the parent item
/// itself (it may be a nested sub-component instance), so this reference must be resolved
/// separately from the parent item used for positioning.
fn lower_popup_owner(
    e: &ElementWeak,
    ctx: &mut ExpressionLoweringCtx,
) -> (Rc<Component>, llr_Expression, usize) {
    let popup_window = e.upgrade().unwrap();
    let pop_comp = popup_window.borrow().enclosing_component.upgrade().unwrap();
    let parent_elem = pop_comp.parent_element().unwrap();
    let parent_component = parent_elem.borrow().enclosing_component.upgrade().unwrap();
    let owner_ref = lower_expression(
        &tree_Expression::ElementReference(Rc::downgrade(&parent_component.root_element)),
        ctx,
    );
    let popup_index = parent_component
        .popup_windows
        .borrow()
        .iter()
        .position(|p| Rc::ptr_eq(&p.component, &pop_comp))
        .unwrap();
    (parent_component, owner_ref, popup_index)
}

fn lower_show_popup_window(
    args: &[tree_Expression],
    ctx: &mut ExpressionLoweringCtx,
) -> llr_Expression {
    if let [tree_Expression::ElementReference(e)] = args {
        let (parent_component, owner_ref, popup_index) = lower_popup_owner(e, ctx);
        let popup_list = parent_component.popup_windows.borrow();
        let popup = &popup_list[popup_index];
        let item_ref = lower_expression(
            &tree_Expression::ElementReference(Rc::downgrade(&popup.parent_element)),
            ctx,
        );

        let mut arguments = vec![
            llr_Expression::NumberLiteral(popup_index as _),
            llr_Expression::EnumerationValue(popup.close_policy.clone()),
            owner_ref,
            item_ref,
        ];
        // Map `is-open` here, at the show site, so it resolves in the same frame as `item_ref`. The
        // popup struct is shared across all show sites and was lowered in a different frame, so this
        // reference must not be cached on it (see the `is_open` handling in the generators).
        if let Some(is_open) = &popup.is_open {
            arguments.push(llr_Expression::PropertyReference(ctx.map_property_reference(is_open)));
        }
        llr_Expression::BuiltinFunctionCall {
            function: BuiltinFunction::ShowPopupWindow,
            arguments,
        }
    } else {
        panic!("invalid arguments to ShowPopupWindow");
    }
}

fn lower_close_popup_window(
    args: &[tree_Expression],
    ctx: &mut ExpressionLoweringCtx,
) -> llr_Expression {
    if let [tree_Expression::ElementReference(e)] = args {
        let (_, owner_ref, popup_index) = lower_popup_owner(e, ctx);
        llr_Expression::BuiltinFunctionCall {
            function: BuiltinFunction::ClosePopupWindow,
            arguments: vec![llr_Expression::NumberLiteral(popup_index as _), owner_ref],
        }
    } else {
        panic!("invalid arguments to ClosePopupWindow");
    }
}

pub fn lower_animation(a: &PropertyAnimation, ctx: &mut ExpressionLoweringCtx<'_>) -> Animation {
    fn lower_animation_element(
        a: &ElementRc,
        ctx: &mut ExpressionLoweringCtx<'_>,
    ) -> llr_Expression {
        llr_Expression::Struct {
            values: animation_fields()
                .map(|(k, ty)| {
                    let e = a.borrow().binding_cell_including_synthetic(&k).map_or_else(
                        || {
                            if k == "enabled" {
                                llr_Expression::BoolLiteral(true)
                            } else {
                                llr_Expression::default_value_for_type(&ty).unwrap()
                            }
                        },
                        |v| lower_expression(&v.borrow().expression, ctx),
                    );
                    (k, e)
                })
                .collect::<_>(),
            ty: animation_ty(),
        }
    }

    fn animation_fields() -> impl Iterator<Item = (SmolStr, Type)> {
        IntoIterator::into_iter([
            (SmolStr::new_static("duration"), Type::Int32),
            (SmolStr::new_static("iteration-count"), Type::Float32),
            (
                SmolStr::new_static("direction"),
                Type::Enumeration(BUILTIN.enums.AnimationDirection.clone()),
            ),
            (SmolStr::new_static("easing"), Type::Easing),
            (SmolStr::new_static("delay"), Type::Int32),
            (SmolStr::new_static("enabled"), Type::Bool),
        ])
    }

    fn animation_ty() -> Arc<Struct> {
        Arc::new(Struct::new(animation_fields().collect(), BuiltinStruct::PropertyAnimation))
    }

    match a {
        PropertyAnimation::Static(a) => Animation::Static(lower_animation_element(a, ctx)),
        PropertyAnimation::Transition { state_ref, animations } => {
            let set_state = llr_Expression::StoreLocalVariable {
                name: "state".into(),
                value: Box::new(lower_expression(state_ref, ctx)),
            };
            let anim_struct_ty = animation_ty();
            let animation_ty = Type::Struct(anim_struct_ty.clone());
            let mut get_anim = llr_Expression::Struct {
                ty: anim_struct_ty,
                values: animation_fields()
                    .map(|(k, ty)| {
                        let e = if k == "enabled" {
                            llr_Expression::BoolLiteral(true)
                        } else {
                            llr_Expression::default_value_for_type(&ty).unwrap()
                        };
                        (k, e)
                    })
                    .collect(),
            };
            for tr in animations.iter().rev() {
                let condition = lower_expression(
                    &tr.condition(tree_Expression::ReadLocalVariable {
                        name: "state".into(),
                        ty: state_ref.ty(),
                    }),
                    ctx,
                );
                get_anim = llr_Expression::Condition {
                    condition: Box::new(condition),
                    true_expr: Box::new(lower_animation_element(&tr.animation, ctx)),
                    false_expr: Box::new(get_anim),
                }
            }
            let result = llr_Expression::Struct {
                // This is going to be a tuple
                ty: Arc::new(Struct::new(
                    IntoIterator::into_iter([
                        (SmolStr::new_static("0"), animation_ty),
                        // The type is an instant, which does not exist in our type system
                        (SmolStr::new_static("1"), Type::Invalid),
                    ])
                    .collect(),
                    StructName::None,
                )),
                values: IntoIterator::into_iter([
                    (SmolStr::new_static("0"), get_anim),
                    (
                        SmolStr::new_static("1"),
                        llr_Expression::StructFieldAccess {
                            base: llr_Expression::ReadLocalVariable {
                                name: "state".into(),
                                ty: state_ref.ty(),
                            }
                            .into(),
                            name: "change_time".into(),
                        },
                    ),
                ])
                .collect(),
            };
            Animation::Transition(llr_Expression::CodeBlock(vec![set_state, result]))
        }
    }
}

fn compile_path(
    path: &crate::expression_tree::Path,
    ctx: &mut ExpressionLoweringCtx,
) -> llr_Expression {
    fn llr_path_elements(elements: Vec<llr_Expression>) -> llr_Expression {
        llr_Expression::Cast {
            from: llr_Expression::Array {
                element_ty: crate::typeregister::path_element_type(),
                values: elements,
                output: llr_ArrayOutput::Slice,
            }
            .into(),
            to: Type::PathData,
        }
    }

    match path {
        crate::expression_tree::Path::Elements(elements) => {
            let converted_elements = elements
                .iter()
                .map(|element| {
                    let element_type = Arc::new(Struct::new(
                        element
                            .element_type
                            .properties
                            .iter()
                            .map(|(k, v)| (k.clone(), v.ty.clone()))
                            .collect(),
                        StructName::Builtin(
                            element
                                .element_type
                                .native_class
                                .builtin_struct
                                .clone()
                                .expect("path elements should have a native_type"),
                        ),
                    ));

                    llr_Expression::Struct {
                        ty: element_type,
                        values: element
                            .element_type
                            .properties
                            .iter()
                            .map(|(element_field_name, element_property)| {
                                (
                                    element_field_name.clone(),
                                    element.bindings.get(element_field_name).map_or_else(
                                        || {
                                            llr_Expression::default_value_for_type(
                                                &element_property.ty,
                                            )
                                            .unwrap()
                                        },
                                        |expr| lower_expression(&expr.borrow().expression, ctx),
                                    ),
                                )
                            })
                            .collect(),
                    }
                })
                .collect();
            llr_path_elements(converted_elements)
        }
        crate::expression_tree::Path::Events(events, points) => {
            if events.is_empty() || points.is_empty() {
                return llr_path_elements(Vec::new());
            }

            let events: Vec<_> = events.iter().map(|event| lower_expression(event, ctx)).collect();

            let event_type = events.first().unwrap().ty(ctx);

            let points: Vec<_> = points.iter().map(|point| lower_expression(point, ctx)).collect();

            let point_type = points.first().unwrap().ty(ctx);

            llr_Expression::Cast {
                from: llr_Expression::Struct {
                    ty: Arc::new(Struct::new(
                        IntoIterator::into_iter([
                            (SmolStr::new_static("events"), Type::Array(event_type.clone().into())),
                            (SmolStr::new_static("points"), Type::Array(point_type.clone().into())),
                        ])
                        .collect(),
                        StructName::None,
                    )),
                    values: IntoIterator::into_iter([
                        (
                            SmolStr::new_static("events"),
                            llr_Expression::Array {
                                element_ty: event_type,
                                values: events,
                                output: llr_ArrayOutput::Slice,
                            },
                        ),
                        (
                            SmolStr::new_static("points"),
                            llr_Expression::Array {
                                element_ty: point_type,
                                values: points,
                                output: llr_ArrayOutput::Slice,
                            },
                        ),
                    ])
                    .collect(),
                }
                .into(),
                to: Type::PathData,
            }
        }
        crate::expression_tree::Path::Commands(commands) => llr_Expression::Cast {
            from: lower_expression(commands, ctx).into(),
            to: Type::PathData,
        },
    }
}

pub fn make_struct(
    name: impl Into<StructName>,
    it: impl IntoIterator<Item = (&'static str, Type, llr_Expression)>,
) -> llr_Expression {
    let mut fields = BTreeMap::<SmolStr, Type>::new();
    let mut values = BTreeMap::<SmolStr, llr_Expression>::new();
    for (name, ty, expr) in it {
        fields.insert(SmolStr::new(name), ty);
        values.insert(SmolStr::new(name), expr);
    }

    llr_Expression::Struct { ty: Arc::new(Struct::new(fields, name)), values }
}
