import asyncio
import functools
import weakref
import carb
import omni.kit.ui
import omni.kit.app
from functools import partial
from enum import IntFlag, Enum
from omni import ui
from typing import Callable, Tuple, Any
from pathlib import Path
from .builder_utils import get_menu_name, get_action_path, create_prebuild_entry, has_delegate_func
from .builder_utils import MenuItemDescription, PrebuiltItemOrder, MenuAlignment


class MenuItemOrder:
    FIRST = "@first"
    LAST = "@last"


class MenuState(IntFlag):
    Invalid = 0
    Created = 1


class MenuActionControl(Enum):
    NONE = "@MenuActionControl.NONE"
    NODELAY = "@MenuActionControl.NODELAY"


class uiMenu(ui.Menu):
        def __init__(self, *args, **kwargs):
            self.glyph = None
            self.submenu = False
            self.menu_hotkey_text = None
            self.menu_checkable = False

            if "glyph" in kwargs:
                self.glyph = kwargs["glyph"]
                del kwargs["glyph"]

            if "menu_checkable" in kwargs:
                self.menu_checkable = kwargs["menu_checkable"]
                del kwargs["menu_checkable"]

            if "menu_hotkey_text" in kwargs:
                self.menu_hotkey_text = kwargs["menu_hotkey_text"]
                del kwargs["menu_hotkey_text"]

            if "submenu" in kwargs:
                self.submenu = kwargs["submenu"]
                del kwargs["submenu"]

            super().__init__(*args, **kwargs, spacing=2, menu_compatibility=False)


class uiMenuItem(ui.MenuItem):
        def __init__(self, *args, **kwargs):
            self.glyph = None
            self.submenu = False
            self.menu_hotkey_text = None
            self.menu_checkable = False

            if "glyph" in kwargs:
                self.glyph = kwargs["glyph"]
                del kwargs["glyph"]

            if "menu_checkable" in kwargs:
                self.menu_checkable = kwargs["menu_checkable"]
                del kwargs["menu_checkable"]

            if "menu_hotkey_text" in kwargs:
                self.menu_hotkey_text = kwargs["menu_hotkey_text"]
                del kwargs["menu_hotkey_text"]

            super().__init__(*args, **kwargs, menu_compatibility=False)


