# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell.  If not, see <http://www.gnu.org/licenses/>.


from __future__ import annotations

from collections.abc import Generator, Iterable
from os import PathLike, fspath
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union, cast, overload

from .. import ifcopenshell_wrapper, open
from .. import entity_instance
from .. import file
from . import has_occ

if TYPE_CHECKING:
    from OCC.Core import TopoDS  # pyright: ignore[reportMissingImports]  # ty:ignore[unresolved-import]

    IteratorOutput = Union["ShapeElementType", "utils.shape_tuple"]

T = TypeVar("T")
ShapeElementType = Union[
    ifcopenshell_wrapper.BRepElement, ifcopenshell_wrapper.TriangulationElement, ifcopenshell_wrapper.SerializedElement
]
ShapeType = Union[ifcopenshell_wrapper.BRep, ifcopenshell_wrapper.Triangulation, ifcopenshell_wrapper.Serialization]


def wrap_shape_creation(settings, shape):
    return shape


if has_occ:
    from . import occ_utils as utils

    try:
        from OCC.Core import TopoDS  # pyright: ignore[reportMissingImports]  # ty:ignore[unresolved-import]
    except ImportError:
        from OCC import TopoDS  # pyright: ignore[reportMissingImports]  # ty:ignore[unresolved-import]

    def wrap_shape_creation(settings: settings, shape: ifcopenshell_wrapper.Element):
        if getattr(settings, "use_python_opencascade", False):
            return utils.create_shape_from_serialization(shape)
        else:
            return shape


SETTING = Literal[
    "angle-unit",
    "apply-default-materials",
    "apply-offset",
    "boolean-attempt-2d",
    "building-local-placement",
    "cache-shapes",
    "cgal-original-edges",
    "cgal-smooth-angle-degrees",
    "circle-segments",
    "compute-curvature",
    "context-identifiers",
    "context-ids",
    "context-types",
    "convert-back-units",
    "debug",
    "defer-processing-first-element",
    "dimensionality",
    "disable-boolean-result",
    "disable-opening-subtractions",
    "edge-arrows",
    "element-hierarchy",
    "enable-layerset-slicing",
    "force-space-transparency",
    "function-step-param",
    "function-step-type",
    "generate-uvs",
    "iterator-output",
    "keep-bounding-boxes",
    "layerset-first",
    "length-unit",
    "make-volume",
    "max-offset-deviation",
    "max-offset",
    "mesher-angular-deflection",
    "mesher-linear-deflection",
    "model-offset",
    "model-rotation",
    "no-clean-triangulation",
    "no-normals",
    "no-parallel-mapping",
    "no-wire-intersection-check",
    "no-wire-intersection-tolerance",
    "permissive-shape-reuse",
    "precision-factor",
    "precision",
    "reorient-shells",
    "site-local-placement",
    "surface-colour",
    "triangulation-type",
    "unify-shapes",
    "use-material-names",
    "use-python-opencascade",
    "use-world-coords",
    "validate",
    "weld-vertices",
]
SERIALIZER_SETTING = Literal[
    "base-uri",
    "use-element-names",
    "use-element-guids",
    "use-element-step-ids",
    "use-element-types",
    "y-up",
    "ecef",
    "digits",
    "wkt-use-section",
    "separate-z-up-node",
]

# NOTE: hybrid-cgal-simple-opencascade is added just as an example
# It's possible to use any hybrid combination by the format below:
# "hybrid-library1-library2".
# List is updated from AbstractKernel.cpp.
GEOMETRY_LIBRARY = Literal["cgal", "cgal-simple", "opencascade", "hybrid-cgal-simple-opencascade"]


class missing_setting:
    def __repr__(self):
        return "-"


