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

//! Passes that resolve the property binding expression.
//!
//! Before this pass, all the expression are of type Expression::Uncompiled,
//! and there should no longer be Uncompiled expression after this pass.
//!
//! Most of the code for the resolving actually lies in the expression_tree module

use crate::diagnostics::{BuildDiagnostics, Spanned};
use crate::expression_tree::*;
use crate::langtype::{ElementType, Struct, Type};
use crate::lookup::{LookupCtx, LookupObject, LookupResult};
use crate::object_tree::*;
use crate::parser::{identifier_text, syntax_nodes, NodeOrToken, SyntaxKind, SyntaxNode};
use crate::typeregister::TypeRegister;
use core::num::IntErrorKind;
use smol_str::{SmolStr, ToSmolStr};
use std::collections::HashMap;
use std::rc::Rc;

/// This represents a scope for the Component, where Component is the repeated component, but
/// does not represent a component in the .slint file
#[derive(Clone)]
struct ComponentScope(Vec<ElementRc>);

fn resolve_expression(
    expr: &mut Expression,
    property_name: Option<&str>,
    property_type: Type,
    scope: &[ElementRc],
    type_register: &TypeRegister,
    type_loader: &crate::typeloader::TypeLoader,
    diag: &mut BuildDiagnostics,
) {
    if let Expression::Uncompiled(node) = expr {
        let mut lookup_ctx = LookupCtx {
            property_name,
            property_type,
            component_scope: scope,
            diag,
            arguments: vec![],
            type_register,
            type_loader: Some(type_loader),
            current_token: None,
        };

        let new_expr = match node.kind() {
            SyntaxKind::CallbackConnection => {
                Expression::from_callback_connection(node.clone().into(), &mut lookup_ctx)
            }
            SyntaxKind::Function => Expression::from_function(node.clone().into(), &mut lookup_ctx),
            SyntaxKind::Expression => {
                //FIXME again: this happen for non-binding expression (i.e: model)
                Expression::from_expression_node(node.clone().into(), &mut lookup_ctx)
                    .maybe_convert_to(lookup_ctx.property_type.clone(), node, diag)
            }
            SyntaxKind::BindingExpression => {
                Expression::from_binding_expression_node(node.clone(), &mut lookup_ctx)
            }
            SyntaxKind::PropertyChangedCallback => Expression::from_codeblock_node(
                syntax_nodes::PropertyChangedCallback::from(node.clone()).CodeBlock(),
                &mut lookup_ctx,
            ),
            SyntaxKind::TwoWayBinding => {
                assert!(diag.has_errors(), "Two way binding should have been resolved already  (property: {property_name:?})");
                Expression::Invalid
            }
            _ => {
                debug_assert!(diag.has_errors());
                Expression::Invalid
            }
        };
        *expr = new_expr;
    }
}

/// Call the visitor for each children of the element recursively, starting with the element itself
///
/// The item that is being visited will be pushed to the scope and popped once visitation is over.
fn recurse_elem_with_scope(
    elem: &ElementRc,
    mut scope: ComponentScope,
    vis: &mut impl FnMut(&ElementRc, &ComponentScope),
) -> ComponentScope {
    scope.0.push(elem.clone());
    vis(elem, &scope);
    for sub in &elem.borrow().children {
        scope = recurse_elem_with_scope(sub, scope, vis);
    }
    scope.0.pop();
    scope
}

pub fn resolve_expressions(
    doc: &Document,
    type_loader: &crate::typeloader::TypeLoader,
    diag: &mut BuildDiagnostics,
) {
    resolve_two_way_bindings(doc, &doc.local_registry, diag);

    for component in doc.inner_components.iter() {
        recurse_elem_with_scope(
            &component.root_element,
            ComponentScope(vec![]),
            &mut |elem, scope| {
                let mut is_repeated = elem.borrow().repeated.is_some();
                visit_element_expressions(elem, |expr, property_name, property_type| {
                    let scope = if is_repeated {
                        // The first expression is always the model and it needs to be resolved with the parent scope
                        debug_assert!(matches!(
                            elem.borrow().repeated.as_ref().unwrap().model,
                            Expression::Invalid
                        )); // should be Invalid because it is taken by the visit_element_expressions function

                        is_repeated = false;

                        debug_assert!(scope.0.len() > 1);
                        &scope.0[..scope.0.len() - 1]
                    } else {
                        &scope.0
                    };

                    resolve_expression(
                        expr,
                        property_name,
                        property_type(),
                        scope,
                        &doc.local_registry,
                        type_loader,
                        diag,
                    );
                });
            },
        );
    }
}

/// To be used in [`Expression::from_qualified_name_node`] to specify if the lookup is performed
/// for two ways binding (which happens before the models and other expressions are resolved),
/// or after that.
#[derive(Default)]
enum LookupPhase {
    #[default]
    UnspecifiedPhase,
    ResolvingTwoWayBindings,
}

impl Expression {
    pub fn from_binding_expression_node(node: SyntaxNode, ctx: &mut LookupCtx) -> Self {
        debug_assert_eq!(node.kind(), SyntaxKind::BindingExpression);
        let e = node
            .children()
            .find_map(|n| match n.kind() {
                SyntaxKind::Expression => Some(Self::from_expression_node(n.into(), ctx)),
                SyntaxKind::CodeBlock => Some(Self::from_codeblock_node(n.into(), ctx)),
                _ => None,
            })
            .unwrap_or(Self::Invalid);
        if ctx.property_type == Type::LogicalLength && e.ty() == Type::Percent {
            // See if a conversion from percentage to length is allowed
            const RELATIVE_TO_PARENT_PROPERTIES: &[&str] =
                &["width", "height", "preferred-width", "preferred-height"];
            let property_name = ctx.property_name.unwrap_or_default();
            if RELATIVE_TO_PARENT_PROPERTIES.contains(&property_name) {
                return e;
            } else {
                ctx.diag.push_error(
                    format!(
                        "Automatic conversion from percentage to length is only possible for the following properties: {}",
                        RELATIVE_TO_PARENT_PROPERTIES.join(", ")
                    ),
                    &node
                );
                return Expression::Invalid;
            }
        };
        if !matches!(ctx.property_type, Type::Callback { .. } | Type::Function { .. }) {
            e.maybe_convert_to(ctx.property_type.clone(), &node, ctx.diag)
        } else {
            // Binding to a callback or function shouldn't happen
            assert!(ctx.diag.has_errors());
            e
        }
    }

    fn from_codeblock_node(node: syntax_nodes::CodeBlock, ctx: &mut LookupCtx) -> Expression {
        debug_assert_eq!(node.kind(), SyntaxKind::CodeBlock);

        let mut statements_or_exprs = node
            .children()
            .filter_map(|n| match n.kind() {
                SyntaxKind::Expression => Some(Self::from_expression_node(n.into(), ctx)),
                SyntaxKind::ReturnStatement => Some(Self::from_return_statement(n.into(), ctx)),
                _ => None,
            })
            .collect::<Vec<_>>();

        let exit_points_and_return_types = statements_or_exprs
            .iter()
            .enumerate()
            .filter_map(|(index, statement_or_expr)| {
                if index == statements_or_exprs.len()
                    || matches!(statement_or_expr, Expression::ReturnStatement(..))
                {
                    Some((index, statement_or_expr.ty()))
                } else {
                    None
                }
            })
            .collect::<Vec<_>>();

        let common_return_type = Self::common_target_type_for_type_list(
            exit_points_and_return_types.iter().map(|(_, ty)| ty.clone()),
        );

        exit_points_and_return_types.into_iter().for_each(|(index, _)| {
            let mut expr = std::mem::replace(&mut statements_or_exprs[index], Expression::Invalid);
            expr = expr.maybe_convert_to(common_return_type.clone(), &node, ctx.diag);
            statements_or_exprs[index] = expr;
        });

        Expression::CodeBlock(statements_or_exprs)
    }

