# Copyright (C) 2020-2022 HERE Global B.V. and its affiliate(s).
# All rights reserved.
#
# This software and other materials contain proprietary information
# controlled by HERE and are protected by applicable copyright legislation.
# Any use and utilization of this software and other materials and
# disclosure to any third parties is conditional upon having a separate
# agreement with HERE for the access, use, utilization or disclosure of this
# software. In the absence of such agreement, the use of the software is not
# allowed.

"""HERE platform, content adaptation, encoding and decoding."""
import dataclasses
from abc import ABC, abstractmethod
from datetime import datetime
from typing import (
    TYPE_CHECKING,
    Callable,
    Dict,
    Iterable,
    Iterator,
    NewType,
    Optional,
    Tuple,
    Union,
)

import geojson
from geojson import LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon
from geojson.geometry import Geometry
from here.platform.exceptions import (
    UnsupportedContentTypeDecodeException,
    UnsupportedContentTypeEncodeException,
)
from here.platform.partition import (
    IndexPartition,
    StreamPartition,
    VersionedPartition,
    VolatilePartition,
)
from here.platform.schema import Schema

if TYPE_CHECKING:
    from here.platform.layer import StreamLayer, VersionedLayer, VolatileLayer


class Encoder(ABC):
    """A content encoder and its capabilities."""

    @property
    @abstractmethod
    def supported_content_types(self) -> Dict[str, Union[type, Tuple[type, ...]]]:
        """
        :return: the dictionary of MIME content types supported when encoding single blobs  # noqa
        with the ``encode_blob`` function of this encoder, each with the type of the encoded data.
        """

    @abstractmethod
    def encode_blob(
        self, data, content_type: str, schema: Optional[Schema] = None, **kwargs
    ) -> bytes:
        """Encode one single blob of data.

        :param data: the data to be encoded, its type corresponds to the type declared in
          the property ``supported_content_types`` for the content type
        :param content_type: the MIME content type to be encoded
        :param schema: the schema, if the content type requires one
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: the encoded data  # noqa
        :raises UnsupportedContentTypeEncodeException: in case the content type is not encodable
        :raises ValueError: if the schema is mandatory for the content type but missing
        :raises EncodeException: in case the blob can't be properly encoded  # noqa
        :raises SchemaException: in case the schema can't be used to encode the content  # noqa
        """

    def _verify_encode_blob(self, data, content_type: str, schema: Optional[Schema]):
        if data is None:
            raise ValueError("Data is not set")
        if not content_type:
            raise ValueError("Content type is not set")
        if content_type not in self.supported_content_types:
            raise UnsupportedContentTypeEncodeException(content_type=content_type)
        if schema is not None and content_type not in schema.supported_content_types:
            raise ValueError(f"Schema does not support content type {content_type}")


class Decoder(ABC):
    """A content decoder and its capabilities."""

    @property
    @abstractmethod
    def supported_content_types(self) -> Dict[str, Union[type, Tuple[type, ...]]]:
        """
        :return: the dictionary of MIME content types supported when decoding single blobs  # noqa
        with the ``decode_blob`` function of this decoder, each with the type of the decoded data.
        """

    @abstractmethod
    def decode_blob(
        self, data: bytes, content_type: str, schema: Optional[Schema] = None, **kwargs
    ):
        """Decode one single blob of data.

        :param data: the encoded data
        :param content_type: the MIME content type to be decoded
        :param schema: the schema, if the content type requires one
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: the decoded blob, its type corresponds to the type declared in
          the property ``supported_content_types`` for the content type  # noqa
        :raises UnsupportedContentTypeDecodeException: in case the content type is not decodable
        :raises ValueError: if the schema is mandatory for the content type but missing
        :raises DecodeException: in case the blob can't be properly decoded  # noqa
        :raises SchemaException: in case the schema can't be used to decode the content  # noqa
        """

    def _verify_decode_blob(self, data: bytes, content_type: str, schema: Optional[Schema]):
        if not data:
            raise ValueError("Data is not set or empty")
        if not content_type:
            raise ValueError("Content type is not set")
        if content_type not in self.supported_content_types:
            raise UnsupportedContentTypeDecodeException(content_type=content_type)
        if schema is not None and content_type not in schema.supported_content_types:
            raise ValueError(f"Schema does not support content type {content_type}")

    def _verify_and_return_decoded(self, decoded, content_type: str):
        assert isinstance(decoded, self.supported_content_types[content_type])
        return decoded