class settings_mixin:
    """
    Pythonic interface mixin to the settings modules and
    to provide an additional setting to enable pythonOCC
    when available
    """

    def __init__(self, **kwargs):
        super().__init__()
        for k, v in kwargs.items():
            self.set(getattr(self, k), v)

    def __repr__(self):
        def safe_get(x):
            try:
                return self.get(x)
            except RuntimeError:
                return missing_setting()

        fmt_pair = lambda x: "%s = %r" % (self.rname(x), safe_get(x))
        return "%s(%s)" % (type(self).__name__, ", ".join(map(fmt_pair, self.setting_names())))

    @staticmethod
    def name(k: str) -> Union[SETTING, SERIALIZER_SETTING]:
        return k.lower().replace("_", "-")

    @staticmethod
    def rname(k: Union[SETTING, SERIALIZER_SETTING]) -> str:
        return k.upper().replace("-", "_")

    @overload
    def set(self: settings, k: SETTING, v: Any) -> None: ...
    @overload
    def set(self: serializer_settings, k: SERIALIZER_SETTING, v: Any) -> None: ...
    def set(self, k: SETTING, v: Any) -> None:
        """
        Set value of the setting named `k` to `v`.

        :raises RuntimeError: If there is no setting with name `k`.
        """
        k = self.name(k)
        if isinstance(self, settings) and k == "use-python-opencascade":
            if not has_occ:
                raise AttributeError("Python OpenCASCADE is not installed")
            if v:
                self.set_("iterator-output", ifcopenshell_wrapper.SERIALIZED)
                self.set_("use-world-coords", True)
                self.use_python_opencascade = True
        else:
            self.set_(self.name(k), v)

    @overload
    def get(self: settings, k: SETTING) -> Any: ...
    @overload
    def get(self: serializer_settings, k: SERIALIZER_SETTING) -> Any: ...
    def get(self, k: str) -> Any:
        """
        Return value of the setting named `k`.

        :raises RuntimeError: If there is no setting with name `k`.
        """
        k = self.name(k)
        if isinstance(self, settings) and k == "use-python-opencascade":
            return self.use_python_opencascade
        return self.get_(k)

    @overload
    def setting_names(self: settings) -> tuple[SETTING, ...]: ...
    @overload
    def setting_names(self: serializer_settings) -> tuple[SERIALIZER_SETTING, ...]: ...
    def setting_names(self) -> tuple[str, ...]:
        setting_names = super().setting_names()
        if isinstance(self, settings):
            setting_names += ("use-python-opencascade",)
        return setting_names

    @overload
    def __getattr__(self: settings, k: str) -> SETTING: ...
    @overload
    def __getattr__(self: serializer_settings, k: str) -> SERIALIZER_SETTING: ...
    def __getattr__(self, k: str) -> str:
        # Swig wrapper will try to access "this",
        # ensure we won't accidentally call any c-extension methods
        # like .setting_names() until wrapper is not completely initialized.
        # See #4861.
        if k == "this":
            raise AttributeError("Swig wrapper's 'this' is unset.")
        if k in map(self.rname, self.setting_names()):
            return k
        else:
            raise AttributeError("'Settings' object has no attribute '%s'" % k)

    def build_parser(self, parser) -> None:
        """
        Accepts an argparse.ArgumentParser object, enumerates the settings in this container and
        adds argument parser rules for each.
        """
        type_factories = {
            "bool": bool,
            "int": int,
            "double": float,
            "std::string": str,
            "std::set<int>": lambda s: list(map(int, s.split(";"))),
            "std::set<std::string>": lambda s: s.split(";"),
            "std::vector<double>": lambda s: list(map(float, s.split(";"))),
            "IteratorOutputOptions": int,
            "FunctionStepMethod": int,
            "OutputDimensionalityTypes": int,
            "TriangulationMethod": int,
        }
        for nm in self.setting_names():
            if nm == "use-python-opencascade":
                ty == "bool"
            else:
                ty = self.get_type(nm)
            if ty == "bool":
                group = parser.add_mutually_exclusive_group()
                group.add_argument(
                    f"--{nm}",
                    dest=nm.replace("-", "_"),
                    action="store_true",
                )
                group.add_argument(
                    f"--no-{nm}",
                    dest=nm.replace("-", "_"),
                    action="store_false",
                )
                parser.set_defaults(**{nm.replace("-", "_"): None})
            else:
                parser.add_argument(f"--{nm}", dest=nm.replace("-", "_"), type=type_factories[ty])

    def apply_namespace(self, namespace) -> None:
        """
        Accepts an argparse.Namespace object, enumerates over the values in this namespace and
        writes them to the settings when available
        """
        names = set(self.setting_names())
        for k, v in namespace._get_kwargs():
            if k.replace("_", "-") in names and v is not None:
                self.set(k.replace("_", "-"), v)


