# Copyright (c) 2018-2020, NVIDIA CORPORATION.  All rights reserved.
#
# NVIDIA CORPORATION and its licensors retain all intellectual property
# and proprietary rights in and to this software, related documentation
# and any modifications thereto.  Any use, reproduction, disclosure or
# distribution of this software and related documentation without an express
# license agreement from NVIDIA CORPORATION is strictly prohibited.
#
import asyncio
import os
import os.path
from enum import Enum
from functools import lru_cache
from string import Template
from typing import Callable, Dict, List, Tuple

import carb.events
import carb.settings
import carb.tokens
import omni.kit.app

from . import ext_controller
from .utils import get_ext_info_dict, get_extpath_git_ext, get_setting, set_default_and_get_setting, show_ok_popup

# Extension root path. Set when extension starts.
EXT_ROOT = None

REGISTRIES_CHANGED_EVENT = carb.events.type_from_string("omni.kit.registry.nucleus.REGISTRIES_CHANGED_EVENT")
REGISTRIES_SETTING = "/exts/omni.kit.registry.nucleus/registries"
USER_REGISTRIES_SETTING = "/persistent/exts/omni.kit.registry.nucleus/userRegistries"

EXTENSION_PULL_STARTED_EVENT = carb.events.type_from_string("omni.kit.window.extensions.EXTENSION_PULL_STARTED_EVENT")
COMMUNITY_TAB_TOGGLE_EVENT = carb.events.type_from_string("omni.kit.window.extensions.COMMUNITY_TAB_TOGGLE_EVENT")

REMOTE_IMAGE_SUPPORTED_EXTS = {".png"}


def path_is_parent(parent_path, child_path):
    try:
        return os.path.commonpath([parent_path]) == os.path.commonpath([parent_path, child_path])
    except ValueError:
        return False


def is_in_omni_documents(path):
    return path_is_parent(get_omni_documents_path(), path)


@lru_cache()
def get_icons_path() -> str:
    assert EXT_ROOT is not None, "This function should be called only after EXT_ROOT is set"
    return f"{EXT_ROOT}/icons"


@lru_cache()
def get_omni_documents_path() -> str:
    return os.path.abspath(carb.tokens.get_tokens_interface().resolve("${omni_documents}"))


@lru_cache()
def get_kit_path() -> str:
    return os.path.abspath(carb.tokens.get_tokens_interface().resolve("${kit}"))


@lru_cache()
def get_categories() -> Dict:
    icons_path = get_icons_path()

    categories = {
        "animation": {"name": "Animation", "image": f"{icons_path}/data/category-animation.png"},
        "graph": {"name": "Graph", "image": f"{icons_path}/data/category-graph.png"},
        "rendering": {"name": "Lighting & Rendering", "image": f"{icons_path}/data/category-rendering.png"},
        "audio": {"name": "Audio", "image": f"{icons_path}/data/category-audio.png"},
        "simulation": {"name": "Simulation", "image": f"{icons_path}/data/category-simulation.png"},
        "services": {"name": "Services", "image": f"{icons_path}/data/category-internal.png"},
        "core": {"name": "Core", "image": f"{icons_path}/data/category-core.png"},
        "example": {"name": "Example", "image": f"{icons_path}/data/category-example.png"},
        "internal": {"name": "Internal", "image": f"{icons_path}/data/category-internal.png", "hidden": False},
        "legacy": {"name": "Legacy", "image": f"{icons_path}/data/category-legacy.png"},
        "other": {"name": "Other", "image": f"{icons_path}/data/category-internal.png"},
    }

    return categories


class ExtSource(Enum):
    NVIDIA = 0
    THIRD_PARTY = 1

    def get_ui_name(self):
        return "NVIDIA" if self == ExtSource.NVIDIA else "THIRD PARTY"


class ExtAuthorGroup(Enum):
    NVIDIA = 0
    PARTNER = 1
    COMMUNITY_VERIFIED = 2
    COMMUNITY_UNVERIFIED = 3
    USER = 4

    def get_ui_name(self):
        if self == ExtAuthorGroup.NVIDIA:
            return "NVIDIA"
        if self == ExtAuthorGroup.PARTNER:
            return "PARTNER"
        if self == ExtAuthorGroup.USER:
            return "USER"
        return "COMMUNITY"


def get_registries():
    settings = carb.settings.get_settings()
    settings_dict = settings.get_settings_dictionary("")
    for key, is_user in [(REGISTRIES_SETTING, False), (USER_REGISTRIES_SETTING, True)]:
        for r in settings_dict.get(key[1:], []):
            name = r.get("name", "")
            url = r.get("url", "")
            url = carb.tokens.get_tokens_interface().resolve(url)
            yield name, url, is_user


@lru_cache()
def get_registry_url(registry_name):
    version = get_setting("/app/extensions/registryVersion", "NOT_SET")
    for name, url, _ in get_registries():
        if name == registry_name:
            return f"{url}/{version}"
    return None


