from collections.abc import Iterable, MutableSequence, MutableMapping, Mapping
from typing import Any


def rec_avro_schema(namespace='rec_avro'):
    """
    Generates an avro schema (as python object) suitable for storing arbitrary
    python nested data structure.

    For fastavro::

        schema.parse_schema(rec_avro.rec_avro_schema())

    For avro use::

        avro.schema.SchemaFromJSONData(rec_avro.rec_avro_schema())

    See also::

        to_rec_avro_destructive()
        from_rec_avro_destructive()

    :param namespace: if you would like to specify the namespace
    :return: python representation of avro schema
    """
    primitive_types: list[Any] = [
        'null', 'boolean', 'int', 'long', 'float', 'double', 'string', 'bytes']

    rec_object_schema = {
        '__rec_avro_schema__': True,
        'namespace': namespace,
        'type': 'record',
        # using a named record is the only way to make a
        # recursive structure in avro, so a nested map becomes {'_': {}}
        # nested list becomes {'_': []}
        # because it is an avro record, the storage is efficient
        'name': 'rec_object',
        'fields': [{
            'name': '_',
            'type': [
                {'type': 'map', 'values': primitive_types + ['rec_object']},
                {'type': 'array', 'items': primitive_types + ['rec_object']}
            ]
        }]
    }

    return rec_object_schema


def to_rec_avro_destructive(o, types_to_str=()):
    """
    Converts a python nested data structure and returns a data structure
    in rec_avro format. The input data structure is destroyed and reused.

    rec_avro format is conforming to the avro schema generated by rec_avro_schema()

    For fastavro::

        fastavro.writer(
            out_stream,
            schema.parse_schema(rec_schema),
            (to_rec_avro_destructive(record) for record in input_records))

    See also::

        rec_avro_schema()
        from_rec_avro_destructive()

    :param o: a data structure in rec_avro format.
    :param types_to_str: values of these types will be converted to str
        in the output data structure.
        Note that this is irreversible, i.e. they will be read back as strings.
    :return: python data structure in rec_avro format
    """

    if isinstance(o, str):
        return o

    if isinstance(o, types_to_str) and types_to_str:
        return str(o)

    if isinstance(o, Mapping):
        if isinstance(o, MutableMapping):
            for k, v in o.items():
                o[k] = to_rec_avro_destructive(v, types_to_str)
            return {'_': o}
        else:
            return {'_': {
                k: to_rec_avro_destructive(v, types_to_str)
                for k, v in o.items()}
            }

    if isinstance(o, Iterable):
        if isinstance(o, MutableSequence):
            for i in range(len(o)):
                o[i] = to_rec_avro_destructive(o[i], types_to_str)
            return {'_': o}
        else:
            return {'_': [to_rec_avro_destructive(i, types_to_str) for i in o]}

    return o


def from_rec_avro_destructive(o):
    """
    Converts a nested data structure in rec_avro format into a python nested
    data structure. The input data structure is destroyed and reused.

    rec_avro format is conforming to the avro schema generated by rec_avro_schema()

    For fastavro::

        records = [from_rec_avro_destructive(record)
                   for record in fastavro.reader(in_stream)]

    See also::

        rec_avro_schema()
        to_rec_avro_destructive()

    :param o: data structure in rec_avro format.
    :return: plain python data structure
    """
    if isinstance(o, str):
        return o

    if isinstance(o, Mapping):
        o = o['_']
        if isinstance(o, Mapping):
            if isinstance(o, MutableMapping):
                for k, v in o.items():
                    o[k] = from_rec_avro_destructive(v)
                return o
            else:
                return {k: from_rec_avro_destructive(v) for k, v in o.items()}
        if isinstance(o, Iterable):
            if isinstance(o, MutableSequence):
                for i in range(len(o)):
                    o[i] = from_rec_avro_destructive(o[i])
                return o
            else:
                return [from_rec_avro_destructive(i) for i in o]

        raise Exception('rec_object {"_": val} val must be Mapping or Iterable')

    return o


def is_rec_avro_schema(s):
    return s.get('__rec_avro_schema__', False)