class serializer_settings(settings_mixin, ifcopenshell_wrapper.SerializerSettings):
    pass


class settings(settings_mixin, ifcopenshell_wrapper.Settings):
    use_python_opencascade = False


class iterator(ifcopenshell_wrapper.Iterator):
    def __init__(
        self,
        settings: settings,
        file_or_filename: Union[file, str],
        num_threads: int = 1,
        include: Optional[Union[list[entity_instance], list[str]]] = None,
        exclude: Optional[Union[list[entity_instance], list[str]]] = None,
        geometry_library: GEOMETRY_LIBRARY = "opencascade",
    ):
        self.settings = settings
        if isinstance(file_or_filename, file):
            self.file = file
            file_or_filename = file_or_filename
        else:
            file_or_filename = self.file = open(file_or_filename)

        if include is not None and exclude is not None:
            raise ValueError("include and exclude cannot be specified simultaneously")

        if include is not None or exclude is not None:
            # Couldn't get the typemaps properly applied using %extend so we
            # replicate the SWIG-generated __init__ call on the output of a
            # free function.
            # @todo verify this works with SWIG 4

            include_or_exclude = include if exclude is None else exclude
            include_or_exclude_type = set(x.__class__.__name__ for x in include_or_exclude)

            if include_or_exclude_type == {"entity_instance"}:
                include_or_exclude = cast(set[entity_instance], include_or_exclude)

                if not all((last_inst := inst).is_a("IfcProduct") for inst in include_or_exclude):
                    raise ValueError(
                        f"include and exclude need to be an aggregate of IfcProduct. Violating element: '{last_inst}'."
                    )

                initializer = ifcopenshell_wrapper.construct_iterator_with_include_exclude_id

                include_or_exclude = [i.id() for i in include_or_exclude]
            else:
                initializer = ifcopenshell_wrapper.construct_iterator_with_include_exclude

            self.this = initializer(
                geometry_library, self.settings, file_or_filename, include_or_exclude, include is not None, num_threads
            )
        else:
            self.this = ifcopenshell_wrapper.construct_iterator(
                geometry_library, self.settings, file_or_filename, num_threads
            )

    if has_occ:

        def get(self):
            return wrap_shape_creation(self.settings, ifcopenshell_wrapper.Iterator.get(self))

    def __iter__(self) -> Generator[IteratorOutput, None, None]:
        if self.initialize():
            while True:
                yield self.get()
                if not self.next():
                    break

    def get_task_products(self):
        return entity_instance.wrap_value(ifcopenshell_wrapper.Iterator.get_task_products(self), self.file)


ClashType = Literal["protrusion", "pierce", "collision", "clearance"]
CLASH_TYPE_ITEMS = ("protrusion", "pierce", "collision", "clearance")


