import inspect
import re
from functools import partial
from typing import _AnnotatedAlias, _LiteralGenericAlias

import holoviews as hv
import panel as pn
import param

from .bokeh_hooks import BokehHooks


class DataPresentation(param.Parameterized):

    dm_funcs: list = param.List()
    widget_mapping: list = param.List(default=list())
    typing_to_param: dict = param.Dict(
        {
            str: param.String,
            int: param.Integer,
            float: param.Number,
            bool: param.Boolean,
            "Literal": param.Selector,
        }
    )

    default_widgets = param.Dict(
        default={
            param.Integer: pn.widgets.IntSlider,
            param.Number: pn.widgets.FloatSlider,
            param.Selector: pn.widgets.RadioButtonGroup,
            param.Boolean: pn.widgets.Checkbox,
            param.String: pn.widgets.TextInput,
        }
    )

    stylesheets: dict = param.Dict(
        {
            "no_blue_dots_selection": """
            .bk-Canvas {
                --highlight-color: rgba(50,50,50,0)
            }
            """,
            "thick_slider": """
            .bk-slider-title {
                display: none;
            }
            :host {
            --handle-width: 15px;
            --handle-height: 31px;
            --slider-size: 31px;
            }
            """,
        }
    )
    bokeh_hooks: BokehHooks = param.ClassSelector(
        default=BokehHooks(), class_=BokehHooks
    )

    mapping = param.Dict(default={})

    #####
    ## DOC TITLE
    #####
    title: str = param.String(default="# An Exploration of Sin, Tan and Whatnot")
    introduction: str = param.String(default="")
    conclusion: str = param.String(default="")

    def __init__(self, **params):

        mapping = dict()
        for k, v in params.copy().items():
            if isinstance(v, param.Parameter):
                self.param.add_parameter(k, v)
                params.pop(k)
                mapping[k] = ""
            elif isinstance(v, param.Parameterized):
                if v.__doc__ != None:
                    doc = v.__doc__.split("\n\x1b[1;32mParameters")[0]
                else:
                    doc = None
                self.param.add_parameter(
                    k,
                    param.ClassSelector(
                        default=v,
                        class_=v.__class__,
                        instantiate=False,
                        metadata={"caption_template": doc if doc else None},
                    ),
                )
                params.pop(k)
                for k_ in list(v.param):
                    mapping[k_] = k
            else:
                ...
        mapping.pop("name", None)
        mapping.update(**{k: "" for k in params.keys()})

        super().__init__(**params)

        self.param.update(mapping=mapping)

        for i, func in enumerate(self.dm_funcs):

            # Handle Parameterized objects.
            if not callable(func):
                self.widget_mapping.append((func, []))
                self.mapping[func] = ""
                doc = self.get_param(func).metadata["caption_template"]
                chart_name = func
            # Handle DynamicMap callback functions.
            else:
                chart_name = f"chart_{i:>03}"
                self._add_class_method(func)
                kdims, widget_vars = self._make_params(func)
                self.widget_mapping.append((func.__name__, widget_vars))
                self.param.add_parameter(
                    chart_name,
                    param.ClassSelector(
                        class_=hv.DynamicMap,
                        default=hv.DynamicMap(
                            getattr(self, func.__name__),
                            streams={name: self.get_param(name) for name in kdims},
                        ),
                        metadata={"caption_template": func.__doc__},
                    ),
                )
                self.mapping[chart_name] = ""
                doc = func.__doc__

            # Add docs if any.
            if doc is not None:
                self.param.add_parameter(
                    f"{chart_name}_caption", param.String(default="")
                )
                self.mapping[f"{chart_name}_caption"] = ""
                changes = set(re.findall("{(.*?)}", doc))
                for chng in changes:
                    self.get_param_space(chng).watch(
                        partial(self.update_doc, chart_name=chart_name), [chng]
                    )
                self.update_doc(None, chart_name)

        self.view: pn.Column = self.make_view()

    def get_param_value(self, param_name):
        return getattr(getattr(self, self.mapping[param_name], self), param_name)

    def get_param(self, param_name) -> param.Parameter:
        return getattr(getattr(self, self.mapping[param_name], self), "param")[
            param_name
        ]

    def get_param_space(self, param_name):
        return getattr(getattr(self, self.mapping[param_name], self), "param")

    @classmethod
    def _add_class_method(cls, func):
        setattr(cls, func.__name__, func)

    def _make_params(self, func):

        sig = inspect.signature(func)

        kdims = list()
        widget_vars = list()

        for prmt in sig.parameters.values():

            x = prmt.annotation
            name = prmt.name

            if name == "self":
                continue

            kdims.append(name)
            if prmt.default != inspect._empty:
                widget_vars.append(name)

            if name in self.mapping.keys():
                continue

            param_kwargs = dict()
            if isinstance(x, _AnnotatedAlias):
                dtype = x.__args__[0]
                if dtype == int or dtype == float:
                    range_str = x.__metadata__[0]
                    param_kwargs["softbounds"] = tuple(
                        [dtype(x_) for x_ in range_str[1:-1].split(",")]
                    )
                else:
                    raise NotImplementedError
            elif isinstance(x, _LiteralGenericAlias):
                dtype = "Literal"
                param_kwargs["objects"] = x.__args__
            elif x in [str, float, int, bool]:
                dtype = x
            else:
                raise NotImplementedError

            if prmt.default == inspect._empty:
                param_kwargs["default"] = None
            else:
                param_kwargs["default"] = prmt.default

            param_ = self.typing_to_param[dtype](**param_kwargs)

            # Add parameter to class if it doesn't exist yet
            if name not in list(self.param):
                self.param.add_parameter(name, param_)
                self.mapping[name] = ""

        return kdims, widget_vars

    def update_doc(self, event, chart_name):

        template = self.get_param(chart_name).metadata["caption_template"]
        changes = set(re.findall("{(.*?)}", template))

        out = template.format(**{x: self.get_param_value(x) for x in changes})
        self.param.update(**{f"{chart_name}_caption": out})

    def add_widgets(self, *param_names):
        widgets = list()
        for param_name in param_names:
            prm = self.get_param(param_name)
            if prm.__class__ in self.default_widgets.keys():
                wdgt = self.default_widgets[prm.__class__]
                widgets.append(wdgt.from_param(prm, width=400, align="center"))
            else:
                widgets.append(prm)
        return widgets

    def make_view(self, repeat_widgets=True):

        out = [
            pn.pane.Markdown(self.param["title"], align="center"),
            pn.pane.Markdown(
                self.param["introduction"],
                align="center",
                styles={"width": "45vw", "min-width": "380px", "font-size": "medium"},
            ),
            pn.layout.Divider(styles={"margin-top": "15px"}),
        ]

        parsed_widgets = list()

        for i, (func_name, widget_names) in enumerate(self.widget_mapping):

            if f"{func_name}_caption" in self.mapping.keys():
                out.append(
                    pn.pane.Markdown(
                        self.get_param(f"{func_name}_caption"),
                        align="center",
                        styles={
                            "width": "45vw",
                            "min-width": "380px",
                            "font-size": "medium",
                        },
                    )
                )

            if func_name in self.mapping.keys():
                value = self.get_param_value(func_name)
                if hasattr(value, "make_view"):
                    out.append(value.make_view())

            chart_name = f"chart_{i:>03}"
            if f"{chart_name}_caption" in self.mapping.keys():
                out.append(
                    pn.pane.Markdown(
                        self.get_param(f"{chart_name}_caption"),
                        align="center",
                        styles={
                            "width": "45vw",
                            "min-width": "380px",
                            "font-size": "medium",
                        },
                    )
                )
            if chart_name in self.mapping.keys():
                out.append(
                    pn.pane.HoloViews(
                        getattr(self, chart_name),
                        name=chart_name,
                        linked_axes=False,
                        align="center",
                        stylesheets=[self.stylesheets["no_blue_dots_selection"]],
                        sizing_mode="stretch_width",
                    ),
                )
            if widget_names:
                if not repeat_widgets:
                    widget_names = [x for x in widget_names if x not in parsed_widgets]
                parsed_widgets += widget_names
                out += self.add_widgets(*widget_names)

            out.append(pn.layout.Divider(styles={"margin-top": "15px"}))

        out.append(
            pn.pane.Markdown(
                self.param["conclusion"],
                align="center",
                styles={"width": "45vw", "min-width": "380px", "font-size": "medium"},
            )
        )

        view: pn.layout = pn.FlexBox(
            pn.Column(
                *out,
                styles={
                    "width": "90vw",
                    "max-width": "800px",
                    "background-color": "rgba(255,255,255,10)",
                    "margin-top": "1rem",
                    "margin-bottom": "1rem",
                    "padding-top": "2rem",
                    "padding-bottom": "2rem",
                    "border-radius": "20px",
                },
            ),
            flex_direction="row",
            justify_content="center",
            styles={
                "background-color": "FloralWhite",
                "padding": "0px",
                "margin": "0px",
            },
        )

        return view
