# 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 os
import asyncio
import omni.kit.app
import omni.ui as ui
from omni.ui import scene as sc

from typing import Callable, Coroutine, Any
from carb import log_warn, log_info
from .style import get_style, ICON_ROOT


def exec_after_redraw(callback: Callable, wait_frames: int = 2):
    async def exec_after_redraw_async(callback: Callable, wait_frames: int):
        try:
            # Wait a few frames before executing
            for _ in range(wait_frames):
                await omni.kit.app.get_app().next_update_async()
            callback()
        except asyncio.CancelledError:
            return
        except Exception as e:
            # Catches exceptions from this delayed function call; often raised by unit tests where the dialog is being rapidly
            # created and destroyed. In this case, it's not worth the effort to relay the exception and have it caught, so better 
            # warn and carry on.
            log_warn(f"Warning: Ignoring a minor exception: {str(e)}")

    return asyncio.ensure_future(exec_after_redraw_async(callback, wait_frames))


def get_user_folders_dict() -> dict:
    """Returns dictionary of user folders as name, path pairs"""
    from pathlib import Path

    user_folders = {}
    subfolders = ["Desktop", "Documents", "Downloads", "Pictures"]
    for subfolder in subfolders:
        path = os.path.join(Path.home(), subfolder)
        if os.path.exists(path):
            user_folders[subfolder] = path.replace("\\", "/")

    return user_folders


class SingletonTask:
    def __init__(self):
        self._task = None

    def destroy(self):
        self.cancel_task()

    def __del__(self):
        self.destroy()

    async def run_task(self, task: Coroutine) -> Any:
        if self._task:
            self.cancel_task()
            await omni.kit.app.get_app().next_update_async()

        self._task = asyncio.create_task(task)
        try:
            return await self._task
        except asyncio.CancelledError:
            log_info(f"Cancelling task ... {self._task}")
            raise
        except Exception as e:
            raise
        finally:
            self._task = None

    def cancel_task(self):
        if self._task is not None:
            self._task.cancel()
        self._task = None


class Spinner(sc.Manipulator):
    def __init__(self, rotation_speed: int = 2):
        super().__init__()
        self.__deg = 0
        self.__rotation_speed = rotation_speed

    def on_build(self):
        self.invalidate()
        self.__deg = self.__deg % 360
        transform = sc.Matrix44.get_rotation_matrix(0, 0, -self.__deg, True)
        with sc.Transform(transform=transform):
            sc.Image(f"{ICON_ROOT}/ov_logo.png", width=1.5, height=1.5)
        self.__deg += self.__rotation_speed


class AnimatedDots():
    def __init__(self, period : int = 30, phase : int = 3, width : int = 10, height : int = 20):
        self._period = period
        self._phase = phase
        self._stop_event = asyncio.Event()
        self._build_ui(width=width, height=height)

    def _build_ui(self, width : int = 0, height: int = 0):
        self._label = ui.Label("...", width=width, height=height)
        asyncio.ensure_future(self._inf_loop())

    async def _inf_loop(self):
        counter = 0
        while not self._stop_event.is_set():
            await omni.kit.app.get_app().next_update_async()
            if self._stop_event.is_set():
                break
            # Animate text
            phase = counter // self._period % self._phase + 1
            self._label.text = phase * "."
            counter += 1

    def __del__(self):
        self._stop_event.set()


class LoadingPane(SingletonTask):
    def __init__(self, frame):
        super().__init__()
        self._frame = frame
        self._scene_view = None
        self._spinner = None
        self._build_ui()

    def _build_ui(self):
        with self._frame:
            with ui.ZStack(style=get_style()):
                ui.Rectangle(style_type_name_override="LoadingPane.Bg")
                with ui.VStack(style=get_style(), spacing=5):
                    ui.Spacer(height=ui.Percent(10))
                    with ui.HStack(height=ui.Percent(50)):
                        ui.Spacer()
                        self._scene_view = sc.SceneView(aspect_ratio_policy=sc.AspectRatioPolicy.PRESERVE_ASPECT_FIT)
                        with self._scene_view.scene:
                            self._spinner = Spinner()
                        ui.Spacer()
                    with ui.HStack(spacing=0):
                        ui.Spacer()
                        ui.Label("Changing Directory", height=20, width=0)
                        AnimatedDots()
                        ui.Spacer()
                    with ui.HStack(height=ui.Percent(20)):
                        ui.Spacer()
                        ui.Button(
                            "Cancel",
                            width=100, height=20,
                            clicked_fn=lambda: self.hide(),
                            style_type_name_override="LoadingPane.Button",
                            alignment=ui.Alignment.CENTER,
                            identifier="LoadingPaneCancel"
                        )
                        ui.Spacer()
                    ui.Spacer(height=ui.Percent(10))

    def show(self):
        self._frame.visible = True

    def hide(self):
        self.cancel_task()
        self._frame.visible = False

    def destroy(self):
        if self._spinner:
            self._spinner.invalidate()
            self._spinner = None
        if self._scene_view:
            self._scene_view.destroy()
            self._scene_view = None
        if self._frame:
            self._frame.visible = False
            self._frame.destroy()
            self._frame = None
        super().destroy()