class tree(ifcopenshell_wrapper.tree):
    def __init__(self, file: Optional[file] = None, settings: Optional[settings] = None):
        args = [self]
        if file is not None:
            args.append(file)
            if settings is not None:
                args.append(settings)
        ifcopenshell_wrapper.tree.__init__(*args)

    def add_file(self, file: file, settings: settings) -> None:
        ifcopenshell_wrapper.tree.add_file(self, file, settings)

    def add_iterator(self, iterator: iterator) -> None:
        ifcopenshell_wrapper.tree.add_file(self, iterator)

    def select(
        self,
        value: Union[entity_instance, ifcopenshell_wrapper.BRepElement, tuple[float, float, float]],
        **kwargs,
    ) -> list[entity_instance]:
        def unwrap(value):
            if isinstance(value, entity_instance):
                return value
            elif all(map(lambda v: hasattr(value, v), "XYZ")):
                return value.X(), value.Y(), value.Z()
            return value

        args = [self, unwrap(value)]
        if isinstance(value, (entity_instance, ifcopenshell_wrapper.BRepElement)):
            args.append(kwargs.get("completely_within", False))
            if "extend" in kwargs:
                args.append(kwargs["extend"])
        elif isinstance(value, (list, tuple)) and len(value) == 3 and set(map(type, value)) == {float}:
            if "extend" in kwargs:
                args.append(kwargs["extend"])
        return ifcopenshell_wrapper.tree.select(*args)

    def select_box(self, value, **kwargs) -> list[entity_instance]:
        def unwrap(value):
            if isinstance(value, entity_instance):
                return value
            elif hasattr(value, "Get"):
                return value.Get()[:3], value.Get()[3:]
            return value

        args = [self, unwrap(value)]
        if "extend" in kwargs or "completely_within" in kwargs:
            args.append(kwargs.get("completely_within", False))
        if "extend" in kwargs:
            args.append(kwargs.get("extend", -1.0e-5))
        return ifcopenshell_wrapper.tree.select_box(*args)

    def clash_intersection_many(
        self,
        set_a: Iterable[entity_instance],
        set_b: Iterable[entity_instance],
        tolerance: float = 0.002,
        check_all: bool = True,
    ) -> tuple[ifcopenshell_wrapper.clash, ...]:
        args = [self, set_a, set_b, tolerance, check_all]
        return ifcopenshell_wrapper.tree.clash_intersection_many(*args)

    def clash_collision_many(
        self, set_a: Iterable[entity_instance], set_b: Iterable[entity_instance], allow_touching=False
    ) -> tuple[ifcopenshell_wrapper.clash, ...]:
        args = [self, set_a, set_b, allow_touching]
        return ifcopenshell_wrapper.tree.clash_collision_many(*args)

    def clash_clearance_many(
        self,
        set_a: Iterable[entity_instance],
        set_b: Iterable[entity_instance],
        clearance: float = 0.05,
        check_all: bool = False,
    ) -> tuple[ifcopenshell_wrapper.clash, ...]:
        args = [self, set_a, set_b, clearance, check_all]
        return ifcopenshell_wrapper.tree.clash_clearance_many(*args)

    @staticmethod
    def get_clash_type(clash_type_i: int) -> ClashType:
        """Convert clash type index to a readable string format.

        :param clash_type_i: Type index that comes from ``clash.clash_type``.
        """
        return CLASH_TYPE_ITEMS[clash_type_i]