    fn from_return_statement(
        node: syntax_nodes::ReturnStatement,
        ctx: &mut LookupCtx,
    ) -> Expression {
        let return_type = ctx.return_type().clone();
        let e = node.Expression();
        if e.is_none() && !matches!(return_type, Type::Void | Type::Invalid) {
            ctx.diag.push_error(format!("Must return a value of type '{return_type}'"), &node);
        }
        Expression::ReturnStatement(e.map(|n| {
            Box::new(Self::from_expression_node(n, ctx).maybe_convert_to(
                return_type,
                &node,
                ctx.diag,
            ))
        }))
    }

    fn from_callback_connection(
        node: syntax_nodes::CallbackConnection,
        ctx: &mut LookupCtx,
    ) -> Expression {
        ctx.arguments =
            node.DeclaredIdentifier().map(|x| identifier_text(&x).unwrap_or_default()).collect();
        Self::from_codeblock_node(node.CodeBlock(), ctx).maybe_convert_to(
            ctx.return_type().clone(),
            &node,
            ctx.diag,
        )
    }

    fn from_function(node: syntax_nodes::Function, ctx: &mut LookupCtx) -> Expression {
        ctx.arguments = node
            .ArgumentDeclaration()
            .map(|x| identifier_text(&x.DeclaredIdentifier()).unwrap_or_default())
            .collect();
        Self::from_codeblock_node(node.CodeBlock(), ctx).maybe_convert_to(
            ctx.return_type().clone(),
            &node,
            ctx.diag,
        )
    }

    fn from_expression_node(node: syntax_nodes::Expression, ctx: &mut LookupCtx) -> Self {
        node.children_with_tokens()
            .find_map(|child| match child {
                NodeOrToken::Node(node) => match node.kind() {
                    SyntaxKind::Expression => Some(Self::from_expression_node(node.into(), ctx)),
                    SyntaxKind::AtImageUrl => Some(Self::from_at_image_url_node(node.into(), ctx)),
                    SyntaxKind::AtGradient => Some(Self::from_at_gradient(node.into(), ctx)),
                    SyntaxKind::AtTr => Some(Self::from_at_tr(node.into(), ctx)),
                    SyntaxKind::QualifiedName => {
                        let exp = Self::from_qualified_name_node(
                            node.clone().into(),
                            ctx,
                            LookupPhase::default(),
                        );
                        if matches!(exp.ty(), Type::Function { .. } | Type::Callback { .. }) {
                            ctx.diag.push_error(
                                format!(
                                    "'{}' must be called. Did you forgot the '()'?",
                                    QualifiedTypeName::from_node(node.clone().into())
                                ),
                                &node,
                            )
                        }
                        Some(exp)
                    }
                    SyntaxKind::FunctionCallExpression => {
                        Some(Self::from_function_call_node(node.into(), ctx))
                    }
                    SyntaxKind::MemberAccess => {
                        Some(Self::from_member_access_node(node.into(), ctx))
                    }
                    SyntaxKind::IndexExpression => {
                        Some(Self::from_index_expression_node(node.into(), ctx))
                    }
                    SyntaxKind::SelfAssignment => {
                        Some(Self::from_self_assignment_node(node.into(), ctx))
                    }
                    SyntaxKind::BinaryExpression => {
                        Some(Self::from_binary_expression_node(node.into(), ctx))
                    }
                    SyntaxKind::UnaryOpExpression => {
                        Some(Self::from_unaryop_expression_node(node.into(), ctx))
                    }
                    SyntaxKind::ConditionalExpression => {
                        Some(Self::from_conditional_expression_node(node.into(), ctx))
                    }
                    SyntaxKind::ObjectLiteral => {
                        Some(Self::from_object_literal_node(node.into(), ctx))
                    }
                    SyntaxKind::Array => Some(Self::from_array_node(node.into(), ctx)),
                    SyntaxKind::CodeBlock => Some(Self::from_codeblock_node(node.into(), ctx)),
                    SyntaxKind::StringTemplate => {
                        Some(Self::from_string_template_node(node.into(), ctx))
                    }
                    _ => None,
                },
                NodeOrToken::Token(token) => match token.kind() {
                    SyntaxKind::StringLiteral => Some(
                        crate::literals::unescape_string(token.text())
                            .map(Self::StringLiteral)
                            .unwrap_or_else(|| {
                                ctx.diag.push_error("Cannot parse string literal".into(), &token);
                                Self::Invalid
                            }),
                    ),
                    SyntaxKind::NumberLiteral => Some(
                        crate::literals::parse_number_literal(token.text().into()).unwrap_or_else(
                            |e| {
                                ctx.diag.push_error(e.to_string(), &node);
                                Self::Invalid
                            },
                        ),
                    ),
                    SyntaxKind::ColorLiteral => Some(
                        crate::literals::parse_color_literal(token.text())
                            .map(|i| Expression::Cast {
                                from: Box::new(Expression::NumberLiteral(i as _, Unit::None)),
                                to: Type::Color,
                            })
                            .unwrap_or_else(|| {
                                ctx.diag.push_error("Invalid color literal".into(), &node);
                                Self::Invalid
                            }),
                    ),

                    _ => None,
                },
            })
            .unwrap_or(Self::Invalid)
    }

    fn from_at_image_url_node(node: syntax_nodes::AtImageUrl, ctx: &mut LookupCtx) -> Self {
        let s = match node
            .child_text(SyntaxKind::StringLiteral)
            .and_then(|x| crate::literals::unescape_string(&x))
        {
            Some(s) => s,
            None => {
                ctx.diag.push_error("Cannot parse string literal".into(), &node);
                return Self::Invalid;
            }
        };

        if s.is_empty() {
            return Expression::ImageReference {
                resource_ref: ImageReference::None,
                source_location: Some(node.to_source_location()),
                nine_slice: None,
            };
        }

        let absolute_source_path = {
            let path = std::path::Path::new(&s);
            if crate::pathutils::is_absolute(path) {
                s
            } else {
                ctx.type_loader
                    .and_then(|loader| {
                        loader.resolve_import_path(Some(&(*node).clone().into()), &s)
                    })
                    .map(|i| i.0.to_string_lossy().into())
                    .unwrap_or_else(|| {
                        crate::pathutils::join(
                            &crate::pathutils::dirname(node.source_file.path()),
                            path,
                        )
                        .map(|p| p.to_string_lossy().into())
                        .unwrap_or(s.clone())
                    })
            }
        };

        let nine_slice = node
            .children_with_tokens()
            .filter_map(|n| n.into_token())
            .filter(|t| t.kind() == SyntaxKind::NumberLiteral)
            .map(|arg| {
                arg.text().parse().unwrap_or_else(|err: std::num::ParseIntError| {
                    match err.kind() {
                        IntErrorKind::PosOverflow | IntErrorKind::NegOverflow => {
                            ctx.diag.push_error("Number too big".into(), &arg)
                        }
                        IntErrorKind::InvalidDigit => ctx.diag.push_error(
                            "Border widths of a nine-slice can't have units".into(),
                            &arg,
                        ),
                        _ => ctx.diag.push_error("Cannot parse number literal".into(), &arg),
                    };
                    0u16
                })
            })
            .collect::<Vec<u16>>();

        let nine_slice = match nine_slice.as_slice() {
            [x] => Some([*x, *x, *x, *x]),
            [x, y] => Some([*x, *y, *x, *y]),
            [x, y, z, w] => Some([*x, *y, *z, *w]),
            [] => None,
            _ => {
                assert!(ctx.diag.has_errors());
                None
            }
        };

        Expression::ImageReference {
            resource_ref: ImageReference::AbsolutePath(absolute_source_path),
            source_location: Some(node.to_source_location()),
            nine_slice,
        }
    }

