# -*- coding: utf-8 -*-

from collections import namedtuple
from enum import Enum
from typing import Any, List, Optional, Union

from faker import Faker
from pydantic import BaseModel, ConfigDict, Field

padding_amount = 1
faker = Faker()

ColorOutputMode = Enum(
    "ColorOutputMode", "EN_LOWER EN_UPPER DE_LOWER DE_UPPER HTML_LOWER HTML_UPPER"
)

color_output_mode = ColorOutputMode.EN_UPPER

KnownColor = namedtuple("KnownColor", "html code_de full_en full_de")

known_colors = {  #                   v--------v--------- for future use
    "BK": KnownColor("#000000", "sw", "black", "schwarz"),
    "WH": KnownColor("#ffffff", "ws", "white", "weiß"),
    "GY": KnownColor("#999999", "gr", "grey", "grau"),
    "PK": KnownColor("#ff66cc", "rs", "pink", "rosa"),
    "RD": KnownColor("#ff0000", "rt", "red", "rot"),
    "OG": KnownColor("#ff8000", "or", "orange", "orange"),
    "YE": KnownColor("#ffff00", "ge", "yellow", "gelb"),
    "OL": KnownColor("#708000", "ol", "olive green", "olivgrün"),
    "GN": KnownColor("#00aa00", "gn", "green", "grün"),
    "TQ": KnownColor("#00ffff", "tk", "turquoise", "türkis"),
    "LB": KnownColor("#a0dfff", "hb", "light blue", "hellblau"),
    "BU": KnownColor("#0066ff", "bl", "blue", "blau"),
    "VT": KnownColor("#8000ff", "vi", "violet", "violett"),
    "BN": KnownColor("#895956", "br", "brown", "braun"),
    "BG": KnownColor("#ceb673", "bg", "beige", "beige"),
    "IV": KnownColor("#f5f0d0", "eb", "ivory", "elfenbein"),
    "SL": KnownColor("#708090", "si", "slate", "schiefer"),
    "CU": KnownColor("#d6775e", "ku", "copper", "Kupfer"),
    "SN": KnownColor("#aaaaaa", "vz", "tin", "verzinkt"),
    "SR": KnownColor("#84878c", "ag", "silver", "Silber"),
    "GD": KnownColor("#ffcf80", "au", "gold", "Gold"),
}


def convert_case(inp):
    if "_LOWER" in color_output_mode.name:
        return inp.lower()
    elif "_UPPER" in color_output_mode.name:
        return inp.upper()
    else:  # currently not used
        return inp


def get_color_by_colorcode_index(color_code: str, index: int) -> str:
    num_colors_in_code = len(COLOR_CODES[color_code])
    actual_index = index % num_colors_in_code  # wrap around if index is out of bounds
    return COLOR_CODES[color_code][actual_index]


class SingleColor(BaseModel):
    code_en: Optional[str] = None
    html: Optional[str] = None

    def __init__(self, inp=None, **data):
        if "code_en" in data or "html" in data:
            super().__init__(**data)
            return
        if isinstance(inp, SingleColor):
            values = {"code_en": inp.code_en, "html": inp.html}
        elif inp is None:
            values = {"code_en": None, "html": None}
        elif isinstance(inp, int):
            hex_str = f"#{inp:06x}"
            values = {"code_en": hex_str, "html": hex_str}
        elif isinstance(inp, str):
            inp_upper = inp.upper()
            if inp_upper in known_colors:
                values = {"code_en": inp_upper, "html": known_colors[inp_upper].html}
            else:
                values = {"code_en": inp, "html": inp}
        else:
            values = {"code_en": str(inp), "html": str(inp)}
        super().__init__(**values, **data)

    model_config = ConfigDict(arbitrary_types_allowed=True)

    @property
    def code_de(self):
        if not self.code_en:
            return None
        return convert_case(known_colors[self.code_en.upper()].code_de)

    @property
    def known(self):
        return self.code_en.upper() in known_colors.keys() if self.code_en else True

    @property
    def html_padded(self):
        html = self.html if self.html else "#000000"
        return ":".join([html] * padding_amount)

    def __len__(self):
        return 1

    def __bool__(self):
        return self.code_en is not None

    def __str__(self):
        if self.html is None:
            return ""
        elif self.known and "EN_" in color_output_mode.name:
            return convert_case(self.code_en)
        elif self.known and "DE_" in color_output_mode.name:
            return convert_case(self.code_de)
        else:
            return convert_case(self.html)


