from tkinter import Menu, Tk, Toplevel


class HostMenu:
    """A persistent ``tk.Menu`` attached to the Host window.

    The menubar has two ordered layers:

    1. **Project layer** — app-wide cascades added once at Host startup via
       :meth:`set_project_items`.  They persist across all tab changes.
    2. **Screen layer** — cascades contributed by the active tab's
       ``configure_menu`` hook via :meth:`set_screen_items`.  All screen
       cascades are cleared automatically when the tab deactivates.

    ``set_screen_items`` **accumulates** — calling it more than once within a
    single ``configure_menu`` hook appends multiple cascades side by side.
    All are removed together by :meth:`clear_screen_items`.

    A third, optional layer is the **shared layer** — persistent cascades
    built once at startup via :meth:`build_shared_menu` whose individual leaf
    items can be patched per-tab with :meth:`apply_overrides` and restored to
    their build-time defaults with :meth:`reset_overrides`.

    Usage::

        host_menu = HostMenu(host_window, quit_command=host.quit_host)
        host_menu.attach()

        # In Host.py, once at startup — project-wide items:
        host_menu.set_project_items([
            {"label": "File", "items": [
                {"label": "New",  "command": new_fn},
                {"separator": True},
                {"label": "Exit", "command": host.quit_host},
            ]},
        ], label="File")

        # From a screen's configure_menu() hook — screen-specific items:
        host_menu.set_screen_items([
            {"label": "Export PDF", "command": export_pdf},
            {"label": "Print",      "command": print_fn},
        ], label="Work Orders")

    Item spec format (list of dicts)::

        {"label": str, "command": callable}              # simple command
        {"label": str, "items": [<item spec>, ...]}      # cascade submenu
        {"separator": True}                              # separator

    Attributes:
        menubar (Menu): The underlying Tk Menu widget.
    """

    def __init__(self, parent: Tk | Toplevel, quit_command=None,
                 close_command=None):
        self.menubar = Menu(parent, tearoff=0)
        self._parent = parent
        self._quit_command = quit_command
        self._close_command = close_command
        self._project_labels: list[str] = []
        self._screen_indices: list[int] = []
        self._shared_menus: dict[str, Menu] = {}
        self._shared_defaults: dict[str, dict[str, dict]] = {}

    # ── Public API ─────────────────────────────────────────────────────────────

    def attach(self):
        """Configure the parent window to show this menu bar."""
        self._parent.config(menu=self.menubar)
        self._build_base()

    def set_project_items(self, items: list[dict], label: str = "Project"):
        """Add one cascade to the project layer.

        May be called multiple times to add multiple project-layer cascades in
        order.  Project cascades persist across all tab changes and are only
        removed by :meth:`clear_project_items`.

        Args:
            items: List of item spec dicts (see class docstring).
            label: Cascade label shown in the menu bar.
        """
        if not items:
            return
        cascade = Menu(self.menubar, tearoff=0)
        self._populate(cascade, items)
        self.menubar.add_cascade(label=label, menu=cascade)
        self._project_labels.append(label)

    def clear_project_items(self):
        """Remove all project-layer cascades.

        Intended for teardown; not normally called during regular use.
        """
        for label in self._project_labels:
            try:
                self.menubar.delete(label)
            except Exception:
                pass
        self._project_labels.clear()

    def set_screen_items(self, items: list[dict], label: str = "Screen"):
        """Add one cascade to the screen layer (accumulates).

        Calling this multiple times within a single ``configure_menu`` hook
        appends multiple cascades side by side — all are cleared together when
        the tab deactivates.

        Args:
            items: List of item spec dicts (see class docstring).
            label: Cascade label shown in the menu bar.
        """
        if not items:
            return
        cascade = Menu(self.menubar, tearoff=0)
        self._populate(cascade, items)
        self.menubar.add_cascade(label=label, menu=cascade)
        self._screen_indices.append(self.menubar.index("end"))

    def clear_screen_items(self):
        """Remove all accumulated screen cascades."""
        for idx in reversed(self._screen_indices):
            try:
                self.menubar.delete(idx)
            except Exception:
                pass
        self._screen_indices.clear()

    def build_shared_menu(self, structure: dict):
        """Build the persistent shared menu from a structure dict.

        Called once at Host startup. Creates one cascade per top-level key
        (e.g. "File", "Edit") and adds them to the menubar as project-layer
        items. Stores Menu widget references in _shared_menus and default
        command/state values for every leaf item in _shared_defaults so that
        reset_overrides() can restore them.

        structure format::

            {
                "File": [
                    {"label": "Open...", "command": None, "state": "disabled"},
                    {"separator": True},
                    {"label": "Exit", "command": exit_fn},
                    {"label": "New", "items": [...]},  # submenu — not patchable
                ],
                "Edit": [...],
            }

        Only top-level items (direct children of a cascade) are tracked for
        override/reset. Nested submenu items are static.

        Args:
            structure: Mapping of cascade label to list of item spec dicts.
                       Each item spec may include a ``state`` key
                       (``"normal"`` or ``"disabled"``); defaults to
                       ``"normal"`` when omitted.
        """
        self._shared_structure = structure
        for label, items in structure.items():
            cascade = Menu(self.menubar, tearoff=0)
            defaults: dict[str, dict] = {}
            for item in items:
                if item.get("separator"):
                    cascade.add_separator()
                elif "items" in item:
                    sub = Menu(cascade, tearoff=0)
                    self._populate(sub, item["items"])
                    cascade.add_cascade(label=item.get("label", ""), menu=sub)
                else:
                    kw = {
                        "label":   item.get("label", ""),
                        "command": item.get("command"),
                        "state":   item.get("state", "normal"),
                    }
                    cascade.add_command(**kw)
                    # Record defaults so reset_overrides can restore them
                    defaults[kw["label"]] = {
                        "command": kw["command"],
                        "state":   kw["state"],
                    }
            self.menubar.add_cascade(label=label, menu=cascade)
            self._project_labels.append(label)
            self._shared_menus[label] = cascade
            self._shared_defaults[label] = defaults

    def apply_overrides(self, overrides: dict):
        """Patch shared menu items with screen-specific commands/states.

        overrides format::

            {
                "File": {
                    "Open...": {"command": fn, "state": "normal"},
                    "Save":    {"command": fn, "state": "normal"},
                },
            }

        Unknown cascade labels or item labels are silently ignored.

        Args:
            overrides: Mapping of cascade label to a mapping of item label
                       to ``entryconfig`` keyword arguments.
        """
        for cascade_label, items in overrides.items():
            cascade = self._shared_menus.get(cascade_label)
            if cascade is None:
                continue
            for item_label, opts in items.items():
                try:
                    cascade.entryconfig(item_label, **opts)
                except Exception:
                    pass

    def reset_overrides(self):
        """Restore all shared menu items to their build-time defaults.

        Called on tab deactivate and before apply_overrides on tab activate.
        """
        for cascade_label, defaults in self._shared_defaults.items():
            cascade = self._shared_menus.get(cascade_label)
            if cascade is None:
                continue
            for item_label, opts in defaults.items():
                try:
                    cascade.entryconfig(item_label, **opts)
                except Exception:
                    pass

    # ── Internal ───────────────────────────────────────────────────────────────

    def _build_base(self):
        pass

    def _populate(self, menu: Menu, items: list[dict]):
        for item in items:
            if item.get("separator"):
                menu.add_separator()
            elif "items" in item:
                sub = Menu(menu, tearoff=0)
                self._populate(sub, item["items"])
                menu.add_cascade(label=item.get("label", ""), menu=sub)
            else:
                menu.add_command(
                    label=item.get("label", ""),
                    command=item.get("command"),
                )