# =============================================================================
# codePost v2.0 SDK
#
# API RESOURCE SUB-MODULE
# =============================================================================

from __future__ import print_function # Python 2

# Python stdlib imports
import copy as _copy
import functools as _functools
import inspect as _inspect
import json as _json
import logging as _logging
import os as _os
import platform as _platform
import threading as _threading
import time as _time
import uuid as _uuid
import sys as _sys
try:
    # Python 3
    from urllib.parse import urljoin
    from urllib.parse import quote as urlquote
    from urllib.parse import urlencode as urlencode
except ImportError: # pragma: no cover
    # Python 2
    from urlparse import urljoin
    from urllib import quote as urlquote
    from urllib import urlencode as urlencode

# External dependencies
import better_exceptions as _better_exceptions
import requests as _requests

# Local imports
import codepost
import codepost.api_requestor as _api_requestor
import codepost.errors as _errors
import codepost.util.custom_logging as _logging
import codepost.util.misc as _misc

# =============================================================================

# Global submodule constants
_LOG_SCOPE = "{}".format(__name__)

# Global submodule protected attributes
_logger = _logging.get_logger(name=_LOG_SCOPE)

# =============================================================================

class AbstractAPIResource(object):
    """
    Abstract class type for a codePost API resource.
    """

    # Class constants
    _FIELD_ID = "id"

    # Class attributes
    _data = None
    _requestor = _api_requestor.STATIC_REQUESTOR
    _static = False
    _cache = None

    def __getattribute__(self, item):
        return super(AbstractAPIResource, self).__getattribute__(item)

    def __setattr__(self, item, value):
        # Reset cache if internal state is modified
        if item == "_data":
            self._cache = None

        return super(AbstractAPIResource, self).__setattr__(item, value)

    def _get_id(self, id=None, obj=None):
        raise NotImplementedError("abstract class not meant to be used")

    def _get_data_and_extend(self, static=False, **kwargs):
        raise NotImplementedError("abstract class not meant to be used")

    def _validate_data(self, data, required=True):
        raise NotImplementedError("abstract class not meant to be used")

    def _validate_id(self, id):
        raise NotImplementedError("abstract class not meant to be used")

    @property
    def class_endpoint(self):
        raise NotImplementedError("abstract class not meant to be used")

    @property
    def instance_endpoint(self):
        raise NotImplementedError("abstract class not meant to be used")

    def instance_endpoint_by_id(self, id=None):
        raise NotImplementedError("abstract class not meant to be used")

# =============================================================================

