// 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 fills the Property::use_count
//!
//! It assumes that use_count of all properties is zero initially

use crate::llr::{
    Animation, BindingExpression, CompilationUnit, EvaluationContext, Expression,
    LocalMemberReference, MemberReference, ParentScope,
};

pub fn count_property_use(root: &CompilationUnit) {
    // Visit the root properties that are used.
    // 1. the public properties
    for c in &root.public_components {
        let root_ctx = EvaluationContext::new_sub_component(root, c.item_tree.root, (), None);
        for p in c.public_properties.values() {
            // A public function can be called from native code, so it is a root.
            visit_property(&p.prop, &root_ctx);
        }
    }
    for (idx, g) in root.globals.iter_enumerated().filter(|(_, g)| g.exported) {
        let ctx = EvaluationContext::new_global(root, idx, ());
        for p in g.public_properties.values() {
            visit_property(&p.prop, &ctx);
        }
    }

    root.for_each_sub_components(&mut |sc, ctx| {
        // 2. the native items and bindings of properties
        for (_, expr) in &sc.property_init {
            let c = expr.use_count.get();
            expr.use_count.set(c + 1);
            if c == 0 {
                visit_binding_expression(expr, ctx)
            }
        }
        // 3. the init code
        for expr in sc.pre_init_code.iter().chain(&sc.init_code) {
            expr.borrow().visit_property_references(ctx, &mut visit_property);
        }
        // 4. the models
        for (idx, r) in sc.repeated.iter_enumerated() {
            r.model.borrow().visit_property_references(ctx, &mut visit_property);
            if let Some(lv) = &r.listview {
                visit_property(&lv.content_y, ctx);
                if let Some(content_height) = &lv.content_height {
                    visit_property(content_height, ctx);
                }
                if let Some(content_width) = &lv.content_width {
                    visit_property(content_width, ctx);
                }
                visit_property(&lv.listview_width, ctx);
                visit_property(&lv.listview_height, ctx);

                let parent_ctx = ParentScope::new(ctx, Some(idx));
                let rep_ctx = EvaluationContext::new_sub_component(
                    root,
                    r.sub_tree.root,
                    (),
                    Some(&parent_ctx),
                );
                visit_property(&lv.prop_y, &rep_ctx);
                visit_property(&lv.prop_height, &rep_ctx);
            }
            for idx in r.data_prop.iter().chain(r.index_prop.iter()) {
                // prevent optimizing model properties
                let p = &root.sub_components[r.sub_tree.root].properties[*idx];
                p.use_count.set(2);
            }
        }

        // 5. the layout info
        sc.layout_info_h.borrow().visit_property_references(ctx, &mut visit_property);
        sc.layout_info_v.borrow().visit_property_references(ctx, &mut visit_property);
        if let Some(e) = &sc.grid_layout_input_for_repeated {
            e.borrow().visit_property_references(ctx, &mut visit_property);
        }
        if let Some(e) = &sc.flexbox_layout_item_info_for_repeated {
            e.borrow().visit_property_references(ctx, &mut visit_property);
        }
        if let Some(e) = &sc.layout_info_v_constrained_for_repeated {
            e.borrow().visit_property_references(ctx, &mut visit_property);
        }
        if let Some(e) = &sc.layout_info_v_at_cross_width_for_repeated {
            e.borrow().visit_property_references(ctx, &mut visit_property);
        }
        if let Some(e) = &sc.layout_info_h_constrained_for_repeated {
            e.borrow().visit_property_references(ctx, &mut visit_property);
        }
        if let Some(e) = &sc.layout_info_h_at_cross_height_for_repeated {
            e.borrow().visit_property_references(ctx, &mut visit_property);
        }
        for child in &sc.grid_layout_children {
            child.layout_info_h.borrow().visit_property_references(ctx, &mut visit_property);
            child.layout_info_v.borrow().visit_property_references(ctx, &mut visit_property);
        }

        // 6. accessibility props and geometries
        for b in sc.accessible_prop.values() {
            b.borrow().visit_property_references(ctx, &mut visit_property)
        }
        for i in sc.geometries.iter().filter_map(Option::as_ref) {
            i.borrow().visit_property_references(ctx, &mut visit_property)
        }

        // 7. aliases (if they were not optimized, they are probably used)
        for twb in &sc.two_way_bindings {
            visit_property(&twb.prop1.clone().into(), ctx);
            visit_property(&twb.prop2, ctx);
            if let &Some(p) = &twb.is_model {
                let mut idx_prop = twb.prop2.clone();
                let MemberReference::Relative { local_reference, .. } = &mut idx_prop else {
                    unreachable!()
                };
                local_reference.reference = p.into();
                visit_property(&idx_prop, ctx);
            }
        }

        // 8. animations (`animate x { … }`): `remove_unused` keeps and remaps these,
        // so the properties they read must be counted too.
        for anim in sc.animations.values() {
            anim.visit_property_references(ctx, &mut visit_property);
        }

        // Function bodies are visited on demand from visit_property when a call to
        // them is found, so that an unreachable function keeps nothing alive.

        // 9. change callbacks
        for (p, e) in &sc.change_callbacks {
            visit_property(p, ctx);
            e.borrow().visit_property_references(ctx, &mut visit_property);
        }

        // 10. popup x/y coordinates
        for popup in &sc.popup_windows {
            let parent_ctx = ParentScope::new(ctx, None);
            let popup_ctx = EvaluationContext::new_sub_component(
                root,
                popup.item_tree.root,
                (),
                Some(&parent_ctx),
            );
            popup.position.borrow().visit_property_references(&popup_ctx, &mut visit_property)
        }
        // 11. timer
        for timer in &sc.timers {
            timer.interval.borrow().visit_property_references(ctx, &mut visit_property);
            timer.running.borrow().visit_property_references(ctx, &mut visit_property);
            timer.triggered.borrow().visit_property_references(ctx, &mut visit_property);
        }
    });

    for (idx, g) in root.globals.iter_enumerated() {
        let ctx = EvaluationContext::new_global(root, idx, ());

        for (p, e) in &g.change_callbacks {
            visit_property(&LocalMemberReference::from(*p).into(), &ctx);
            e.borrow().visit_property_references(&ctx, &mut visit_property);
        }
    }

    if let Some(p) = &root.popup_menu {
        let ctx = EvaluationContext::new_sub_component(root, p.item_tree.root, (), None);
        visit_property(&p.entries, &ctx);
        visit_property(&p.sub_menu, &ctx);
        visit_property(&p.activated, &ctx);
    }

    clean_unused_bindings(root);
}

