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

//! Passe lower the `MenuBar` and `ContextMenuArea` as well as all their contents
//!
//! We can't have properties of type model because that is not binary compatible with C++,
//! so all the code that handle model of MenuEntry need to be handle by code in the generated code
//! and transformed into a `SharedVector<MenuEntry>` that is passed to Slint runtime.
//!
//! ## MenuBar
//!
//! ```slint
//! Window {
//!      menu-bar := MenuBar {
//!        Menu {
//!           title: "File";
//!           MenuItem {
//!             title: "A";
//!             activated => { ... }
//!           }
//!           Menu {
//!               title: "B";
//!               MenuItem { title: "C"; }
//!           }
//!        }
//!      }
//!      content := ...
//! }
//! ```
//! Is transformed to
//! ```slint
//! Window {
//!     menu-bar := VerticalLayout {
//!        property <[MenuEntry]> entries : [ { id: "0", title: "File", has-sub-menu: true } ];
//!        callback sub-menu(entry: MenuEntry) => {
//!            if(entry.id == "0") { return [ { id: "1", title: "A" }, { id: "2", title: "B", has-sub-menu: true } ]; }
//!            else if(entry.id == "2") { return [ { id: "3", title: "C" } ]; } else { return []; }
//!        }
//!        callback activated() => { if (entry.id == "2") { ... } }
//!        if !Builtin.supports_native_menu_bar() : MenuBarImpl {
//!           entries: parent.entries
//!           sub-menu(..) => { parent.sub-menu(..) }
//!           activated(..) => { parent.activated(..) }
//!        }
//!        Empty {
//!           content := ...
//!        }
//!    }
//!    init => {
//!        // ... rest of init ...
//!        Builtin.setup_native_menu_bar(menu-bar.entries, menu-bar.sub-menu, menu-bar.activated)
//!    }
//! }
//! ```
//!
//! ## ContextMenuInternal
//!
//! ```slint
//! menu := ContextMenuInternal {
//!     entries: [...]
//!     sub-menu => ...
//!     activated => ...
//! }
//! Button { clicked => {menu.show({x: 0, y: 0;})} }
//! ```
//! Is transformed to
//!
//! ```slint
//! menu := ContextMenu {
//!    property <[MenuEntry]> entries : ...
//!    sub-menu => { ... }
//!    activated => { ... }
//!
//!    // show is actually a callback called by the native code when right clicking
//!    callback show(point) => { Builtin.show_context_menu(entries, sub-menu, activated, point) }
//! }
//!
//! ## ContextMenuArea
//!
//! This is the same as ContextMenuInternal, but entries, sub-menu, and activated are generated
//! from the MenuItem similar to MenuBar

use crate::diagnostics::{BuildDiagnostics, Spanned};
use crate::expression_tree::{BuiltinFunction, Callable, Expression, NamedReference};
use crate::langtype::{ElementType, Type};
use crate::object_tree::*;
use core::cell::RefCell;
use smol_str::{format_smolstr, SmolStr};
use std::collections::HashMap;
use std::rc::{Rc, Weak};

const HEIGHT: &str = "height";
const ENTRIES: &str = "entries";
const SUB_MENU: &str = "sub-menu";
const ACTIVATED: &str = "activated";
const SHOW: &str = "show";

struct UsefulMenuComponents {
    menubar_impl: ElementType,
    vertical_layout: ElementType,
    context_menu_internal: ElementType,
    empty: ElementType,
    menu_entry: Type,
    menu_item_element: ElementType,
}

