import operator

from haystack.query import SearchQuerySet, SQ

from tastypie import fields
from tastypie.bundle import Bundle
from tastypie.resources import Resource, ResourceOptions, DeclarativeMetaclass


class SearchOptions(ResourceOptions):
    # One of the great strengths of Haystack is its extensibility. We have
    # subclassed many of Haystack's internal classes, including a subclass
    # of SearchQuerySet. I did not want to be locked in to using Haystack's
    # built-in SearchQuerySet nor its SQ object in this module, so I put in
    # the ``query_object`` attribute on the metaclass.
    resource_name = 'search'
    object_class = SearchQuerySet
    query_object = SQ
    index_fields = []
    # Override document_uid_field with whatever field in your index
    # you use to uniquely identify a single document. This value will be
    # used wherever the ModelResource references the ``pk`` kwarg.
    document_uid_field = 'id'
    lookup_sep = ','
    

class SearchDeclarativeMetaclass(DeclarativeMetaclass):
    def __new__(cls, name, bases, attrs):
        new_class = super(SearchDeclarativeMetaclass, cls)\
            .__new__(cls, name, bases, attrs)
        opts = getattr(new_class, 'Meta', None)
        new_class._meta = SearchOptions(opts)
        include_fields = getattr(new_class._meta, 'fields', [])
        excludes = getattr(new_class._meta, 'excludes', [])
        field_names = new_class.base_fields.keys()
        
        for field_name in field_names:
            if field_name == 'resource_uri':
                continue
            if field_name in new_class.declared_fields:
                continue
            if len(include_fields) and not field_name in include_fields:
                del(new_class.base_fields[field_name])
            if len(excludes) and field_name in excludes:
                del(new_class.base_fields[field_name])

        if getattr(new_class._meta, 'include_absolute_url', True):
            if not 'absolute_url' in new_class.base_fields:
                new_class.base_fields['absolute_url'] = fields.CharField(
                    attribute='get_absolute_url', readonly=True)
        elif 'absolute_url' in new_class.base_fields and not 'absolute_url' in attrs:
            del(new_class.base_fields['absolute_url'])

        return new_class
        

class SearchResource(Resource):
    """
    Blueprint for implementing an HTTP API to access documents in a
    search engine via Haystack. The design of the class adds some
    additional configuration overhead (i.e. a handful of metaclass
    fields) in exchange for flexibility & portability.

    To implement this class in your own application, you will need to:
    1. Define which fields to return in your results;
    2. Override index_fields in the metaclass to limit or expand which
       fields consumers can access from your index via the API;
    3. Override document_uid_field in the metaclass to specify which
       field in the index is used to uniquely identify individual
       documents;
    4. Additionally, you will override query_object and object_class to
       utilize any subclasses you may be using in your project.

    """
    __metaclass__ = SearchDeclarativeMetaclass
    
    def apply_filters(self, request, applicable_filters):
        objects = self.get_object_list(request)

        if applicable_filters:
            return objects.filter(applicable_filters)
        else:
            return objects

    def build_filters(self, filters=None):
        """
        Create a single SQ filter from querystring parameters that
        correspond to SearchIndex fields that have been "registered" in
        the ``self._meta.index_fields``.

        Default behavior is to ``OR`` terms for the same parameter, and
        ``AND`` between parameters. For example:

        ``?format=json&state_exact=Indiana,Illinois&company_exact=IBM``

        would yield an SQ expressing the following logic:

        ``q=state_exact:(Indiana OR Illinois) AND company_exact:IBM``

        Any querystring parameters that are not registered in
        self._meta.index_fields and are not consumed elsewhere in the
        response operation will be ignored.

        """
        terms = []

        if filters is None:
            filters = {}

        for param, value in filters.items():
            
            if param not in self._meta.index_fields:
                continue
                
            tokens = value.split(self._meta.lookup_sep)
            field_queries = []
            
            for token in tokens:
                
                if token:
                    field_queries.append(self._meta.query_object((param,
                                                                  token)))

            terms.append(reduce(operator.or_,
                                filter(lambda x: x, field_queries)))

        if terms:
            return reduce(operator.and_, filter(lambda x: x, terms))
        else:
            return terms
        
    def get_resource_uri(self, bundle_or_obj):
        """
        Generate direct link to individual document in our datastore.

        """
        kwargs = {
            'resource_name': self._meta.resource_name
        }
        uid = self._meta.document_uid_field
        
        if isinstance(bundle_or_obj, Bundle):
            kwargs['pk'] = getattr(bundle_or_obj.obj, uid, '')
        else:
            kwargs['pk'] = getattr(bundle_or_obj, uid, '')

        if self._meta.api_name is not None:
            kwargs['api_name'] = self._meta.api_name

        return self._build_reverse_url("api_dispatch_detail", kwargs=kwargs)
            
    def get_object_list(self, request):
        """
        A Haystack-specific implementation of ``get_object_list``.

        Returns a SearchQuerySet that may have been limited by other
        filter/narrow/etc. operations.
        
        """
        return self._meta.object_class()._clone()

    def obj_get_list(self, request=None, **kwargs):
        filters = {}

        if hasattr(request, 'GET'):
            filters = request.GET.copy()

        filters.update(kwargs)
        applicable_filters = self.build_filters(filters=filters)
        return self.apply_filters(request, applicable_filters)

    def obj_get(self, request=None, **kwargs):
        """
        Fetch a single document from the datastore according to whatever
        unique identifier is available for that document in the
        SearchIndex.

        """
        # Don't let the use of 'pk' here and throughout confuse you.
        # Think of it as a metaphor standing for "whatever field there
        # is in your SearchIndex that uniquely identifies a single
        # document."
        doc_uid = kwargs.get('pk')
        uid_field = self._meta.document_uid_field
        sqs = self.get_object_list(request)
        
        if doc_uid:
            sqs = sqs.filter(self._meta.query_object((uid_field, doc_uid)))

            if sqs:
                return sqs[0]
            else:
                return sqs
        