fn visit_property(pr: &MemberReference, ctx: &EvaluationContext) {
    let p_info = ctx.property_info(pr);
    if let Some(use_count) = &p_info.use_count {
        use_count.set(use_count.get() + 1);
    }
    if let Some((binding, map)) = &p_info.binding {
        let c = binding.use_count.get();
        binding.use_count.set(c + 1);
        if c == 0 {
            let ctx2 = map.map_context(ctx);
            visit_binding_expression(binding, &ctx2);
        }
    }
    // A reference to a function is a call: count it and, the first time, visit
    // its body so an unreachable function doesn't keep its property reads alive.
    if let Some((function, map)) = ctx.function_info(pr) {
        let c = function.use_count.get();
        function.use_count.set(c + 1);
        if c == 0 {
            let ctx2 = map.map_context(ctx);
            function.code.borrow().visit_property_references(&ctx2, &mut visit_property);
        }
    }
}

fn visit_binding_expression(binding: &BindingExpression, ctx: &EvaluationContext) {
    binding.expression.borrow().visit_property_references(ctx, &mut visit_property);
    match &binding.animation {
        Some(Animation::Static(e) | Animation::Transition(e)) => {
            e.visit_property_references(ctx, &mut visit_property);
        }
        None => (),
    }
}

/// Bindings which have a use_count of zero can be cleared so that we won't ever visit them later.
fn clean_unused_bindings(root: &CompilationUnit) {
    root.for_each_sub_components(&mut |sc, _| {
        for (_, e) in &sc.property_init {
            if e.use_count.get() == 0 {
                e.expression.replace(Expression::CodeBlock(Vec::new()));
            }
        }
    });
    for g in &root.globals {
        for e in g.init_values.values() {
            if e.use_count.get() == 0 {
                e.expression.replace(Expression::CodeBlock(Vec::new()));
            }
        }
    }
}