pub async fn lower_menus(
    doc: &mut Document,
    type_loader: &mut crate::typeloader::TypeLoader,
    diag: &mut BuildDiagnostics,
) {
    // Ignore import errors
    let mut build_diags_to_ignore = BuildDiagnostics::default();

    let menubar_impl = type_loader
        .import_component("std-widgets.slint", "MenuBarImpl", &mut build_diags_to_ignore)
        .await
        .expect("MenuBarImpl should be in std-widgets.slint");

    let menu_item_element = type_loader
        .global_type_registry
        .borrow()
        .lookup_builtin_element("ContextMenuArea")
        .unwrap()
        .as_builtin()
        .additional_accepted_child_types
        .get("Menu")
        .expect("ContextMenuArea should accept Menu")
        .additional_accepted_child_types
        .get("MenuItem")
        .expect("Menu should accept MenuItem")
        .clone()
        .into();

    let useful_menu_component = UsefulMenuComponents {
        menubar_impl: menubar_impl.clone().into(),
        context_menu_internal: type_loader
            .global_type_registry
            .borrow()
            .lookup_builtin_element("ContextMenuInternal")
            .expect("ContextMenuInternal is a builtin type"),
        vertical_layout: type_loader
            .global_type_registry
            .borrow()
            .lookup_builtin_element("VerticalLayout")
            .expect("VerticalLayout is a builtin type"),
        empty: type_loader.global_type_registry.borrow().empty_type(),
        menu_entry: type_loader.global_type_registry.borrow().lookup("MenuEntry"),
        menu_item_element,
    };
    assert!(matches!(&useful_menu_component.menu_entry, Type::Struct(..)));

    let mut has_menu = false;

    doc.visit_all_used_components(|component| {
        recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
            if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "Window") {
                has_menu |= process_window(elem, &useful_menu_component, diag);
            }
            if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal")) {
                has_menu |= process_context_menu(elem, &useful_menu_component, diag);
            }
        })
    });

    if has_menu {
        let popup_menu_impl = type_loader
            .import_component("std-widgets.slint", "PopupMenuImpl", &mut build_diags_to_ignore)
            .await
            .expect("PopupMenuImpl should be in std-widgets.slint");
        {
            let mut root = popup_menu_impl.root_element.borrow_mut();

            for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
                match root.property_declarations.get_mut(prop) {
                    Some(d) => d.expose_in_public_api = true,
                    None => diag.push_error(format!("PopupMenuImpl doesn't have {prop}"), &*root),
                }
            }
            root.property_analysis
                .borrow_mut()
                .entry(SmolStr::new_static(ENTRIES))
                .or_default()
                .is_set = true;
        }

        recurse_elem_including_sub_components_no_borrow(&popup_menu_impl, &(), &mut |elem, _| {
            if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
            {
                process_context_menu(elem, &useful_menu_component, diag);
            }
        });
        doc.popup_menu_impl = popup_menu_impl.into();
    }
}

fn process_context_menu(
    context_menu_elem: &ElementRc,
    components: &UsefulMenuComponents,
    diag: &mut BuildDiagnostics,
) -> bool {
    let is_internal = matches!(&context_menu_elem.borrow().base_type, ElementType::Builtin(b) if b.name == "ContextMenuInternal");

    let item_tree_root = if !is_internal {
        // Lower Menu into entries
        let menu_element_type = context_menu_elem
            .borrow()
            .base_type
            .as_builtin()
            .additional_accepted_child_types
            .get("Menu")
            .expect("ContextMenu should accept Menu")
            .clone()
            .into();

        context_menu_elem.borrow_mut().base_type = components.context_menu_internal.clone();

        let mut menu_elem = None;
        context_menu_elem.borrow_mut().children.retain(|x| {
            if x.borrow().base_type == menu_element_type {
                if menu_elem.is_some() {
                    diag.push_error(
                        "Only one Menu is allowed in a ContextMenu".into(),
                        &*x.borrow(),
                    );
                } else {
                    menu_elem = Some(x.clone());
                }
                false
            } else {
                true
            }
        });

        let item_tree_root = if let Some(menu_elem) = menu_elem {
            if menu_elem.borrow().repeated.is_some() {
                diag.push_error(
                    "ContextMenuArea's root Menu cannot be in a conditional or repeated element"
                        .into(),
                    &*menu_elem.borrow(),
                );
            }

            let children = std::mem::take(&mut menu_elem.borrow_mut().children);
            lower_menu_items(context_menu_elem, children, components)
                .map(|c| Expression::ElementReference(Rc::downgrade(&c.root_element)))
        } else {
            diag.push_error(
                "ContextMenuArea should have a Menu".into(),
                &*context_menu_elem.borrow(),
            );
            None
        };

        for (name, _) in &components.context_menu_internal.property_list() {
            if let Some(decl) = context_menu_elem.borrow().property_declarations.get(name) {
                diag.push_error(format!("Cannot re-define internal property '{name}'"), &decl.node);
            }
        }

        item_tree_root
    } else {
        None
    };

    let entries = if let Some(item_tree_root) = item_tree_root {
        item_tree_root
    } else {
        // Materialize the entries property
        context_menu_elem.borrow_mut().property_declarations.insert(
            SmolStr::new_static(ENTRIES),
            Type::Array(components.menu_entry.clone().into()).into(),
        );
        Expression::PropertyReference(NamedReference::new(
            context_menu_elem,
            SmolStr::new_static(ENTRIES),
        ))
    };

    // generate the show callback
    let source_location = Some(context_menu_elem.borrow().to_source_location());
    let expr = Expression::FunctionCall {
        function: BuiltinFunction::ShowPopupMenu.into(),
        arguments: vec![
            Expression::ElementReference(Rc::downgrade(context_menu_elem)),
            entries,
            Expression::FunctionParameterReference {
                index: 0,
                ty: crate::typeregister::logical_point_type(),
            },
        ],
        source_location,
    };
    let old = context_menu_elem
        .borrow_mut()
        .bindings
        .insert(SmolStr::new_static(SHOW), RefCell::new(expr.into()));
    if let Some(old) = old {
        diag.push_error("'show' is not a callback in ContextMenuArea".into(), &old.borrow().span);
    }

    true
}

