import linecache
import re
import uuid
from dataclasses import is_dataclass
from threading import local
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    Dict,
    Mapping,
    Optional,
    Type,
    TypeVar,
)

import attr
from attr import NOTHING, resolve_types

from ._compat import (
    adapted_fields,
    get_args,
    get_origin,
    is_annotated,
    is_bare,
    is_generic,
)
from ._generics import deep_copy_with

if TYPE_CHECKING:  # pragma: no cover
    from cattr.converters import Converter


@attr.s(slots=True, frozen=True)
class AttributeOverride:
    omit_if_default: Optional[bool] = attr.ib(default=None)
    rename: Optional[str] = attr.ib(default=None)
    omit: bool = attr.ib(default=False)  # Omit the field completely.


def override(omit_if_default=None, rename=None, omit: bool = False):
    return AttributeOverride(
        omit_if_default=omit_if_default, rename=rename, omit=omit
    )


_neutral = AttributeOverride()
_already_generating = local()
T = TypeVar("T")


def make_dict_unstructure_fn(
    cl,
    converter,
    _cattrs_omit_if_default: bool = False,
    _cattrs_use_linecache: bool = True,
    **kwargs,
):
    """
    Generate a specialized dict unstructuring function for an attrs class or a
    dataclass.
    """
    origin = get_origin(cl)
    attrs = adapted_fields(origin or cl)  # type: ignore

    if any(isinstance(a.type, str) for a in attrs):
        # PEP 563 annotations - need to be resolved.
        resolve_types(cl)

    mapping = {}
    if is_generic(cl):
        mapping = _generate_mapping(cl, mapping)

        for base in getattr(origin, "__orig_bases__", ()):
            if is_generic(base) and not str(base).startswith("typing.Generic"):
                mapping = _generate_mapping(base, mapping)
                break
        cl = origin

    cl_name = cl.__name__
    fn_name = "unstructure_" + cl_name
    globs = {}
    lines = []
    post_lines = []

    # We keep track of what we're generating to help with recursive
    # class graphs.
    try:
        working_set = _already_generating.working_set
    except AttributeError:
        working_set = set()
        _already_generating.working_set = working_set
    if cl in working_set:
        raise RecursionError()
    else:
        working_set.add(cl)

    try:
        lines.append(f"def {fn_name}(instance):")
        lines.append("    res = {")
        for a in attrs:
            attr_name = a.name
            override = kwargs.pop(attr_name, _neutral)
            if override.omit:
                continue
            kn = attr_name if override.rename is None else override.rename
            d = a.default

            # For each attribute, we try resolving the type here and now.
            # If a type is manually overwritten, this function should be
            # regenerated.
            handler = None
            if a.type is not None:
                t = a.type
                if isinstance(t, TypeVar):
                    if t.__name__ in mapping:
                        t = mapping[t.__name__]
                    else:
                        handler = converter.unstructure
                elif is_generic(t) and not is_bare(t) and not is_annotated(t):
                    t = deep_copy_with(t, mapping)

                if handler is None:
                    try:
                        handler = converter._unstructure_func.dispatch(t)
                    except RecursionError:
                        # There's a circular reference somewhere down the line
                        handler = converter.unstructure
            else:
                handler = converter.unstructure

            is_identity = handler == converter._unstructure_identity

            if not is_identity:
                unstruct_handler_name = f"unstructure_{attr_name}"
                globs[unstruct_handler_name] = handler
                invoke = f"{unstruct_handler_name}(instance.{attr_name})"
            else:
                invoke = f"instance.{attr_name}"

            if d is not attr.NOTHING and (
                (
                    _cattrs_omit_if_default
                    and override.omit_if_default is not False
                )
                or override.omit_if_default
            ):
                def_name = f"__cattr_def_{attr_name}"

                if isinstance(d, attr.Factory):
                    globs[def_name] = d.factory
                    if d.takes_self:
                        post_lines.append(
                            f"    if instance.{attr_name} != {def_name}(instance):"
                        )
                    else:
                        post_lines.append(
                            f"    if instance.{attr_name} != {def_name}():"
                        )
                    post_lines.append(f"        res['{kn}'] = {invoke}")
                else:
                    globs[def_name] = d
                    post_lines.append(
                        f"    if instance.{attr_name} != {def_name}:"
                    )
                    post_lines.append(f"        res['{kn}'] = {invoke}")

            else:
                # No default or no override.
                lines.append(f"        '{kn}': {invoke},")
        lines.append("    }")

        total_lines = lines + post_lines + ["    return res"]
        script = "\n".join(total_lines)

        fname = _generate_unique_filename(
            cl, "unstructure", reserve=_cattrs_use_linecache
        )

        eval(compile(script, fname, "exec"), globs)

        fn = globs[fn_name]
        if _cattrs_use_linecache:
            linecache.cache[fname] = len(script), None, total_lines, fname
    finally:
        working_set.remove(cl)

    return fn