# Some types supported by the ContentAdapter interface.
# These model single values of well-known types with additional, clear semantic.
# These types can be used in data classes and in composite types below.

Partition = NewType("Partition", str)
Identifier = NewType("Identifier", str)
DecodedMessage = NewType("DecodedMessage", dict)


# Specialized types and helper functions to instantiate them


@dataclasses.dataclass(frozen=True)
class Ref:
    """A reference to an object in a partition."""

    partition: Partition
    identifier: Identifier


@dataclasses.dataclass(frozen=True)
class Range:
    """A [0, 1] range."""

    start: float
    end: float


def make_ref(partition: Union[str, int, Partition], identifier: Union[str, Identifier]) -> Ref:
    """
    Construct a new Reference given partition and identifier.

    This supports a variety of parameter types and performs basics checks.

    :param partition: the partition name
    :param identifier: the object or feature identifier
    :return: a reference
    :raises ValueError: in case of invalid or empty parameters
    """
    if not partition:
        raise ValueError("Partition parameter not set")
    if not identifier:
        raise ValueError("Identifier parameter not set")
    return Ref(Partition(str(partition)), Identifier(identifier))


# TODO (OLPSUP-18956) remove this method once admin place category data are referenced correctly
def make_cat_ref(partition: Union[str, int, Partition], identifier: Union[str, Identifier]) -> Ref:
    """
    Construct a new Category Reference given identifier and/or partition.

    This supports a variety of parameter types and performs basics checks.

    :param partition: the partition name
    :param identifier: the object or feature identifier
    :return: a reference
    :raises ValueError: in case of invalid or empty parameters
    """
    if not identifier:
        raise ValueError("Identifier parameter not set")
    return Ref(Partition(str(partition)), Identifier(identifier))


def make_range(
    start: Union[int, float, None],
    end: Union[int, float, None],
) -> Range:
    """
    Construct a new Range given start and end offsets.

    This supports a variety of parameter types and performs basics checks.

    :param start: the start offset, when available and different from 0.0
    :param end: the end offset, when available and different from 1.0.
    :return: a range
    :raises ValueError: in case of parameters out of range
    """
    start_offset = float(start or 0)
    end_offset = float(end or 1)
    if not (0 <= start_offset <= 1):
        raise ValueError(f"Start offset {start} must be in [0, 1] range")
    if not (0 <= end_offset <= 1):
        raise ValueError(f"End offset {end} must be in [0, 1] range")
    # Disabled because HMC violates this condition in some special cases
    # if start_offset > end_offset:
    #     raise ValueError(f"Start offset {start} greater than end offset {end}")
    return Range(start_offset, end_offset)


# Content is described by Python data classes. Each field of a data class must
# have one of the supported types. Type are not only used as hints but affect
# the behavior of the software at runtime. Data classes are used to specify
# the fields on the objects passed to ``from_objects``.
#
# The hints describe the data classes provided to ``from_objects``,
# they don't describe the output format. The output format is adapter-specific,
# although based on the actual type of the field and its type hint.
# Adapter implementations use type information to better represent each value,
# handle partition and object id, validate the data and convert non-basic types
# like ranges and geometries to the most appropriate format for the output.
# It is important that partition and object identifiers are marked
# with the corresponding type hint to enable automatic indexing.

supported_single_value_types = [
    bool,
    int,
    float,
    str,
    datetime,
    DecodedMessage,
    Partition,
    Identifier,
    Ref,
    Range,
    Geometry,
    Point,
    MultiPoint,
    LineString,
    MultiLineString,
    Polygon,
    MultiPolygon,
]


# The data class that specifies the content, as mentioned, should have fields of one
# of the types mentioned above. In addition, composite types are supported,
# although with some constraints:
#
# - Optional[T], of a supported type T
# - Dict[str, T], key must be str, elements must be of a supported type T
# - List[T],  elements must be of a supported type T
# - a data class with supported fields
#
# These and other constrains are checked at runtime and in tests.


