import collections


def get_named_tuple(name, dict):
    return collections.namedtuple(name, dict.keys())(*dict.values())


Align = get_named_tuple(
    "align",
    {"default": "", "left": "<", "right": ">", "center": "^", "right_sign": "="},
)

Group = get_named_tuple("group", {"no": "", "yes": ","})

Padding = get_named_tuple("padding", {"no": "", "yes": "0"})

Prefix = get_named_tuple(
    "prefix",
    {
        "yocto": 10**-24,
        "zepto": 10**-21,
        "atto": 10**-18,
        "femto": 10**-15,
        "pico": 10**-12,
        "nano": 10**-9,
        "micro": 10**-6,
        "milli": 10**-3,
        "none": None,
        "kilo": 10**3,
        "mega": 10**6,
        "giga": 10**9,
        "tera": 10**12,
        "peta": 10**15,
        "exa": 10**18,
        "zetta": 10**21,
        "yotta": 10**24,
    },
)

Scheme = get_named_tuple(
    "scheme",
    {
        "default": "",
        "decimal": "r",
        "decimal_integer": "d",
        "decimal_or_exponent": "g",
        "decimal_si_prefix": "s",
        "exponent": "e",
        "fixed": "f",
        "percentage": "%",
        "percentage_rounded": "p",
        "binary": "b",
        "octal": "o",
        "lower_case_hex": "x",
        "upper_case_hex": "X",
        "unicode": "c",
    },
)

Sign = get_named_tuple(
    "sign",
    {"default": "", "negative": "-", "positive": "+", "parantheses": "(", "space": " "},
)

Symbol = get_named_tuple(
    "symbol", {"no": "", "yes": "$", "binary": "#b", "octal": "#o", "hex": "#x"}
)

Trim = get_named_tuple("trim", {"no": "", "yes": "~"})


class Format:
    def __init__(self, **kwargs):
        self._locale = {}
        self._nully = ""
        self._prefix = Prefix.none
        self._specifier = {
            "align": Align.default,
            "fill": "",
            "group": Group.no,
            "width": "",
            "padding": Padding.no,
            "precision": "",
            "sign": Sign.default,
            "symbol": Symbol.no,
            "trim": Trim.no,
            "type": Scheme.default,
        }

        valid_methods = [
            m for m in dir(self.__class__) if m[0] != "_" and m != "to_plotly_json"
        ]

        for kw, val in kwargs.items():
            if kw not in valid_methods:
                raise TypeError(
                    "{0} is not a format method. Expected one of".format(kw),
                    str(list(valid_methods)),
                )

            getattr(self, kw)(val)

    def _validate_char(self, value):
        self._validate_string(value)

        if len(value) != 1:
            raise ValueError("expected value to a string of length one")

    def _validate_non_negative_integer_or_none(self, value):
        if value is None:
            return

        if not isinstance(value, int):
            raise TypeError("expected value to be an integer")

        if value < 0:
            raise ValueError("expected value to be non-negative", str(value))

    def _validate_named(self, value, named_values):
        if value not in named_values:
            raise TypeError("expected value to be one of", str(list(named_values)))

    def _validate_string(self, value):
        if not isinstance(value, (str, "".__class__)):
            raise TypeError("expected value to be a string")

    # Specifier
    def align(self, value):
        self._validate_named(value, Align)

        self._specifier["align"] = value
        return self

    def fill(self, value):
        self._validate_char(value)

        self._specifier["fill"] = value
        return self

    def group(self, value):
        if isinstance(value, bool):
            value = Group.yes if value else Group.no

        self._validate_named(value, Group)

        self._specifier["group"] = value
        return self

    def padding(self, value):
        if isinstance(value, bool):
            value = Padding.yes if value else Padding.no

        self._validate_named(value, Padding)

        self._specifier["padding"] = value
        return self

    def padding_width(self, value):
        self._validate_non_negative_integer_or_none(value)

        self._specifier["width"] = value if value is not None else ""
        return self

    def precision(self, value):
        self._validate_non_negative_integer_or_none(value)

        self._specifier["precision"] = ".{0}".format(value) if value is not None else ""
        return self

    def scheme(self, value):
        self._validate_named(value, Scheme)

        self._specifier["type"] = value
        return self

    def sign(self, value):
        self._validate_named(value, Sign)

        self._specifier["sign"] = value
        return self

    def symbol(self, value):
        self._validate_named(value, Symbol)

        self._specifier["symbol"] = value
        return self

    def trim(self, value):
        if isinstance(value, bool):
            value = Trim.yes if value else Trim.no

        self._validate_named(value, Trim)

        self._specifier["trim"] = value
        return self

    # Locale
    def symbol_prefix(self, value):
        self._validate_string(value)

        if "symbol" not in self._locale:
            self._locale["symbol"] = [value, ""]
        else:
            self._locale["symbol"][0] = value

        return self

    def symbol_suffix(self, value):
        self._validate_string(value)

        if "symbol" not in self._locale:
            self._locale["symbol"] = ["", value]
        else:
            self._locale["symbol"][1] = value

        return self

    def decimal_delimiter(self, value):
        self._validate_char(value)

        self._locale["decimal"] = value
        return self

    def group_delimiter(self, value):
        self._validate_char(value)

        self._locale["group"] = value
        return self

    def groups(self, groups):
        groups = (
            groups
            if isinstance(groups, list)
            else [groups]
            if isinstance(groups, int)
            else None
        )

        if not isinstance(groups, list):
            raise TypeError("expected groups to be an integer or a list of integers")
        if len(groups) == 0:
            raise ValueError(
                "expected groups to be an integer or a list of " "one or more integers"
            )

        for group in groups:
            if not isinstance(group, int):
                raise TypeError("expected entry to be an integer")

            if group <= 0:
                raise ValueError("expected entry to be a non-negative integer")

        self._locale["grouping"] = groups
        return self

    # Nully
    def nully(self, value):
        self._nully = value
        return self

    # Prefix
    def si_prefix(self, value):
        self._validate_named(value, Prefix)

        self._prefix = value
        return self

    def to_plotly_json(self):
        f = {}
        f["locale"] = self._locale.copy()
        f["nully"] = self._nully
        f["prefix"] = self._prefix
        aligned = self._specifier["align"] != Align.default
        f["specifier"] = "{}{}{}{}{}{}{}{}{}{}".format(
            self._specifier["fill"] if aligned else "",
            self._specifier["align"],
            self._specifier["sign"],
            self._specifier["symbol"],
            self._specifier["padding"],
            self._specifier["width"],
            self._specifier["group"],
            self._specifier["precision"],
            self._specifier["trim"],
            self._specifier["type"],
        )

        return f