class APIResource(AbstractAPIResource):
    """
    Base class type to store and manipulate a codePost API resource.
    """

    # Class attributes
    _field_names = None

    def __init__(self, requestor=None, static=False, **kwargs):

        # Initialize requestor
        self._requestor = requestor
        if not isinstance(self._requestor, _api_requestor.APIRequestor):
            self._requestor = _api_requestor.STATIC_REQUESTOR

        # Initialize dictionary fields
        _fields = getattr(self, "_FIELDS", list())
        if isinstance(_fields, dict):
            _fields = dict(_fields)
            _fields = list(_fields.keys())
        if getattr(self, "_FIELD_ID", ""):
            _fields.append(self._FIELD_ID)
        self._field_names = _fields

        self._static = static

        if not self._static:
            self._data = getattr(self, "_data", dict())
            if not self._data:
                self._data = dict()

        for key in kwargs.keys():
            if key in self._field_names:
                self._data[key] = kwargs[key]

    def _validate_id(self, id):
        return (id is not None) and (type(id) is int and id > 0)

    def _get_id(self, id=None, obj=None):
        """
        Obtain the internal identifier of an API resource based on the provided
        arguments, using the following resolution order:

        1. If the `obj` parameter is provided with a valid API resource object
           or some integer ID representative of an object, extract identifier
           of that object.

        2. If the `id` parameter is provided with a valid positive integer,
           return `id`.

        3. Otherwise, if the current object is an instantiated API resource,
           return the internal identifier of that object.
        """

        # CASE 1: Obtain ID from an API resource object.
        if obj is not None:

            # Seems we are asked to extract ID from an object
            if isinstance(obj, AbstractAPIResource):
                # Delegate to its own `_get_id` method
                return obj._get_id(id=id)

            # Seems we are asked to use an ID as an object
            elif isinstance(obj, int):
                return self._get_id(id=obj)

            else:
                raise _errors.InvalidAPIResourceError()

        # CASE 2: Obtain ID from provided integer.
        if id is not None:

            if isinstance(id, int) and id > 0:
                return id

            raise _errors.UnknownAPIResourceError()

        # CASE 3: Obtain ID from instance's data
        if self._static:
            raise _errors.StaticObjectError()

        data = getattr(self, "_data", None)
        if data is None or not isinstance(data, dict):
            raise _errors.InvalidAPIResourceError()

        if self._FIELD_ID in data:
            return self._get_id(id=data[self._FIELD_ID])

        # If we made it here, then something went wrong
        raise _errors.UnknownAPIResourceError()

    def _validate_data(self, data, required=True):
        return True

    def  __getstate__(self):
        """
        Returns the full state of the API resource, except for the `requestor`
        object which cannot be pickled.
        """
        state = dict(self.__dict__)
        if "_requestor" in state:
            # This class cannot be pickled for the moment
            del state["_requestor"]
        return state

    def __setstate__(self, state):
        """
        Reloads the full state of the API resource, except for the `requestor`
        object, which is replaced by the standard static requestor
        (`STATIC_REQUESTOR`).
        """
        self.__dict__ = state
        if self.__dict__.get("requestor", None) is None:
            self.__dict__["requestor"] = _api_requestor.STATIC_REQUESTOR
        return self

    def _get_data_and_extend(self, static=False, exclude_read_only=False, **kwargs):
        """
        Internal helper method to combine the keyword arguments (with some
        arguments possibly equal to a VOID placeholder value which must be
        ignored) with, possibly, the internal representation of the
        instantiated API resource.
        """
        data = dict()

        # If this is a static object, we should ignore self._data

        if not static and (not self._static and isinstance(self._data, dict)):
            # Combine instance data and extended (typically kwargs) argument
            data.update(_copy.deepcopy(self._data))

        if kwargs:
            # Make sure not to erase fields

            # NOTE: In a more controled and documented manner, field erasure
            # could be a feature.

            kwargs_copy = {
                key: _copy.deepcopy(value)
                for (key, value) in kwargs.items()
                if _misc.is_field_set_in_kwargs(key, kwargs)
            }

            data.update(kwargs_copy)

        # Remove extraneous (unexpected) data + blank fields + read_only fields (if read_only arg is
        # switched on)
        read_only_filter = (lambda k: k not in self._FIELDS_READ_ONLY) if exclude_read_only else (lambda k: True)

        new_data = {
            key : data[key]
            for key in data.keys()
            if (key in self._field_names) and (data[key] != None) and (read_only_filter(key))
        }

        return new_data

    @property
    def class_endpoint(self):
        """
        The base endpoint designating the current kind of API resource, thus
        if the API resource is an assignment, then `/assignments/`
        """
        classname = getattr(self, "_OBJECT_NAME", "")
        if classname:
            classname = classname.replace("..", "/{}/")
            classname = classname.replace(".", "/")
            endpoint = "/{}/".format(classname)
            return endpoint

    def instance_endpoint_by_id(self, id=None):
        """
        Returns the endpoint designating some instantiated API resource of the
        same kind. If no `id` is provided, will use the `id` of the currently
        instantiated resource. If this is called from a static object, then
        returns `None`.
        """
        _id = self._get_id(id=id)

        if _id:
            # CASE 1: The class end point might have a formatting parameter
            # NOTE: This is for the weird case of submissions of an assignment
            try:
                tmp = self.class_endpoint.format(_id)
                if tmp != self.class_endpoint:
                    return tmp
            except IndexError: # means formatting didn't work
                pass

            # CASE 2: The class end point has not formatting parameter
            # NOTE: Trailing slash important (API bug)
            return urljoin(self.class_endpoint, "{}/".format(_id))

    @property
    def instance_endpoint(self):
        """
        The endpoint designating the currently instantiated API resource, thus
        if the API resource is an assignment with ID 1, then `/assignments/1/`.
        """
        if getattr(self, "_data", None):
            return self.instance_endpoint_by_id(id=self._data.get("id"))

    def _request(self, **kwargs):
        """
        Make an HTTP request through the API resource's underlying requestor
        object.
        """
        self._requestor._request(**kwargs)

    def __repr__(self):
        """
        Provide a representation of the API resource, as a dump of its internal
        dictionary.
        """
        if getattr(self, "_data", None):
            return self._data.__repr__()
        return dict().__repr__()

# =============================================================================