class BaseType(object):
    def __init__(self, name, nullable):
        self.name = name
        self.nullable = nullable

    def __repr__(self):
        return "<{} class name:{}>".format(self.__type__, self.name)


################################################
#
#
#  Classes for simple entities:
#
#  Strings, Dates(timestamp), Boolean,
#  Long, Integer
#
#
################################################


class StringType(BaseType):
    __type__ = "StringType"

    def __init__(self, name, nullable):
        super().__init__(name, nullable)

    def mapping(self):
        return {
            "fields": {"keyword": {"ignore_above": 256, "type": "keyword"}},
            "type": "text",
        }


class DateType(BaseType):
    __type__ = "DateType"

    def __init__(self, name, nullable):
        super().__init__(name, nullable)

    def mapping(self):
        return {"type": "timestamp"}


class BooleanType(BaseType):
    __type__ = "BooleanType"

    def __init__(self, name, nullable):
        super().__init__(name, nullable)

    def mapping(self):
        return {"type": "boolean"}


class LongType(BaseType):
    __type__ = "LongType"

    def __init__(self, name, nullable):
        super().__init__(name, nullable)

    def mapping(self):
        return {"type": "long"}


class IntType(BaseType):
    __type__ = "IntType"

    def __init__(self, name, nullable):
        super().__init__(name, nullable)

    def mapping(self):
        return {"type": "integer"}


################################################
#
#
#  Classes for complex entities:
#
#    Objects
#    Nested (arrays)
#
#
################################################


class ObjectType(BaseType):
    # spark type: struct
    # ES type:    object
    __type__ = "ObjectType"

    def __init__(self, name, nullable):
        super().__init__(name, nullable)
        self.fields = {}

    def mapping(self):
        return {
            "properties": dict(
                (k, self.fields[k].mapping()) for k in self.fields.keys()
            )
        }


class NestedType(BaseType):
    # spark type: array
    # ES type:    nested
    __type__ = "NestedType"

    def __init__(self, name, nullable):
        super().__init__(name, nullable)
        self.fields = {}

    def mapping(self):
        return {
            "include_in_parent": True,
            "type": "nested",
            "properties": dict(
                (k, self.fields[k].mapping()) for k in self.fields.keys()
            ),
        }


def walk_schema(schema, parent=None):
    """Walk a pyspark "schema.jsonValue" schema

    This function converts a pyspark schema into
    a set of classes that can then convert the schema
    to an elasticsearch mapping.

    Parameters
    ----------
    schema: a dict
        Typically, this would come from `df.schema.jsonValue()`. It could
        also be loaded from a string or file using the `json` library.
    parent: a container class, so an `ObjectType` or a `NestedType`
        If empty, will be initialized with an `ObjectType` with
        name = "root".

    >>> s = walk_schema(df.schema.jsonValue(), ObjectType(name='root'))
    >>> s.mapping()
    """

    if parent is None:
        parent = ObjectType(name="root")

    for field in schema["fields"]:
        if field["type"] == "string":
            parent.fields[field["name"]] = StringType(
                name=field["name"], nullable=field["nullable"]
            )
        if field["type"] == "integer":
            parent.fields[field["name"]] = IntType(
                name=field["name"], nullable=field["nullable"]
            )
        if field["type"] == "long":
            parent.fields[field["name"]] = LongType(
                name=field["name"], nullable=field["nullable"]
            )
        if field["type"] == "timestamp":
            parent.fields[field["name"]] = DateType(
                name=field["name"], nullable=field["nullable"]
            )
        if field["type"] == "boolean":
            parent.fields[field["name"]] = BooleanType(
                name=field["name"], nullable=field["nullable"]
            )
        if type(field["type"]) == dict:
            n_field = field["type"]
            if n_field["type"] == "struct":
                newp = ObjectType(field["name"], field["nullable"])
                parent.fields[field["name"]] = walk_schema(n_field, parent=newp)

            if n_field["type"] == "array":
                newp = NestedType(field["name"], field["nullable"])
                parent.fields[field["name"]] = walk_schema(
                    n_field["elementType"], parent=newp
                )
    return parent
