from functools import partial
from urlparse import urlparse
import logging
import json
from types import GeneratorType
from rdflib import URIRef, Graph, RDF
from oldman.exception import OMUnauthorizedTypeChangeError, OMInternalError, OMUserError
from oldman.exception import OMAttributeAccessError, OMUniquenessError, OMWrongResourceError, OMEditError
from oldman.common import OBJECT_PROPERTY


class Resource(object):
    """A :class:`~oldman.resource.resource.Resource` object is a subject-centric representation of a Web resource.
    A set of :class:`~oldman.resource.resource.Resource` objects is equivalent to a RDF graph.

    In RDF, a resource is identified by an IRI (globally) or a blank node (locally).
    Because blank node support is complex and limited (:class:`rdflib.plugins.stores.sparqlstore.SPARQLStore`
    stores do not support them), **every** :class:`~oldman.resource.Resource` **object has an IRI**.

    This IRI is either given or generated by a :class:`~oldman.iri.IriGenerator` object.
    Some generators generate recognizable `skolem IRIs
    <http://www.w3.org/TR/2014/REC-rdf11-concepts-20140225/#section-skolemization>`_
    that are treated as blank nodes when the resource is serialized into JSON, JSON-LD
    or another RDF format (for external consumption).

    A resource is usually instance of some RDFS classes. These classes are grouped in its attribute `types`.
    :class:`~oldman.model.Model` objects are found from these classes, by calling the method
    :func:`oldman.resource.manager.ResourceManager.find_models_and_types`.
    Models give access to Python methods and to :class:`~oldman.attribute.OMAttribute` objects.
    Their ordering determines inheritance priorities.
    The main model is the first one of this list.

    Values of :class:`~oldman.attribute.OMAttribute` objects are accessible and modifiable
    like ordinary Python attribute values.
    However, these values are checked so some :class:`~oldman.exception.OMAccessError`
    or :class:`~oldman.exception.OMEditError` errors may be raised.

    This abstract class accepts two concrete classes: :class:`~oldman.resource.resource.StoreResource` and
    :class:`~oldman.resource.resource.ClientResource`. The former is serializable and can be saved directly
    by the datastore while the latter has to be converted into a :class:`~oldman.resource.resource.StoreResource`
    so as to be saved.

    Example::

        >>> alice = StoreResource(model_manager, data_store, types=["http://schema.org/Person"], name=u"Alice")
        >>> alice.id
        u'http://localhost/persons/1'
        >>> alice.name
        u'Alice'
        >>> alice.save()
        >>> alice.name = "Alice A."
        >>> print alice.to_jsonld()
        {
           "@context": "http://localhost/person.jsonld",
           "id": "http://localhost/persons/1",
           "types": [
                      "http://schema.org/Person"
                    ],
           "name": "Alice A."
        }
        >>> alice.name = 5
        oldman.exception.OMAttributeTypeCheckError: 5 is not a (<type 'str'>, <type 'unicode'>)

    .. admonition:: Resource creation

        :class:`~oldman.resource.resource.Resource` objects are normally created by a
        :class:`~oldman.model.model.Model` or a
        :class:`~oldman.resource.manager.ResourceManager` object. Please use the
        methods :func:`oldman.model.model.Model.create`, :func:`oldman.model.Model.new`,
        :func:`oldman.resource.manager.ResourceManager.create` or
        :func:`oldman.resource.manager.ResourceManager.new`  for creating new
        :class:`~oldman.resource.Resource` objects.

    :param model_manager: :class:`~oldman.model.manager.ModelManager` object. Gives access to its models.
    :param data_store: :class:`~oldman.store.datastore.DataStore` object. Datastore that has authority
                    on this resource.
    :param id: IRI of the resource. If not given, this IRI is generated by the main model. Defaults to `None`.
    :param types:  IRI list or set of the RDFS classes the resource is instance of. Defaults to `set()`.
    :param hashless_iri: Hash-less IRI that is given to the main model for generating a new IRI if no `id` is given.
                     The IRI generator may ignore it. Defaults to `None`. Must be `None` if `collection_iri` is given.
    :param collection_iri: IRI of the controller to which this resource belongs. This information
                     is used to generate a new IRI if no `id` is given. The IRI generator may ignore it.
                     Defaults to `None`. Must be `None` if `hashless_iri` is given.
    :param is_new: When is `True` and `id` given, checks that the IRI is not already existing in the
                   `data_store`. Defaults to `True`.
    :param former_types: IRI list or set of the RDFS classes the resource was instance of. Defaults to `set()`.
    :param kwargs: values indexed by their attribute names.
    """

    _special_attribute_names = ["_models", "_id", "_types", "_is_blank_node", "_model_manager",
                                "_store", "_former_types", "_logger", "_resource_manager", "_is_new"]
    _pickle_attribute_names = ["_id", '_types', '_is_new']

    def __init__(self, model_manager, data_store, id=None, types=None, hashless_iri=None, collection_iri=None,
                 is_new=True, former_types=None, **kwargs):
        """Inits but does not save it (in the `data_graph`)."""
        self._models, self._types = model_manager.find_models_and_types(types)
        if former_types is not None:
            self._former_types = set(former_types)
        else:
            self._former_types = set(self._types) if not is_new else set()
        main_model = self._models[0]
        self._model_manager = model_manager
        self._store = data_store
        self._is_new = is_new

        if hashless_iri is not None and collection_iri is not None:
            raise OMUserError(u"Hashless_iri (%s) and collection_iri (%s) cannot be given in the same time."
                              % (hashless_iri, collection_iri))

        if id is not None:
            # Anticipated because used in __hash__
            self._id = id