# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""``dataclasses``-style helpers unified over stdlib, ``@c_class``, and ``@py_class``."""

from __future__ import annotations

import copy
from collections.abc import Callable
from typing import Any

from ..container import Array, Dict, List, Map
from .field import Field

__all__ = ["asdict", "astuple", "fields", "is_dataclass", "replace"]

# Exact-type fast path for atomic (immutable, non-recursive) values.  Mirrors
# :data:`dataclasses._ATOMIC_TYPES` from the standard library.
_ATOMIC_TYPES: frozenset[type] = frozenset(
    {
        bool,
        bytes,
        complex,
        float,
        int,
        str,
        type(None),
        type(Ellipsis),
        type(NotImplemented),
    }
)


def is_dataclass(obj: Any) -> bool:
    """Return True if ``obj`` is a ``@c_class`` / ``@py_class`` type or instance.

    Returns False for FFI container types (:class:`~tvm_ffi.Array`,
    :class:`~tvm_ffi.List`, :class:`~tvm_ffi.Map`, :class:`~tvm_ffi.Dict`)
    even though they also carry ``__tvm_ffi_type_info__``; those are
    reflected through :func:`~tvm_ffi.register_object` directly, not
    through the dataclass decorators.
    """
    cls = obj if isinstance(obj, type) else type(obj)
    return getattr(cls, "__tvm_ffi_is_dataclass__", False) is True


def fields(obj_or_cls: Any) -> tuple[Field, ...]:
    """Return the :class:`~tvm_ffi.dataclasses.Field` descriptors for a type.

    Accepts a ``@c_class`` / ``@py_class`` type or instance and walks the
    parent chain so inherited fields appear parent-first, matching the
    order of the auto-generated ``__init__``.

    Raises
    ------
    TypeError
        If ``obj_or_cls`` is not a ``@c_class`` / ``@py_class`` type or instance.

    """
    if not is_dataclass(obj_or_cls):
        raise TypeError(
            f"fields() argument must be a c_class or py_class type or instance, "
            f"got {type(obj_or_cls).__name__}"
        )
    cls = obj_or_cls if isinstance(obj_or_cls, type) else type(obj_or_cls)
    ti = getattr(cls, "__tvm_ffi_type_info__", None)
    chain = []
    while ti is not None:
        chain.append(ti)
        ti = ti.parent_type_info
    out: list[Field] = []
    for ti in reversed(chain):
        for tf in ti.fields or ():
            if tf.dataclass_field is not None:
                out.append(tf.dataclass_field)
    return tuple(out)


def replace(obj: Any, /, **changes: Any) -> Any:
    """Return a copy of ``obj`` with selected fields replaced.

    Drop-in for :func:`dataclasses.replace` for FFI-backed instances: the
    call is forwarded to ``obj.__replace__`` (installed by the decorator),
    which uses the ``FFIProperty.set()`` escape hatch so frozen fields are
    still replaceable.
    """
    return obj.__replace__(**changes)


def _is_ffi_dataclass_instance(obj: Any) -> bool:
    """Return True when *obj* is a ``@c_class`` / ``@py_class`` **instance** (not a type)."""
    if isinstance(obj, type):
        return False
    return is_dataclass(obj)


def _asdict_inner(  # noqa: PLR0911, PLR0912
    obj: Any, dict_factory: Callable[..., Any]
) -> Any:
    obj_type = type(obj)
    if obj_type in _ATOMIC_TYPES:
        return obj
    # FFI containers are treated as their stdlib analogues so the result is
    # plain Python data — handy for JSON serialisation, the main use case.
    if isinstance(obj, (Array, List)):
        return [_asdict_inner(v, dict_factory) for v in obj]
    if isinstance(obj, (Map, Dict)):
        return dict_factory(
            [
                (_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory))
                for k, v in obj.items()
            ]
        )
    if _is_ffi_dataclass_instance(obj):
        fs = fields(obj)
        if dict_factory is dict:
            return {f.name: _asdict_inner(getattr(obj, f.name), dict) for f in fs}  # ty: ignore[invalid-argument-type]
        return dict_factory(
            [(f.name, _asdict_inner(getattr(obj, f.name), dict_factory)) for f in fs]  # ty: ignore[invalid-argument-type]
        )
    if obj_type is list:
        return [_asdict_inner(v, dict_factory) for v in obj]
    if obj_type is dict:
        return {
            _asdict_inner(k, dict_factory): _asdict_inner(v, dict_factory) for k, v in obj.items()
        }
    if obj_type is tuple:
        return tuple(_asdict_inner(v, dict_factory) for v in obj)
    if issubclass(obj_type, tuple):
        if hasattr(obj, "_fields"):  # namedtuple
            return obj_type(*[_asdict_inner(v, dict_factory) for v in obj])
        return obj_type(_asdict_inner(v, dict_factory) for v in obj)
    if issubclass(obj_type, dict):
        if hasattr(obj_type, "default_factory"):
            result = obj_type(obj.default_factory)
            for k, v in obj.items():
                result[_asdict_inner(k, dict_factory)] = _asdict_inner(v, dict_factory)
            return result
        return obj_type(
            (_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory)) for k, v in obj.items()
        )
    if issubclass(obj_type, list):
        return obj_type(_asdict_inner(v, dict_factory) for v in obj)
    return copy.deepcopy(obj)