class ExtensionCommonInfo:
    def __init__(self, ext_id, ext_info, is_local):
        self.id = ext_id
        package_dict = ext_info.get("package", {})
        self.fullname = package_dict["name"]
        self.title = package_dict.get("title", "").upper() or self.fullname
        self.name = self.title.lower()  # used for sorting
        self.description = package_dict.get("description", "") or "No Description"
        self.version = package_dict.get("version", None)
        self.keywords = package_dict.get("keywords", [])
        self.toggleable = package_dict.get("toggleable", True)
        self.feature = package_dict.get("feature", False)
        self.authors = package_dict.get("authors", "")
        self.repository = package_dict.get("repository", "")
        self.package_id = package_dict.get("packageId", "")
        self.is_kit_file = ext_info.get("isKitFile", False)
        self.is_app = ("app" in self.keywords) or package_dict.get("app", self.is_kit_file)
        self.is_startup = ext_controller.is_startup_ext(self.fullname)
        self.is_startup_version = ext_controller.is_startup_ext(ext_id)
        self.is_featured = ext_controller.is_featured_ext(self.fullname)
        self.is_local = is_local
        state_dict = ext_info.get("state", {})
        self.failed = state_dict.get("failed", False)
        self.enabled = state_dict.get("enabled", False)
        self.reloadable = state_dict.get("reloadable", False)
        self.is_pulled = state_dict.get("isPulled", False)
        self.is_toggle_blocked = (self.enabled and not self.reloadable) or (not self.toggleable)
        self.path = ext_info.get("path", "")
        self.provider_name = None

        # Only if solve_extensions is called
        self.non_toggleable_deps = []
        self.solver_error = ""

        self.category = package_dict.get("category", "other").lower()

        # Ext sources
        if package_dict.get("exchange", False):
            if package_dict.get("partner", False):
                self.author_group = ExtAuthorGroup.PARTNER
            else:
                self.author_group = ExtAuthorGroup.COMMUNITY_VERIFIED
        elif not package_dict.get("trusted", True):
            self.author_group = ExtAuthorGroup.COMMUNITY_UNVERIFIED
        else:
            self.author_group = ExtAuthorGroup.NVIDIA

        # Extension location tag for UI and user extensions
        self.location_tag = ""
        if is_local:
            abs_path = os.path.abspath(self.path)
            if ext_info.get("isInCache", False):
                self.location_tag = "INSTALLED"
            elif self.author_group == ExtAuthorGroup.NVIDIA and (
                ext_info.get("isUser", False) or is_in_omni_documents(abs_path)
            ):
                self.author_group = ExtAuthorGroup.USER
            else:
                git_ext = get_extpath_git_ext()
                if git_ext and path_is_parent(git_ext.get_cache_path(), abs_path):
                    self.location_tag = "GIT"

        # Finalize source
        self.ext_source = ExtSource.NVIDIA if self.author_group == ExtAuthorGroup.NVIDIA else ExtSource.THIRD_PARTY
        self.is_untrusted = self.author_group == ExtAuthorGroup.COMMUNITY_UNVERIFIED

        # Remote exts
        if not is_local:
            self.location_tag = "REMOTE"
            self.provider_name = ext_info.get("registryProviderName", None)

        # If unknown category -> fallback to other
        categories = get_categories()
        if self.category not in categories:
            self.category = "other"

        # For icon by default use category icon, can be overriden with custom:
        self.icon_path = self._build_image_resource_path(package_dict, "icon", categories[self.category]["image"])
        # For preview image there is no default
        self.preview_image_path = self._build_image_resource_path(package_dict, "preview_image")

    def _build_image_resource_path(self, package_dict, key, default_path=None):
        resource_path = default_path
        if self.is_local:
            path = package_dict.get(key, None)
            if path:
                icon_path = os.path.join(self.path, path)
                if os.path.exists(icon_path):
                    resource_path = icon_path
        else:
            path = package_dict.get(key + "_remote", None)
            if path:
                if os.path.splitext(path)[1] not in REMOTE_IMAGE_SUPPORTED_EXTS:
                    return default_path
                url = get_registry_url(self.provider_name)
                if url:
                    resource_path = url + "/" + path
        return resource_path

    def solve_extensions(self):
        # This function is costly, so we don't run it for each item. Only on demand when UI shows selected extension.
        manager = omni.kit.app.get_app().get_extension_manager()
        result, exts, err = manager.solve_extensions([self.id], add_enabled=True, return_only_disabled=True)
        if not result:
            self.failed = True

        self.non_toggleable_deps = []
        self.solver_error = err
        if result:
            for ext in exts:
                ext_dict, _ = get_ext_info_dict(manager, ext)
                if not ext_dict.get("package", {}).get("toggleable", True):
                    self.non_toggleable_deps.append(ext["id"])