class IconMenuDelegate(ui.MenuDelegate):
    EXTENSION_FOLDER_PATH = Path()
    ICON_PATH  = EXTENSION_FOLDER_PATH.joinpath("data/icons")
    ICON_SIZE = 14
    TICK_SIZE = 14
    TICK_SPACING = [3, 6]
    MARGIN_SIZE = [4, 4]
    ROOT_SPACING = 1
    SUBMENU_PRE_SPACING = 5
    SUBMENU_SPACING = 4
    SUBMENU_ICON_SIZE = 10
    ITEM_SPACING = 4
    ICON_SPACING = 4
    HOTKEY_SPACING = [8, 108]
    COLOR_LABEL_ENABLED = 0xFFCCCCCC
    COLOR_LABEL_DISABLED = 0xFF6F6F6F
    COLOR_TICK_ENABLED = 0xFFCCCCCC
    COLOR_TICK_DISABLED = 0xFF4F4F4F
    COLOR_SEPARATOR = 0xFF6F6F6F

    # doc compiler breaks if `omni.kit.app.get_app()` or `carb.settings.get_settings()` is called
    try:
        EXTENSION_FOLDER_PATH = Path(omni.kit.app.get_app().get_extension_manager().get_extension_path_by_module(__name__))
        ICON_PATH  = EXTENSION_FOLDER_PATH.joinpath("data/icons")
        ICON_SIZE = carb.settings.get_settings().get("exts/omni.kit.menu.utils/icon_size")
        TICK_SIZE = carb.settings.get_settings().get("exts/omni.kit.menu.utils/tick_size")
        TICK_SPACING = carb.settings.get_settings().get("exts/omni.kit.menu.utils/tick_spacing")
        MARGIN_SIZE = carb.settings.get_settings().get("exts/omni.kit.menu.utils/margin_size")
        ROOT_SPACING = carb.settings.get_settings().get("exts/omni.kit.menu.utils/root_spacing")
        SUBMENU_PRE_SPACING = carb.settings.get_settings().get("exts/omni.kit.menu.utils/submenu_pre_spacing")
        SUBMENU_SPACING = carb.settings.get_settings().get("exts/omni.kit.menu.utils/submenu_spacing")
        SUBMENU_ICON_SIZE = carb.settings.get_settings().get("exts/omni.kit.menu.utils/submenu_icon_size")
        ITEM_SPACING = carb.settings.get_settings().get("exts/omni.kit.menu.utils/item_spacing")
        ICON_SPACING = carb.settings.get_settings().get("exts/omni.kit.menu.utils/icon_spacing")
        HOTKEY_SPACING = carb.settings.get_settings().get("exts/omni.kit.menu.utils/hotkey_spacing")
        COLOR_LABEL_ENABLED = carb.settings.get_settings().get("exts/omni.kit.menu.utils/color_label_enabled")
        COLOR_LABEL_DISABLED = carb.settings.get_settings().get("exts/omni.kit.menu.utils/color_label_disabled")
        COLOR_TICK_ENABLED = carb.settings.get_settings().get("exts/omni.kit.menu.utils/color_tick_enabled")
        COLOR_TICK_DISABLED = carb.settings.get_settings().get("exts/omni.kit.menu.utils/color_tick_disabled")
        COLOR_SEPARATOR = carb.settings.get_settings().get("exts/omni.kit.menu.utils/color_separator")
    except Exception as exc:
        carb.log_warn(f"IconMenuDelegate: failed to get EXTENSION_FOLDER_PATH and defaults {exc}")


    MENU_STYLE = {"Label::Enabled":      {"margin_width": MARGIN_SIZE[0], "margin_height": MARGIN_SIZE[1], "color": COLOR_LABEL_ENABLED},
                  "Label::Disabled":     {"margin_width": MARGIN_SIZE[0], "margin_height": MARGIN_SIZE[1], "color": COLOR_LABEL_DISABLED},
                  "Image::Icon":         {"margin_width": 0, "margin_height": 0, "color": COLOR_LABEL_ENABLED},
                  "Image::SubMenu":      {"image_url": f"{ICON_PATH}/subdir.svg",  "margin_width": 0, "margin_height": 0, "color": COLOR_LABEL_ENABLED},
                  "Image::TickEnabled":  {"image_url": f"{ICON_PATH}/checked.svg", "margin_width": 0, "margin_height": 0, "color": COLOR_TICK_ENABLED},
                  "Image::TickDisabled": {"image_url": f"{ICON_PATH}/checked.svg", "margin_width": 0, "margin_height": 0, "color": COLOR_TICK_DISABLED},
                  "Menu.Separator": {
                      "color": COLOR_SEPARATOR
                      },
                 }

    def build_item(self, item: ui.Menu):
        if isinstance(item, ui.Separator):
            with ui.HStack(height=0, style=IconMenuDelegate.MENU_STYLE):
                return super().build_item(item)

        with ui.HStack(height=0, style=IconMenuDelegate.MENU_STYLE):
            if not (isinstance(item, uiMenu) and not  item.submenu):
                ui.Spacer(width=IconMenuDelegate.ITEM_SPACING)
            else:
                ui.Spacer(width=IconMenuDelegate.ROOT_SPACING)

            # build tick
            if item.menu_checkable:
                ui.Spacer(width=IconMenuDelegate.TICK_SPACING[0])
                if item.checkable:
                    ui.Image("", width=IconMenuDelegate.TICK_SIZE, name="TickEnabled" if item.checked else "TickDisabled")
                else:
                    ui.Spacer(width=IconMenuDelegate.TICK_SIZE)
                ui.Spacer(width=IconMenuDelegate.TICK_SPACING[1])

            # build glyph
            if item.glyph:
                glyph_path = item.glyph if "/" in item.glyph.replace("\\", "/") else carb.tokens.get_tokens_interface().resolve("${glyphs}/"+ item.glyph)
                ui.Spacer(width=IconMenuDelegate.ICON_SPACING)
                ui.Image(glyph_path, width=IconMenuDelegate.ICON_SIZE, name="Icon")
                ui.Spacer(width=IconMenuDelegate.ICON_SPACING)

            # build label
            if isinstance(item, uiMenu):
                ui.Label(f"{item.text}", height=IconMenuDelegate.ICON_SIZE, name="Enabled" if item.enabled else "Disabled")
                ui.Spacer(width=IconMenuDelegate.ROOT_SPACING)
            else:
                ui.Label(f"{item.text}  ", height=IconMenuDelegate.ICON_SIZE, name="Enabled" if item.enabled else "Disabled")

            # build hotkey text
            if item.hotkey_text:
                ui.Spacer(width=IconMenuDelegate.HOTKEY_SPACING[0])
                ui.Label(item.hotkey_text.title(), height=IconMenuDelegate.ICON_SIZE, name="Disabled", width=100)
            elif item.menu_hotkey_text:
                ui.Spacer(width=IconMenuDelegate.HOTKEY_SPACING[1])

            # build subdir marker
            if item.submenu:
                ui.Spacer(width=IconMenuDelegate.SUBMENU_PRE_SPACING)
                ui.Image("", width=IconMenuDelegate.SUBMENU_ICON_SIZE, name="SubMenu")
                ui.Spacer(width=IconMenuDelegate.SUBMENU_SPACING)