fn process_window(
    win: &ElementRc,
    components: &UsefulMenuComponents,
    diag: &mut BuildDiagnostics,
) -> bool {
    let mut window = win.borrow_mut();
    let mut menu_bar = None;
    window.children.retain(|x| {
        if matches!(&x.borrow().base_type, ElementType::Builtin(b) if b.name == "MenuBar") {
            if menu_bar.is_some() {
                diag.push_error("Only one MenuBar is allowed in a Window".into(), &*x.borrow());
            } else {
                menu_bar = Some(x.clone());
            }
            false
        } else {
            true
        }
    });

    let Some(menu_bar) = menu_bar else {
        return false;
    };
    if menu_bar.borrow().repeated.is_some() {
        diag.push_error(
            "MenuBar cannot be in a conditional or repeated element".into(),
            &*menu_bar.borrow(),
        );
    }

    // Lower MenuItem's into entries
    let children = std::mem::take(&mut menu_bar.borrow_mut().children);
    let item_tree_root = if !children.is_empty() {
        lower_menu_items(&menu_bar, children, components)
            .map(|c| Expression::ElementReference(Rc::downgrade(&c.root_element)))
    } else {
        None
    };

    let menubar_impl = Element {
        id: format_smolstr!("{}-menulayout", window.id),
        base_type: components.menubar_impl.clone(),
        enclosing_component: window.enclosing_component.clone(),
        repeated: Some(crate::object_tree::RepeatedElementInfo {
            model: Expression::UnaryOp {
                op: '!',
                sub: Expression::FunctionCall {
                    function: BuiltinFunction::SupportsNativeMenuBar.into(),
                    arguments: vec![],
                    source_location: None,
                }
                .into(),
            },
            model_data_id: SmolStr::default(),
            index_id: SmolStr::default(),
            is_conditional_element: true,
            is_listview: None,
        }),
        ..Default::default()
    }
    .make_rc();

    // Create a child that contains all the children of the window but the menubar
    let child = Element {
        id: format_smolstr!("{}-child", window.id),
        base_type: components.empty.clone(),
        enclosing_component: window.enclosing_component.clone(),
        children: std::mem::take(&mut window.children),
        ..Default::default()
    }
    .make_rc();

    let child_height = NamedReference::new(&child, SmolStr::new_static(HEIGHT));

    let source_location = Some(menu_bar.borrow().to_source_location());

    for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
        // materialize the properties and callbacks
        let ty = components.menubar_impl.lookup_property(prop).property_type;
        assert_ne!(ty, Type::Invalid, "Can't lookup type for {prop}");
        let nr = NamedReference::new(&menu_bar, SmolStr::new_static(prop));
        let forward_expr = if let Type::Callback(cb) = &ty {
            Expression::FunctionCall {
                function: Callable::Callback(nr),
                arguments: cb
                    .args
                    .iter()
                    .enumerate()
                    .map(|(index, ty)| Expression::FunctionParameterReference {
                        index,
                        ty: ty.clone(),
                    })
                    .collect(),
                source_location: source_location.clone(),
            }
        } else {
            Expression::PropertyReference(nr)
        };
        menubar_impl.borrow_mut().bindings.insert(prop.into(), RefCell::new(forward_expr.into()));
        let old = menu_bar
            .borrow_mut()
            .property_declarations
            .insert(prop.into(), PropertyDeclaration { property_type: ty, ..Default::default() });
        if let Some(old) = old {
            diag.push_error(format!("Cannot re-define internal property '{prop}'"), &old.node);
        }
    }

    // Transform the MenuBar in a layout
    menu_bar.borrow_mut().base_type = components.vertical_layout.clone();
    menu_bar.borrow_mut().children = vec![menubar_impl, child];

    let mut arguments = vec![
        Expression::PropertyReference(NamedReference::new(&menu_bar, SmolStr::new_static(ENTRIES))),
        Expression::PropertyReference(NamedReference::new(
            &menu_bar,
            SmolStr::new_static(SUB_MENU),
        )),
        Expression::PropertyReference(NamedReference::new(
            &menu_bar,
            SmolStr::new_static(ACTIVATED),
        )),
    ];

    if let Some(item_tree_root) = item_tree_root {
        arguments.push(item_tree_root.into());
    }

    for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
        menu_bar
            .borrow()
            .property_analysis
            .borrow_mut()
            .entry(SmolStr::new_static(prop))
            .or_default()
            .is_set = true;
    }

    let setup_menubar = Expression::FunctionCall {
        function: BuiltinFunction::SetupNativeMenuBar.into(),
        arguments,
        source_location: source_location.clone(),
    };

    window.children.push(menu_bar);
    let component = window.enclosing_component.upgrade().unwrap();
    drop(window);

    // Rename every access to `root.height` into `child.height`
    let win_height = NamedReference::new(win, SmolStr::new_static(HEIGHT));
    crate::object_tree::visit_all_named_references(&component, &mut |nr| {
        if nr == &win_height {
            *nr = child_height.clone()
        }
    });
    // except for the actual geometry
    win.borrow_mut().geometry_props.as_mut().unwrap().height = win_height;

    component.init_code.borrow_mut().constructor_code.push(setup_menubar.into());
    true
}