class FakeSingleColorFactory:
    """faker-backed factory for SingleColor."""

    @classmethod
    def create(
        cls, allow_unknown: bool = True, unknown_chance: int = 20
    ) -> SingleColor:
        """Create a SingleColor; unknown_chance is a 0-100 probability for random hex."""
        # Use known codes most of the time; optionally allow unknown HTML colors.
        if allow_unknown and faker.boolean(chance_of_getting_true=unknown_chance):
            return SingleColor(html=faker.hex_color())
        code = faker.random_element(elements=list(known_colors.keys()))
        return SingleColor(code)


class MultiColor(BaseModel):
    colors: List[SingleColor] = Field(default_factory=list)

    model_config = ConfigDict(arbitrary_types_allowed=True)

    def __init__(self, inp=None, **data):
        if "colors" in data:
            colors_value = self._normalize_colors(data.pop("colors"))
        else:
            colors_value = self._normalize_colors(inp)
        super().__init__(colors=colors_value, **data)

    def __iter__(self):
        return iter(self.colors)

    def __len__(self):
        return len(self.colors)

    def __bool__(self):
        return len(self.colors) >= 1

    def __str__(self):
        if "EN_" in color_output_mode.name or "DE_" in color_output_mode.name:
            joiner = "" if self.all_known else ":"
        elif "HTML_" in color_output_mode.name:
            joiner = ":"
        else:
            joiner = "???"
        return joiner.join([str(color) for color in self.colors])

    def __getitem__(self, idx):
        if not self:
            return SingleColor(None)
        else:
            return self.colors[idx % len(self)]

    @property
    def all_known(self):
        return all([color.known for color in self.colors])

    @property
    def html(self):
        return ":".join([color.html or "" for color in self.colors])

    @property
    def html_padded_list(self):
        # padding only properly works for padding_amount 1 or 3
        if padding_amount == 1:
            out = [color.html for color in self.colors]
        elif len(self) == 0:
            out = []
        elif len(self) == 1:
            out = [self.colors[0].html for i in range(3)]
        elif len(self) == 2:
            out = [self.colors[0].html, self.colors[1].html, self.colors[0].html]
        elif len(self) == 3:
            out = [color.html for color in self.colors]
        else:
            from filare.errors import ColorPaddingUnsupported

            raise ColorPaddingUnsupported(len(self))
        return [str(color) for color in out]

    @property
    def len(self):
        return len(self)

    @property
    def html_padded(self):
        if len(self.html_padded_list) == 0:
            return "#FFFFFF"
        return ":".join(self.html_padded_list)

    @classmethod
    def _normalize_colors(cls, inp):
        colors: List[SingleColor] = []
        if inp is None:
            return colors
        if isinstance(inp, MultiColor):
            return list(inp.colors)
        if isinstance(inp, (list, tuple)):
            for item in inp:
                if item is None:
                    continue
                colors.append(
                    item if isinstance(item, SingleColor) else SingleColor(item)
                )
            return colors
        if isinstance(inp, SingleColor):
            return [inp]

        inp_str = str(inp)
        if ":" in inp_str:
            return [SingleColor(item) for item in inp_str.split(":")]
        if isinstance(inp, int):
            return [SingleColor(inp)]
        if len(inp_str) % 2 == 0:
            items = [inp_str[i : i + 2] for i in range(0, len(inp_str), 2)]
            known = [item.upper() in known_colors.keys() for item in items]
            if all(known):
                return [SingleColor(item) for item in items]
        return [SingleColor(inp_str)]