    fn from_at_gradient(node: syntax_nodes::AtGradient, ctx: &mut LookupCtx) -> Self {
        enum GradKind {
            Linear { angle: Box<Expression> },
            Radial,
        }

        let mut subs = node
            .children_with_tokens()
            .filter(|n| matches!(n.kind(), SyntaxKind::Comma | SyntaxKind::Expression));

        let grad_token = node.child_token(SyntaxKind::Identifier).unwrap();
        let grad_text = grad_token.text();

        let grad_kind = if grad_text.starts_with("linear") {
            let angle_expr = match subs.next() {
                Some(e) if e.kind() == SyntaxKind::Expression => {
                    syntax_nodes::Expression::from(e.into_node().unwrap())
                }
                _ => {
                    ctx.diag.push_error("Expected angle expression".into(), &node);
                    return Expression::Invalid;
                }
            };
            if subs.next().map_or(false, |s| s.kind() != SyntaxKind::Comma) {
                ctx.diag.push_error(
                    "Angle expression must be an angle followed by a comma".into(),
                    &node,
                );
                return Expression::Invalid;
            }
            let angle = Box::new(
                Expression::from_expression_node(angle_expr.clone(), ctx).maybe_convert_to(
                    Type::Angle,
                    &angle_expr,
                    ctx.diag,
                ),
            );
            GradKind::Linear { angle }
        } else if grad_text.starts_with("radial") {
            if !matches!(subs.next(), Some(NodeOrToken::Node(n)) if n.text().to_string().trim() == "circle")
            {
                ctx.diag.push_error("Expected 'circle': currently, only @radial-gradient(circle, ...) are supported".into(), &node);
                return Expression::Invalid;
            }
            let comma = subs.next();
            if matches!(&comma, Some(NodeOrToken::Node(n)) if n.text().to_string().trim() == "at") {
                ctx.diag.push_error("'at' in @radial-gradient is not yet supported".into(), &comma);
                return Expression::Invalid;
            }
            if comma.as_ref().map_or(false, |s| s.kind() != SyntaxKind::Comma) {
                ctx.diag.push_error(
                    "'circle' must be followed by a comma".into(),
                    comma.as_ref().map_or(&node, |x| x as &dyn Spanned),
                );
                return Expression::Invalid;
            }
            GradKind::Radial
        } else {
            // Parser should have ensured we have one of the linear or radial gradient
            panic!("Not a gradient {grad_text:?}");
        };

        let mut stops = vec![];
        enum Stop {
            Empty,
            Color(Expression),
            Finished,
        }
        let mut current_stop = Stop::Empty;
        for n in subs {
            if n.kind() == SyntaxKind::Comma {
                match std::mem::replace(&mut current_stop, Stop::Empty) {
                    Stop::Empty => {
                        ctx.diag.push_error("Expected expression".into(), &n);
                        break;
                    }
                    Stop::Finished => {}
                    Stop::Color(col) => stops.push((
                        col,
                        if stops.is_empty() {
                            Expression::NumberLiteral(0., Unit::None)
                        } else {
                            Expression::Invalid
                        },
                    )),
                }
            } else {
                // To facilitate color literal conversion, adjust the expected return type.
                let e = {
                    let old_property_type = std::mem::replace(&mut ctx.property_type, Type::Color);
                    let e =
                        Expression::from_expression_node(n.as_node().unwrap().clone().into(), ctx);
                    ctx.property_type = old_property_type;
                    e
                };
                match std::mem::replace(&mut current_stop, Stop::Finished) {
                    Stop::Empty => {
                        current_stop = Stop::Color(e.maybe_convert_to(Type::Color, &n, ctx.diag))
                    }
                    Stop::Finished => {
                        ctx.diag.push_error("Expected comma".into(), &n);
                        break;
                    }
                    Stop::Color(col) => {
                        stops.push((col, e.maybe_convert_to(Type::Float32, &n, ctx.diag)))
                    }
                }
            }
        }
        match current_stop {
            Stop::Color(col) => stops.push((col, Expression::NumberLiteral(1., Unit::None))),
            Stop::Empty => {
                if let Some((_, e @ Expression::Invalid)) = stops.last_mut() {
                    *e = Expression::NumberLiteral(1., Unit::None)
                }
            }
            Stop::Finished => (),
        };

        // Fix the stop so each has a position.
        let mut start = 0;
        while start < stops.len() {
            start += match stops[start..].iter().position(|s| matches!(s.1, Expression::Invalid)) {
                Some(p) => p,
                None => break,
            };
            let (before, rest) = stops.split_at_mut(start);
            let pos =
                rest.iter().position(|s| !matches!(s.1, Expression::Invalid)).unwrap_or(rest.len());
            if pos > 0 && pos < rest.len() {
                let (middle, after) = rest.split_at_mut(pos);
                let begin = before
                    .last()
                    .map(|s| &s.1)
                    .unwrap_or(&Expression::NumberLiteral(1., Unit::None));
                let end = &after.first().expect("The last should never be invalid").1;
                for (i, (_, e)) in middle.iter_mut().enumerate() {
                    debug_assert!(matches!(e, Expression::Invalid));
                    // e = begin + (i+1) * (end - begin) / (pos+1)
                    *e = Expression::BinaryExpression {
                        lhs: Box::new(begin.clone()),
                        rhs: Box::new(Expression::BinaryExpression {
                            lhs: Box::new(Expression::BinaryExpression {
                                lhs: Box::new(Expression::NumberLiteral(i as f64 + 1., Unit::None)),
                                rhs: Box::new(Expression::BinaryExpression {
                                    lhs: Box::new(end.clone()),
                                    rhs: Box::new(begin.clone()),
                                    op: '-',
                                }),
                                op: '*',
                            }),
                            rhs: Box::new(Expression::NumberLiteral(pos as f64 + 1., Unit::None)),
                            op: '/',
                        }),
                        op: '+',
                    };
                }
            }
            start += pos + 1;
        }

        match grad_kind {
            GradKind::Linear { angle } => Expression::LinearGradient { angle, stops },
            GradKind::Radial => Expression::RadialGradient { stops },
        }
    }