/// Lower the MenuItem's and Menu's to either
///  - `entries` and `activated` and `sub-menu` properties/callback, in which cases it returns None
///  - or a Component which is a tree of MenuItem, in which case returns the component that is within the enclosing component's menu_item_trees
fn lower_menu_items(
    parent: &ElementRc,
    children: Vec<ElementRc>,
    components: &UsefulMenuComponents,
) -> Option<Rc<Component>> {
    let mut has_repeated = false;
    for i in &children {
        recurse_elem(i, &(), &mut |e, _| {
            if e.borrow().repeated.is_some() {
                has_repeated = true;
            }
        });
        if has_repeated {
            break;
        }
    }
    if !has_repeated {
        let menu_entry = &components.menu_entry;
        let mut state = GenMenuState {
            id: 0,
            menu_entry: menu_entry.clone(),
            activate: Vec::new(),
            sub_menu: Vec::new(),
        };
        let entries = generate_menu_entries(children.into_iter(), &mut state);
        parent.borrow_mut().bindings.insert(
            ENTRIES.into(),
            RefCell::new(
                Expression::Array { element_ty: menu_entry.clone(), values: entries }.into(),
            ),
        );
        let entry_id = Expression::StructFieldAccess {
            base: Expression::FunctionParameterReference { index: 0, ty: menu_entry.clone() }
                .into(),
            name: SmolStr::new_static("id"),
        };

        let sub_entries = build_cases_function(
            &entry_id,
            Expression::Array { element_ty: menu_entry.clone(), values: vec![] },
            state.sub_menu,
        );
        parent.borrow_mut().bindings.insert(SUB_MENU.into(), RefCell::new(sub_entries.into()));

        let activated =
            build_cases_function(&entry_id, Expression::CodeBlock(vec![]), state.activate);
        parent.borrow_mut().bindings.insert(ACTIVATED.into(), RefCell::new(activated.into()));
        None
    } else {
        let component = Rc::new_cyclic(|component_weak| {
            let root_element = Rc::new(RefCell::new(Element {
                base_type: components.empty.clone(),
                children,
                enclosing_component: component_weak.clone(),
                ..Default::default()
            }));
            recurse_elem(&root_element, &true, &mut |element: &ElementRc, is_root| {
                if !is_root {
                    debug_assert!(Weak::ptr_eq(
                        &element.borrow().enclosing_component,
                        &parent.borrow().enclosing_component
                    ));
                    element.borrow_mut().enclosing_component = component_weak.clone();
                    element.borrow_mut().geometry_props = None;
                    // Menu -> MenuItem
                    element.borrow_mut().base_type = components.menu_item_element.clone();
                }
                false
            });
            Component {
                node: parent.borrow().debug.first().map(|n| n.node.clone().into()),
                id: SmolStr::default(),
                root_element,
                parent_element: Rc::downgrade(parent),
                ..Default::default()
            }
        });
        parent
            .borrow()
            .enclosing_component
            .upgrade()
            .unwrap()
            .menu_item_tree
            .borrow_mut()
            .push(component.clone());
        Some(component)
    }
}

