# 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.
#

# pylint: disable=protected-access, access-member-before-definition

import asyncio
import contextlib
import glob
import os
import platform
import shutil
import subprocess
from functools import lru_cache
from typing import Callable, Tuple

import carb
import carb.dictionary
import carb.settings
import omni.kit.commands


def call_once_with_delay(fn: Callable, delay: float):
    """Call function once after `delay` seconds.
    If this function called again before `delay` is passed the timer gets reset."""

    async def _delayed_refresh():
        await asyncio.sleep(delay)
        fn()

    with contextlib.suppress(Exception):
        fn._delay_call_task.cancel()

    fn._delay_call_task = asyncio.ensure_future(_delayed_refresh())


@lru_cache()
def is_windows():
    return platform.system().lower() == "windows"


def run_process(args):
    print(f"running process: {args}")
    kwargs = {"close_fds": False}
    if is_windows():
        kwargs["creationflags"] = subprocess.CREATE_NEW_CONSOLE | subprocess.CREATE_NEW_PROCESS_GROUP
    subprocess.Popen(args, **kwargs)  # pylint: disable=consider-using-with


def version_to_str(version: Tuple[int, int, int, str, str]) -> str:
    """Generate string `0.0.0-tag+tag`"""
    delimiters = ("", ".", ".", "-", "+")
    return "".join(f"{d}{v}" for d, v in zip(delimiters, version) if str(v) and v is not None)


def ext_id_to_fullname(ext_id: str) -> str:
    return omni.ext.get_extension_name(ext_id)


def ext_id_to_name_version(ext_id: str) -> Tuple[str, str]:
    """Convert 'omni.foo-tag-1.2.3' to 'omni.foo-tag' and '1.2.3'"""
    a, b, *rest = ext_id.split("-")
    if b:
        if not b[0:1].isdigit():
            return f"{a}-{b}", "-".join(rest)
        return a, "-".join([b] + rest)
    return a, ""


def get_ext_info_dict(ext_manager, ext_info) -> Tuple[carb.dictionary.Item, bool]:
    ext_dict = ext_manager.get_extension_dict(ext_info["id"])
    if ext_dict is not None:
        return (ext_dict, True)
    return (ext_manager.get_registry_extension_dict(ext_info["package_id"]), False)


def get_setting(path, default=None):
    setting = carb.settings.get_settings().get(path)
    return setting if setting is not None else default


def set_default_and_get_setting(path, default=None):
    settings = carb.settings.get_settings()
    settings.set_default(path, default)
    return settings.get(path)


def clip_text(s, max_count=80):
    return s[:max_count] + ("..." if len(s) > max_count else "")


def open_file_using_os_default(path: str):
    if platform.system() == "Darwin":  # macOS
        subprocess.call(("open", path))
    elif platform.system() == "Windows":  # Windows
        os.startfile(path)  # noqa: PLE1101
    else:  # linux variants
        subprocess.call(("xdg-open", path))


def open_url(url):
    import webbrowser

    webbrowser.open(url)


def open_using_os_default(path: str):
    if os.path.isfile(path):
        open_file_using_os_default(path)
    else:
        # open dir
        import webbrowser

        webbrowser.open(path)


def open_in_vscode(path: str):
    subprocess.call(["code", path], shell=is_windows())


@lru_cache()
def is_vscode_installed():
    try:
        cmd = ["code", "--version"]
        return subprocess.call(cmd, shell=is_windows(), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) == 0
    except FileNotFoundError:
        return False


def _search_path_entry_up(path, entry, max_steps_up=3):
    for _ in range(0, max_steps_up + 1):
        if os.path.exists(os.path.join(path, entry)):
            return os.path.normpath(path)
        new_path = os.path.normpath(os.path.join(path, ".."))
        if new_path == path:
            break
        path = new_path
    return None


def open_in_vscode_if_enabled(path: str, prefer_vscode: bool = True):
    if prefer_vscode and is_vscode_installed():
        # Search for .vscode folder few folders up to open project instead of just extension
        # 5 steps is enough for '_build/window/release/exts/omni.foo' for instance
        if os.path.isdir(path):
            path = _search_path_entry_up(path, ".vscode", max_steps_up=5) or path
        open_in_vscode(path)
    else:
        open_using_os_default(path)


def copy_text(text):
    omni.kit.clipboard.copy(text)


def cleanup_folder(path):
    try:
        for p in glob.glob(f"{path}/*"):
            if os.path.isdir(p):
                if omni.ext.is_link(p):
                    omni.ext.destroy_link(p)
                else:
                    shutil.rmtree(p)
            else:
                os.remove(p)
    except Exception as exc:  # pylint: disable=broad-except
        carb.log_warn(f"Unable to clean up files: {path}: {exc}")


@lru_cache()
def get_extpath_git_ext():
    try:
        import omni.kit.extpath.git as git_ext

        return git_ext
    except ImportError:
        return None


async def _load_popup_dialog_ext():
    app = omni.kit.app.get_app()
    manager = app.get_extension_manager()
    if not manager.is_extension_enabled("omni.kit.window.popup_dialog"):
        manager.set_extension_enabled("omni.kit.window.popup_dialog", True)

        await app.next_update_async()
        await app.next_update_async()


async def show_ok_popup(title, message, **dialog_kwargs):
    await _load_popup_dialog_ext()
    from omni.kit.window.popup_dialog import MessageDialog

    app = omni.kit.app.get_app()

    done = False

    def on_click(d):
        nonlocal done
        done = True

    dialog_kwargs.setdefault("disable_cancel_button", True)
    dialog = MessageDialog(title=title, message=message, ok_handler=on_click, **dialog_kwargs)

    dialog.show()

    while not done:
        await app.next_update_async()

    dialog.destroy()


async def show_user_input_popup(title, label, default):
    await _load_popup_dialog_ext()
    from omni.kit.window.popup_dialog import InputDialog

    app = omni.kit.app.get_app()

    value = None

    def on_click(dialog: InputDialog):
        nonlocal value
        value = dialog.get_value()

    dialog = InputDialog(
        message=title,
        pre_label=label,
        post_label="",
        default_value=default,
        ok_handler=on_click,
        ok_label="Ok",
    )

    dialog.show()

    while not value:
        await app.next_update_async()

    dialog.destroy()

    return value