def _astuple_inner(obj: Any, tuple_factory: Callable[..., Any]) -> Any:  # noqa: PLR0911
    obj_type = type(obj)
    if obj_type in _ATOMIC_TYPES:
        return obj
    if isinstance(obj, (Array, List)):
        return [_astuple_inner(v, tuple_factory) for v in obj]
    if isinstance(obj, (Map, Dict)):
        return {
            _astuple_inner(k, tuple_factory): _astuple_inner(v, tuple_factory)
            for k, v in obj.items()
        }
    if _is_ffi_dataclass_instance(obj):
        return tuple_factory(
            [_astuple_inner(getattr(obj, f.name), tuple_factory) for f in fields(obj)]  # ty: ignore[invalid-argument-type]
        )
    if isinstance(obj, tuple) and hasattr(obj, "_fields"):  # namedtuple
        return obj_type(*[_astuple_inner(v, tuple_factory) for v in obj])
    if isinstance(obj, (list, tuple)):
        return obj_type(_astuple_inner(v, tuple_factory) for v in obj)
    if isinstance(obj, dict):
        if hasattr(obj_type, "default_factory"):
            result = obj_type(obj.default_factory)
            for k, v in obj.items():
                result[_astuple_inner(k, tuple_factory)] = _astuple_inner(v, tuple_factory)
            return result
        return obj_type(
            (_astuple_inner(k, tuple_factory), _astuple_inner(v, tuple_factory))
            for k, v in obj.items()
        )
    return copy.deepcopy(obj)


def asdict(obj: Any, *, dict_factory: Callable[..., Any] = dict) -> Any:
    r"""Return the fields of a ``@c_class`` / ``@py_class`` instance as a new dict.

    Mirrors :func:`dataclasses.asdict` from the standard library.  The
    function recurses into nested FFI dataclass instances, FFI containers
    (:class:`~tvm_ffi.Array`, :class:`~tvm_ffi.List`,
    :class:`~tvm_ffi.Map`, :class:`~tvm_ffi.Dict`), and the built-in
    ``list`` / ``tuple`` / ``dict``.  FFI sequence containers are
    converted to Python ``list``\ s and FFI mapping containers to
    Python ``dict``\ s so the result is plain Python data, e.g. for
    JSON serialisation.  Any other value is copied with
    :func:`copy.deepcopy`.

    Parameters
    ----------
    obj
        A ``@c_class`` / ``@py_class`` instance.  Passing a type raises
        :class:`TypeError`.
    dict_factory
        Callable used to construct the outer mapping and any nested
        mapping recursed from an FFI dataclass.  Defaults to :class:`dict`.

    Raises
    ------
    TypeError
        If ``obj`` is not a ``@c_class`` / ``@py_class`` instance.

    """
    if not _is_ffi_dataclass_instance(obj):
        raise TypeError("asdict() should be called on c_class / py_class instances")
    return _asdict_inner(obj, dict_factory)


def astuple(obj: Any, *, tuple_factory: Callable[..., Any] = tuple) -> Any:
    """Return the fields of a ``@c_class`` / ``@py_class`` instance as a new tuple.

    Mirrors :func:`dataclasses.astuple` from the standard library.  The
    function recurses into nested FFI dataclass instances, FFI containers
    (:class:`~tvm_ffi.Array`, :class:`~tvm_ffi.List`,
    :class:`~tvm_ffi.Map`, :class:`~tvm_ffi.Dict`), and the built-in
    ``list`` / ``tuple`` / ``dict``.  Any other value is copied with
    :func:`copy.deepcopy`.

    Parameters
    ----------
    obj
        A ``@c_class`` / ``@py_class`` instance.  Passing a type raises
        :class:`TypeError`.
    tuple_factory
        Callable used to construct the outer tuple and any nested tuple
        recursed from an FFI dataclass.  Defaults to :class:`tuple`.

    Raises
    ------
    TypeError
        If ``obj`` is not a ``@c_class`` / ``@py_class`` instance.

    """
    if not _is_ffi_dataclass_instance(obj):
        raise TypeError("astuple() should be called on c_class / py_class instances")
    return _astuple_inner(obj, tuple_factory)