    fn from_at_tr(node: syntax_nodes::AtTr, ctx: &mut LookupCtx) -> Expression {
        let Some(string) = node
            .child_text(SyntaxKind::StringLiteral)
            .and_then(|s| crate::literals::unescape_string(&s))
        else {
            ctx.diag.push_error("Cannot parse string literal".into(), &node);
            return Expression::Invalid;
        };
        let context = node.TrContext().map(|n| {
            n.child_text(SyntaxKind::StringLiteral)
                .and_then(|s| crate::literals::unescape_string(&s))
                .unwrap_or_else(|| {
                    ctx.diag.push_error("Cannot parse string literal".into(), &n);
                    Default::default()
                })
        });
        let plural = node.TrPlural().map(|pl| {
            let s = pl
                .child_text(SyntaxKind::StringLiteral)
                .and_then(|s| crate::literals::unescape_string(&s))
                .unwrap_or_else(|| {
                    ctx.diag.push_error("Cannot parse string literal".into(), &pl);
                    Default::default()
                });
            let n = pl.Expression();
            let expr = Expression::from_expression_node(n.clone(), ctx).maybe_convert_to(
                Type::Int32,
                &n,
                ctx.diag,
            );
            (s, expr)
        });

        let domain = ctx
            .type_loader
            .and_then(|tl| tl.compiler_config.translation_domain.clone())
            .unwrap_or_default();

        let subs = node.Expression().map(|n| {
            Expression::from_expression_node(n.clone(), ctx).maybe_convert_to(
                Type::String,
                &n,
                ctx.diag,
            )
        });
        let values = subs.collect::<Vec<_>>();

        // check format string
        {
            let mut arg_idx = 0;
            let mut pos_max = 0;
            let mut pos = 0;
            let mut has_n = false;
            while let Some(mut p) = string[pos..].find(['{', '}']) {
                if string.len() - pos < p + 1 {
                    ctx.diag.push_error(
                        "Unescaped trailing '{' in format string. Escape '{' with '{{'".into(),
                        &node,
                    );
                    break;
                }
                p += pos;

                // Skip escaped }
                if string.get(p..=p) == Some("}") {
                    if string.get(p + 1..=p + 1) == Some("}") {
                        pos = p + 2;
                        continue;
                    } else {
                        ctx.diag.push_error(
                            "Unescaped '}' in format string. Escape '}' with '}}'".into(),
                            &node,
                        );
                        break;
                    }
                }

                // Skip escaped {
                if string.get(p + 1..=p + 1) == Some("{") {
                    pos = p + 2;
                    continue;
                }

                // Find the argument
                let end = if let Some(end) = string[p..].find('}') {
                    end + p
                } else {
                    ctx.diag.push_error(
                        "Unterminated placeholder in format string. '{' must be escaped with '{{'"
                            .into(),
                        &node,
                    );
                    break;
                };
                let argument = &string[p + 1..end];
                if argument.is_empty() {
                    arg_idx += 1;
                } else if let Ok(n) = argument.parse::<u16>() {
                    pos_max = pos_max.max(n as usize + 1);
                } else if argument == "n" {
                    has_n = true;
                    if plural.is_none() {
                        ctx.diag.push_error(
                            "`{n}` placeholder can only be found in plural form".into(),
                            &node,
                        );
                    }
                } else {
                    ctx.diag
                        .push_error("Invalid '{...}' placeholder in format string. The placeholder must be a number, or braces must be escaped with '{{' and '}}'".into(), &node);
                    break;
                };
                pos = end + 1;
            }
            if arg_idx > 0 && pos_max > 0 {
                ctx.diag.push_error(
                    "Cannot mix positional and non-positional placeholder in format string".into(),
                    &node,
                );
            } else if arg_idx > values.len() || pos_max > values.len() {
                let num = arg_idx.max(pos_max);
                let note = if !has_n && plural.is_some() {
                    ". Note: use `{n}` for the argument after '%'"
                } else {
                    ""
                };
                ctx.diag.push_error(
                    format!("Format string contains {num} placeholders, but only {} extra arguments were given{note}", values.len()),
                    &node,
                );
            }
        }

        let plural =
            plural.unwrap_or((SmolStr::default(), Expression::NumberLiteral(1., Unit::None)));

        let get_component_name = || {
            ctx.component_scope
                .first()
                .and_then(|e| e.borrow().enclosing_component.upgrade())
                .map(|c| c.id.clone())
        };

        Expression::FunctionCall {
            function: Box::new(Expression::BuiltinFunctionReference(
                BuiltinFunction::Translate,
                Some(node.to_source_location()),
            )),
            arguments: vec![
                Expression::StringLiteral(string),
                Expression::StringLiteral(context.or_else(get_component_name).unwrap_or_default()),
                Expression::StringLiteral(domain.into()),
                Expression::Array { element_ty: Type::String, values },
                plural.1,
                Expression::StringLiteral(plural.0),
            ],
            source_location: Some(node.to_source_location()),
        }
    }

    /// Perform the lookup
    fn from_qualified_name_node(
        node: syntax_nodes::QualifiedName,
        ctx: &mut LookupCtx,
        phase: LookupPhase,
    ) -> Self {
        let mut it = node
            .children_with_tokens()
            .filter(|n| n.kind() == SyntaxKind::Identifier)
            .filter_map(|n| n.into_token());

        let first = if let Some(first) = it.next() {
            first
        } else {
            // There must be at least one member (parser should ensure that)
            debug_assert!(ctx.diag.has_errors());
            return Self::Invalid;
        };

        ctx.current_token = Some(first.clone().into());
        let first_str = crate::parser::normalize_identifier(first.text());
        let global_lookup = crate::lookup::global_lookup();
        let result = match global_lookup.lookup(ctx, &first_str) {
            None => {
                if let Some(minus_pos) = first.text().find('-') {
                    // Attempt to recover if the user wanted to write "-" for minus
                    let first_str = &first.text()[0..minus_pos];
                    if global_lookup
                        .lookup(ctx, &crate::parser::normalize_identifier(first_str))
                        .is_some()
                    {
                        ctx.diag.push_error(format!("Unknown unqualified identifier '{}'. Use space before the '-' if you meant a subtraction", first.text()), &node);
                        return Expression::Invalid;
                    }
                }
                for (prefix, e) in
                    [("self", ctx.component_scope.last()), ("root", ctx.component_scope.first())]
                {
                    if let Some(e) = e {
                        if e.lookup(ctx, &first_str).is_some() {
                            ctx.diag.push_error(format!("Unknown unqualified identifier '{0}'. Did you mean '{prefix}.{0}'?", first.text()), &node);
                            return Expression::Invalid;
                        }
                    }
                }

                if it.next().is_some() {
                    ctx.diag.push_error(format!("Cannot access id '{}'", first.text()), &node);
                } else {
                    ctx.diag.push_error(
                        format!("Unknown unqualified identifier '{}'", first.text()),
                        &node,
                    );
                }
                return Expression::Invalid;
            }
            Some(x) => x,
        };

        if let Some(depr) = result.deprecated() {
            ctx.diag.push_property_deprecation_warning(&first_str, depr, &first);
        }

        match result {
            LookupResult::Expression { expression: Expression::ElementReference(e), .. } => {
                continue_lookup_within_element(&e.upgrade().unwrap(), &mut it, node, ctx)
            }
            LookupResult::Expression {
                expression: r @ Expression::CallbackReference(..), ..
            } => {
                if let Some(x) = it.next() {
                    ctx.diag.push_error("Cannot access fields of callback".into(), &x)
                }
                r
            }
            LookupResult::Expression {
                expression: r @ Expression::FunctionReference(..), ..
            } => {
                if let Some(x) = it.next() {
                    ctx.diag.push_error("Cannot access fields of a function".into(), &x)
                }
                r
            }
            LookupResult::Enumeration(enumeration) => {
                if let Some(next_identifier) = it.next() {
                    match enumeration
                        .lookup(ctx, &crate::parser::normalize_identifier(next_identifier.text()))
                    {
                        Some(LookupResult::Expression { expression, .. }) => {
                            maybe_lookup_object(expression, it, ctx)
                        }
                        _ => {
                            ctx.diag.push_error(
                                format!(
                                    "'{}' is not a member of the enum {}",
                                    next_identifier.text(),
                                    enumeration.name
                                ),
                                &next_identifier,
                            );
                            Expression::Invalid
                        }
                    }
                } else {
                    ctx.diag.push_error("Cannot take reference to an enum".to_string(), &node);
                    Expression::Invalid
                }
            }
            LookupResult::Expression {
                expression: Expression::RepeaterModelReference { .. },
                ..
            } if matches!(phase, LookupPhase::ResolvingTwoWayBindings) => {
                ctx.diag.push_error(
                    "Two-way bindings to model data is not supported yet".to_string(),
                    &node,
                );
                Expression::Invalid
            }
            LookupResult::Expression { expression, .. } => maybe_lookup_object(expression, it, ctx),
            LookupResult::Namespace(_) => {
                if let Some(next_identifier) = it.next() {
                    match result
                        .lookup(ctx, &crate::parser::normalize_identifier(next_identifier.text()))
                    {
                        Some(LookupResult::Expression { expression, .. }) => {
                            maybe_lookup_object(expression, it, ctx)
                        }
                        _ => {
                            ctx.diag.push_error(
                                format!(
                                    "'{}' is not a member of the namespace {}",
                                    next_identifier.text(),
                                    first_str
                                ),
                                &next_identifier,
                            );
                            Expression::Invalid
                        }
                    }
                } else {
                    ctx.diag.push_error("Cannot take reference to a namespace".to_string(), &node);
                    Expression::Invalid
                }
            }
        }
    }