def build_ext_info(ext_id, package_id=None) -> Tuple[ExtensionCommonInfo, dict]:
    manager = omni.kit.app.get_app().get_extension_manager()
    package_id = package_id or ext_id
    ext_info_remote = manager.get_registry_extension_dict(package_id)
    ext_info_local = manager.get_extension_dict(ext_id)
    is_local = ext_info_local is not None
    ext_info = ext_info_local or ext_info_remote

    if not ext_info:
        return None, None

    ext_info = ext_info.get_dict()  # convert to python dict to prolong lifetime
    return ExtensionCommonInfo(ext_id, ext_info, is_local), ext_info


def check_can_be_toggled(ext_id: str, for_autoload=False):
    ext_item, _ = build_ext_info(ext_id)
    if ext_item:
        ext_item.solve_extensions()

        # Error to solve?
        if ext_item.solver_error:
            text = "Failed to solve all extension dependencies:"
            text += f"\n{ext_item.solver_error}"
            asyncio.ensure_future(show_ok_popup("Error", text))
            return False

        if not for_autoload and ext_item.non_toggleable_deps:
            text = "This extension cannot be enabled at runtime.\n"
            text += "Some of dependencies require an app restart to be enabled (toggleable=false):\n\n"
            for ext in ext_item.non_toggleable_deps:
                text += f" * {ext}"
            text += "\n\n"
            text += "Set this extension to autoload and restart an app to enable it."
            asyncio.ensure_future(show_ok_popup("Warning", text))
            return False
    return True


def toggle_extension(ext_id: str, enable: bool):
    if enable and not check_can_be_toggled(ext_id):
        return False
    omni.kit.commands.execute("ToggleExtension", ext_id=ext_id, enable=enable)
    return True


async def pull_extension_async(ext_item: ExtensionCommonInfo, on_pull_started_fn: Callable = None):

    if ext_item.is_untrusted:
        cancel = False

        def on_cancel(dialog):
            nonlocal cancel
            cancel = True
            dialog.hide()

        message = """
    ATTENTION: UNVERIFIED COMMUNITY EXTENSION

    This extension will be installed directly from the repository (see "Repo Url").
    It is not verified by NVIDIA and may contain bugs or malicious code. Do you want to proceed?
    """
        await show_ok_popup(
            "Warning",
            message,
            ok_label="Install",
            cancel_label="Cancel",
            disable_cancel_button=False,
            cancel_handler=on_cancel,
            width=600,
        )

        if cancel:
            return

    ext_manager = omni.kit.app.get_app().get_extension_manager()
    ext_manager.pull_extension_async(ext_item.id)
    omni.kit.app.get_app().get_message_bus_event_stream().push(EXTENSION_PULL_STARTED_EVENT)
    if on_pull_started_fn:
        on_pull_started_fn()


class SettingBoolValue:
    def __init__(self, path: str, default: bool):
        self._path = path
        self._value = set_default_and_get_setting(path, default)

    def get(self) -> bool:
        return self._value

    def set_bool(self, value: bool):
        self._value = value
        carb.settings.get_settings().set(self._path, self._value)

    def __bool__(self):
        return self.get()


def build_doc_urls(ext_item: ExtensionCommonInfo) -> List[str]:
    """Produce possible candiates for doc urls"""

    # Base url
    if omni.kit.app.get_app().is_app_external():
        doc_url = get_setting("/exts/omni.kit.window.extensions/docUrlExternal", "")
    else:
        doc_url = get_setting("/exts/omni.kit.window.extensions/docUrlInternal", "")
    if not doc_url:
        return []

    # Build candidates. Prefer exact version, fallback to just a link to latest
    candidates = []
    for v in [ext_item.version, "latest", ""]:
        candidates.append(Template(doc_url).safe_substitute({"version": v, "extName": ext_item.fullname}))

    return candidates


@lru_cache()
def is_community_tab_always_enabled() -> bool:
    return get_setting("/exts/omni.kit.window.extensions/communityTabEnabled", True)


def is_community_tab_enabled_as_option() -> bool:
    if is_community_tab_always_enabled():
        return False
    return get_setting("/exts/omni.kit.window.extensions/communityTabOption", True)


class ExtOptions:
    def __init__(self):
        self.community_tab = None
        if is_community_tab_enabled_as_option():
            self.community_tab = SettingBoolValue(
                "/persistent/exts/omni.kit.window.extensions/communityTabEnabled", default=False
            )

        self.publishing = SettingBoolValue(
            "/persistent/exts/omni.kit.window.extensions/publishingEnabled", default=False
        )


@lru_cache()
def get_options() -> ExtOptions:
    return ExtOptions()


def is_community_tab_enabled() -> bool:
    # Can be either always enabled, or as a toggleable preference
    if is_community_tab_always_enabled():
        return True

    return get_options().community_tab is not None and get_options().community_tab


@lru_cache()
def get_open_example_links():
    return get_setting("/exts/omni.kit.window.extensions/openExampleLinks", [])
