#!/usr/bin/env python
# coding=utf-8
"""Defines the stock-commands that every sacred experiment ships with."""

import pprint
import pydoc
import re
import sys
from collections import namedtuple, OrderedDict

from colorama import Fore, Style

from sacred.config import save_config_file
from sacred.serializer import flatten
from sacred.utils import PATHCHANGE, iterate_flattened_separately

__all__ = (
    "print_config",
    "print_dependencies",
    "save_config",
    "help_for_command",
    "print_named_configs",
)

COLOR_DIRTY = Fore.RED
COLOR_TYPECHANGED = Fore.RED  # prepend Style.BRIGHT for bold
COLOR_ADDED = Fore.GREEN
COLOR_MODIFIED = Fore.BLUE
COLOR_DOC = Style.DIM
ENDC = Style.RESET_ALL  # '\033[0m'

LEGEND = (
    "("
    + COLOR_MODIFIED
    + "modified"
    + ENDC
    + ", "
    + COLOR_ADDED
    + "added"
    + ENDC
    + ", "
    + COLOR_TYPECHANGED
    + "typechanged"
    + ENDC
    + ", "
    + COLOR_DOC
    + "doc"
    + ENDC
    + ")"
)

ConfigEntry = namedtuple("ConfigEntry", "key value added modified typechanged doc")
PathEntry = namedtuple("PathEntry", "key added modified typechanged doc")


def _non_unicode_repr(objekt, context, maxlevels, level):
    """
    Used to override the pprint format method to get rid of unicode prefixes.

    E.g.: 'John' instead of u'John'.
    """
    if sys.version_info[0] == 3 and sys.version_info[1] >= 8:
        repr_string, isreadable, isrecursive = pprint._safe_repr(
            objekt, context, maxlevels, level, sort_dicts=None
        )
    else:
        repr_string, isreadable, isrecursive = pprint._safe_repr(
            objekt, context, maxlevels, level
        )
    if repr_string.startswith('u"') or repr_string.startswith("u'"):
        repr_string = repr_string[1:]
    return repr_string, isreadable, isrecursive


PRINTER = pprint.PrettyPrinter()
PRINTER.format = _non_unicode_repr


def print_config(_run):
    """
    Print the updated configuration and exit.

    Text is highlighted:
      green:  value modified
      blue:   value added
      red:    value modified but type changed
    """
    final_config = _run.config
    config_mods = _run.config_modifications
    print(_format_config(final_config, config_mods))


def _format_named_config(indent, path, named_config):
    indent = " " * indent
    assign = path
    if hasattr(named_config, "__doc__") and named_config.__doc__ is not None:
        doc_string = named_config.__doc__
        if doc_string.strip().count("\n") == 0:
            assign += COLOR_DOC + "   # {}".format(doc_string.strip()) + ENDC
        else:
            doc_string = doc_string.replace("\n", "\n" + indent)
            assign += (
                COLOR_DOC + '\n{}"""{}"""'.format(indent + "  ", doc_string) + ENDC
            )
    return indent + assign


def _format_named_configs(named_configs, indent=2):
    lines = ["Named Configurations (" + COLOR_DOC + "doc" + ENDC + "):"]
    for path, named_config in named_configs.items():
        lines.append(_format_named_config(indent, path, named_config))
    if len(lines) < 2:
        lines.append(" " * indent + "No named configs")
    return "\n".join(lines)


def print_named_configs(ingredient):  # noqa: D202
    """Returns a command that prints named configs recursively.

    The command function prints the available named configs for the
    ingredient and all sub-ingredients and exits.

    Example
    -------
    The output is highlighted:
        white: config names
        grey:  doc
    """

    def print_named_configs():
        """Print the available named configs and exit."""
        named_configs = OrderedDict(ingredient.gather_named_configs())
        print(_format_named_configs(named_configs, 2))

    return print_named_configs


def help_for_command(command):
    """Get the help text (signature + docstring) for a command (function)."""
    help_text = pydoc.text.document(command)
    # remove backspaces
    return re.subn(".\\x08", "", help_text)[0]


def print_dependencies(_run):
    """Print the detected source-files and dependencies."""
    print("Dependencies:")
    for dep in _run.experiment_info["dependencies"]:
        pack, _, version = dep.partition("==")
        print("  {:<20} == {}".format(pack, version))

    print("\nSources:")
    for source, digest in _run.experiment_info["sources"]:
        print("  {:<43}  {}".format(source, digest))

    if _run.experiment_info["repositories"]:
        repos = _run.experiment_info["repositories"]
        print("\nVersion Control:")
        for repo in repos:
            mod = COLOR_DIRTY + "M" if repo["dirty"] else " "
            print("{} {:<43}  {}".format(mod, repo["url"], repo["commit"]) + ENDC)
    print("")


def save_config(_config, _log, config_filename="config.json"):
    """
    Store the updated configuration in a file.

    By default uses the filename "config.json", but that can be changed by
    setting the config_filename config entry.
    """
    if "config_filename" in _config:
        del _config["config_filename"]
    _log.info('Saving config to "{}"'.format(config_filename))
    save_config_file(flatten(_config), config_filename)


def _iterate_marked(cfg, config_mods):
    for path, value in iterate_flattened_separately(cfg, ["__doc__"]):
        if value is PATHCHANGE:
            yield path, PathEntry(
                key=path.rpartition(".")[2],
                added=path in config_mods.added,
                modified=path in config_mods.modified,
                typechanged=config_mods.typechanged.get(path),
                doc=config_mods.docs.get(path),
            )
        else:
            yield path, ConfigEntry(
                key=path.rpartition(".")[2],
                value=value,
                added=path in config_mods.added,
                modified=path in config_mods.modified,
                typechanged=config_mods.typechanged.get(path),
                doc=config_mods.docs.get(path),
            )


def _format_entry(indent, entry):
    color = ""
    indent = " " * indent
    if entry.typechanged:
        color = COLOR_TYPECHANGED  # red
    elif entry.added:
        color = COLOR_ADDED  # green
    elif entry.modified:
        color = COLOR_MODIFIED  # blue
    if entry.key == "__doc__":
        color = COLOR_DOC  # grey
        doc_string = entry.value.replace("\n", "\n" + indent)
        assign = '{}"""{}"""'.format(indent, doc_string)
    elif isinstance(entry, ConfigEntry):
        assign = indent + entry.key + " = " + PRINTER.pformat(entry.value)
    else:  # isinstance(entry, PathEntry):
        assign = indent + entry.key + ":"
    if entry.doc:
        doc_string = COLOR_DOC + "# " + entry.doc + ENDC
        if len(assign) <= 35:
            assign = "{:<35}  {}".format(assign, doc_string)
        else:
            assign += "    " + doc_string
    end = ENDC if color else ""
    return color + assign + end


def _format_config(cfg, config_mods):
    lines = ["Configuration " + LEGEND + ":"]
    for path, entry in _iterate_marked(cfg, config_mods):
        indent = 2 + 2 * path.count(".")
        lines.append(_format_entry(indent, entry))
    return "\n".join(lines)