    fn from_function_call_node(
        node: syntax_nodes::FunctionCallExpression,
        ctx: &mut LookupCtx,
    ) -> Expression {
        let mut arguments = Vec::new();

        let mut sub_expr = node.Expression();

        let function = sub_expr.next().map_or(Self::Invalid, |n| {
            // Treat the QualifiedName separately so we can catch the uses of uncalled signal
            n.QualifiedName()
                .map(|qn| Self::from_qualified_name_node(qn, ctx, LookupPhase::default()))
                .unwrap_or_else(|| Self::from_expression_node(n, ctx))
        });

        let sub_expr = sub_expr.map(|n| {
            (Self::from_expression_node(n.clone(), ctx), Some(NodeOrToken::from((*n).clone())))
        });

        let mut adjust_arg_count = 0;

        let function = match function {
            Expression::BuiltinMacroReference(mac, n) => {
                arguments.extend(sub_expr);
                return crate::builtin_macros::lower_macro(mac, n, arguments.into_iter(), ctx.diag);
            }
            Expression::MemberFunction { base, base_node, member } => {
                arguments.push((*base, base_node));
                if let Expression::BuiltinMacroReference(mac, n) = *member {
                    arguments.extend(sub_expr);
                    return crate::builtin_macros::lower_macro(
                        mac,
                        n,
                        arguments.into_iter(),
                        ctx.diag,
                    );
                }
                adjust_arg_count = 1;
                member
            }
            _ => Box::new(function),
        };
        arguments.extend(sub_expr);

        let arguments = match function.ty() {
            Type::Function(function) | Type::Callback(function) => {
                if arguments.len() != function.args.len() {
                    ctx.diag.push_error(
                        format!(
                            "The callback or function expects {} arguments, but {} are provided",
                            function.args.len() - adjust_arg_count,
                            arguments.len() - adjust_arg_count,
                        ),
                        &node,
                    );
                    arguments.into_iter().map(|x| x.0).collect()
                } else {
                    arguments
                        .into_iter()
                        .zip(function.args.iter())
                        .map(|((e, node), ty)| e.maybe_convert_to(ty.clone(), &node, ctx.diag))
                        .collect()
                }
            }
            Type::Invalid => {
                debug_assert!(
                    ctx.diag.has_errors() || {
                        let mut has_macro = false;
                        function.visit_recursive(&mut |e| {
                            if matches!(e, Expression::BuiltinMacroReference(..)) {
                                has_macro = true
                            }
                        });
                        has_macro
                    },
                    "The error must already have been reported. (unless it is a BuiltinMacroReference, then it will be reported later)"
                );
                arguments.into_iter().map(|x| x.0).collect()
            }
            _ => {
                ctx.diag.push_error("The expression is not a function".into(), &node);
                arguments.into_iter().map(|x| x.0).collect()
            }
        };

        Expression::FunctionCall {
            function,
            arguments,
            source_location: Some(node.to_source_location()),
        }
    }

    fn from_member_access_node(
        node: syntax_nodes::MemberAccess,
        ctx: &mut LookupCtx,
    ) -> Expression {
        let base = Self::from_expression_node(node.Expression(), ctx);
        maybe_lookup_object(base, node.child_token(SyntaxKind::Identifier).into_iter(), ctx)
    }

    fn from_self_assignment_node(
        node: syntax_nodes::SelfAssignment,
        ctx: &mut LookupCtx,
    ) -> Expression {
        let (lhs_n, rhs_n) = node.Expression();
        let mut lhs = Self::from_expression_node(lhs_n.clone(), ctx);
        let op = node
            .children_with_tokens()
            .find_map(|n| match n.kind() {
                SyntaxKind::PlusEqual => Some('+'),
                SyntaxKind::MinusEqual => Some('-'),
                SyntaxKind::StarEqual => Some('*'),
                SyntaxKind::DivEqual => Some('/'),
                SyntaxKind::Equal => Some('='),
                _ => None,
            })
            .unwrap_or('_');
        if lhs.ty() != Type::Invalid {
            lhs.try_set_rw(ctx, if op == '=' { "Assignment" } else { "Self assignment" }, &node);
        }
        let ty = lhs.ty();
        let expected_ty = match op {
            '=' => ty,
            '+' if ty == Type::String || ty.as_unit_product().is_some() => ty,
            '-' if ty.as_unit_product().is_some() => ty,
            '/' | '*' if ty.as_unit_product().is_some() => Type::Float32,
            _ => {
                if ty != Type::Invalid {
                    ctx.diag.push_error(
                        format!("the {}= operation cannot be done on a {}", op, ty),
                        &lhs_n,
                    );
                }
                Type::Invalid
            }
        };
        let rhs = Self::from_expression_node(rhs_n.clone(), ctx);
        Expression::SelfAssignment {
            lhs: Box::new(lhs),
            rhs: Box::new(rhs.maybe_convert_to(expected_ty, &rhs_n, ctx.diag)),
            op,
            node: Some(NodeOrToken::Node(node.into())),
        }
    }

    fn from_binary_expression_node(
        node: syntax_nodes::BinaryExpression,
        ctx: &mut LookupCtx,
    ) -> Expression {
        let op = node
            .children_with_tokens()
            .find_map(|n| match n.kind() {
                SyntaxKind::Plus => Some('+'),
                SyntaxKind::Minus => Some('-'),
                SyntaxKind::Star => Some('*'),
                SyntaxKind::Div => Some('/'),
                SyntaxKind::LessEqual => Some('≤'),
                SyntaxKind::GreaterEqual => Some('≥'),
                SyntaxKind::LAngle => Some('<'),
                SyntaxKind::RAngle => Some('>'),
                SyntaxKind::EqualEqual => Some('='),
                SyntaxKind::NotEqual => Some('!'),
                SyntaxKind::AndAnd => Some('&'),
                SyntaxKind::OrOr => Some('|'),
                _ => None,
            })
            .unwrap_or('_');

        let (lhs_n, rhs_n) = node.Expression();
        let lhs = Self::from_expression_node(lhs_n.clone(), ctx);
        let rhs = Self::from_expression_node(rhs_n.clone(), ctx);

        let expected_ty = match operator_class(op) {
            OperatorClass::ComparisonOp => {
                Self::common_target_type_for_type_list([lhs.ty(), rhs.ty()].iter().cloned())
            }
            OperatorClass::LogicalOp => Type::Bool,
            OperatorClass::ArithmeticOp => {
                let (lhs_ty, rhs_ty) = (lhs.ty(), rhs.ty());
                if op == '+' && (lhs_ty == Type::String || rhs_ty == Type::String) {
                    Type::String
                } else if op == '+' || op == '-' {
                    if lhs_ty.default_unit().is_some() {
                        lhs_ty
                    } else if rhs_ty.default_unit().is_some() {
                        rhs_ty
                    } else if matches!(lhs_ty, Type::UnitProduct(_)) {
                        lhs_ty
                    } else if matches!(rhs_ty, Type::UnitProduct(_)) {
                        rhs_ty
                    } else {
                        Type::Float32
                    }
                } else if op == '*' || op == '/' {
                    let has_unit = |ty: &Type| {
                        matches!(ty, Type::UnitProduct(_)) || ty.default_unit().is_some()
                    };
                    match (has_unit(&lhs_ty), has_unit(&rhs_ty)) {
                        (true, true) => {
                            return Expression::BinaryExpression {
                                lhs: Box::new(lhs),
                                rhs: Box::new(rhs),
                                op,
                            }
                        }
                        (true, false) => {
                            return Expression::BinaryExpression {
                                lhs: Box::new(lhs),
                                rhs: Box::new(rhs.maybe_convert_to(
                                    Type::Float32,
                                    &rhs_n,
                                    ctx.diag,
                                )),
                                op,
                            }
                        }
                        (false, true) => {
                            return Expression::BinaryExpression {
                                lhs: Box::new(lhs.maybe_convert_to(
                                    Type::Float32,
                                    &lhs_n,
                                    ctx.diag,
                                )),
                                rhs: Box::new(rhs),
                                op,
                            }
                        }
                        (false, false) => Type::Float32,
                    }
                } else {
                    unreachable!()
                }
            }
        };
        Expression::BinaryExpression {
            lhs: Box::new(lhs.maybe_convert_to(expected_ty.clone(), &lhs_n, ctx.diag)),
            rhs: Box::new(rhs.maybe_convert_to(expected_ty, &rhs_n, ctx.diag)),
            op,
        }
    }

