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

//! This pass resolves the type of two way bindings.
//!
//! Before this pass, two way bindings that did not specify the type have Type::Void
//! type and their bindings are still a Expression::Uncompiled.
//! This pass will attempt to assign a type to these based on the type of property they alias.

use crate::diagnostics::BuildDiagnostics;
use crate::expression_tree::TwoWayBinding;
use crate::langtype::Type;
use crate::lookup::LookupCtx;
use crate::object_tree::{Document, ElementRc};
use crate::symbol_counters::SymbolCounters;
use crate::typeregister::TypeRegister;
use std::rc::Rc;

#[derive(Clone)]
struct ComponentScope(Vec<ElementRc>);

pub fn resolve_aliases(
    doc: &Document,
    diag: &mut BuildDiagnostics,
    symbol_counters: &Rc<SymbolCounters>,
) {
    for component in doc.inner_components.iter() {
        let scope = ComponentScope(Vec::new());
        crate::object_tree::recurse_elem_no_borrow(
            &component.root_element,
            &scope,
            &mut |elem, scope| {
                let mut new_scope = scope.clone();
                new_scope.0.push(elem.clone());

                let mut need_resolving = Vec::new();
                for (prop, decl) in elem.borrow().property_declarations.iter() {
                    if matches!(decl.property_type, Type::InferredProperty | Type::InferredCallback)
                    {
                        need_resolving.push(prop.clone());
                    }
                }
                // make it deterministic
                need_resolving.sort();
                for n in need_resolving {
                    resolve_alias(elem, &n, &new_scope, &doc.local_registry, diag, symbol_counters);
                }
                new_scope
            },
        );
    }
}

fn resolve_alias(
    elem: &ElementRc,
    prop: &str,
    scope: &ComponentScope,
    type_register: &TypeRegister,
    diag: &mut BuildDiagnostics,
    symbol_counters: &Rc<SymbolCounters>,
) {
    let mut borrow_mut = elem.borrow_mut();
    let old_type = match borrow_mut.property_declarations.get_mut(prop) {
        Some(decl) => {
            if !matches!(decl.property_type, Type::InferredCallback | Type::InferredProperty) {
                // already processed;
                return;
            };
            // mark the type as invalid now so that we catch recursion
            std::mem::replace(&mut decl.property_type, Type::Invalid)
        }
        None => {
            // Unresolved callback from a base component?
            debug_assert!(matches!(
                borrow_mut.lookup_property(prop).property_type,
                Type::InferredCallback | Type::InferredProperty
            ));
            // It is still unresolved because there is an error in that component
            assert!(diag.has_errors());
            return;
        }
    };
    drop(borrow_mut);

    let twb = {
        let Some(node) = elem.borrow().two_way_binding_node(prop) else {
            // The parser only allows omitting the type for a two-way binding, so a missing
            // alias node here means an error was already reported for this component.
            assert!(diag.has_errors(), "The parser only avoid missing types for two way bindings");
            return;
        };
        let mut lookup_ctx = LookupCtx::empty_context(type_register, diag, symbol_counters.clone());
        lookup_ctx.property_name = Some(prop);
        lookup_ctx.property_type = old_type.clone();
        lookup_ctx.expected_type = old_type.clone();
        lookup_ctx.component_scope = &scope.0;
        crate::passes::resolving::resolve_two_way_binding(node, &mut lookup_ctx)
    };

    let mut ty = Type::Invalid;
    match &twb {
        Some(twb @ TwoWayBinding::Property { property, .. }) => {
            let element = property.element();
            let same_element = Rc::ptr_eq(&element, elem);
            if same_element && property.name() == prop {
                diag.push_error(
                    "Cannot alias to itself".to_string(),
                    &elem.borrow().property_declarations[prop].type_node(),
                );
                return;
            }
            ty = twb.ty();
            if matches!(ty, Type::InferredCallback | Type::InferredProperty) {
                let s = if same_element { scope } else { &recompute_scope(&element) };
                resolve_alias(&element, property.name(), s, type_register, diag, symbol_counters);
                ty = twb.ty();
            }
        }
        Some(TwoWayBinding::ModelData { .. }) if old_type == Type::InferredProperty => {
            // Defer to `resolve_expressions`: the model expression is not
            // resolved yet. Restore the `InferredProperty` marker
            // (replaced by `Invalid` above as the recursion guard).
            elem.borrow_mut().property_declarations.get_mut(prop).unwrap().property_type = old_type;
            return;
        }
        Some(TwoWayBinding::ModelData { .. }) => {
            // `InferredCallback`: fall through with `ty = Invalid` so the
            // callback check below reports the error.
        }
        None => (),
    };

    if old_type == Type::InferredProperty {
        if !ty.is_property_type() {
            diag.push_error(
                format!("Could not infer type of property '{prop}'"),
                &elem.borrow().property_declarations[prop].type_node(),
            );
        } else {
            elem.borrow_mut().property_declarations.get_mut(prop).unwrap().property_type = ty;
        }
    } else if old_type == Type::InferredCallback {
        if !matches!(ty, Type::Callback { .. }) {
            if twb.is_some() && ty == Type::Invalid {
                debug_assert!(diag.has_errors());
            } else {
                diag.push_error(
                    format!("Binding to callback '{prop}' must bind to another callback"),
                    &elem.borrow().property_declarations[prop].type_node(),
                );
            }
        } else if let Some(nr) = twb.unwrap().property() {
            let target_is_global =
                nr.element().borrow().base_type == crate::langtype::ElementType::Global;
            let purity = nr.element().borrow().lookup_property(nr.name()).declared_pure;
            // A global aliasing another global's callback is the supported way for one
            // global to implement another's callback, so it isn't deprecated.
            let aliasing_global = elem.borrow().base_type == crate::langtype::ElementType::Global;
            let mut elem = elem.borrow_mut();
            let decl = elem.property_declarations.get_mut(prop).unwrap();
            if decl.pure.unwrap_or(false) != purity.unwrap_or(false) {
                diag.push_error(
                    format!("Purity of callbacks '{prop}' and '{nr:?}' doesn't match"),
                    &decl.type_node(),
                );
            }
            if target_is_global && !aliasing_global {
                diag.push_warning("Aliases to global callback are deprecated. Export the global to access the global callback directly from native code".into(), &decl.node);
            }
            decl.property_type = ty;
        }
    }
}

/// Recompute the scope of element
///
/// (since there is no parent mapping, we need to recursively search for the element)
fn recompute_scope(element: &ElementRc) -> ComponentScope {
    fn recurse(
        base: &ElementRc,
        needle: &ElementRc,
        scope: &mut Vec<ElementRc>,
    ) -> std::ops::ControlFlow<()> {
        scope.push(base.clone());
        if Rc::ptr_eq(base, needle) {
            return std::ops::ControlFlow::Break(());
        }
        for child in &base.borrow().children {
            if recurse(child, needle, scope).is_break() {
                return std::ops::ControlFlow::Break(());
            }
        }
        scope.pop();
        std::ops::ControlFlow::Continue(())
    }

    let root = element.borrow().enclosing_component.upgrade().unwrap().root_element.clone();
    let mut scope = Vec::new();
    let _ = recurse(&root, element, &mut scope);
    ComponentScope(scope)
}