def _generate_mapping(
    cl: Type, old_mapping: Dict[str, type]
) -> Dict[str, type]:
    mapping = {}
    for p, t in zip(get_origin(cl).__parameters__, get_args(cl)):
        if isinstance(t, TypeVar):
            continue
        mapping[p.__name__] = t

    if not mapping:
        return old_mapping

    return mapping


def make_dict_structure_fn(
    cl: Type[T],
    converter: "Converter",
    _cattrs_forbid_extra_keys: bool = False,
    _cattrs_use_linecache: bool = True,
    _cattrs_prefer_attrib_converters: bool = False,
    **kwargs,
) -> Callable[[Mapping[str, Any]], T]:
    """Generate a specialized dict structuring function for an attrs class."""

    mapping = {}
    if is_generic(cl):
        base = get_origin(cl)
        mapping = _generate_mapping(cl, mapping)
        cl = base

    for base in getattr(cl, "__orig_bases__", ()):
        if is_generic(base) and not str(base).startswith("typing.Generic"):
            mapping = _generate_mapping(base, mapping)
            break

    if isinstance(cl, TypeVar):
        cl = mapping.get(cl.__name__, cl)

    cl_name = cl.__name__
    fn_name = "structure_" + cl_name

    # We have generic parameters and need to generate a unique name for the function
    for p in getattr(cl, "__parameters__", ()):
        # This is nasty, I am not sure how best to handle `typing.List[str]` or `TClass[int, int]` as a parameter type here
        name_base = mapping[p.__name__]
        name = getattr(name_base, "__name__", None) or str(name_base)
        name = re.sub(r"[\[\.\] ,]", "_", name)
        fn_name += f"_{name}"

    globs = {"__c_s": converter.structure, "__cl": cl}
    lines = []
    post_lines = []

    attrs = adapted_fields(cl)
    is_dc = is_dataclass(cl)

    if any(isinstance(a.type, str) for a in attrs):
        # PEP 563 annotations - need to be resolved.
        resolve_types(cl)

    lines.append(f"def {fn_name}(o, *_):")
    lines.append("  res = {")
    allowed_fields = set()
    for a in attrs:
        an = a.name
        override = kwargs.pop(an, _neutral)
        if override.omit:
            continue
        t = a.type
        if isinstance(t, TypeVar):
            t = mapping.get(t.__name__, t)
        elif is_generic(t) and not is_bare(t) and not is_annotated(t):
            t = deep_copy_with(t, mapping)

        # For each attribute, we try resolving the type here and now.
        # If a type is manually overwritten, this function should be
        # regenerated.
        if a.converter is not None and _cattrs_prefer_attrib_converters:
            handler = None
        elif (
            a.converter is not None
            and not _cattrs_prefer_attrib_converters
            and t is not None
        ):
            handler = converter._structure_func.dispatch(t)
            if handler == converter._structure_error:
                handler = None
        elif t is not None:
            handler = converter._structure_func.dispatch(t)
        else:
            handler = converter.structure

        struct_handler_name = f"structure_{an}"
        globs[struct_handler_name] = handler

        ian = an if (is_dc or an[0] != "_") else an[1:]
        kn = an if override.rename is None else override.rename
        allowed_fields.add(kn)
        globs[f"type_{an}"] = t
        if a.default is NOTHING:
            if handler:
                lines.append(
                    f"    '{ian}': {struct_handler_name}(o['{kn}'], type_{an}),"
                )
            else:
                lines.append(f"    '{ian}': o['{kn}'],")
        else:
            post_lines.append(f"  if '{kn}' in o:")
            if handler:
                post_lines.append(
                    f"    res['{ian}'] = {struct_handler_name}(o['{kn}'], type_{an})"
                )
            else:
                post_lines.append(f"    res['{ian}'] = o['{kn}']")

    lines.append("    }")
    if _cattrs_forbid_extra_keys:
        globs["__c_a"] = allowed_fields
        post_lines += [
            "  unknown_fields = set(o.keys()) - __c_a",
            "  if unknown_fields:",
            "    raise Exception(",
            f"      'Extra fields in constructor for {cl_name}: ' + ', '.join(unknown_fields)"
            "    )",
        ]

    total_lines = lines + post_lines + ["  return __cl(**res)"]

    fname = _generate_unique_filename(
        cl, "structure", reserve=_cattrs_use_linecache
    )
    script = "\n".join(total_lines)
    eval(compile(script, fname, "exec"), globs)
    if _cattrs_use_linecache:
        linecache.cache[fname] = len(script), None, total_lines, fname

    return globs[fn_name]


