# visualize.py

import tkinter as tk
import sys
from PIL import Image, ImageTk
import os
import warnings
from typing import Iterable, Callable, Any

import pandas as pd
import matplotlib.pyplot as plt

from market_break.process import up_movement, down_movement
from market_break.evaluation import results_report, EvaluationResults
from market_break.utils.base import assets

__all__ = [
    "EvaluationResultsWindow",
    "report_repr",
    "visualize_evaluation_results_plot",
    "visualize_evaluation_results_window"
]

if 'win' in sys.platform:
    from ctypes import windll

    app_id_stamp = (
        'ArbitrageScreener.application.Desktop-Application.1.0'
    )

    windll.user32.ShowWindow(
        windll.kernel32.GetConsoleWindow(), False
    )
    windll.shell32.SetCurrentProcessExplicitAppUserModelID(
        app_id_stamp
    )
# end if

plt.style.core.update_nested_dict(
    plt.style.library,
    plt.style.core.read_style_directory(
        f"{assets()}\\themes\\"
    )
)

def report_repr(data: dict[str, str | float], title: str = None) -> str:
    """
    Returns a string representation of the results.

    :param data: The data to convert into a string.
    :param title: The title of the window.

    :return: The string representation of the data.
    """

    index = [0]

    for i, value in enumerate(data.values(), start=1):
        if value != "":
            index.append(index[-1] + 1)

        else:
            index.append(index[-1])
        # end if
    # end for

    return (
        "\n" + title + "\n\n" if isinstance(title, str) else ""
    ) + "\n".join(
        [
            (
                f"[{i}]{' ': <{3 - len(str(i))}}"
                f"{message: <35}" if value != '' else f'\n{message}'
            ) +
            f"{str(value if isinstance(value, str) else round(value, 3))}"
            for i, (message, value) in zip(index[1:], data.items())
        ]
    ) + "\n"
# end report_repr

