import carb
import omni.kit.ui
from typing import Callable, Tuple, Union, List
from omni import ui


def get_menu_name(menu_entry):
    if menu_entry.name_fn:
        try:
            return f"{menu_entry.name_fn()}"
        except Exception as exc:
            carb.log_warn(f"get_menu_name error:{str(exc)}")
    return f"{menu_entry.name}"

def get_action_path(action_prefix: str, menu_entry_name: str):
    if not menu_entry_name:
        return action_prefix.replace(" ", "_").replace("/", "_").replace(".", "")
    return f"{action_prefix}/{menu_entry_name}".replace(" ", "_").replace("/", "_").replace(".", "")

def has_delegate_func(delegate, func_name):
    return bool(delegate and hasattr(delegate, func_name) and callable(getattr(delegate, func_name)))

def create_prebuild_entry(prebuilt_menus: dict, prefix_name: str, action_prefix: str):
    if not action_prefix in prebuilt_menus:
        # prefix_name cannot be "" as creating submenus with "" causes problems
        prebuilt_menus[action_prefix] = {}
        prebuilt_menus[action_prefix]["items"] = []
        prebuilt_menus[action_prefix]["prefix_name"] = prefix_name if prefix_name else "Empty"
        prebuilt_menus[action_prefix]["remapped"] = []
        prebuilt_menus[action_prefix]["action_prefix"] = action_prefix


class PrebuiltItemOrder:
    UNORDERED = 0x7FFFFFFF
    LAYOUT_ORDERED = 0x00000000
    LAYOUT_SUBMENU_SORTED = 0x00001000
    LAYOUT_ITEM_SORTED = 0x00002000

class MenuAlignment:
    DEFAULT = 0
    RIGHT = 1

class LayoutSourceSearch:
    EVERYWHERE = 0
    LOCAL_ONLY = 1