def make_iterable_unstructure_fn(cl: Any, converter, unstructure_to=None):
    """Generate a specialized unstructure function for an iterable."""
    handler = converter.unstructure

    fn_name = "unstructure_iterable"

    # Let's try fishing out the type args.
    if getattr(cl, "__args__", None) is not None:
        type_arg = get_args(cl)[0]
        # We don't know how to handle the TypeVar on this level,
        # so we skip doing the dispatch here.
        if not isinstance(type_arg, TypeVar):
            handler = converter._unstructure_func.dispatch(type_arg)

    globs = {"__cattr_seq_cl": unstructure_to or cl, "__cattr_u": handler}
    lines = []

    lines.append(f"def {fn_name}(iterable):")
    lines.append("    res = __cattr_seq_cl(__cattr_u(i) for i in iterable)")

    total_lines = lines + ["    return res"]

    eval(compile("\n".join(total_lines), "", "exec"), globs)

    fn = globs[fn_name]

    return fn


def make_hetero_tuple_unstructure_fn(cl: Any, converter, unstructure_to=None):
    """Generate a specialized unstructure function for a heterogenous tuple."""
    fn_name = "unstructure_tuple"

    type_args = get_args(cl)

    # We can do the dispatch here and now.
    handlers = [
        converter._unstructure_func.dispatch(type_arg)
        for type_arg in type_args
    ]

    globs = {f"__cattr_u_{i}": h for i, h in enumerate(handlers)}
    if unstructure_to is not tuple:
        globs["__cattr_seq_cl"] = unstructure_to or cl
    lines = []

    lines.append(f"def {fn_name}(tup):")
    if unstructure_to is not tuple:
        lines.append("    res = __cattr_seq_cl((")
    else:
        lines.append("    res = (")
    for i in range(len(handlers)):
        if handlers[i] == converter._unstructure_identity:
            lines.append(f"        tup[{i}],")
        else:
            lines.append(f"        __cattr_u_{i}(tup[{i}]),")

    if unstructure_to is not tuple:
        lines.append("    ))")
    else:
        lines.append("    )")

    total_lines = lines + ["    return res"]

    eval(compile("\n".join(total_lines), "", "exec"), globs)

    fn = globs[fn_name]

    return fn