class FakeMultiColorFactory:
    """faker-backed factory for MultiColor."""

    @classmethod
    def create(
        cls,
        count: int = 2,
        allow_unknown: bool = True,
        unknown_chance: int = 20,
        color_code: Optional[str] = None,
    ) -> MultiColor:
        """Create a MultiColor; optionally follow a COLOR_CODES sequence in order."""
        if color_code and color_code in COLOR_CODES:
            palette = COLOR_CODES[color_code]
            colors: List[SingleColor] = []
            for idx in range(count):
                code_str = palette[idx % len(palette)]
                if len(code_str) == 2 and code_str.upper() in known_colors:
                    colors.append(SingleColor(code_str))
                else:
                    colors.append(SingleColor(code_str))
            return MultiColor(colors=colors)

        colors = [
            FakeSingleColorFactory.create(
                allow_unknown=allow_unknown, unknown_chance=unknown_chance
            )
            for _ in range(count)
        ]
        return MultiColor(colors=colors)


COLOR_CODES = {
    # fmt: off
    "DIN": [
        "WH", "BN", "GN", "YE", "GY", "PK", "BU", "RD", "BK", "VT", "GYPK", "RDBU",
        "WHGN", "BNGN", "WHYE", "YEBN", "WHGY", "GYBN", "WHPK", "PKBN", "WHBU", "BNBU",
        "WHRD", "BNRD", "WHBK", "BNBK", "GYGN", "YEGY", "PKGN", "YEPK", "GNBU", "YEBU",
        "GNRD", "YERD", "GNBK", "YEBK", "GYBU", "PKBU", "GYRD", "PKRD", "GYBK", "PKBK",
        "BUBK", "RDBK", "WHBNBK", "YEGNBK", "GYPKBK", "RDBUBK", "WHGNBK", "BNGNBK",
        "WHYEBK", "YEBNBK", "WHGYBK", "GYBNBK", "WHPKBK", "PKBNBK", "WHBUBK",
        "BNBUBK", "WHRDBK", "BNRDBK",
    ],
    # fmt: on
    "IEC": ["BN", "RD", "OG", "YE", "GN", "BU", "VT", "GY", "WH", "BK"],
    "BW": ["BK", "WH"],
    # 25-pair color code - see also https://en.wikipedia.org/wiki/25-pair_color_code
    # 5 major colors (WH,RD,BK,YE,VT) combined with 5 minor colors (BU,OG,GN,BN,SL).
    # Each POTS pair tip (+) had major/minor color, and ring (-) had minor/major color.
    # fmt: off
    "TEL": [  # 25x2: Ring and then tip of each pair
        "BUWH", "WHBU", "OGWH", "WHOG", "GNWH", "WHGN", "BNWH", "WHBN", "SLWH", "WHSL",
        "BURD", "RDBU", "OGRD", "RDOG", "GNRD", "RDGN", "BNRD", "RDBN", "SLRD", "RDSL",
        "BUBK", "BKBU", "OGBK", "BKOG", "GNBK", "BKGN", "BNBK", "BKBN", "SLBK", "BKSL",
        "BUYE", "YEBU", "OGYE", "YEOG", "GNYE", "YEGN", "BNYE", "YEBN", "SLYE", "YESL",
        "BUVT", "VTBU", "OGVT", "VTOG", "GNVT", "VTGN", "BNVT", "VTBN", "SLVT", "VTSL",
    ],
    "TELALT": [  # 25x2: Tip and then ring of each pair
        "WHBU", "BU", "WHOG", "OG", "WHGN", "GN", "WHBN", "BN", "WHSL", "SL",
        "RDBU", "BURD", "RDOG", "OGRD", "RDGN", "GNRD", "RDBN", "BNRD", "RDSL", "SLRD",
        "BKBU", "BUBK", "BKOG", "OGBK", "BKGN", "GNBK", "BKBN", "BNBK", "BKSL", "SLBK",
        "YEBU", "BUYE", "YEOG", "OGYE", "YEGN", "GNYE", "YEBN", "BNYE", "YESL", "SLYE",
        "VTBU", "BUVT", "VTOG", "OGVT", "VTGN", "GNVT", "VTBN", "BNVT", "VTSL", "SLVT",
    ],
    # fmt: on
    "T568A": ["WHGN", "GN", "WHOG", "BU", "WHBU", "OG", "WHBN", "BN"],
    "T568B": ["WHOG", "OG", "WHGN", "BU", "WHBU", "GN", "WHBN", "BN"],
}