class AppMenu():
    def __init__(self, get_instance: callable):
        from omni.kit.mainwindow import get_main_window

        self._get_instance = get_instance
        self._menu_hooks = []
        self._active_menus = {}
        self._main_menus = {}
        self.set_right_padding(0)
        self._dirty_menus = set()
        self._stats = omni.kit.menu.utils.get_debug_stats()
        if not "triggered_refresh" in self._stats:
            self._stats["triggered_refresh"] = 0

        # setup window
        self._menu_bar = get_main_window().get_main_menu_bar()
        self._menu_bar.visible = True

    def destroy(self):
        for action_path in self._active_menus:
            self._active_menus[action_path] = None
        del self._active_menus
        self._active_menus = {}
        self._main_menus = {}

        self._menu_bar.clear()
        self._menu_bar = None

    def add_hook(self, callback: Callable):
        self._menu_hooks.append(callback)

    def remove_hook(self, callback: Callable):
        try:
            self._menu_hooks.remove(callback)
        except Exception as exc:
            carb.log_warn(f"omni.kit.menu.utils remove_hook failed {exc}")

    def merge_menus(self, menu_keys: list, menu_defs: list, menu_order: list, delegates: dict={}):
        merged_menu = {}

        def order_sort(name):
            if name in menu_order:
                return menu_order[name]
            return 0

        def get_item_menu_index(menu: list, name: str):
            for index, item in enumerate(menu):
                if item.name == name:
                    return index
            return -1

        def process_part(name, part):
            used = False
            if isinstance(part.appear_after, list):
                for after in part.appear_after:
                    if after == MenuItemOrder.FIRST:
                        merged_menu[name].insert(0, part)
                        used = True
                        break
                    elif after == MenuItemOrder.LAST:
                        merged_menu[name].append(part)
                        used = True
                        break
                    index = get_item_menu_index(merged_menu[name], after)
                    if index != -1:
                        merged_menu[name].insert(index + 1, part)
                        used = True
                        break
            else:
                index = get_item_menu_index(merged_menu[name], part.appear_after)
                if index != -1:
                    merged_menu[name].insert(index + 1, part)
                    used = True
            return used

        def get_items(sorted_keys, use_appear_after):
            appear_after_retries = []
            for name in sorted_keys:
                if not name in merged_menu:
                    merged_menu[name] = []
                for parts in menu_defs[name]:
                    for part in parts:
                        if part.appear_after:
                            if use_appear_after:
                                used = process_part(name, part)
                                if not used:
                                    appear_after_retries.append((name, part))
                        elif not use_appear_after:
                            merged_menu[name].append(part)

            for name, part in appear_after_retries:
                used = process_part(name, part)
                if not used:
                    carb.log_verbose(f"Warning: menu item failed to find appear_after index {part.appear_after}")
                    merged_menu[name].append(part)

        # order using menu_order
        sorted_keys = list(menu_keys)
        if menu_order:
            sorted_keys.sort(key=order_sort)

        # get menu non-appear_after items & appear_after items
        get_items(sorted_keys, False)
        get_items(sorted_keys, True)

        for hook_fn in self._menu_hooks:
            hook_fn(merged_menu)

        # prebuild menus so layout can be applied before building
        self._prebuilt_menus = {}
        for name in merged_menu.keys():
            self.prebuild_menu(merged_menu[name], name, get_action_path(name, None), delegates[name][0] if name in delegates else None)

        prebuilt_menus = self._prebuilt_menus
        self._prebuilt_menus = None
        self.get_menu_layout().apply_layout(prebuilt_menus)
        return prebuilt_menus

    def prebuild_menu(self, menus, prefix_name, action_prefix, delegate):
        try:
            import copy

            def prebuild_menu(entry, prefix_name, action_prefix):
                item = copy.copy(entry)
                item.user["prebuilt_order"] = PrebuiltItemOrder.UNORDERED

                # add submenu items to list
                create_prebuild_entry(self._prebuilt_menus, prefix_name, action_prefix)

                # check if item already on list, to avoid duplicates
                duplicate = False
                if item.name != "":
                    for old_item in self._prebuilt_menus[action_prefix]["items"]:
                        if item.name == old_item.name:
                            duplicate = True
                            break

                if not duplicate:
                    # prefix_name cannot be "" as creating submenus with "" causes problems
                    self._prebuilt_menus[action_prefix]["items"].append(item)
                    self._prebuilt_menus[action_prefix]["prefix_name"] = prefix_name if prefix_name else "Empty"
                    self._prebuilt_menus[action_prefix]["remapped"] = []
                    self._prebuilt_menus[action_prefix]["action_prefix"] = action_prefix
                    self._prebuilt_menus[action_prefix]["menu_alignment"] = MenuAlignment.DEFAULT
                    self._prebuilt_menus[action_prefix]["delegate"] = delegate

                    if has_delegate_func(delegate, "get_menu_alignment"):
                        self._prebuilt_menus[action_prefix]["menu_alignment"] = delegate.get_menu_alignment()

                if item.sub_menu and entry.name:
                    sub_prefix = get_menu_name(entry)
                    sub_action = get_action_path(action_prefix, entry.name)
                    for sub_item in item.sub_menu:
                        prebuild_menu(sub_item, sub_prefix, sub_action)
                    self._prebuilt_menus[sub_action]["sub_menu"] = True
                    item.sub_menu = sub_action

            for menu_entry in menus:
                prebuild_menu(menu_entry, prefix_name, action_prefix)
        except Exception as e:
            carb.log_error(f"Error {e} creating menu {menu_entry}")
            import traceback, sys
            traceback.print_exc(file=sys.stdout)

    def get_menu_layout(self):
        instance = self._get_instance()
        if instance:
            return instance.get_menu_layout()

        return None

    def get_fn_result(self, menu_entry: MenuItemDescription, name: str, default: bool=True):
        fn = getattr(menu_entry, name)
        if not fn:
            return default
        if isinstance(fn, list):
            for fn_item in fn:
                if fn_item and not fn_item():
                    return False
        else:
            if fn and not fn():
                return False
        return default

    def get_menu_data(self):
        instance = self._get_instance()
        if instance:
            return instance.get_menu_data()
        return None, None, None

    def set_right_padding(self, padding):
        self._right_padding = padding

    def create_menu(self):
        menu_defs, menu_order, delegates = self.get_menu_data()

        self._menu_bar.clear()
        self._active_menus = {}
        self._main_menus = {}
        self._visible_menus = {}
        self._last_item = {}

        menus = self.merge_menus(menu_defs.keys(), menu_defs, menu_order, delegates)
        right_menus = []
        with self._menu_bar:
            for name in menus.keys():
                if not "sub_menu" in menus[name]:
                    if menus[name]["menu_alignment"] == MenuAlignment.RIGHT:
                        right_menus.append(name)
                    else:
                        self._build_menu(menus[name], menus, True)

            if right_menus:
                ui.Spacer().identifier = "right_aligned_menus"
                for name in right_menus:
                    self._build_menu(menus[name], menus, True)
                ui.Spacer(width=self._right_padding).identifier = "right_padding"

        for key in self._main_menus.keys():
            if key not in self._visible_menus:
                self._main_menus[key].visible = False
            if key in self._last_item:
                if isinstance(self._last_item[key], ui.Separator):
                    self._last_item[key].visible = False

        self._last_item = None
        self._visible_menus = None

        return menus

    def refresh_menu_items(self, name: str):
        self._dirty_menus.add(name)

    def _submenu_is_shown(self, action_prefix: str, visible: bool, delegate: Any, refresh :bool):
        if action_prefix in self._main_menus:
            self._main_menus[action_prefix].visible = visible
            if has_delegate_func(delegate, "update_menu_item"):
                delegate.update_menu_item(self._main_menus[action_prefix], refresh)

    # update to use new prebuild/layouts
    def _refresh_menu(self, prebuilt_menus, prebuilt_menus_root, visible_override=None):
        menus = prebuilt_menus["items"]
        prefix_name = prebuilt_menus["prefix_name"]
        action_prefix = prebuilt_menus["action_prefix"]
        delegate = prebuilt_menus["delegate"] if "delegate" in prebuilt_menus else None

        visible_count = 0
        for menu_entry in sorted(menus, key=lambda a: a.user["prebuilt_order"]):
            try:
                menu_name = get_menu_name(menu_entry)
                action_path = (get_action_path(action_prefix, menu_entry.name))

                enabled = menu_entry.enabled
                value = menu_entry.ticked_value
                visible = True
                if menu_entry.enable_fn:
                    enabled = self.get_fn_result(menu_entry, "enable_fn")
                if menu_entry.ticked_fn:
                    value = self.get_fn_result(menu_entry, "ticked_fn")
                if menu_entry.show_fn:
                    visible = self.get_fn_result(menu_entry, "show_fn")

                # handle sub_menu before action_path/active_menus can skip it
                if menu_entry.sub_menu and menu_entry.sub_menu in prebuilt_menus_root:
                    visible = self._refresh_menu(prebuilt_menus_root[menu_entry.sub_menu], prebuilt_menus_root, self.get_fn_result(menu_entry, "show_fn", None))
                    # update visibility
                    if visible_override is not None:
                        visible = visible_override
                    visible_count += 1 if visible else 0
                    self._submenu_is_shown(menu_entry.sub_menu, visible, delegate, True)
                    continue

                if not action_path in self._active_menus:
                    continue

                item = self._active_menus[action_path]
                item.enabled = enabled
                item.checked = value
                item.visible = visible
                if has_delegate_func(delegate, "update_menu_item"):
                    delegate.update_menu_item(item, True)
                visible_count += 1 if item.visible else 0
            except Exception as e:
                carb.log_error(f"Error {e} refreshing menu {menu_entry}")
                import traceback, sys
                traceback.print_exc(file=sys.stdout)

        return bool(visible_count > 0)

    def _build_menu(self, prebuilt_menus, prebuilt_menus_root, is_root_menu, visible_override=None, menu_checkable=False, menu_hotkey_text=False, glyph=None) -> int:
        def on_triggered():
            if self._dirty_menus:
                self._stats["triggered_refresh"] += 1
                menu_defs, menu_order, menu_delegates = self.get_menu_data()
                menus = self.merge_menus(menu_defs.keys(), menu_defs, menu_order)

                for name in self._dirty_menus:
                    action_path = get_action_path(name, None)
                    if action_path in menus:
                        self._refresh_menu(menus[action_path], menus)
                        for remapped in menus[action_path]["remapped"]:
                            self._refresh_menu(menus[remapped], menus)
                self._dirty_menus.clear()

        def prep_menu(main_menu, prefix_name: str, action_prefix: str, visible: bool, delegate: callable, glyph: str, menu_checkable: bool, menu_hotkey_text: bool):
            if not action_prefix in main_menu:
                main_menu[action_prefix] = uiMenu(prefix_name, visible=visible, delegate=delegate, glyph=glyph, menu_checkable=menu_checkable, menu_hotkey_text=menu_hotkey_text, submenu=not is_root_menu)
                if is_root_menu:
                    main_menu[action_prefix].set_triggered_fn(on_triggered)

        menus = prebuilt_menus["items"]
        prefix_name = prebuilt_menus["prefix_name"]
        action_prefix = prebuilt_menus["action_prefix"]
        delegate = prebuilt_menus["delegate"] if "delegate" in prebuilt_menus else None
        visible_count = 0
        icon_delegate = IconMenuDelegate()

        # this menu item is built with menu_checkable passed via caller
        prep_menu(self._main_menus, prefix_name, action_prefix, True if visible_override is None else visible_override, delegate if delegate else icon_delegate, glyph, menu_checkable, menu_hotkey_text)

        # get delegate for icons
        menu_hotkey_text = False
        menu_checkable = False
        for menu_entry in sorted(menus, key=lambda a: a.user["prebuilt_order"]):
            if menu_entry.ticked:
                menu_checkable = True
            if menu_entry.original_menu_item and menu_entry.original_menu_item.hotkey:
                menu_hotkey_text = True

        with self._main_menus[action_prefix]:
            for menu_entry in sorted(menus, key=lambda a: a.user["prebuilt_order"]):
                try:
                    item = None
                    menu_name = get_menu_name(menu_entry)
                    action_path = (get_action_path(action_prefix, menu_entry.name))

                    on_lmb_click = None
                    if menu_entry.onclick_action and menu_entry.unclick_action:
                        def on_hotkey_action(onclick_action, unclick_action):
                            self._execute_action(onclick_action)
                            self._execute_action(unclick_action)
                        on_lmb_click = lambda oca=menu_entry.onclick_action, uca=menu_entry.unclick_action: on_hotkey_action(oca, uca)
                    elif menu_entry.onclick_action:
                        on_lmb_click = lambda oca=menu_entry.onclick_action: self._execute_action(oca)
                    elif menu_entry.onclick_fn and menu_entry.unclick_fn:
                        def on_hotkey_click():
                            menu_entry.onclick_fn()
                            menu_entry.unclick_fn()
                        on_lmb_click = on_hotkey_click
                    else:
                        on_lmb_click = menu_entry.onclick_fn

                    hotkey = None
                    if menu_entry.original_menu_item:
                        hotkey = menu_entry.original_menu_item.hotkey
                    enabled = menu_entry.enabled
                    ticked = menu_entry.ticked
                    value = menu_entry.ticked_value
                    visible = True
                    if menu_entry.enable_fn:
                        enabled = self.get_fn_result(menu_entry, "enable_fn")
                    if menu_entry.ticked_fn:
                        value = self.get_fn_result(menu_entry, "ticked_fn")
                    if menu_entry.show_fn:
                        visible = self.get_fn_result(menu_entry, "show_fn")

                    if menu_entry.name and menu_entry.sub_menu:
                        if menu_entry.sub_menu in prebuilt_menus_root:
                            self._visible_menus[action_prefix] = visible
                            visible = self._build_menu(prebuilt_menus_root[menu_entry.sub_menu], prebuilt_menus_root, False, self.get_fn_result(menu_entry, "show_fn", None), menu_checkable, menu_hotkey_text, menu_entry.glyph)
                            if visible_override is not None:
                                visible = visible_override
                            visible_count += 1 if visible else 0
                            self._submenu_is_shown(menu_entry.sub_menu, visible, delegate, False)
                        continue
                    elif menu_entry.header != None:
                        if action_prefix in self._last_item:
                            if isinstance(self._last_item[action_prefix], ui.Separator):
                                # last thing was a Separator, change the text to new value
                                self._last_item[action_prefix].text = menu_entry.header
                            else:
                                item = ui.Separator(menu_entry.header)
                        else:
                            item = ui.Separator(menu_entry.header)
                    elif menu_entry.name == "":
                        if action_prefix in self._last_item:
                            if action_prefix in self._last_item and not isinstance(self._last_item[action_prefix], ui.Separator):
                                item = ui.Separator()
                        else:
                            item = ui.Separator()
                    else:
                        item = uiMenuItem(menu_name, triggered_fn=on_lmb_click if on_lmb_click else None, checkable=ticked, checked=value, enabled=enabled, visible=visible, style=menu_entry.user.get("user_style", {}), delegate=icon_delegate, glyph=menu_entry.glyph, menu_checkable=menu_checkable, menu_hotkey_text=menu_hotkey_text)

                        if has_delegate_func(delegate, "update_menu_item"):
                            delegate.update_menu_item(item, False)
                        self._active_menus[action_path] = item
                        if hotkey:
                            item.hotkey_text = "  " + menu_entry.original_menu_item.hotkey_text
                            setup_hotkey = True
                            try:
                                import omni.kit.hotkeys.core
                                # Always setup hotkey because hotkey may changed
                                setup_hotkey = True
                            except ImportError:
                                if hasattr(menu_entry.original_menu_item, "_action_setting_sub_id"):
                                    setup_hotkey = False
                            if setup_hotkey:
                                hotkey_text = self._setup_hotkey(menu_entry.original_menu_item, action_path)
                                if hotkey_text is not None:
                                    item.hotkey_text = hotkey_text
                    if item:
                        self._last_item[action_prefix] = item
                        self._visible_menus[action_prefix] = visible
                        visible_count += 1 if visible else 0

                except Exception as e:
                    carb.log_error(f"Error {e} creating menu {menu_entry}")
                    import traceback, sys
                    traceback.print_exc(file=sys.stdout)

        return bool(visible_count > 0)

    def _execute_action(self, action: Tuple):
        if not action:
            return

        async_delay = True

        # check for MenuActionControl in action & remove
        actioncontrol_list = [e for e in MenuActionControl]
        if any(a in action for a in actioncontrol_list):
            if MenuActionControl.NODELAY in action:
                async_delay = False
            action = tuple([item for item in action if item not in actioncontrol_list])

        try:
            import omni.kit.actions.core
            import omni.kit.app

            if async_delay:
                async def execute_action(action):
                    await omni.kit.app.get_app().next_update_async()
                    omni.kit.actions.core.execute_action(*action)

                # omni.ui can sometimes crash is menu callback does ui calls.
                # To avoid this, use async function with frame delay
                asyncio.ensure_future(execute_action(action))
            else:
                omni.kit.actions.core.execute_action(*action)
        except ModuleNotFoundError:
            carb.log_warn(f"menu_action: error omni.kit.actions.core not loaded")
        except Exception as exc:
            carb.log_warn(f"menu_action: error {exc}")

    def _setup_hotkey(self, menu_entry: MenuItemDescription, action_path: str) -> str:
        def action_trigger(on_action_pressed_fn, on_action_release_fn, evt, *_):
            if evt.flags & carb.input.BUTTON_FLAG_PRESSED:
                if on_action_pressed_fn:
                    on_action_pressed_fn()
            elif on_action_release_fn:
                on_action_release_fn()

        if not menu_entry.hotkey:
            return None

        self._clear_hotkey(menu_entry)
        self._unregister_hotkey(menu_entry)

        import omni.appwindow
        appwindow = omni.appwindow.get_default_app_window()
        input = carb.input.acquire_input_interface()
        settings = carb.settings.get_settings()

        action_mapping_set_path = appwindow.get_action_mapping_set_path()
        action_mapping_set = input.get_action_mapping_set_by_path(action_mapping_set_path)
        input_string = menu_entry.get_action_mapping_desc()
        menu_entry._action_setting_input_path = action_mapping_set_path + "/" + action_path + "/0"
        settings.set_default_string(menu_entry._action_setting_input_path, input_string)
        if menu_entry.onclick_action:
            try:
                from omni.kit.hotkeys.core import get_hotkey_registry
                if not get_hotkey_registry():
                    raise ImportError
                current_action_triggger = None
                self._register_hotkey(menu_entry)
                menu_entry.add_on_hotkey_update_func(lambda me, a=action_path: self._setup_hotkey(me, a))
                if hasattr(menu_entry, "_pressed_hotkey"):
                    return menu_entry._pressed_hotkey.key_text if menu_entry._pressed_hotkey else ""
                elif hasattr(menu_entry, "_released_hotkey"):
                    return menu_entry._released_hotkey.key_text if menu_entry._released_hotkey else ""
                else:
                    return None
            except ImportError:
                current_action_triggger = functools.partial(action_trigger, lambda oca=menu_entry.onclick_action: self._execute_action(oca), lambda uca=menu_entry.unclick_action: self._execute_action(uca))
            
        else:
            current_action_triggger = functools.partial(action_trigger, menu_entry.onclick_fn, menu_entry.unclick_fn)

        if current_action_triggger:
            menu_entry._action_setting_sub_id = input.subscribe_to_action_events(action_mapping_set, action_path, current_action_triggger)
            menu_entry.add_on_delete_func(self._clear_hotkey)
            menu_entry.add_on_hotkey_update_func(lambda me, a=action_path: self._setup_hotkey(me, a))

            # menu needs to refresh when self._action_setting_input_path changes
            def hotkey_changed(changed_item: carb.dictionary.Item, change_event_type: carb.settings.ChangeEventType, weak_entry: weakref, action_path: str):
                import carb.input

                def set_hotkey_text(action_path, hotkey_text, active_menu_text):
                    menu_entry.hotkey_text = hotkey_text
                    if action_path in self._active_menus:
                        self._active_menus[action_path].hotkey_text = active_menu_text

                menu_entry = weak_entry()
                if not menu_entry:
                    return

                # "changed_item" was DESTROYED, so remove hotkey
                if change_event_type == carb.settings.ChangeEventType.DESTROYED:
                    set_hotkey_text(action_path, "", "")
                    return

                hotkey_mapping = changed_item.get_dict()
                if isinstance(hotkey_mapping, str):
                    set_hotkey_text(action_path, hotkey_mapping.replace("Keyboard::", ""), "  " + menu_entry.hotkey_text)
                else:
                    set_hotkey_text(action_path, "", "")

            menu_entry._action_setting_changed_sub = settings.subscribe_to_node_change_events(menu_entry._action_setting_input_path, lambda ci, ev, m=weakref.ref(menu_entry), a=action_path: hotkey_changed(ci, ev, m, a))
            return None

    def _clear_hotkey(self, menu_entry):
        if hasattr(menu_entry, "_action_setting_sub_id"):
            try:
                input = carb.input.acquire_input_interface()
                input.unsubscribe_to_action_events(menu_entry._action_setting_sub_id)
                del menu_entry._action_setting_sub_id
                settings = carb.settings.get_settings()
                settings.unsubscribe_to_change_events(menu_entry._action_setting_changed_sub)
                del menu_entry._action_setting_changed_sub
                del menu_entry._action_setting_input_path
            except:
                pass

    def _register_hotkey(self, menu_entry: MenuItemDescription):
        try:
            from omni.kit.hotkeys.core import get_hotkey_registry, KeyCombination
            hotkey_registry = get_hotkey_registry()
            if not hotkey_registry:
                raise ImportError
            HOTKEY_EXT_ID = "omni.kit.menu.utils"
            if menu_entry.onclick_action:
                key = KeyCombination(menu_entry.hotkey[1], modifiers=menu_entry.hotkey[0])
                menu_entry._pressed_hotkey = hotkey_registry.register_hotkey(HOTKEY_EXT_ID, key, menu_entry.onclick_action[0], menu_entry.onclick_action[1])
            if menu_entry.unclick_action:
                key = KeyCombination(menu_entry.hotkey[1], modifiers=menu_entry.hotkey[0], trigger_press=False)
                menu_entry._released_hotkey = hotkey_registry.register_hotkey(HOTKEY_EXT_ID, key, menu_entry.unclick_action[0], menu_entry.unclick_action[1])
            menu_entry.add_on_delete_func(self._unregister_hotkey)
        except ImportError:
            pass
        
    def _unregister_hotkey(self, menu_entry: MenuItemDescription):
        try:
            from omni.kit.hotkeys.core import get_hotkey_registry
            hotkey_registry = get_hotkey_registry()
            if hotkey_registry:
                if hasattr(menu_entry, "_pressed_hotkey"):
                    if menu_entry._pressed_hotkey:
                        hotkey_registry.deregister_hotkey(menu_entry._pressed_hotkey)
                if hasattr(menu_entry, "_released_hotkey"):
                    if menu_entry._released_hotkey:
                        hotkey_registry.deregister_hotkey(menu_entry._released_hotkey)
            
            if hasattr(menu_entry, "__hotkey_changed_event_sub"):
                menu_entry.__hotkey_changed_event_sub = None
        except ImportError:
            pass