def make_mapping_unstructure_fn(
    cl: Any, converter, unstructure_to=None, key_handler=None
):
    """Generate a specialized unstructure function for a mapping."""
    kh = key_handler or converter.unstructure
    val_handler = converter.unstructure

    fn_name = "unstructure_mapping"

    # Let's try fishing out the type args.
    if getattr(cl, "__args__", None) is not None:
        args = get_args(cl)
        if len(args) == 2:
            key_arg, val_arg = args
        else:
            # Probably a Counter
            key_arg, val_arg = args, Any
        # We can do the dispatch here and now.
        kh = key_handler or converter._unstructure_func.dispatch(key_arg)
        if kh == converter._unstructure_identity:
            kh = None

        val_handler = converter._unstructure_func.dispatch(val_arg)
        if val_handler == converter._unstructure_identity:
            val_handler = None

    globs = {
        "__cattr_mapping_cl": unstructure_to or cl,
        "__cattr_k_u": kh,
        "__cattr_v_u": val_handler,
    }

    k_u = "__cattr_k_u(k)" if kh is not None else "k"
    v_u = "__cattr_v_u(v)" if val_handler is not None else "v"

    lines = []

    lines.append(f"def {fn_name}(mapping):")
    lines.append(
        f"    res = __cattr_mapping_cl(({k_u}, {v_u}) for k, v in mapping.items())"
    )

    total_lines = lines + ["    return res"]

    eval(compile("\n".join(total_lines), "", "exec"), globs)

    fn = globs[fn_name]

    return fn


def make_mapping_structure_fn(
    cl: Any, converter, structure_to=dict, key_type=NOTHING, val_type=NOTHING
):
    """Generate a specialized unstructure function for a mapping."""
    fn_name = "structure_mapping"

    globs = {"__cattr_mapping_cl": structure_to}

    lines = []
    lines.append(f"def {fn_name}(mapping, _):")

    # Let's try fishing out the type args.
    if not is_bare(cl):
        args = get_args(cl)
        if len(args) == 2:
            key_arg_cand, val_arg_cand = args
            if key_type is NOTHING:
                key_type = key_arg_cand
            if val_type is NOTHING:
                val_type = val_arg_cand
        else:
            if key_type is not NOTHING and val_type is NOTHING:
                (val_type,) = args
            elif key_type is NOTHING and val_type is not NOTHING:
                (key_type,) = args
            else:
                # Probably a Counter
                (key_type,) = args
                val_type = Any

        is_bare_dict = val_type is Any and key_type is Any
        if not is_bare_dict:
            # We can do the dispatch here and now.
            key_handler = converter._structure_func.dispatch(key_type)
            if key_handler == converter._structure_call:
                key_handler = key_type

            val_handler = converter._structure_func.dispatch(val_type)
            if val_handler == converter._structure_call:
                val_handler = val_type

            globs["__cattr_k_t"] = key_type
            globs["__cattr_v_t"] = val_type
            globs["__cattr_k_s"] = key_handler
            globs["__cattr_v_s"] = val_handler
            k_s = (
                "__cattr_k_s(k, __cattr_k_t)"
                if key_handler != key_type
                else "__cattr_k_s(k)"
            )
            v_s = (
                "__cattr_v_s(v, __cattr_v_t)"
                if val_handler != val_type
                else "__cattr_v_s(v)"
            )
    else:
        is_bare_dict = True

    if is_bare_dict:
        # No args, it's a bare dict.
        lines.append("    res = dict(mapping)")
    else:
        lines.append(f"    res = {{{k_s}: {v_s} for k, v in mapping.items()}}")
    if structure_to is not dict:
        lines.append("    res = __cattr_mapping_cl(res)")

    total_lines = lines + ["    return res"]

    eval(compile("\n".join(total_lines), "", "exec"), globs)

    fn = globs[fn_name]

    return fn


def _generate_unique_filename(cls, func_name, reserve=True):
    """
    Create a "filename" suitable for a function being generated.
    """
    unique_id = uuid.uuid4()
    extra = ""
    count = 1

    while True:
        unique_filename = "<cattrs generated {0} {1}.{2}{3}>".format(
            func_name,
            cls.__module__,
            getattr(cls, "__qualname__", cls.__name__),
            extra,
        )
        if not reserve:
            return unique_filename
        # To handle concurrency we essentially "reserve" our spot in
        # the linecache with a dummy line.  The caller can then
        # set this value correctly.
        cache_line = (1, None, (str(unique_id),), unique_filename)
        if (
            linecache.cache.setdefault(unique_filename, cache_line)
            == cache_line
        ):
            return unique_filename

        # Looks like this spot is taken. Try again.
        count += 1
        extra = "-{0}".format(count)
