"""
Functions and sources for the markdown tutorial slides!
"""

import inspect
import re
from collections import OrderedDict
from typing import Callable, List, Optional, Union

import yaml

import lookatme
import lookatme.config as config
import lookatme.utils as utils


class Tutor:
    """A class to handle/process tutorials for specific functionality

    In addition to name, group, and slides content of the tutor, each Tutor
    must also be associated with the implementation.
    """

    def __init__(
        self,
        name: str,
        group: str,
        slides_md: str,
        impl_fn: Callable,
        order: int,
        lazy_formatting: Optional[Callable] = None,
    ):
        """Create a new Tutor

        Args:
            lazy_formatting. Callable. Should return a dictionary that will be
              unpacked into the kwargs of str.format()
        """
        self.name = name
        self.group = group
        self.slides_md = inspect.cleandoc(slides_md).strip()
        self.impl_fn = impl_fn
        self.order = order
        self.lazy_formatting = lazy_formatting

    def get_md(self, rendered_example=True) -> str:
        """Get the tutor's markdown text after resolving any special markup
        contained in it.
            opts. Dict[str, Any]
              slides. Current can include `{
        """
        slides_md = self.slides_md
        if self.lazy_formatting is not None:
            slides_md = slides_md.format(**self.lazy_formatting())

        tag_handlers = {
            "EXAMPLE": lambda contents: self._handle_show_and_render(contents, rendered_example),
            "STYLE": self._handle_style_yaml,
        }

        res_md = []
        last_idx = 0
        regex = "<(?P<tag>TUTOR:(?P<type>[A-Z_]+))>(?P<inner>.*)</(?P=tag)>"
        for match in re.finditer(regex, slides_md, re.MULTILINE | re.DOTALL):
            res_md.append(slides_md[last_idx: match.start()])
            match_groups = match.groupdict()
            handler = tag_handlers.get(match_groups["type"], None)
            if handler is None:
                raise ValueError(
                    "No handler defined for 'TUTOR:{}' tags".format(
                        match_groups["type"]
                    )
                )
            res_md.append(handler(match_groups["inner"]))
            last_idx = match.end() + 1

        res_md.append(slides_md[last_idx:])

        return "\n\n".join([
            self._get_heading(),
            "".join(res_md),
            self._get_source_note(),
        ])

    def _get_heading(self) -> str:
        return "# {group}: {name}".format(
            group=self.group.title(),
            name=self.name.title(),
        )

    def _get_source_note(self) -> str:
        link = self._get_source_link()
        return "> This is implemented in {}".format(link)

    def _get_source_link(self):
        file_name = inspect.getsourcefile(inspect.unwrap(self.impl_fn))
        if file_name is None:
            return "??"

        relpath = file_name.split("lookatme/", 1)[1]
        _, lineno = inspect.getsourcelines(self.impl_fn)

        version = "v" + lookatme.VERSION

        return "[{module}.{fn_name}]({link})".format(
            module=self.impl_fn.__module__,
            fn_name=self.impl_fn.__qualname__,
            link="https://github.com/d0c-s4vage/lookatme/blob/{version}/{path}#L{lineno}".format(
                version=version,
                path=relpath,
                lineno=lineno,
            )
        )

    def _handle_show_and_render(self, contents, rendered_example: bool = True) -> str:
        contents = contents.strip()

        markdown_example = "\n".join([
            "~~~markdown",
            contents,
            "~~~",
        ])
        quoted_example = utils.prefix_text(markdown_example, "> ")

        res = [
            "***Markdown***:",
            quoted_example,
        ]

        if rendered_example:
            res += [
                "***Rendered***:",
                contents + "\n",
            ]

        return "\n\n".join(res)

    def _handle_style_yaml(self, contents: str) -> str:
        contents = contents.strip()
        style = config.get_style()[contents]
        style = {"styles": {contents: style}}
        return "```yaml\n---\n{style_yaml}---\n```".format(
            style_yaml=yaml.dump(style).encode().decode("unicode-escape"),
        )


GROUPED_TUTORIALS = OrderedDict()
NAMED_TUTORIALS = OrderedDict()


