import dataclasses
import hashlib
import json
import re

import click
from click import Choice
from tabulate import tabulate


class Arguments(object):
    def __init__(self, obj: dict):
        object.__setattr__(self, "obj", obj)

    def __getstate__(self):
        return self.obj.items()

    def __setstate__(self, items):
        if not hasattr(self, "obj"):
            self.obj = {}
        for key, val in items:
            self.obj[key] = val

    def __getattr__(self, name):
        if name in self.obj:
            return self.obj.get(name)
        else:
            raise ValueError("key {} does not exists.".format(name))

    def __setattr__(self, name, value):
        self.obj[name] = value

    def items(self):
        return self.obj.items()

    def keys(self):
        return self.obj.keys()

    def hash(self):
        return hashlib.md5(
            (json.dumps(self.obj,
                        sort_keys=True)).encode("utf-8")).hexdigest()

    def save(self, path):
        with open(path, "w") as f:
            json.dump(self.obj, f, indent=4, sort_keys=True)

    def __str__(self):
        rows = []
        for key, value in sorted(self.items(), key=lambda x: x[0]):
            rows.append([key, value])
        return tabulate(rows, headers=["argument", "value"], tablefmt="rst")


class Context(click.Context):
    def invoke_with_hyperparams(self, callback, hyperparam_keys, arguments):
        if len(hyperparam_keys) == 0:
            arguments = Arguments(arguments)
            return super().invoke(callback, arguments)

        hyperparams = {}
        for key in hyperparam_keys:
            if key.lower() in arguments:
                hyperparams[key] = arguments[key.lower()]

        arguments = Arguments(arguments)
        hyperparam_instance_list = []
        for hyperparam_class in callback.__hyperparam_classes__:
            hyperparam_instance_list.append(hyperparam_class())
        hyperparam_instance_list = list(reversed(hyperparam_instance_list))
        for key, value in hyperparams.items():
            for hyperparam_instance in hyperparam_instance_list:
                if hyperparam_instance.prefix is not None:
                    _key = key.replace(hyperparam_instance.prefix + "_", "")
                    if hasattr(hyperparam_instance, _key):
                        setattr(hyperparam_instance, _key, value)
                else:
                    if hasattr(hyperparam_instance, key):
                        setattr(hyperparam_instance, key, value)
        super().invoke(callback, arguments, *hyperparam_instance_list)


class Command(click.Command):
    def make_context(self, info_name, args, parent=None, **extra):
        for key, value in click.core.iteritems(self.context_settings):
            if key not in extra:
                extra[key] = value
        ctx = Context(self, info_name=info_name, parent=parent, **extra)
        with ctx.scope(cleanup=False):
            self.parse_args(ctx, args)
        return ctx

    def invoke(self, ctx):
        click.core._maybe_show_deprecated_notice(self)
        if self.callback is not None:
            hyperparam_keys = []
            if hasattr(self.callback, "__hyperparam_keys__"):
                hyperparam_keys = self.callback.__hyperparam_keys__
            return ctx.invoke_with_hyperparams(self.callback,
                                               hyperparam_keys=hyperparam_keys,
                                               arguments=ctx.params)


class Group(click.Group):
    def __init__(self, name=None, commands=None, **attrs):
        super().__init__(name, commands, **attrs)

    def make_context(self, info_name, args, parent=None, **extra):
        for key, value in click._compat.iteritems(self.context_settings):
            if key not in extra:
                extra[key] = value
        ctx = Context(self, info_name=info_name, parent=parent, **extra)
        with ctx.scope(cleanup=False):
            self.parse_args(ctx, args)
        return ctx

    def command(self, *args, **kwargs):
        def decorator(f):
            cmd = command(*args, **kwargs)(f)
            self.add_command(cmd)
            return cmd

        return decorator

    def get_command(self, ctx, cmd_name):
        return self.commands.get(cmd_name)


def command(name=None, cls=None, **attrs):
    if cls is None:
        cls = Command

    def decorator(f):
        cmd = click.decorators._make_command(f, name, attrs, cls)
        cmd.__doc__ = f.__doc__
        return cmd

    return decorator


def group(name=None, **attrs):
    attrs.setdefault("cls", Group)
    return command(name, **attrs)


def _register(func, field, param, hidden=False):
    if field.type is bool:
        click.decorators._param_memo(
            func, click.Option((param, ), is_flag=True, hidden=hidden))
    else:
        click.decorators._param_memo(
            func,
            click.Option((param, ),
                         type=field.type,
                         default=field.default,
                         hidden=hidden))


def hyperparameter_class(Hyperparameters):
    def decorator(func):
        prefix = ""
        if hasattr(Hyperparameters, "prefix"):
            if Hyperparameters.prefix is not None:
                prefix = Hyperparameters.prefix + "_"
        fields = dataclasses.fields(Hyperparameters)
        for field in fields:
            # --xxx_xxx_xxx
            param = f"--{prefix}{field.name}"
            _register(func, field, param)
            # --xxx-xxx-xxx
            hyphen_prefix = prefix.replace("_", "-")
            hyphen_param = field.name.replace("_", "-")
            param = f"--{hyphen_prefix}{hyphen_param}"
            _register(func, field, param, hidden=True)

            if not hasattr(func, "__hyperparam_keys__"):
                func.__hyperparam_keys__ = []
            func.__hyperparam_keys__.append(f"{prefix}{field.name}")
        if not hasattr(func, "__hyperparam_classes__"):
            func.__hyperparam_classes__ = []
        func.__hyperparam_classes__.append(Hyperparameters)
        return func

    return decorator


def argument(*params, **attrs):
    param = params[0]
    underscore_param = re.sub(r"(?<!-)(?<!^)-", "_", param)
    hyphen_param = param.replace("_", "-")
    decorator = click.option(underscore_param, hidden=True, **attrs)
    if underscore_param == param:
        return decorator
    return decorator(click.option(hyphen_param, hidden=False, **attrs))