    fn from_unaryop_expression_node(
        node: syntax_nodes::UnaryOpExpression,
        ctx: &mut LookupCtx,
    ) -> Expression {
        let exp_n = node.Expression();
        let exp = Self::from_expression_node(exp_n, ctx);

        let op = node
            .children_with_tokens()
            .find_map(|n| match n.kind() {
                SyntaxKind::Plus => Some('+'),
                SyntaxKind::Minus => Some('-'),
                SyntaxKind::Bang => Some('!'),
                _ => None,
            })
            .unwrap_or('_');

        let exp = match op {
            '!' => exp.maybe_convert_to(Type::Bool, &node, ctx.diag),
            '+' | '-' => {
                let ty = exp.ty();
                if ty.default_unit().is_none()
                    && !matches!(
                        ty,
                        Type::Int32
                            | Type::Float32
                            | Type::Percent
                            | Type::UnitProduct(..)
                            | Type::Invalid
                    )
                {
                    ctx.diag.push_error(format!("Unary '{op}' not supported on {ty}"), &node);
                }
                exp
            }
            _ => {
                assert!(ctx.diag.has_errors());
                exp
            }
        };

        Expression::UnaryOp { sub: Box::new(exp), op }
    }

    fn from_conditional_expression_node(
        node: syntax_nodes::ConditionalExpression,
        ctx: &mut LookupCtx,
    ) -> Expression {
        let (condition_n, true_expr_n, false_expr_n) = node.Expression();
        // FIXME: we should we add bool to the context
        let condition = Self::from_expression_node(condition_n.clone(), ctx).maybe_convert_to(
            Type::Bool,
            &condition_n,
            ctx.diag,
        );
        let true_expr = Self::from_expression_node(true_expr_n.clone(), ctx);
        let false_expr = Self::from_expression_node(false_expr_n.clone(), ctx);
        let result_ty = Self::common_target_type_for_type_list(
            [true_expr.ty(), false_expr.ty()].iter().cloned(),
        );
        let true_expr = true_expr.maybe_convert_to(result_ty.clone(), &true_expr_n, ctx.diag);
        let false_expr = false_expr.maybe_convert_to(result_ty, &false_expr_n, ctx.diag);
        Expression::Condition {
            condition: Box::new(condition),
            true_expr: Box::new(true_expr),
            false_expr: Box::new(false_expr),
        }
    }

    fn from_index_expression_node(
        node: syntax_nodes::IndexExpression,
        ctx: &mut LookupCtx,
    ) -> Expression {
        let (array_expr_n, index_expr_n) = node.Expression();
        let array_expr = Self::from_expression_node(array_expr_n, ctx);
        let index_expr = Self::from_expression_node(index_expr_n.clone(), ctx).maybe_convert_to(
            Type::Int32,
            &index_expr_n,
            ctx.diag,
        );

        let ty = array_expr.ty();
        if !matches!(ty, Type::Array(_) | Type::Invalid | Type::Function(_) | Type::Callback(_)) {
            ctx.diag.push_error(format!("{} is not an indexable type", ty), &node);
        }
        Expression::ArrayIndex { array: Box::new(array_expr), index: Box::new(index_expr) }
    }

    fn from_object_literal_node(
        node: syntax_nodes::ObjectLiteral,
        ctx: &mut LookupCtx,
    ) -> Expression {
        let values: HashMap<SmolStr, Expression> = node
            .ObjectMember()
            .map(|n| {
                (
                    identifier_text(&n).unwrap_or_default(),
                    Expression::from_expression_node(n.Expression(), ctx),
                )
            })
            .collect();
        let ty = Type::Struct(Rc::new(Struct {
            fields: values.iter().map(|(k, v)| (k.clone(), v.ty())).collect(),
            name: None,
            node: None,
            rust_attributes: None,
        }));
        Expression::Struct { ty, values }
    }

    fn from_array_node(node: syntax_nodes::Array, ctx: &mut LookupCtx) -> Expression {
        let mut values: Vec<Expression> =
            node.Expression().map(|e| Expression::from_expression_node(e, ctx)).collect();

        let element_ty = if values.is_empty() {
            Type::Void
        } else {
            Self::common_target_type_for_type_list(values.iter().map(|expr| expr.ty()))
        };

        for e in values.iter_mut() {
            *e = core::mem::replace(e, Expression::Invalid).maybe_convert_to(
                element_ty.clone(),
                &node,
                ctx.diag,
            );
        }

        Expression::Array { element_ty, values }
    }

    fn from_string_template_node(
        node: syntax_nodes::StringTemplate,
        ctx: &mut LookupCtx,
    ) -> Expression {
        let mut exprs = node.Expression().map(|e| {
            Expression::from_expression_node(e.clone(), ctx).maybe_convert_to(
                Type::String,
                &e,
                ctx.diag,
            )
        });
        let mut result = exprs.next().unwrap_or_default();
        for x in exprs {
            result = Expression::BinaryExpression {
                lhs: Box::new(std::mem::take(&mut result)),
                rhs: Box::new(x),
                op: '+',
            }
        }
        result
    }