def create_shape(
    settings: settings,
    inst: entity_instance,
    repr: Optional[entity_instance] = None,
    geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Union[ShapeType, ShapeElementType, ifcopenshell_wrapper.Transformation, utils.shape_tuple, TopoDS.TopoDS_Shape]:
    """
    Returns a geometric interpretation of the IFC entity instance

    Note that in Python, you must store a reference to the element returned by this function to prevent garbage
    collection when you access its children. See #1124.

    :raises RuntimeError: If failed to process shape. You can turn detailed logging to get more details.

    :return:
        - `inst` is IfcProduct and `repr` provided / None -> ShapeElementType\n
        - `inst` is IfcRepresentation and `repr` is None -> ShapeType\n
        - `inst` is IfcRepresentationItem and `repr` is None -> ShapeType\n
        - `inst` is IfcProfileDef and `repr` is None -> ShapeType\n
        - `inst` is IfcPlacement / IfcObjectPlacement -> Transformation\n
        - `inst` is IfcTypeProduct and `repr` is None -> None\n
        - `inst` is IfcTypeProduct and `repr` is provided -> RuntimeError
        (for IfcTypeProducts provide just IfcRepresentation as `inst`).\n

        If 'use-python-opencascade' is enabled in settings then\n
        - instead of ShapeElementType it returns shape_tuple, \n
        - instead of ShapeType it returns TopoDS.TopoDS_Shape.

    Example:

    .. code:: python

        settings = ifcopenshell.geom.settings()
        settings.set("use-python-opencascade", True)

        ifc_file = ifcopenshell.open(file_path)
        products = ifc_file.by_type("IfcProduct")

        for i, product in enumerate(products):
            if product.Representation is not None:
                try:
                    created_shape = geom.create_shape(settings, inst=product)
                    shape = created_shape.geometry # see #1124
                    shape_gpXYZ = shape.Location().Transformation().TranslationPart() # These are methods of the TopoDS_Shape class from pythonOCC
                    print(shape_gpXYZ.X(), shape_gpXYZ.Y(), shape_gpXYZ.Z()) # These are methods of the gpXYZ class from pythonOCC
                except:
                    print("Shape creation failed")
    """
    return wrap_shape_creation(
        settings,
        (
            ifcopenshell_wrapper.create_shape(settings, inst, repr, geometry_library)
            if repr
            else ifcopenshell_wrapper.create_shape(settings, inst, geometry_library)
        ),
    )


def map_shape(settings: settings, inst: entity_instance) -> ifcopenshell_wrapper.item:
    """
    Returns an interpretation of the geometry encoded as per IfcOpenShell's taxonomy layer.
    In many cases this is somewhat equivalent to the raw IFC data (but schema-agnostic in C++), but
    in other cases such as IfcParameterizedProfileDef the returned item is the equivalent
    of an explicit composite curve.

    >>> point = ifc_file.by_type('IfcCartesianPoint')[0]
    >>> ifcopenshell.geom.map_shape(ifcopenshell.geom.settings(), point).components
    (0.0, 0.0, 0.0)
    """
    return ifcopenshell_wrapper.map_shape(settings, inst)


@overload
def consume_iterator(it: iterator, with_progress: Literal[False] = False) -> Generator[IteratorOutput, None, None]: ...
@overload
def consume_iterator(
    it: iterator, with_progress: Literal[True]
) -> Generator[tuple[int, IteratorOutput], None, None]: ...
@overload
def consume_iterator(
    it: iterator, with_progress: bool
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: ...
def consume_iterator(
    it: iterator, with_progress: bool = False
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]:
    if it.initialize():
        while True:
            if with_progress:
                yield it.progress(), it.get()
            else:
                yield it.get()
            if not it.next():
                break


# Overloads need to cover different return types
# based on `with_progress` argument.
@overload
def iterate(
    settings: settings,
    file_or_filename: Union[file, str],
    num_threads: int = 1,
    include: Optional[Union[list[entity_instance], list[str]]] = None,
    exclude: Optional[Union[list[entity_instance], list[str]]] = None,
    *,
    with_progress: Literal[False] = False,
    geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[IteratorOutput, None, None]: ...
@overload
def iterate(
    settings: settings,
    file_or_filename: Union[file, str],
    num_threads: int = 1,
    include: Optional[Union[list[entity_instance], list[str]]] = None,
    exclude: Optional[Union[list[entity_instance], list[str]]] = None,
    *,
    with_progress: Literal[True] = True,
    geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[tuple[int, IteratorOutput], None, None]: ...
@overload
def iterate(
    settings: settings,
    file_or_filename: Union[file, str],
    num_threads: int = 1,
    include: Optional[Union[list[entity_instance], list[str]]] = None,
    exclude: Optional[Union[list[entity_instance], list[str]]] = None,
    *,
    with_progress: bool = False,
    geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: ...
def iterate(
    settings: settings,
    file_or_filename: Union[file, str],
    num_threads: int = 1,
    include: Optional[Union[list[entity_instance], list[str]]] = None,
    exclude: Optional[Union[list[entity_instance], list[str]]] = None,
    *,
    with_progress: bool = False,
    geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]:
    """Get a geometry iterator for the provided file."""
    it = iterator(settings, file_or_filename, num_threads, include, exclude, geometry_library)
    yield from consume_iterator(it, with_progress=with_progress)


def make_shape_function(fn):
    if has_occ:

        def _(schema, string_or_shape, *args):
            if isinstance(string_or_shape, TopoDS.TopoDS_Shape):
                string_or_shape = utils.serialize_shape(string_or_shape)
            return fn(schema, string_or_shape, *args)

    else:

        def _(schema, string, *args):
            return fn(schema, string, *args)

    return _


# @todo, plug-ins now
# serialise = make_shape_function(ifcopenshell_wrapper.serialise)
# tesselate = make_shape_function(ifcopenshell_wrapper.tesselate)


class _serializer_factory:
    _stream_serializers = {"obj", "svg", "ttl"}

    def __init__(self, name: str, extension: str):
        self.name = name
        self.extension = extension
        self.__name__ = name

    def __call__(self, out_filename: Union[str, PathLike[str]], *args: Any) -> ifcopenshell_wrapper.GeometrySerializer:
        if self.name == "obj" and len(args) == 3:
            output_filename = args[0]
            output_temp_filename = out_filename
            geometry_settings, serializer_settings = args[1], args[2]
        elif len(args) == 2:
            output_filename = out_filename
            output_temp_filename = out_filename
            geometry_settings, serializer_settings = args
        else:
            obj_signature = " or (out_filename, mtl_filename, geometry_settings, serializer_settings)"
            raise TypeError(
                f"serializers.{self.name}() expects (out_filename, geometry_settings, serializer_settings)"
                + (obj_signature if self.name == "obj" else "")
            )

        if self._is_buffer(output_filename) or self._is_buffer(output_temp_filename):
            if self.name not in self._stream_serializers:
                raise TypeError(f"serializers.{self.name}() requires a filesystem path")
            output_filename = self._buffer(output_filename)
            output_temp_filename = self._buffer(output_temp_filename)
        else:
            output_filename = self._path(output_filename)
            output_temp_filename = self._path(output_temp_filename)

        return ifcopenshell_wrapper.create_geometry_serializer(
            self.extension, output_filename, output_temp_filename, geometry_settings, serializer_settings
        )

    def _is_buffer(self, value: Any) -> bool:
        return isinstance(value, ifcopenshell_wrapper.buffer)

    def _buffer(self, value: Union[str, PathLike[str], ifcopenshell_wrapper.buffer]) -> ifcopenshell_wrapper.buffer:
        if self._is_buffer(value):
            return value
        return ifcopenshell_wrapper.buffer(self._path(value))

    def _path(self, value: Union[str, PathLike[str]]) -> str:
        try:
            path = fspath(value)
        except TypeError:
            raise TypeError(f"serializers.{self.name}() requires a filesystem path") from None
        if not isinstance(path, str):
            raise TypeError(f"serializers.{self.name}() requires a text filesystem path")
        return path


class _serializers_meta(type):
    _extensions = {
        "obj": "obj",
        "svg": "svg",
        "ttl": "ttl",
        "gltf": "glb",
        "glb": "glb",
        "collada": "dae",
        "dae": "dae",
        "stp": "stp",
        "step": "stp",
        "igs": "igs",
        "iges": "igs",
        "usd": "usd",
        "usda": "usd",
        "usdc": "usd",
    }

    def __getattr__(cls, name: str) -> _serializer_factory:
        extension = cls._extensions.get(name)
        if extension is None:
            raise AttributeError(f"type object 'serializers' has no attribute '{name}'")
        factory = _serializer_factory(name, extension)
        setattr(cls, name, factory)
        return factory

    def __dir__(cls) -> list[str]:
        return sorted(set(super().__dir__()) | set(cls._extensions))


class serializers(metaclass=_serializers_meta):
    buffer = ifcopenshell_wrapper.buffer

    @classmethod
    def guess_from_extension(cls, filepath: str):
        ext = filepath.rsplit(".", 1)[-1].lower()
        mapping = {
            "glb": "gltf",
            "obj": "obj",
            "svg": "svg",
            "ttl": "ttl",
            "dae": "collada",
            "stp": "stp",
            "step": "step",
            "igs": "igs",
            "iges": "iges",
            "usd": "usd",
            "usda": "usda",
            "usdc": "usdc",
        }
        serializer_name = mapping.get(ext)
        if not serializer_name:
            raise ValueError(f"No serializer available for .{ext} file")
        return getattr(cls, serializer_name)
