"""
Base classes for endpoint and entitys
"""

import json
import collections
import sys
import os
from .exceptions import _ApiResponseError
from .exceptions import SdkEntityNotFound
from .exceptions import SdkEndpointNotFound
from .constants import PYTHON_3_0_0_HEXVERSION

__copyright__ = "Copyright 2017, Datera, Inc."


###############################################################################

def _is_stringtype(value):
    try:
        return isinstance(value, basestring)
    except NameError:
        return isinstance(value, str)


def get_init_func(klass):
    def paste_init(self, *args):
        super(klass, self).__init__(*args)
    return paste_init


def snake_to_camel(name):
    parts = name.split('_')
    return "".join(x.title() for x in parts)

###############################################################################


class Entity(collections.Mapping):
    """
    Entity object

    This is a mapping, so its attributes can be accessed just like a dict
    """
    _name = "base_entity"

    def __init__(self, context, data, name, parent_path, strict=False):
        """
        Parameters:
          context (dateraapi.context.ApiContext)
          data (dict)
        """
        self.context = context
        self._data = data

        # Set self._path:
        if 'path' in data:
            self._path = data['path']
            self._type = self.context.reader.get_entity(
                os.path.dirname(self._path))
            if not self._type:
                self._type = self.context.reader.get_entity(self._path)
                if not self._type and strict:
                    raise SdkEntityNotFound(
                        "/api endpoint did not contain entity: {} or entity:"
                        " {}".format(os.path.dirname(self._path), self._path))
                else:
                    self._type = {'name': self._name}
        else:
            self._path = parent_path
            self._type = {'name': self._name}
        if 'tenant' in data:
            self._tenant = data['tenant']
        else:
            self._tenant = None

        if name:
            self._name = name

        # In Python 2, dicts have has_key(); it was removed in Python 3.
        # So, do the same thing here:
        if sys.hexversion < PYTHON_3_0_0_HEXVERSION:
            self.has_key = self._has_key

    #
    # Implement dict-like interface

    def __len__(self):
        return len(self._data)

    def __getitem__(self, key):
        return self._data[key]

    def __iter__(self):
        return iter(self._data)

    def _has_key(self, key):
        """ True if this entity contains the given key, else False """
        return key in self

    def keys(self):
        return self._data.keys()

    def values(self):
        return self._data.values()

    def items(self):
        return self._data.items()

    ######

    def __str__(self):
        """ A human-readable representation of this entity """
        try:
            # Python 2
            return json.dumps(self._data, encoding='utf-8', indent=4)
        except TypeError:
            # Python 3
            return json.dumps(self._data, indent=4)

    def __repr__(self, **kwargs):
        version = self.context.connection._version
        t = snake_to_camel(self._type.get('name', ''))
        if t == "BaseEntity":
            t = self._name
        return "".join(("<", version, " ", t,
                        " ", str(self._path), " at 0x", str(id(self)), ">"))

    def __getattr__(self, attr):
        """ An attempt to give a functioning object back
        if no subendpoint of the requested name exists

        Lookup Resolution Order:
            Function --> Endpoint --> Data Key

        If data key is preferred, use dictionary syntax:
            ai.storage_instances --> Endpoint
            ai['storage_instances'] --> Data Key List
        """
        if attr in self.__dict__:
            return self.__dict__[attr]
        else:
            if attr not in self.context.reader._ep_name_set:
                if attr in self._data:
                    return self._data[attr]
                else:
                    raise SdkEndpointNotFound(
                        "No {} Endpoint found for {}".format(
                            self.context.connection._version, attr))

            klass = type(snake_to_camel(attr), (GenericEndpoint,), {})
            klass.__init__ = get_init_func(klass)
            klass._name = attr
            return klass(self.context, self._path)

    ######
    # TODO(mss): Allow passing string, then determine the endpoint version
    def _set_subendpoint(self, klass):
        """ Create a sub-endpoint of the given endpoint type """
        assert(issubclass(klass, Endpoint))
        subendpoint = klass(self.context, self._path)
        subendpoint = self.context.prepare_endpoint(subendpoint)
        subendpoint_name = klass._name
        setattr(self, subendpoint_name, subendpoint)

    def reload(self, **params):
        """ Load a new instance of this entity from the API """
        if self._tenant:
            params = {'tenant': self._tenant}

        data = self.context.connection.read_entity(self._path, params)
        entity = self.__class__(self.context, data, self._name, self._path)
        entity = self.context.prepare_entity(entity)
        if self._tenant:
            entity._tenant = self._tenant
        return entity

    def set(self, **params):
        """ Send an API request to modify this entity """
        data = self.context.connection.update_entity(self._path, params)
        entity = self.__class__(self.context, data, self._name, self._path)
        entity = self.context.prepare_entity(entity)
        return entity

    def delete(self, **params):
        """ Send an API request to delete this entity """
        data = self.context.connection.delete_entity(self._path, data=params)
        entity = self.__class__(self.context, data, self._name, self._path)
        entity = self.context.prepare_entity(entity)
        entity = self.context.on_entity_delete(entity)
        return entity


###############################################################################