    /// This function is used to find a type that's suitable for casting each instance of a bunch of expressions
    /// to a type that captures most aspects. For example for an array of object literals the result is a merge of
    /// all seen fields.
    pub fn common_target_type_for_type_list(types: impl Iterator<Item = Type>) -> Type {
        types.fold(Type::Invalid, |target_type, expr_ty| {
            if target_type == expr_ty {
                target_type
            } else if target_type == Type::Invalid {
                expr_ty
            } else {
                match (target_type, expr_ty) {
                    (Type::Struct(ref result), Type::Struct(ref elem)) => {
                        let mut fields = result.fields.clone();
                        for (elem_name, elem_ty) in elem.fields.iter() {
                            match fields.entry(elem_name.clone()) {
                                std::collections::btree_map::Entry::Vacant(free_entry) => {
                                    free_entry.insert(elem_ty.clone());
                                }
                                std::collections::btree_map::Entry::Occupied(
                                    mut existing_field,
                                ) => {
                                    *existing_field.get_mut() =
                                        Self::common_target_type_for_type_list(
                                            [existing_field.get().clone(), elem_ty.clone()]
                                                .into_iter(),
                                        );
                                }
                            }
                        }
                        Type::Struct(Rc::new(Struct {
                            name: result.name.as_ref().or(elem.name.as_ref()).cloned(),
                            fields,
                            node: result.node.as_ref().or(elem.node.as_ref()).cloned(),
                            rust_attributes: result
                                .rust_attributes
                                .as_ref()
                                .or(elem.rust_attributes.as_ref())
                                .cloned(),
                        }))
                    }
                    (Type::Array(lhs), Type::Array(rhs)) => Type::Array(if *lhs == Type::Void {
                        rhs
                    } else if *rhs == Type::Void {
                        lhs
                    } else {
                        Self::common_target_type_for_type_list(
                            [(*lhs).clone(), (*rhs).clone()].into_iter(),
                        )
                        .into()
                    }),
                    (Type::Color, Type::Brush) | (Type::Brush, Type::Color) => Type::Brush,
                    (target_type, expr_ty) => {
                        if expr_ty.can_convert(&target_type) {
                            target_type
                        } else if target_type.can_convert(&expr_ty)
                            || (expr_ty.default_unit().is_some()
                                && matches!(target_type, Type::Float32 | Type::Int32))
                        {
                            // in the or case: The `0` literal.
                            expr_ty
                        } else {
                            // otherwise, use the target type and let further conversion report an error
                            target_type
                        }
                    }
                }
            }
        })
    }
}

fn continue_lookup_within_element(
    elem: &ElementRc,
    it: &mut impl Iterator<Item = crate::parser::SyntaxToken>,
    node: syntax_nodes::QualifiedName,
    ctx: &mut LookupCtx,
) -> Expression {
    let second = if let Some(second) = it.next() {
        second
    } else if matches!(ctx.property_type, Type::ElementReference) {
        return Expression::ElementReference(Rc::downgrade(elem));
    } else {
        // Try to recover in case we wanted to access a property
        let mut rest = String::new();
        if let Some(LookupResult::Expression {
            expression: Expression::PropertyReference(nr),
            ..
        }) = crate::lookup::InScopeLookup.lookup(ctx, &elem.borrow().id)
        {
            let e = nr.element();
            let e_borrowed = e.borrow();
            let mut id = e_borrowed.id.as_str();
            if id.is_empty() {
                if ctx.component_scope.last().map_or(false, |x| Rc::ptr_eq(&e, x)) {
                    id = "self";
                } else if ctx.component_scope.first().map_or(false, |x| Rc::ptr_eq(&e, x)) {
                    id = "root";
                } else if ctx
                    .component_scope
                    .iter()
                    .nth_back(1)
                    .map_or(false, |x| Rc::ptr_eq(&e, x))
                {
                    id = "parent";
                }
            };
            if !id.is_empty() {
                rest =
                    format!(". Use '{id}.{}' to access the property with the same name", nr.name());
            }
        } else if let Some(LookupResult::Expression {
            expression: Expression::EnumerationValue(value),
            ..
        }) = crate::lookup::ReturnTypeSpecificLookup.lookup(ctx, &elem.borrow().id)
        {
            rest = format!(
                ". Use '{}.{value}' to access the enumeration value",
                value.enumeration.name
            );
        }
        ctx.diag.push_error(format!("Cannot take reference of an element{rest}"), &node);
        return Expression::Invalid;
    };
    let prop_name = crate::parser::normalize_identifier(second.text());

    let lookup_result = elem.borrow().lookup_property(&prop_name);
    let local_to_component = lookup_result.is_local_to_component && ctx.is_local_element(elem);

    if lookup_result.property_type.is_property_type() {
        if !local_to_component && lookup_result.property_visibility == PropertyVisibility::Private {
            ctx.diag.push_error(format!("The property '{}' is private. Annotate it with 'in', 'out' or 'in-out' to make it accessible from other components", second.text()), &second);
            return Expression::Invalid;
        } else if lookup_result.property_visibility == PropertyVisibility::Fake {
            ctx.diag.push_error(format!("This special property can only be used to make a binding and cannot be accessed"), &second);
            return Expression::Invalid;
        } else if lookup_result.resolved_name != prop_name.as_str() {
            ctx.diag.push_property_deprecation_warning(
                &prop_name,
                &lookup_result.resolved_name,
                &second,
            );
        }
        let prop = Expression::PropertyReference(NamedReference::new(
            elem,
            lookup_result.resolved_name.to_smolstr(),
        ));
        maybe_lookup_object(prop, it, ctx)
    } else if matches!(lookup_result.property_type, Type::Callback { .. }) {
        if let Some(x) = it.next() {
            ctx.diag.push_error("Cannot access fields of callback".into(), &x)
        }
        Expression::CallbackReference(
            NamedReference::new(elem, lookup_result.resolved_name.to_smolstr()),
            Some(NodeOrToken::Token(second)),
        )
    } else if matches!(lookup_result.property_type, Type::Function { .. }) {
        if lookup_result.property_visibility == PropertyVisibility::Private && !local_to_component {
            let message = format!("The function '{}' is private. Annotate it with 'public' to make it accessible from other components", second.text());
            if !lookup_result.is_local_to_component {
                ctx.diag.push_error(message, &second);
            } else {
                ctx.diag.push_warning(message+". Note: this used to be allowed in previous version, but this should be considered an error", &second);
            }
        } else if lookup_result.property_visibility == PropertyVisibility::Protected
            && !local_to_component
            && !(lookup_result.is_in_direct_base
                && ctx.component_scope.first().map_or(false, |x| Rc::ptr_eq(x, elem)))
        {
            ctx.diag.push_error(format!("The function '{}' is protected", second.text()), &second);
        }
        if let Some(x) = it.next() {
            ctx.diag.push_error("Cannot access fields of a function".into(), &x)
        }
        if let Some(f) =
            elem.borrow().base_type.lookup_member_function(&lookup_result.resolved_name)
        {
            // builtin member function
            Expression::MemberFunction {
                base: Box::new(Expression::ElementReference(Rc::downgrade(elem))),
                base_node: Some(NodeOrToken::Node(node.into())),
                member: Expression::BuiltinFunctionReference(f, Some(second.to_source_location()))
                    .into(),
            }
        } else {
            Expression::FunctionReference(
                NamedReference::new(elem, lookup_result.resolved_name.to_smolstr()),
                Some(NodeOrToken::Token(second)),
            )
        }
    } else {
        let mut err = |extra: &str| {
            let what = match &elem.borrow().base_type {
                ElementType::Global => {
                    let global = elem.borrow().enclosing_component.upgrade().unwrap();
                    assert!(global.is_global());
                    format!("'{}'", global.id)
                }
                ElementType::Component(c) => format!("Element '{}'", c.id),
                ElementType::Builtin(b) => format!("Element '{}'", b.name),
                ElementType::Native(_) => unreachable!("the native pass comes later"),
                ElementType::Error => {
                    assert!(ctx.diag.has_errors());
                    return;
                }
            };
            ctx.diag.push_error(
                format!("{} does not have a property '{}'{}", what, second.text(), extra),
                &second,
            );
        };
        if let Some(minus_pos) = second.text().find('-') {
            // Attempt to recover if the user wanted to write "-"
            if elem
                .borrow()
                .lookup_property(&crate::parser::normalize_identifier(&second.text()[0..minus_pos]))
                .property_type
                != Type::Invalid
            {
                err(". Use space before the '-' if you meant a subtraction");
                return Expression::Invalid;
            }
        }
        err("");
        Expression::Invalid
    }
}