class ContentAdapter(ABC):
    """
    Interface of the content adapters, an adapter specialized to
    work with content via the `here-content` package.
    """

    @abstractmethod
    def from_tabular(self, columns, data, geometry_column="geometry"):
        """
        Convert the given tabular data.

        :param columns: column names
        :param data: tabular data
        :param geometry_column: geometry column string
        :return: data representation is function of the adapter  # noqa
        """

    @abstractmethod
    def from_objects(
        self,
        fields: type,
        data: Iterator[object],
        single_element: bool = False,
        index_partition: Union[None, str, Callable[[object], Partition]] = None,
        index_id: Union[None, str, Callable[[object], Identifier]] = None,
        index_ref: Union[None, str, Callable[[object], Union[Ref, Iterable[Ref]]]] = None,
    ):
        """
        Adapt content form an object representation to the target format.

        It can optionally perform indexing of objects, based on their partition,
        identifier and set of references to other objects. Indexing is specified by
        naming the field of the object that contains the value to index, or by
        passing a function that calculates that value from the object.

        :param fields: the fields to extract, as specified by a dataclass. Field names are looked
            up among the attributes of each object via ``getattr`. When missing, ``None`` or
            equivalent is used. Each field has a type that describes its semantic:
            it is used to adapt the value to the most appropriate representation for
            the output format. ``TypeError`` is raised in case this is not possible.
        :param data: the objects to adapt to the target format. Fields not mentioned in ``fields``
            are discarded. Expected but missing fields and identifiers are considered ``None``.
            Field values may be of any type compatible with the type declared for the field.
            Partition ids don't have to be unique, but they have to be contiguous: all the objects
            with a given partition identifier must be returned in sequence. Object identifiers,
            when present, must be unique across the whole content.
        :param single_element: the data contains exactly one element, the content adapter
            case use this information to optimize or return a specialized representation
        :param index_partition: index the content by partition
        :param index_id: index the content by object identifier
        :param index_ref: index the content by references. Each object can contain zero,
            one or more references, and references can be shared among multiple objects.
        :return: an adapter-specific representation of the structured content.
        :raises:
            ValueError: if the fields are not described by a dataclass
            KeyError: in case partition id, object id or reference is needed but not present
            TypeError: in case partition or object id is not of type int or string.
            Also raised in case field values are not of the type declared for the field,
            or if they can't be converted to it.
        """

    # The following are support functions shared across implementations

    @staticmethod
    def _validate_params(
        single_element: bool,
        index_partition: Union[None, str, Callable[[object], Partition]],
        index_id: Union[None, str, Callable[[object], Identifier]],
        index_ref: Union[None, str, Callable[[object], Union[Ref, Iterable[Ref]]]],
    ):
        if single_element and index_ref:
            raise ValueError(
                "Single element and indexing by references "
                "to other objects are mutually-exclusive"
            )
        if index_ref and (index_partition or index_id):
            raise ValueError(
                "Indexing by partition or object identifier and indexing by "
                "references to other objects are mutually-exclusive"
            )

    @staticmethod
    def _validate_fields(fields: type):
        # This check validates the constraints on the data class (fields)
        # as described by the comment block where `supported_single_value_types`
        # and the types are defined, in `adapter.py`.
        # This is a sanity check, it may be too strict or too loose sometimes.
        # As runtime type checking with Python is complex, especially when dealing
        # with generics, if this fails and can't be fixed it's always possible
        # to skip the check. An alternative would be relying on some library like
        # https://github.com/ilevkivskyi/typing_inspect
        # although, at the time of writing, this library seems to be still in its early stages
        # More standard (generic) type checks are available only in Python 3.9+
        def validate_type(name, t):
            if t in supported_single_value_types:
                return
            if dataclasses.is_dataclass(t):
                for f in dataclasses.fields(t):
                    validate_type(name + "." + f.name, f.type)
                return
            if getattr(t, "__origin__", None) is Union:
                args = getattr(t, "__args__")
                if len(args) == 2:
                    (tt, tt2) = args
                    if tt2 is not type(None):  # noqa: E721
                        raise TypeError("Unions are not supported")
                    validate_type(name + "[T]", tt)
                    return
            if getattr(t, "__origin__", None) is list:
                (tt,) = getattr(t, "__args__")
                validate_type(name + "[T]", tt)
                return
            if getattr(t, "__origin__", None) is dict:
                (kt, vt) = getattr(t, "__args__")
                if kt is not str:
                    raise TypeError(f"Field {name} has key type {t}, when only str is supported")
                validate_type(name + "[_, V]", vt)
                return
            raise TypeError(f"Field {name} of unsupported type {t}")

        if not dataclasses.is_dataclass(fields):
            raise TypeError("Fields must be specified using a dataclass")
        for field in dataclasses.fields(fields):
            validate_type(field.name, field.type)

    @staticmethod
    def _validate_indices(
        fields: type,
        index_partition: Union[None, str, Callable[[object], Partition]],
        index_id: Union[None, str, Callable[[object], Identifier]],
    ):
        field_names = [f.name for f in dataclasses.fields(fields)]
        if isinstance(index_partition, str) and index_partition not in field_names:
            raise ValueError(
                f"Cannot index by {index_partition} as {fields} doesn't have this field"
            )
        if isinstance(index_id, str) and index_id not in field_names:
            raise ValueError(f"Cannot index by {index_id} as {fields} doesn't have this field")

    @staticmethod
    def _validate_value(field, value):
        # if not isinstance(value, field.type):
        # raise ValueError(
        #   f"Value of field '{field.name}' of unsupported type {type(value)}")
        pass

    @staticmethod
    def _validate_object(fields: type, x: object) -> object:
        if not isinstance(x, fields):
            raise TypeError(f"Expected {x} to be of type {fields}, instead of {type(x)}")
        for field in dataclasses.fields(fields):
            ContentAdapter._validate_value(field, getattr(x, field.name, None))
        return x

    @staticmethod
    def _extract_partition(
        index_partition: Union[str, Callable[[object], Partition]], x: object
    ) -> Partition:
        assert index_partition
        pid = (
            getattr(x, index_partition) if isinstance(index_partition, str) else index_partition(x)
        )
        if not pid:
            raise KeyError(f"Missing partition in object {x}")
        if not isinstance(pid, str):
            raise ValueError(f"Partition identifier {pid} is not a string")
        # isinstance can't be used if Partition is NewType
        # if not isinstance(pid, Partition):
        # raise TypeError(
        #     f"{partition_field} of unsupported type {type(pid)} in object {x}"
        # )
        return Partition(pid)

    @staticmethod
    def _extract_identifier(
        index_id: Union[str, Callable[[object], Identifier]], x: object
    ) -> Identifier:
        assert index_id
        oid = getattr(x, index_id) if isinstance(index_id, str) else index_id(x)
        if not oid:
            raise KeyError(f"Missing identifier in object {x}")
        if not isinstance(oid, str):
            raise ValueError(f"Object identifier {oid} is not a string")
        # isinstance can't be used if Identifier is NewType
        # if not isinstance(oid, Identifier):
        # raise TypeError(
        #     f"{identifier_field} of unsupported type {type(oid)} in object {x}"
        # )
        return Identifier(oid)

    @staticmethod
    def _extract_refs(
        index_ref: Union[str, Callable[[object], Union[Ref, Iterable[Ref]]]], x: object
    ) -> Iterable[Ref]:
        assert index_ref
        refs: Union[Ref, Iterable[Ref]] = (
            getattr(x, index_ref) if isinstance(index_ref, str) else index_ref(x)
        )
        # We support both single-reference and multi-references
        return [refs] if isinstance(refs, Ref) else refs


