import dataclasses
from typing import Any, List, Type, Union, get_type_hints


def is_type_List(f_type: Type) -> bool:
    return (
        hasattr(f_type, "__origin__") and f_type.__origin__ == list
    ) or f_type == list


def is_type_SpecificOptional(f_type) -> bool:
    """
    Returns true for types such as Optional[T], but not Optional, or T.
    """
    return (
        hasattr(f_type, "__origin__")
        and f_type.__origin__ == Union
        and f_type.__args__[1]() is None
    )


def is_type_Tuple(f_type: Type) -> bool:
    return (
        hasattr(f_type, "__origin__") and f_type.__origin__ == tuple
    ) or f_type == tuple


def strictdataclass(cls: Any):
    class _Local:
        """
        Dataclass where all fields must be type annotated, and type checking is performed
        at initialization, even recursively through Lists. Non-annotated fields are ignored.
        Also, for any fields which have a type with .from_bytes(bytes) or constructor(bytes),
        bytes can be passed in and the type can be constructed.
        """

        def parse_item(self, item: Any, f_name: str, f_type: Type) -> Any:
            if is_type_List(f_type):
                collected_list: List = []
                inner_type: Type = f_type.__args__[0]
                assert inner_type != List.__args__[0]  # type: ignore
                if not is_type_List(type(item)):
                    raise ValueError(f"Wrong type for {f_name}, need a list.")
                for el in item:
                    collected_list.append(self.parse_item(el, f_name, inner_type))
                return collected_list
            if is_type_SpecificOptional(f_type):
                if item is None:
                    return None
                else:
                    inner_type: Type = f_type.__args__[0]  # type: ignore
                    return self.parse_item(item, f_name, inner_type)
            if is_type_Tuple(f_type):
                collected_list = []
                if not is_type_Tuple(type(item)) and not is_type_List(type(item)):
                    raise ValueError(f"Wrong type for {f_name}, need a tuple.")
                if len(item) != len(f_type.__args__):
                    raise ValueError(f"Wrong number of elements in tuple {f_name}.")
                for i in range(len(item)):
                    inner_type = f_type.__args__[i]
                    tuple_item = item[i]
                    collected_list.append(
                        self.parse_item(tuple_item, f_name, inner_type)
                    )
                return tuple(collected_list)
            if not isinstance(item, f_type):
                try:
                    item = f_type(item)
                except (TypeError, AttributeError, ValueError):
                    try:
                        item = f_type.from_bytes(item)
                    except Exception:
                        item = f_type.from_bytes(bytes(item))
            if not isinstance(item, f_type):
                raise ValueError(f"Wrong type for {f_name}")
            return item

        def __post_init__(self):
            fields = get_type_hints(self)
            data = self.__dict__
            for (f_name, f_type) in fields.items():
                if f_name not in data:
                    raise ValueError(f"Field {f_name} not present")
                object.__setattr__(
                    self, f_name, self.parse_item(data[f_name], f_name, f_type)
                )

    class NoTypeChecking:
        __no_type_check__ = True

    cls1 = dataclasses.dataclass(cls, init=False, frozen=True)  # type: ignore
    if dataclasses.fields(cls1) == ():
        return type(cls.__name__, (cls1, _Local, NoTypeChecking), {})
    return type(cls.__name__, (cls1, _Local), {})