class Endpoint(object):
    """ REST API endpoint
        There should be a corresponding Entity Object created
        Eg, /network
    """

    _name = "base_endpoint"  # Subclass must initialize it
    _entity_cls = Entity

    def __init__(self, context, parent_path):
        """
        Parameters:
          context (dateraapi.context.ApiContext)
        """
        self.context = context
        self._path = None
        if (not parent_path and
                (self._name == "" or self._name == "base_endpoint")):
            self._path = ""  # root endpoint
        else:
            self._path = parent_path + '/' + self._name

    def __repr__(self):
        name = snake_to_camel(self._name)
        if not name:
            name = 'Root'
        path = repr(self._path)
        if path in ("''", '""'):
            path = repr('/')

        return "".join(("<", self.context.connection._version, ".",
                        name, "Ep", " ", path, ">"))

    def __getattr__(self, attr):
        """ An attempt to give a functioning object back
        if no subendpoint of the requested name exists
        """
        if attr in self.__dict__:
            return self.__dict__[attr]
        else:
            if not self.context.connection:
                self.context.connection = self._create_connection(self.context)
                self.context.connection.login()
            if (attr not in self.context.reader._ep_name_set
                    and self.context.strict):
                raise SdkEndpointNotFound(
                    "No {} Endpoint found for {}".format(
                        self.context.connection._version, attr))

            klass = type(snake_to_camel(attr), (GenericEndpoint,), {})
            klass.__init__ = get_init_func(klass)
            klass._name = attr
            return klass(self.context, self._path)

    # TODO(mss): Allow passing string, then determine the endpoint version
    def _set_subendpoint(self, klass):
        """ Create a sub-endpoint of the given endpoint type """
        assert(issubclass(klass, Endpoint))
        subendpoint = klass(self.context, self._path)
        subendpoint = self.context.prepare_endpoint(subendpoint)
        subendpoint_name = klass._name
        setattr(self, subendpoint_name, subendpoint)

    def _get_list(self, _path, data):
        """ Returns a list of objects or strings, depending on the endpoint """
        if isinstance(data, list):
            return [self._prepare_data(value) for value in data]
        elif isinstance(data, dict):
            return [self._prepare_data(value)
                    for value in data.values()]
        else:
            raise ValueError("Unexpected response: " + repr(data))

    def _link_unlink(self, op, entity, tenant=None):
        if _is_stringtype(entity):
            path = entity
        else:
            path = entity._path
        params = dict(op=op, path=path)
        if tenant:
            params['tenant'] = tenant
        data = self.context.connection.update_endpoint(self._path, params)
        return self._new_contained_entity(data)

    def _prepare_data(self, value):
        """
        If the data looks like a entity, create a Entity object from it,
        else just return it unchanged
        """
        if isinstance(value, dict):
            return self._new_contained_entity(value)
        else:
            return value

    def _new_contained_entity(self, data):
        """ Creates an Entity object """
        name = data.get('name')
        if not name:
            name = snake_to_camel(self._name + "_entity")
        entity = self._entity_cls(self.context, data, name, self._path)
        entity = self.context.prepare_entity(entity)
        return entity

    def get(self, *args, **params):
        """
        Get a entity by its ID
        If no ID, return the whole collection
        """
        if len(args) == 0:
            # GET the whole collection
            path = self._path  # Eg. /storage_templates
            data = self.context.connection.read_endpoint(path, params)
            if isinstance(data, dict):
                # Try preparing entities for each key
                try:
                    new_data = {}
                    for key in data:
                        new_data[key] = self._new_contained_entity(data[key])
                    return new_data
                # Fall back to just preparing the whole thing
                # (SingletonEndpoints ususally do this)
                except AttributeError:
                    return self._new_contained_entity(data)
            elif isinstance(data, list):
                return self._get_list(path, data)
            else:
                return data
        elif len(args) == 1:
            # GET a entity in the collection
            entity_id = args[0]
            # /storage_template/MyTemplate
            path = self._path + "/" + entity_id
            data = self.context.connection.read_entity(path, params=params)
            # This should return a single object in a dictionary form
            if isinstance(data, list):
                return self._get_list(path, data)
            return self._new_contained_entity(data)
        else:
            raise TypeError("Too many arguments for get()")


class GenericEndpoint(Endpoint):
    """ Catch-all endpoint for when one isn't defined by a type-spec
    but we still need to access it.  Allows most common operations.
    """

    def create(self, **params):
        """
        Create an entity in this collection

        Keyword arguments define the attributes of the created entity.
        """
        data = self.context.connection.create_entity(self._path, params)
        entity = self._new_contained_entity(data)
        entity = self.context.on_entity_create(entity)
        return entity

    def upload(self, **params):
        """Upload multipart/file data to and endpoint.  Use parameter 'files'
           to specify the files to be uploaded"""
        files = params.pop('files')
        data = self.context.connection.upload_endpoint(
            self._path, files, params)
        return data

    def stream(self, **params):
        """Stream data from the endpoint with specified 'interval' and
           'timeout' values in seconds"""
        interval = params.pop('interval', 0)
        timeout = params.pop('timeout', 0)
        return self.context.connection.stream_endpoint(
            self._path, params, interval, timeout)

    def set(self, **params):
        """Sets the endpoint with list passed"""
        data = self.context.connection.update_endpoint(self._path, params)
        return data

    def list(self, **params):
        """ Return all entities in this collection as a list """
        path = self._path
        try:
            data = self.context.connection.read_endpoint(path, params)
            return self._get_list(path, data)
        except _ApiResponseError as ex:
            if ex.resp_data['message'].startswith("No data at "):
                return []
            raise

    def get(self, *args, **params):
        return super(GenericEndpoint, self).get(*args, **params)

    def add(self, entity, tenant=None):
        return self._link_unlink('add', entity, tenant=tenant)

    def remove(self, entity, tenant=None):
        return self._link_unlink('remove', entity, tenant=tenant)

    def entity_from_path(self, path, **params):
        data = self.context.connection.read_endpoint(path, params)
        return self._new_contained_entity(data)