fn build_cases_function(
    entry_id: &Expression,
    default_case: Expression,
    cases: Vec<(SmolStr, Expression)>,
) -> Expression {
    let mut result = default_case;
    for (id, expr) in cases.into_iter().rev() {
        result = Expression::Condition {
            condition: Expression::BinaryExpression {
                lhs: entry_id.clone().into(),
                rhs: Expression::StringLiteral(id).into(),
                op: '=',
            }
            .into(),
            true_expr: expr.into(),
            false_expr: result.into(),
        }
    }
    result
}

struct GenMenuState {
    id: usize,
    /// Maps `entry.id` to the callback
    activate: Vec<(SmolStr, Expression)>,
    /// Maps `entry.id` to the sub-menu entries
    sub_menu: Vec<(SmolStr, Expression)>,

    menu_entry: Type,
}

/// Recursively generate the menu entries for the given menu items
fn generate_menu_entries(
    menu_items: impl Iterator<Item = ElementRc>,
    state: &mut GenMenuState,
) -> Vec<Expression> {
    let mut entries = Vec::new();

    for item in menu_items {
        let mut borrow_mut = item.borrow_mut();
        let base_name = borrow_mut.base_type.type_name().unwrap();
        let is_sub_menu = base_name == "Menu";
        if !is_sub_menu {
            assert_eq!(base_name, "MenuItem");
        }

        borrow_mut
            .enclosing_component
            .upgrade()
            .unwrap()
            .optimized_elements
            .borrow_mut()
            .push(item.clone());

        assert!(borrow_mut.repeated.is_none());

        let mut values = HashMap::<SmolStr, Expression>::new();
        for prop in ["title"] {
            if let Some(binding) = borrow_mut.bindings.remove(prop) {
                values.insert(SmolStr::new_static(prop), binding.into_inner().expression);
            }
        }

        state.id += 1;
        let id_str = format_smolstr!("{}", state.id);
        values.insert(SmolStr::new_static("id"), Expression::StringLiteral(id_str.clone()));

        if let Some(callback) = borrow_mut.bindings.remove(ACTIVATED) {
            state.activate.push((id_str.clone(), callback.into_inner().expression));
        }

        if is_sub_menu {
            let sub_entries =
                generate_menu_entries(std::mem::take(&mut borrow_mut.children).into_iter(), state);

            state.sub_menu.push((
                id_str,
                Expression::Array { element_ty: state.menu_entry.clone(), values: sub_entries },
            ));
            values
                .insert(SmolStr::new_static("has-sub-menu"), Expression::BoolLiteral(true).into());
        }

        entries.push(mk_struct(state.menu_entry.clone(), values));
    }

    entries
}

fn mk_struct(ty: Type, mut values: HashMap<SmolStr, Expression>) -> Expression {
    let Type::Struct(ty) = ty else { panic!("Not a struct") };
    for (k, v) in ty.fields.iter() {
        values.entry(k.clone()).or_insert_with(|| Expression::default_value_for_type(v));
    }
    Expression::Struct { ty, values }
}
