// 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 root component's used_types.structs_and_enums

use crate::expression_tree::Expression;
use crate::langtype::{StructName, Type};
use crate::object_tree::*;
use smol_str::SmolStr;
use std::collections::BTreeMap;
use std::rc::Rc;

/// Fill the root_component's used_types.structs
pub fn collect_structs_and_enums(doc: &Document) {
    let mut hash = BTreeMap::new();

    for (_, exp) in doc.exports.iter() {
        if let Some(ty) = exp.as_ref().right() {
            maybe_collect_struct(ty, &mut hash);
        }
    }

    doc.visit_all_used_components(|component| collect_types_in_component(component, &mut hash));

    let mut used_types = doc.used_types.borrow_mut();
    used_types.structs_and_enums = Vec::with_capacity(hash.len());
    while let Some(next) = hash.iter().next() {
        let key = next.0.clone();

        sort_types(&mut hash, &key, &mut |key, ty| {
            // Here, using BTreeMap::pop_first would be great when it is stable
            let used_struct_and_enums = &mut used_types.structs_and_enums;

            if let Some(library_info) = doc.library_exports.get(key.as_str()) {
                // This is a type imported from an external library, just skip it for code generation
                used_types.library_types_imports.push((key.clone(), library_info.clone()));
            } else {
                used_struct_and_enums.push(ty.clone());
            }
        });
    }
}

fn maybe_collect_struct(ty: &Type, hash: &mut BTreeMap<SmolStr, Type>) {
    visit_declared_type(ty, &mut |name, sub_ty| {
        hash.entry(name.clone()).or_insert_with(|| sub_ty.clone());
    });
}

fn collect_types_in_component(root_component: &Rc<Component>, hash: &mut BTreeMap<SmolStr, Type>) {
    recurse_elem_including_sub_components_no_borrow(root_component, &(), &mut |elem, _| {
        for x in elem.borrow().property_declarations.values() {
            maybe_collect_struct(&x.property_type, hash);
        }
    });

    visit_all_expressions(root_component, |expr, _| {
        expr.visit_recursive(&mut |expr| match expr {
            Expression::Struct { ty, .. } => maybe_collect_struct(&Type::Struct(ty.clone()), hash),
            Expression::Array { element_ty, .. } => maybe_collect_struct(element_ty, hash),
            Expression::EnumerationValue(ev) => {
                maybe_collect_struct(&Type::Enumeration(ev.enumeration.clone()), hash)
            }
            _ => (),
        })
    });
}

/// Move the object named `key` from hash to vector, making sure that all object used by
/// it are placed before in the vector
fn sort_types(
    hash: &mut BTreeMap<SmolStr, Type>,
    key: &SmolStr,
    visitor: &mut impl FnMut(&SmolStr, &Type),
) {
    let ty = if let Some(ty) = hash.remove(key) { ty } else { return };
    if let Type::Struct(s) = &ty
        && let StructName::User { .. } = &s.name
    {
        for sub_ty in s.fields.values() {
            visit_declared_type(sub_ty, &mut |name, _| sort_types(hash, name, visitor));
        }
    }
    visitor(key, &ty)
}

/// Will call the `visitor` for every named struct or enum that is not builtin
fn visit_declared_type(ty: &Type, visitor: &mut impl FnMut(&SmolStr, &Type)) {
    match ty {
        Type::Struct(s) => {
            if s.node().is_some()
                && let StructName::User { name, .. } = &s.name
            {
                visitor(name, ty);
            }
            for sub_ty in s.fields.values() {
                visit_declared_type(sub_ty, visitor);
            }
        }
        Type::Array(x) => visit_declared_type(x, visitor),
        Type::Function(function) | Type::Callback(function) => {
            visit_declared_type(&function.return_type, visitor);
            for a in &function.args {
                visit_declared_type(a, visitor);
            }
        }
        Type::Enumeration(en) if en.node.is_some() => visitor(&en.name, ty),
        _ => {}
    }
}