class EvaluationResultsWindow(tk.Toplevel):
    """
    A class to represent a results window.

    The object to create and display a window to show the results
    of the simulation process.

    - results:
        The results object to display its data.

    - title:
        The title of the window to display.

    - theme:
        The theme of the window to display.

    - symbols:
        The symbols of the assets.

    - icon:
        The icon for the window.

    >>> from market_break.visualize import EvaluationResultsWindow
    >>>
    >>> EvaluationResultsWindow(...).mainloop()
    """

    parent = None

    def __init__(
            self,
            results: EvaluationResults = None,
            title: str = None,
            theme: str = None,
            icon: str = None,
            image: str = None
    ) -> None:
        """
        Defines the class attributes.

        :param results: The results object.
        :param title: The title of the window.
        :param theme: The theme of the window.
        :param icon: The icon for the window.
        :param image: The path to the image file.
        """

        if EvaluationResultsWindow.parent is None:
            EvaluationResultsWindow.parent = tk.Tk()

            EvaluationResultsWindow.parent.withdraw()
        # end if

        super().__init__(EvaluationResultsWindow.parent)

        self.title_text = title
        self.theme = theme
        self.icon = icon or f"{assets()}/icon/icon.ico"
        self.image = image

        self.results = results

        self.data: dict[str, float] = {}

        self.build(
            results=results, title=title,
            theme=theme, icon=icon, image=image
        )
    # end __init__

    def __str__(self) -> str:
        """
        Returns a string representation of the object.

        :return: The string with the results.
        """

        return report_repr(
            data=self.data, title=self.title_text
        )
    # end __str__

    def build(
        self,
        results: EvaluationResults,
        title: str = None,
        theme: str = None,
        icon: str = None,
        image: str = None
    ) -> None:
        """
        Defines the class attributes.

        :param results: The results object.
        :param title: The title of the window.
        :param theme: The theme of the window.
        :param icon: The icon for the window.
        :param image: The path to the image file.
        """

        try:
            previous = self.winfo_children()

            if title:
                self.title_text = title
            # end if

            if theme:
                self.theme = theme
            # end if

            if icon:
                self.icon = icon
            # end if

            if image:
                self.image = image
            # end if

            title = title or self.title_text or f"Arbitrage Results"
            theme = theme or self.theme or "light"
            image = image or self.image

            self.title(title)
            self.iconbitmap(
                (
                    icon if (icon is not None and os.path.exists(icon)) else None
                ) or self.icon or f"{assets()}/icon/icon.ico"
            )

            self.results = results or self.results or EvaluationResults()

            if theme == "dark":
                bg = "#151515"
                fg = "#FFFFFF"

                self.config(bg=bg)

            elif theme == "light":
                fg = "#151515"
                bg = "#FFFFFF"

                self.config(bg=bg)

            else:
                bg = None
                fg = None
            # end if

            self.data = results_report(results=self.results)

            root = self

            frames = []

            for i, (message, value) in enumerate(self.data.items()):
                i: int

                if value == "":
                    root = tk.Frame(self, bg=bg)

                    root.grid_columnconfigure(0, weight=1)
                    root.grid_columnconfigure(1, weight=1)

                    frames.append(root)

                    tk.Label(
                        root, bg=bg, fg=fg, text=str(message),
                        font=("Ariel", 15, "bold")
                    ).grid(
                        row=i, column=0, sticky="nsw", padx=20, columnspan=2,
                        pady=(10 if i != 0 else (20, 10))
                    )

                else:
                    tk.Label(
                        root, bg=bg, fg=fg, text=str(message), font=("Ariel", 13)
                    ).grid(
                        row=i, column=0, sticky="nsw", padx=(20, 10),
                        pady=(5 if i != 0 else (10, 5))
                    )
                    tk.Label(
                        root, bg=bg, fg="#00b5fa", font=("Ariel", 13),
                        text=str(value if isinstance(value, (str, int)) else f"{value:.3f}")
                    ).grid(
                        row=i, column=1, sticky="nse", padx=(10, 20),
                        pady=(5 if i != len(self.data) - 1 else (5, 10))
                    )
                # end if
            # end for

            left_frames = frames[:len(frames) // 2 + 1 - len(frames) % 2]
            right_frames = frames[len(frames) // 2:]

            if len(left_frames) == 1:
                right_frames = left_frames + right_frames
                left_frames = []
            # end if

            if left_frames:
                self.grid_columnconfigure(0, weight=1)
            # end if

            if right_frames:
                self.grid_columnconfigure(1, weight=1)
            # end if

            for i, frame in enumerate(left_frames):
                frame.grid(
                    row=i, column=0, sticky="nsew",
                    padx=(15, 0), pady=(
                        (0, 15) if (i == len(left_frames) - 1) else 0
                    )
                )
            # end for

            for i in range(max(len(right_frames), len(left_frames))):
                self.grid_rowconfigure(i, weight=1)
            # end for

            for i, frame in enumerate(right_frames):
                frame.grid(
                    row=i, column=1, sticky="nsew",
                    padx=(15, 0), pady=(
                        (0, 15) if (i == len(right_frames) - 1) else 0
                    )
                )
            # end for

            if image and os.path.exists(image):
                position = 1

                if left_frames:
                    position += 1
                # end if

                if right_frames:
                    position += 1
                # end if

                image_object = ImageTk.PhotoImage(Image.open(image))

                image_label = tk.Label(self, image=image_object)
                image_label.image = image_object

                image_label.grid(
                    row=0, column=position, sticky="nsew", padx=20, pady=20,
                    rowspan=max(len(left_frames), len(right_frames))
                )

                self.columnconfigure(position, weight=1)
            # end if

            for child in previous:
                child.destroy()
            # end for

        except tk.TclError:
            pass
        # end try
    # end build
# end SimulationResultsWindow

def visualize_evaluation_results_window(
        results: EvaluationResults = None,
        title: str = None,
        theme: str = None,
        icon: str = None
) -> EvaluationResultsWindow:
    """
    Defines the class attributes.

    :param results: The results object.
    :param title: The title of the window.
    :param theme: The theme of the window.
    :param icon: The icon for the window.
    """

    return EvaluationResultsWindow(
        results=results, title=title,
        theme=theme, icon=icon
    )
# end visualize_evaluation_results_window

def visualize_evaluation_results_plot(
        results: EvaluationResults,
        save: bool | str = None,
        additions: (
            Iterable[Callable[[], Any]] |
            dict[str, Iterable[float]] |
            Iterable[Iterable[float]] |
            bool
        ) = None,
        trade_index: Iterable = None,
        show: bool = True,
        title: str = None,
        icon: str = None,
        theme: str = None
) -> plt.Figure:
    """
    Visualizes the process results.

    :param results: The results object.
    :param save: The value to save the image.
    :param show: The value to show the image.
    :param theme: The theme of the window.
    :param additions: The additions to run to add to the plot.
    :param title: The title of the window.
    :param trade_index: The index for the plot.
    :param icon: The path to an icon.
    """

    icon = icon or f"{assets()}/icons/icon.ico"
    theme = theme or "light"

    additions = additions or {}
    commands = {}

    if theme:
        plt.style.use(theme)
    # end if

    warnings.resetwarnings()

    if isinstance(additions, dict):
        additions.update(commands)

    else:
        additions = [*additions, *commands.values()]
    # end if

    with warnings.catch_warnings():
        warnings.filterwarnings("ignore")
        plot = plt.figure(
            "Arbitrage Trading",
            figsize=(13 if additions else 10, 7.5 if additions else 6)
        )

    plt.subplots_adjust(
        top=0.938, bottom=0.067, right=0.974,
        left=0.06, hspace=0.283, wspace=0
    )

    labeled = False

    if additions:
        plt.subplot(2, 1, 1)
        plt.title(title or f"Arbitrage Trading Performance")

        for addition in additions:
            if (addition is not None) and callable(addition):
                addition()

            elif addition is not None:
                if isinstance(additions, dict):
                    if not callable(additions[addition]):
                        labeled = True

                        plt.plot(
                            (
                                list(range(len(additions[addition])))
                                if not isinstance(additions[addition], pd.Series)
                                else additions[addition].index
                            ),
                            additions[addition],
                            lw=1.25, label=addition
                        )

                    else:
                        additions[addition]()
                    # end if

                elif isinstance(addition, pd.Series):
                    labeled = True

                    plt.plot(
                        (
                            list(range(len(additions[addition])))
                            if not isinstance(addition, pd.Series)
                            else addition.index
                        ),
                        addition, lw=1.25, label=addition.name
                    )

                else:
                    plt.plot(list(range(len(addition))), addition, lw=1.25)
                # end if
            # end if
        # end for

        plt.xlabel("Time")
        plt.ylabel("Normalized Price Spread")

        if labeled:
            plt.legend()
        # end if
    # end if

    trade_index = trade_index or range(len(results.balances))

    plt.subplot(2 if additions else 1, 1, 2 if additions else 1)
    plt.title(
        title or (
            "Arbitrage Trading Results "
            f"({(results.balances[-1] / results.balances[0] - 1) * 100:.3f}%)"
        )
    )

    predictions = None

    if (results.predictions is not None) and (len(results.predictions) > 0):
        predictions = ((results.predictions / results.predictions[0]) - 1) * 100
    # end if

    balances = (results.balances / results.balances[0] - 1) * 100

    profits = up_movement(balances)
    losses = -down_movement(balances)

    if predictions is not None:
        plt.plot(trade_index, predictions, lw=1.25, label=f'Predicted Balance', color='blue')
        plt.fill_between(trade_index, predictions, balances, color="blue", alpha=0.1)
        plt.fill_between(trade_index, balances, predictions, color="blue", alpha=0.1)
    # end if

    plt.plot(trade_index, balances, lw=1.25, label=f'Balance', color='orange')
    plt.plot(trade_index, profits, lw=1.25, label=f'Profits', color='green')
    plt.plot(trade_index, losses, lw=1.25, label=f'Losses', color='red')

    plt.fill_between(trade_index, profits, balances, color="green", alpha=0.1)
    plt.fill_between(trade_index, balances, losses, color="yellow", alpha=0.1)
    plt.fill_between(trade_index, losses, color="red", alpha=0.1)
    plt.xlabel("Time")
    plt.ylabel("Produced Balance (%)")
    plt.legend()

    if save:
        if os.path.split(save)[0]:
            os.makedirs(os.path.split(save)[0], exist_ok=True)
        # end if

        plt.savefig(save)
    # end if

    if isinstance(icon, str) and os.path.exists(icon):
        manager = plt.get_current_fig_manager()

        # noinspection PyProtectedMember
        manager.window.tk.call('wm', 'iconbitmap', manager.window._w, icon)
    # end if

    warnings.resetwarnings()

    if show:
        plt.show()
    # end if

    return plot
# end visualize_evaluation_results_plot