class MenuItemDescription:
    """
    name is name shown on menu. (if name is "" then a menu spacer is added. Can be combined with show_fn)
    glyph is icon shown on menu
    header is True/False to show seperator above item
    hotkey hotkey for menu item
    enabled enabled True/False is item enabled
    ticked menu item is ticked when True
    sub_menu is sub menu to this menu
    name_fn is function to get menu name
    show_fn funcion or list of functions used to decide if menu item is shown. All functions must return True to show
    enable_fn funcion or list of functions used to decide if menu item is enabled. All functions must return True to be enabled
    onclick_action action called when user clicks menu item
    unclick_action action called when user releases click on menu item
    onclick_fn function called when user clicks menu item (deprecated)
    unclick_fn function called when user releases click on menu item (deprecated)
    onclick_right_fn function called when user right clicks menu item (deprecated)
    ticked_fn funcion or list of functions used to decide if menu item is ticked
    appear_after is name of menu item to insert after. Used for appending menus, can be a list or string
    user is user dictonary that is passed to menu
    """

    MAX_DEPTH = 16

    def __init__(
        self,
        name: str = "",
        glyph: str = "",
        header: str = None,
        appear_after: Union[list, str] = "",
        enabled: bool = True,
        ticked: bool = False,
        ticked_value: Union[bool, None] = None,
        sub_menu=None,
        hotkey: Tuple[int, int] = None,
        name_fn: Callable = None,
        show_fn: Callable = None,
        enable_fn: Callable = None,
        ticked_fn: Callable = None,
        onclick_action: Tuple = None,
        unclick_action: Tuple = None,
        onclick_fn: Callable = None,        # deprecated
        unclick_fn: Callable = None,        # deprecated
        onclick_right_fn: Callable = None,  # deprecated
        original_svg_color: bool = False,   # deprecated - only used for editor_menu
        original_menu_item = None,          # private for hotkey processing
        user = {}
    ):
        self._on_delete_funcs = []
        self._on_hotkey_update_funcs = []
        # don't allow unnamed submenus as causes Menu problems
        if name == "" and sub_menu:
            self.name = "SubMenu"
        else:
            self.name = name
        self.glyph = glyph
        self.header = header
        self.appear_after = appear_after
        self.ticked = True if ticked or ticked_value is not None or ticked_fn is not None else False
        self.ticked_value = ticked_value
        self.enabled = enabled
        self.sub_menu = sub_menu
        self.name_fn = name_fn
        self.show_fn = show_fn
        self.enable_fn = enable_fn
        self.ticked_fn = ticked_fn
        self.onclick_action = onclick_action
        self.unclick_action = unclick_action
        self.set_hotkey(hotkey)
        self.original_menu_item = original_menu_item
        self.user = user.copy()

        # Deprecated
        self.original_svg_color = original_svg_color
        self.onclick_fn = onclick_fn
        self.unclick_fn = unclick_fn
        self.onclick_right_fn = onclick_right_fn

        log_deprecated = bool((carb.settings.get_settings().get_as_string("/exts/omni.kit.menu.utils/logDeprecated") or "true") == "true")
        if log_deprecated and not "shown_deprecated_warning" in self.user:
            def deprecated_msg(fn_name, func):
                carb.log_warn(f"Menu item \"{name}\" from {func.__module__} uses {fn_name}")

            self.user["shown_deprecated_warning"] = True
            if onclick_fn or unclick_fn or onclick_right_fn or original_svg_color:
                carb.log_warn(f"********************* MenuItemDescription {name} *********************")
            if onclick_fn:
                deprecated_msg("onclick_fn", onclick_fn)
            if unclick_fn:
                deprecated_msg("unclick_fn", unclick_fn)
            if onclick_right_fn:
                deprecated_msg("onclick_right_fn", onclick_right_fn)
            if original_svg_color:
                deprecated_msg("original_svg_color", original_svg_color)

    def add_on_delete_func(self, on_delete_fn: callable):
        self._on_delete_funcs.append(on_delete_fn)

    def remove_on_delete_func(self, on_delete_fn: callable):
        try:
            self._on_delete_funcs.remove(on_delete_fn)
        except Exception as exc:
            carb.log_warn(f"omni.kit.menu.utils remove_on_delete_func failed {str(exc)}")

    def add_on_hotkey_update_func(self, hotkey_update_fn: callable):
        self._on_hotkey_update_funcs.append(hotkey_update_fn)

    def remove_on_hotkey_update_func(self, hotkey_update_fn: callable):
        try:
            self._on_hotkey_update_funcs.remove(hotkey_update_fn)
        except Exception as exc:
            carb.log_warn(f"omni.kit.menu.utils remove_on_hotkey_update_func failed {str(exc)}")

    def set_hotkey(self, hotkey):
        self.hotkey = None
        self.hotkey_text = ""
        if hotkey:
            self.hotkey = hotkey
            self.hotkey_text = self.get_action_mapping_desc().replace("Keyboard::", "")

        for on_hotkey_update_fn in self._on_hotkey_update_funcs:
            on_hotkey_update_fn(self)

    def get_action_mapping_desc(self):
        if isinstance(self.hotkey, carb.input.GamepadInput):
            return carb.input.get_string_from_action_mapping_desc(self.hotkey)
        return carb.input.get_string_from_action_mapping_desc(self.hotkey[1], self.hotkey[0])

    def has_action(self):
        return bool(self.onclick_fn or self.onclick_action or self.unclick_fn or self.unclick_action)

    def __del__(self):
        self.original_menu_item = None
        for on_delete_fn in self._on_delete_funcs:
            on_delete_fn(self)
        del self._on_delete_funcs
        del self._on_hotkey_update_funcs

    def __repr__(self):
        return f"<{self.__class__} name:{self.name} user:{self.user}>"

    def __copy__(self):
        # as hotkeys are removed on copyed item delete, hotkeys are accessed via original_menu_item as these are not deleted until the menu is removed
        def get_original(item):
            original = item.original_menu_item
            if original:
                while(original.original_menu_item and original.original_menu_item.original_menu_item):
                    original = original.original_menu_item
                return original
            return item

        sub_menu = self.sub_menu
        if sub_menu:
            def copy_sub_menu(sub_menu, new_sub_menu, max_depth=MenuItemDescription.MAX_DEPTH):
                if max_depth == 0:
                    carb.log_warn(f"Recursive sub_menu {sub_menu} aborting copy")
                    return

                for sub_item in sub_menu:
                    item = MenuItemDescription(name=sub_item.name,
                                               glyph=sub_item.glyph,
                                               header=sub_item.header,
                                               appear_after=sub_item.appear_after,
                                               enabled=sub_item.enabled,
                                               ticked=sub_item.ticked,
                                               ticked_value=sub_item.ticked_value,
                                               sub_menu=None,
                                               hotkey=None,
                                               name_fn=sub_item.name_fn,
                                               show_fn=sub_item.show_fn,
                                               enable_fn=sub_item.enable_fn,
                                               ticked_fn=sub_item.ticked_fn,
                                               onclick_action=sub_item.onclick_action,
                                               unclick_action=sub_item.unclick_action,
                                               onclick_fn=sub_item.onclick_fn,
                                               unclick_fn=sub_item.unclick_fn,
                                               onclick_right_fn=sub_item.onclick_right_fn,
                                               original_svg_color=sub_item.original_svg_color,
                                               original_menu_item=get_original(sub_item),
                                               user=sub_item.user)

                    new_sub_menu.append(item)
                    if sub_item.sub_menu:
                        item.sub_menu = []
                        copy_sub_menu(sub_item.sub_menu, item.sub_menu, max_depth-1)

            new_sub_menu = []
            copy_sub_menu(sub_menu, new_sub_menu)
            sub_menu = new_sub_menu

        return MenuItemDescription(name=self.name,
                                   glyph=self.glyph,
                                   header=self.header,
                                   appear_after=self.appear_after,
                                   enabled=self.enabled,
                                   ticked=self.ticked,
                                   ticked_value=self.ticked_value,
                                   sub_menu=sub_menu,
                                   hotkey=None,
                                   name_fn=self.name_fn,
                                   show_fn=self.show_fn,
                                   enable_fn=self.enable_fn,
                                   ticked_fn=self.ticked_fn,
                                   onclick_action=self.onclick_action,
                                   unclick_action=self.unclick_action,
                                   onclick_fn=self.onclick_fn,
                                   unclick_fn=self.unclick_fn,
                                   onclick_right_fn=self.onclick_right_fn,
                                   original_svg_color=self.original_svg_color,
                                   original_menu_item=get_original(self),
                                   user=self.user)

    def json_enc(self):
        values = {  "name": self.name,
                    "glyph": self.glyph,
                    "header": self.header,
                    "enabled": self.enabled,
                    "sub_menu": self.sub_menu,
                    "hotkey": self.hotkey,
                    "name_fn": self.name_fn,
                    "show_fn": self.show_fn,
                    "enable_fn": self.enable_fn,
                    "onclick_action": self.onclick_action,
                    "unclick_action": self.unclick_action,
                    "onclick_fn": self.onclick_fn,
                    "unclick_fn": self.unclick_fn,
                    "onclick_right_fn": self.onclick_right_fn,
                    "original_svg_color": self.original_svg_color,
                    "user": self.user}

        if self.ticked:
            values["ticked"] = self.ticked
            values["ticked_value"] = self.ticked_value
            values["ticked_fn"] = self.ticked_fn

        if self.appear_after:
            values["appear_after"] = self.appear_after

        return {k: v for k, v in values.items() if v is not None}

    # Make a dict like interface for MenuItemDescription so it can be used as an entry for omni.kit.context_menu
    # overrides [] and get for read-only access
    def __getitem__(self, key, default_value=None):
        return getattr(self, key, default_value)

    def __contains__(self, key):
        return self.__getitem__(key) is not None

    def get(self, key, default_value=None):
        return self.__getitem__(key, default_value)