class Adapter(ABC):
    """
    An adapter controls the encoding and decoding process of platform data.
    It transforms data from and to adapter-specific data structure and supports
    reading, writing, encoding and decoding a variety of MIME content types.

    For the list of MIME content types supported when reading and writing
    a layer with ``read_*`` and ``write_*`` functions of the :class:`Layer`
    and its subclasses, please see documentation of specific adapters,
    and the respective :class:`Decoder` and :class:`Encoder` class documentation.

    All the operations involving content passes through an adapter when
    the parameters ``encode`` or ``decode`` are ``True``, their default value.
    These are parameters of the ``read_*`` and ``write_*`` functions.
    If a content type is not supported, or if reading or writing raw content is preferred,
    pass ``False`` to skip encoding or decoding and deal with raw bytes instead.

    If no adapter is specified, a default one is provided that is generic
    enough to encode and decode a common content types from and to
    standard Python classes, such as lists and dictionaries.

    More sophisticated adapters, subclasses of ``Adapter``, are provided in separate packages.
    """

    @property
    @abstractmethod
    def encoder(self) -> Encoder:
        """The encoder associated with the adapter."""

    @property
    @abstractmethod
    def decoder(self) -> Decoder:
        """The decoder associated with the adapter."""

    @property
    @abstractmethod
    def content_adapter(self) -> ContentAdapter:
        """The adapter specialized for content."""

    @abstractmethod
    def from_versioned_metadata(self, partitions: Iterator[VersionedPartition], **kwargs):
        """Adapt versioned partition metadata to the target format.

        :param partitions: sequence of partition metadata from a versioned layer
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: adapter-specific, please consult the documentation
          of the specific adapter to for the formats and types it supports  # noqa
        """

    @abstractmethod
    def from_versioned_data(
        self,
        partitions_data: Iterator[Tuple[VersionedPartition, bytes]],
        content_type: str,
        schema: Optional[Schema],
        **kwargs,
    ):
        """Adapt versioned partition metadata and data to the target format.

        :param partitions_data: sequence of partition metadata and data from a versioned layer
        :param content_type: the MIME content type of the layer
        :param schema: optional :class:`Schema` of the layer
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: adapter-specific, please consult the documentation
          of the specific adapter to for the formats and types it supports  # noqa
        """

    @abstractmethod
    def to_versioned_metadata(
        self, layer: "VersionedLayer", partitions_update, partitions_delete, **kwargs
    ) -> Tuple[Iterator[VersionedPartition], Iterator[Union[str, int]]]:
        """Adapt what update and delete from the target format to versioned partition metadata.

        :param layer: the layer all the metadata and data belong to
        :param partitions_update: adapter-specific, the partitions metadata to update, if any
        :param partitions_delete: adapter-specific, the partitions metadata to delete, if any
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: `Iterator` with the :class:`VersionedPartition` that are adapted
        """

    @abstractmethod
    def to_versioned_data(
        self,
        layer: "VersionedLayer",
        data,
        content_type: str,
        schema: Optional[Schema],
        **kwargs,
    ) -> Iterator[Tuple[Union[str, int], bytes]]:
        """Adapt data from the target format to versioned partition metadata and data.

        :param layer: the layer all the metadata and data belong to
        :param data: adapter-specific, the data to adapt
        :param content_type: the MIME content type of the layer
        :param schema: optional :class:`Schema` of the layer
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: sequence of partition id and data for the versioned layer  # noqa
        """

    @abstractmethod
    def from_volatile_metadata(self, partitions: Iterator[VolatilePartition], **kwargs):
        """Adapt volatile partition metadata to the target format.

        :param partitions: sequence of partition metadata from a volatile layer
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: adapter-specific, please consult the documentation
          of the specific adapter to for the formats and types it supports  # noqa
        """

    @abstractmethod
    def from_volatile_data(
        self,
        partitions_data: Iterator[Tuple[VolatilePartition, bytes]],
        content_type: str,
        schema: Optional[Schema],
        **kwargs,
    ):
        """Adapt volatile partition metadata and data to the target format.

        :param partitions_data: sequence of partition metadata and data from a volatile layer
        :param content_type: the MIME content type of the layer
        :param schema: optional :class:`Schema` of the layer
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: adapter-specific, please consult the documentation
          of the specific adapter to for the formats and types it supports  # noqa
        """

    @abstractmethod
    def to_volatile_metadata(
        self, layer: "VolatileLayer", partitions_update, partitions_delete, **kwargs
    ) -> Tuple[Iterator[VolatilePartition], Iterator[Union[str, int]]]:
        """Adapt what update and delete from the target format to volatile partition metadata.

        :param layer: the layer all the metadata and data belong to
        :param partitions_update: adapter-specific, the partitions metadata to update, if any
        :param partitions_delete: adapter-specific, the partitions metadata to delete, if any
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: tuple of `Iterator`, the first with the :class:`VolatilePartition`
          that are adapted, the second with the partition ids to delete  # noqa
        """

    @abstractmethod
    def to_volatile_data(
        self,
        layer: "VolatileLayer",
        data,
        content_type: str,
        schema: Optional[Schema],
        **kwargs,
    ) -> Iterator[Tuple[Union[str, int], bytes]]:
        """Adapt data from the target format to volatile partition metadata and data.

        :param layer: the layer all the metadata and data belong to
        :param data: adapter-specific, the data to adapt
        :param content_type: the MIME content type of the layer
        :param schema: optional :class:`Schema` of the layer
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: sequence of partition id and data for the volatile layer  # noqa
        """

    @abstractmethod
    def from_stream_metadata(self, partitions: Iterator[StreamPartition], **kwargs):
        """Adapt stream partition metadata to the target format.

        :param partitions: sequence of partition metadata from a stream layer
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: adapter-specific, please consult the documentation
          of the specific adapter to for the formats and types it supports  # noqa
        """

    @abstractmethod
    def from_stream_data(
        self,
        partitions_data: Iterator[Tuple[StreamPartition, bytes]],
        content_type: str,
        schema: Optional[Schema],
        **kwargs,
    ):
        """Adapt stream partition metadata and data to the target format.

        :param partitions_data: sequence of partition metadata and data from a stream layer
        :param content_type: the MIME content type of the layer
        :param schema: optional :class:`Schema` of the layer
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: adapter-specific, please consult the documentation
          of the specific adapter to for the formats and types it supports  # noqa
        """

    @abstractmethod
    def to_stream_metadata(
        self, layer: "StreamLayer", partitions, **kwargs
    ) -> Iterator[StreamPartition]:
        """Adapt what to publish from the target format to stream partition metadata.

        :param layer: the layer all the metadata and data belong to
        :param partitions: adapter-specific, the partitions metadata to publish
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: `Iterator` with the :class:`StreamPartition` that are adapted
        """

    @abstractmethod
    def to_stream_data(
        self,
        layer: "StreamLayer",
        data,
        content_type: str,
        schema: Optional[Schema],
        timestamp: Optional[int],
        **kwargs,
    ) -> Iterator[Tuple[Union[str, int], bytes, Optional[int]]]:
        """Adapt data from the target format to stream partition metadata and data.

        :param layer: the layer all the metadata and data belong to
        :param data: adapter-specific, the data to adapt
        :param content_type: the MIME content type of the layer
        :param schema: optional :class:`Schema` of the layer
        :param timestamp: optional timestamp for all the messages, if none is specified in data:
            in milliseconds since Unix epoch (1970-01-01T00:00:00 UTC)
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: sequence of partition id, data and timestamp for the stream layer  # noqa
        """

    @abstractmethod
    def from_index_metadata(self, partitions: Iterator[IndexPartition], **kwargs):
        """Adapt index partition metadata to the target format.

        :param partitions: sequence of partition metadata from an index layer
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: adapter-specific, please consult the documentation
          of the specific adapter to for the formats and types it supports  # noqa
        """

    @abstractmethod
    def from_index_data(
        self,
        partitions_data: Iterator[Tuple[IndexPartition, bytes]],
        content_type: str,
        schema: Optional[Schema],
        **kwargs,
    ):
        """Adapt index partition metadata and data to the target format.

        :param partitions_data: sequence of partition metadata and data from an index layer
        :param content_type: the MIME content type of the layer
        :param schema: optional :class:`Schema` of the layer
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: adapter-specific, please consult the documentation
          of the specific adapter to for the formats and types it supports  # noqa
        """

    @abstractmethod
    def to_index_single_data(
        self,
        data,
        content_type: str,
        schema: Optional[Schema],
        **kwargs,
    ) -> bytes:
        """Adapt index data from the target format.

        :param data: adapter-specific, please consult the documentation
          of the specific adapter to for the formats and types it supports
        :param content_type: the MIME content type of the layer
        :param schema: optional :class:`Schema` of the layer
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: data encoded for an index layer  # noqa
        """

    @abstractmethod
    def from_feature_ids(self, feature_ids: Iterator[str], **kwargs):
        """Adapt a sequence of feature identifiers to the target format.

        :param feature_ids: sequence of feature identifiers
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: adapter-specific, please consult the documentation
          of the specific adapter to for the formats and types it supports  # noqa
        """

    @abstractmethod
    def to_feature_ids(self, data, **kwargs) -> Iterator[str]:
        """Adapt data from the target format to a sequence of feature identifiers.

        :param data: adapter-specific, the data to adapt
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: sequence of feature identifiers  # noqa
        """

    @abstractmethod
    def from_geo_features(self, features: Iterator[geojson.Feature], **kwargs):
        """Adapt a sequence of geographic features to the target format.

        :param features: sequence of geographic features
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: adapter-specific, please consult the documentation
          of the specific adapter to for the formats and types it supports  # noqa
        """

    @abstractmethod
    def to_geo_features(self, data, **kwargs) -> Iterator[geojson.Feature]:
        """Adapt data from the target format to a sequence of geographic features.

        :param data: adapter-specific, the data to adapt
        :param kwargs: adapter-specific, please consult the documentation
          of the specific adapter to for the parameters and types it supports
        :return: sequence of geographic features  # noqa
        """
