from __future__ import annotations

import sys
from functools import partial
from typing import Any, Callable, Tuple, Type, cast

from attrs import fields, has, resolve_types
from cattrs import Converter
from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override
from cattrs.gen._consts import AttributeOverride
from fontTools.misc.transform import Transform

is_py37 = sys.version_info[:2] == (3, 7)

if is_py37:

    def get_origin(cl: Type[Any]) -> Any:
        return getattr(cl, "__origin__", None)

else:
    from typing import get_origin  # type: ignore


__all__ = [
    "register_hooks",
    "structure",
    "unstructure",
]


def is_ufoLib2_class(cls: Type[Any]) -> bool:
    mod: str = getattr(cls, "__module__", "")
    return mod.split(".")[0] == "ufoLib2"


def is_ufoLib2_attrs_class(cls: Type[Any]) -> bool:
    return is_ufoLib2_class(cls) and (has(cls) or has(get_origin(cls)))


def is_ufoLib2_class_with_custom_unstructure(cls: Type[Any]) -> bool:
    return is_ufoLib2_class(cls) and hasattr(cls, "_unstructure")


def is_ufoLib2_class_with_custom_structure(cls: Type[Any]) -> bool:
    return is_ufoLib2_class(cls) and hasattr(cls, "_structure")


def register_hooks(conv: Converter, allow_bytes: bool = True) -> None:
    def attrs_hook_factory(
        cls: Type[Any], gen_fn: Callable[..., Callable[..., Any]], structuring: bool
    ) -> Callable[..., Any]:
        base = get_origin(cls)
        if base is None:
            base = cls
        attribs = fields(base)
        # PEP563 postponed annotations need resolving as we check Attribute.type below
        resolve_types(base)
        kwargs: dict[str, bool | AttributeOverride] = {
            "_cattrs_detailed_validation": conv.detailed_validation
        }
        if structuring:
            kwargs["_cattrs_forbid_extra_keys"] = conv.forbid_extra_keys
            kwargs["_cattrs_prefer_attrib_converters"] = conv._prefer_attrib_converters
        else:
            kwargs["_cattrs_omit_if_default"] = conv.omit_if_default
        for a in attribs:
            if a.type in conv.type_overrides:
                # cattrs' gen_(un)structure_attrs_fromdict (used by default for attrs
                # classes that don't have a custom hook registered) check for any
                # type_overrides (Dict[Type, AttributeOverride]); they allow a custom
                # converter to omit specific attributes of given type e.g.:
                # >>> conv = Converter(type_overrides={Image: override(omit=True)})
                attrib_override = conv.type_overrides[a.type]
            else:
                # by default, we omit all Optional attributes (i.e. with None default),
                # overriding a Converter's global 'omit_if_default' option. Specific
                # attibutes can still define their own 'omit_if_default' behavior in
                # the Attribute.metadata dict.
                attrib_override = override(
                    omit_if_default=a.metadata.get(
                        "omit_if_default", a.default is None or None
                    ),
                    rename=a.metadata.get(
                        "rename_attr", a.name[1:] if a.name[0] == "_" else None
                    ),
                    omit=not a.init,
                )
            kwargs[a.name] = attrib_override

        return gen_fn(cls, conv, **kwargs)

    def custom_unstructure_hook_factory(cls: Type[Any]) -> Callable[[Any], Any]:
        return partial(cls._unstructure, converter=conv)

    def custom_structure_hook_factory(cls: Type[Any]) -> Callable[[Any, Any], Any]:
        return partial(cls._structure, converter=conv)

    def unstructure_transform(t: Transform) -> Tuple[float]:
        return cast(Tuple[float], tuple(t))

    def structure_transform(t: Tuple[float], _: Any) -> Transform:
        return Transform(*t)

    conv.register_unstructure_hook_factory(
        is_ufoLib2_attrs_class,
        partial(attrs_hook_factory, gen_fn=make_dict_unstructure_fn, structuring=False),
    )
    conv.register_unstructure_hook_factory(
        is_ufoLib2_class_with_custom_unstructure,
        custom_unstructure_hook_factory,
    )
    conv.register_unstructure_hook(
        cast(Type[Transform], Transform), unstructure_transform
    )

    conv.register_structure_hook(cast(Type[Transform], Transform), structure_transform)
    conv.register_structure_hook_factory(
        is_ufoLib2_attrs_class,
        partial(attrs_hook_factory, gen_fn=make_dict_structure_fn, structuring=True),
    )
    conv.register_structure_hook_factory(
        is_ufoLib2_class_with_custom_structure,
        custom_structure_hook_factory,
    )

    if not allow_bytes:
        from base64 import b64decode, b64encode

        def unstructure_bytes(v: bytes) -> str:
            return (b64encode(v) if v else b"").decode("utf8")

        def structure_bytes(v: str, _: Any) -> bytes:
            return b64decode(v)

        conv.register_unstructure_hook(bytes, unstructure_bytes)
        conv.register_structure_hook(bytes, structure_bytes)


default_converter = Converter(
    omit_if_default=True,
    forbid_extra_keys=True,
    prefer_attrib_converters=False,
)
register_hooks(default_converter, allow_bytes=False)

structure = default_converter.structure
unstructure = default_converter.unstructure

# same as default_converter but allows bytes
binary_converter = Converter(
    omit_if_default=True,
    forbid_extra_keys=True,
    prefer_attrib_converters=False,
)
register_hooks(binary_converter, allow_bytes=True)