fn maybe_lookup_object(
    mut base: Expression,
    it: impl Iterator<Item = crate::parser::SyntaxToken>,
    ctx: &mut LookupCtx,
) -> Expression {
    for next in it {
        let next_str = crate::parser::normalize_identifier(next.text());
        ctx.current_token = Some(next.clone().into());
        match base.lookup(ctx, &next_str) {
            Some(LookupResult::Expression { expression, .. }) => {
                base = expression;
            }
            _ => {
                if let Some(minus_pos) = next.text().find('-') {
                    if base.lookup(ctx, &SmolStr::new(&next.text()[0..minus_pos])).is_some() {
                        ctx.diag.push_error(format!("Cannot access the field '{}'. Use space before the '-' if you meant a subtraction", next.text()), &next);
                        return Expression::Invalid;
                    }
                }
                let ty_descr = match base.ty() {
                    Type::Struct { .. } => String::new(),
                    ty => format!(" of {}", ty),
                };
                ctx.diag.push_error(
                    format!("Cannot access the field '{}'{}", next.text(), ty_descr),
                    &next,
                );
                return Expression::Invalid;
            }
        }
    }
    base
}

/// Go through all the two way binding and resolve them first
fn resolve_two_way_bindings(
    doc: &Document,
    type_register: &TypeRegister,
    diag: &mut BuildDiagnostics,
) {
    for component in doc.inner_components.iter() {
        recurse_elem_with_scope(
            &component.root_element,
            ComponentScope(vec![]),
            &mut |elem, scope| {
                for (prop_name, binding) in &elem.borrow().bindings {
                    let mut binding = binding.borrow_mut();
                    if let Expression::Uncompiled(node) = binding.expression.clone() {
                        if let Some(n) = syntax_nodes::TwoWayBinding::new(node.clone()) {
                            let lhs_lookup = elem.borrow().lookup_property(prop_name);
                            if !lhs_lookup.is_valid() {
                                // An attempt to resolve this already failed when trying to resolve the property type
                                assert!(diag.has_errors());
                                continue;
                            }
                            let mut lookup_ctx = LookupCtx {
                                property_name: Some(prop_name.as_str()),
                                property_type: lhs_lookup.property_type.clone(),
                                component_scope: &scope.0,
                                diag,
                                arguments: vec![],
                                type_register,
                                type_loader: None,
                                current_token: Some(node.clone().into()),
                            };

                            binding.expression = Expression::Invalid;

                            if let Some(nr) = resolve_two_way_binding(n, &mut lookup_ctx) {
                                binding.two_way_bindings.push(nr.clone());

                                nr.element()
                                    .borrow()
                                    .property_analysis
                                    .borrow_mut()
                                    .entry(nr.name().clone())
                                    .or_default()
                                    .is_linked = true;

                                if matches!(
                                    lhs_lookup.property_visibility,
                                    PropertyVisibility::Private | PropertyVisibility::Output
                                ) && !lhs_lookup.is_local_to_component
                                {
                                    // invalid property assignment should have been reported earlier
                                    assert!(diag.has_errors());
                                    continue;
                                }

                                // Check the compatibility.
                                let mut rhs_lookup =
                                    nr.element().borrow().lookup_property(nr.name());
                                if rhs_lookup.property_type == Type::Invalid {
                                    // An attempt to resolve this already failed when trying to resolve the property type
                                    assert!(diag.has_errors());
                                    continue;
                                }
                                rhs_lookup.is_local_to_component &=
                                    lookup_ctx.is_local_element(&nr.element());

                                if !rhs_lookup.is_valid_for_assignment() {
                                    match (
                                        lhs_lookup.property_visibility,
                                        rhs_lookup.property_visibility,
                                    ) {
                                        (PropertyVisibility::Input, PropertyVisibility::Input)
                                            if !lhs_lookup.is_local_to_component =>
                                        {
                                            assert!(rhs_lookup.is_local_to_component);
                                            marked_linked_read_only(elem, prop_name);
                                        }
                                        (
                                            PropertyVisibility::Output
                                            | PropertyVisibility::Private,
                                            PropertyVisibility::Output | PropertyVisibility::Input,
                                        ) => {
                                            assert!(lhs_lookup.is_local_to_component);
                                            marked_linked_read_only(elem, prop_name);
                                        }
                                        (PropertyVisibility::Input, PropertyVisibility::Output)
                                            if !lhs_lookup.is_local_to_component =>
                                        {
                                            assert!(!rhs_lookup.is_local_to_component);
                                            marked_linked_read_only(elem, prop_name);
                                        }
                                        _ => {
                                            if lookup_ctx.is_legacy_component() {
                                                diag.push_warning(
                                                    format!(
                                                        "Link to a {} property is deprecated",
                                                        rhs_lookup.property_visibility
                                                    ),
                                                    &node,
                                                );
                                            } else {
                                                diag.push_error(
                                                    format!(
                                                        "Cannot link to a {} property",
                                                        rhs_lookup.property_visibility
                                                    ),
                                                    &node,
                                                )
                                            }
                                        }
                                    }
                                } else if !lhs_lookup.is_valid_for_assignment() {
                                    if rhs_lookup.is_local_to_component
                                        && rhs_lookup.property_visibility
                                            == PropertyVisibility::InOut
                                    {
                                        if lookup_ctx.is_legacy_component() {
                                            debug_assert!(!diag.is_empty()); // warning should already be reported
                                        } else {
                                            diag.push_error(
                                                "Cannot link input property".into(),
                                                &node,
                                            );
                                        }
                                    } else if rhs_lookup.property_visibility
                                        == PropertyVisibility::InOut
                                    {
                                        diag.push_warning("Linking input properties to input output properties is deprecated".into(), &node);
                                        marked_linked_read_only(&nr.element(), nr.name());
                                    } else {
                                        // This is allowed, but then the rhs must also become read only.
                                        marked_linked_read_only(&nr.element(), nr.name());
                                    }
                                }
                            }
                        }
                    }
                }
            },
        );
    }

    fn marked_linked_read_only(elem: &ElementRc, prop_name: &str) {
        elem.borrow()
            .property_analysis
            .borrow_mut()
            .entry(prop_name.into())
            .or_default()
            .is_linked_to_read_only = true;
    }
}

pub fn resolve_two_way_binding(
    node: syntax_nodes::TwoWayBinding,
    ctx: &mut LookupCtx,
) -> Option<NamedReference> {
    let e = if let Some(n) = node.Expression().QualifiedName() {
        Expression::from_qualified_name_node(n, ctx, LookupPhase::ResolvingTwoWayBindings)
    } else {
        ctx.diag.push_error(
            "The expression in a two way binding must be a property reference".into(),
            &node.Expression(),
        );
        return None;
    };
    // If type is invalid, error has already been reported,  when inferring, the error will be reported by the inferring code
    let report_error = !matches!(
        ctx.property_type,
        Type::InferredProperty | Type::InferredCallback | Type::Invalid
    );
    let ty = e.ty();
    match e {
        Expression::PropertyReference(n) => {
            if report_error && ty != ctx.property_type {
                ctx.diag.push_error(
                    "The property does not have the same type as the bound property".into(),
                    &node,
                );
            }
            Some(n)
        }
        Expression::CallbackReference(n, _) => {
            if report_error && ty != ctx.property_type {
                ctx.diag.push_error("Cannot bind to a callback".into(), &node);
                None
            } else {
                Some(n)
            }
        }
        Expression::FunctionReference(..) => {
            if report_error {
                ctx.diag.push_error("Cannot bind to a function".into(), &node);
            }
            None
        }
        Expression::Invalid => {
            debug_assert!(ctx.diag.has_errors());
            None
        }
        _ => {
            ctx.diag.push_error(
                "The expression in a two way binding must be a property reference".into(),
                &node,
            );
            None
        }
    }
}