def get_tutorial_help() -> str:
    res = []
    res.append(inspect.cleandoc("""
        Help for 'lookatme --tutorial'

        Specific tutorials can be run with a comma-separated list of group or
        tutorial names. Below are the groups and tutorial names currently defined.
    """).strip())

    for group_name, group_tutors in GROUPED_TUTORIALS.items():
        res.append("")
        res.append("  " + group_name)
        for tutor_name in group_tutors.keys():
            res.append("    " + tutor_name)

    res.append("")
    res.append(inspect.cleandoc("""
        Substring matching is used to identify tutorials and groups. All matching
        tutorials and groups are then run.

        Examples:
            lookatme --tutorial
            lookatme --tutorial link,table
            lookatme --tutorial general,list
    """).strip())

    return "\n".join(res)


def print_tutorial_help():
    print(get_tutorial_help())


def tutor(
    group: str,
    name: str,
    slides_md: str,
    order: int = 99999,
    lazy_formatting: Optional[Callable] = None
):
    """Define tutorial slides by using this as a decorator on a function!"""
    def capture_fn(fn):
        tutor = Tutor(name, group, slides_md, fn, order, lazy_formatting)
        tutor_list = (
            GROUPED_TUTORIALS
            .setdefault(group, OrderedDict())
            .setdefault(name, [])
        )
        tutor_list.append(tutor)
        NAMED_TUTORIALS.setdefault(name, []).append(tutor)
        return fn
    return capture_fn


def pretty_close_match(str1, str2):
    str1 = str1.lower()
    str2 = str2.lower()
    if str1 in str2 or str2 in str1:
        return True


@tutor(
    "general",
    "tutorial",
    r"""
    Lookatme has a built-in tutorial feature that can be used for reference.

    To launch lookatme's tutorial slides, run lookatme with the `--tutorial`
    argument:

    ```
    lookatme --tutorial
    ```

    ## Specific Tutorials

    Tutorials each have individual names and are organized into groups. You can
    pass comma-separated values of strings that ~match group or tutorial names
    to only run those tutorial slides:

    ```bash
    # only run the general slide group, and the list-related tutorials
    lookatme --tutorial general,list

    # only run the table tutorial slides
    lookatme --tutorial table
    ```

    ## Seeing What's Available

    If you pass the `help` value to `--tutorial`, lookatme me will list all
    groups defined and all tutorial names within those groups. Currently this
    is the output of `lookatme --tutorial help`. Notice that tutorial names
    are nested under the group names:

    ```
    {lookatme_help}
    ```
    """,
    order=99,  # last
    lazy_formatting=lambda: {
        "lookatme_help": get_tutorial_help()
    }
)
def get_tutors(group_or_tutor: str) -> List[Tutor]:
    for group_name, group_value in GROUPED_TUTORIALS.items():
        if pretty_close_match(group_name, group_or_tutor):
            return list(group_value.values())

    res = []
    for grouped_tutors in GROUPED_TUTORIALS.values():
        for tutor_name, tutor_list in grouped_tutors.items():
            if pretty_close_match(tutor_name, group_or_tutor):
                res.append(tutor_list)

    return res


def _sort_tutors_by_order():
    for group_name, tutors in list(GROUPED_TUTORIALS.items()):
        del GROUPED_TUTORIALS[group_name]
        tutor_list = list(tutors.items())
        tutor_list = list(sorted(
            tutor_list,
            key=lambda x: min(tutor.order for tutor in x[1]),
        ))
        GROUPED_TUTORIALS[group_name] = OrderedDict(tutor_list)


def get_tutorial_md(groups_or_tutors: List[str]) -> Union[None, str]:
    _sort_tutors_by_order()

    tutors = []
    for group_or_tutor in groups_or_tutors:
        tutors += get_tutors(group_or_tutor)

    if len(tutors) == 0:
        return None

    res_slides = []
    for tutor in tutors:
        tutor_md = "\n\n".join(t.get_md() for t in tutor)
        res_slides.append(tutor_md)

    meta = inspect.cleandoc("""
        ---
        title: lookatme Tutorial
        author: lookatme devs
        ---
    """).strip()

    return meta + "\n" + "\n\n---\n\n".join(res_slides) + "\n\n---\n\n# End"
