from django.utils import simplejson
from django.utils.encoding import smart_unicode
from django.core.paginator import Paginator

from utils import get_fields_and_servicemethods
from exceptions import StoreException, ServiceException
from services import JsonService, servicemethod

__all__ = ('Store', 'ModelQueryStore')

class StoreMetaclass(type):
    """ This class (mostly) came from django/forms/forms.py
        See the original class 'DeclarativeFieldsMetaclass' for doc and comments.
    """
    def __new__(cls, name, bases, attrs):

        # Get the declared StoreFields and service methods
        fields, servicemethods = get_fields_and_servicemethods(bases, attrs)

        attrs['servicemethods'] = servicemethods

        # Tell each field the name of the attribute used to reference it
        # in the Store
        for fieldname, field in fields.items():
            setattr(field, '_store_attr_name', fieldname)
        attrs['fields'] = fields

        return super(StoreMetaclass, cls).__new__(cls, name, bases, attrs)

class BaseStore(object):
    """ The base Store from which all Stores derive
    """

    class Meta(object):
        """ Inner class to hold store options.

            Same basic concept as Django's Meta class
            on Model definitions.
        """
        pass

    def __init__(self, objects=None, stores=None, identifier=None, label=None, is_nested=False):
        """ Store instance constructor.

            Arguments (all optional):

                objects:
                    The list (or any iterable, ie QuerySet) of objects that will
                    fill the store.

                stores:
                    One or more Store objects to combine together into a single
                    store.  Useful when using ReferenceFields to build a store
                    with objects of more than one 'type' (like Django models
                    via ForeignKeys, ManyToManyFields etc.)

                identifier:
                    The 'identifier' attribute used in the store.

                label:
                    The 'label' attribute used in the store.
                
                is_nested:
                    This is required, if we want to return the items as direct
                    array and not as dictionary including 
                    {'identifier': "id", 'label', ...}
                    It mainly is required, if children of a tree structure needs
                    to be rendered (see TreeStore).
        """

        # Instantiate the inner Meta class
        self._meta = self.Meta()

        # Move the fields into the _meta instance
        self.set_option('fields', self.fields)

        # Set the identifier
        if identifier:
            self.set_option('identifier', identifier)
        elif not self.has_option('identifier'):
            self.set_option('identifier', 'id')

        # Set the label
        if label:
            self.set_option('label', label)
        elif not self.has_option('label'):
            self.set_option('label', 'label')
        
        # Is this a nested store? (indicating that it should be rendered as array)
        self.is_nested = is_nested

        # Set the objects
        if objects != None:
            self.set_option('objects', objects)
        elif not self.has_option('objects'):
            self.set_option('objects', [])

        # Set the stores
        if stores:
            self.set_option('stores', stores)
        elif not self.has_option('stores'):
            self.set_option('stores', [])

        # Instantiate the stores (if required)
        self.set_option('stores', [ isinstance(s, Store) and s or s() for s in self.get_option('stores') ])

        # Do we have service set?
        try:
            self.service = self.get_option('service')
            self.service.store = self

            # Populate all the declared servicemethods
            for method in self.servicemethods.values():
                self.service.add_method(method)

        except StoreException:
            self.service = None

        self.request = None # Placeholder for the Request object (if used)
        self.data = self.is_nested and [] or {} # The serialized data in it's final form

    def has_option(self, option):
        """ True/False whether the given option is set in the store
        """
        try:
            self.get_option(option)
        except StoreException:
            return False
        return True

    def get_option(self, option):
        """ Returns the given store option.
            Raises a StoreException if the option isn't set.
        """
        try:
            return getattr(self._meta, option)
        except AttributeError:
            raise StoreException('Option "%s" not set in store' % option)

    def set_option(self, option, value):
        """ Sets a store option.
        """
        setattr(self._meta, option, value)

    def __call__(self, request):
        """ Called when an instance of this store is called
            (ie as a Django 'view' function from a URLConf).

            It accepts the Request object as it's only param, which
            it makes available to other methods at 'self.request'.

            Returns the serialized store as Json.
        """
        self.request = request

        if self.service:
            self._merge_servicemethods()
            if not self.is_nested:
                self.data['SMD'] = self.service.get_smd( request.get_full_path() )

            if request.method == 'POST':
                return self.service(request)

        return self.to_json()

    def __str__(self):
        """ Renders the store as Json.
        """
        return self.to_json()

    def __repr__(self):
        """ Renders the store as Json.
        """
        count = getattr(self.get_option('objects'), 'count', '__len__')()
        return '<%s: identifier: %s, label: %s, objects: %d>' % (
            self.__class__.__name__, self.get_option('identifier'), self.get_option('label'), count)

    def get_identifier(self, obj):
        """ Returns a (theoretically) unique key for a given
            object of the form: <appname>.<modelname>__<pk>
        """
        return smart_unicode('%s__%s' % (
            obj._meta,
            obj._get_pk_val(),
        ), strings_only=True)

    def get_label(self, obj):
        """ Calls the object's __unicode__ method
            to get the label if available or just returns
            the identifier.
        """
        try:
            return obj.__unicode__()
        except AttributeError:
            return self.get_identifier(obj)

    def _merge_servicemethods(self):
        """ Merges the declared service methods from multiple
            stores into a single store.  The store reference on each
            method will still point to the original store.
        """
        # only run if we have a service set
        if self.service:

            for store in self.get_option('stores'):
                if not store.service: # Ignore when no service is defined.
                    continue

                for name, method in store.service.methods.items():
                    try:
                        self.service.get_method(name)
                        raise StoreException('Combined stores have conflicting service method name "%s"' % name)
                    except ServiceException: # This is what we want

                        # Don't use service.add_method since we want the 'foreign' method to
                        # stay attached to the original store
                        self.service.methods[name] = method

    def _merge_stores(self):
        """ Merge all the stores into one.
        """
        for store in self.get_option('stores'):

            # The other stores will (temporarily) take on this store's 'identifier' and
            # 'label' settings
            orig_identifier = store.get_option('identifier')
            orig_label = store.get_option('label')
            for attr in ('identifier', 'label'):
                store.set_option(attr, self.get_option(attr))

            self.data['items'] += store.to_python()['items']

            # Reset the old values for label and identifier
            store.set_option('identifier', orig_identifier)
            store.set_option('label', orig_label)

    def add_store(self, *stores):
        """ Add one or more stores to this store.

            Arguments (required):

                stores:
                    One or many Stores (or Store instances) to add to this store.

            Usage:

                >>> store.add_store(MyStore1, MyStore2(), ...)
                >>>
        """
        # If a non-instance Store is given, instantiate it.
        stores = [ isinstance(s, Store) and s or s() for s in stores ]
        self.set_option('stores', list( self.get_option('stores') ) + stores )

    def to_python(self, objects=None):
        """ Serialize the store into a Python dictionary.

            Arguments (optional):

                objects:
                    The list (or any iterable, ie QuerySet) of objects that will
                    fill the store -- the previous 'objects' setting will be restored
                    after serialization is finished.
        """

        if objects is not None:
            # Save the previous objects setting
            old_objects = self.get_option('objects')
            self.set_option('objects', objects)
            self._serialize()
            self.set_option('objects', old_objects)
        else:
            self._serialize()

        return self.data

    def to_json(self, *args, **kwargs):
        """ Serialize the store as Json.

            Arguments (all optional):

                objects:
                    (The kwarg 'objects')
                    The list (or any iterable, ie QuerySet) of objects that will
                    fill the store.

                All other args and kwargs are passed to simplejson.dumps
        """
        objects = kwargs.pop('objects', None)
        return simplejson.dumps( self.to_python(objects), *args, **kwargs )

    def _start_serialization(self):
        """ Called when serialization of the store begins
        """
        if not self.is_nested:
            self.data['identifier'] = self.get_option('identifier')

        # Don't set a label field in the store if it's not wanted
        if bool( self.get_option('label') ) and not self.is_nested:
            self.data['label'] = self.get_option('label')

        if self.is_nested:
            self.data = []
        else:
            self.data['items'] = []

    def _start_object(self, obj):
        """ Called when starting to serialize each object in 'objects'

            Requires an object as the only argument.
        """
        # The current object in it's serialized state.
        self._item = {self.get_option('identifier'): self.get_identifier(obj)}

        label = self.get_option('label')

        # Do we have a 'label' and is it already the
        # name of one of the declared fields?
        if label and ( label not in self.get_option('fields').keys() ):

            # Have we defined a 'get_label' method on the store?
            if callable( getattr(self, 'get_label', None) ):
                self._item[label] = self.get_label(obj)

    def _handle_field(self, obj, field):
        """ Handle the given field in the Store
        """
        # Fill the proxied_args on the field (for get_value methods that use them)
        field.proxied_args.update({
            'RequestArg': self.request,
            'ObjectArg': obj,
            'ModelArg': obj.__class__,
            'FieldArg': field,
            'StoreArg': self,
        })

        # Get the value
        self._item[field.store_field_name] = field.get_value()

    def _end_object(self, obj):
        """ Called when serializing an object ends.
        """
        if self.is_nested:
            self.data.append(self._item)
        else:
            self.data['items'].append(self._item)
        self._item = None

    def _end_serialization(self):
        """ Called when serialization of the store ends
        """
        pass

    def _serialize(self):
        """ Serialize the defined objects and stores into it's final form
        """
        self._start_serialization()
        for obj in self.get_option('objects'):
            self._start_object(obj)

            for field in self.get_option('fields').values():
                self._handle_field(obj, field)

            self._end_object(obj)
        self._end_serialization()
        self._merge_stores()

class Store(BaseStore):
    """ Just defines the __metaclass__

        All the real functionality is implemented in
        BaseStore
    """
    __metaclass__ = StoreMetaclass

class ModelQueryStore(Store):
    """ A store designed to be used with dojox.data.QueryReadStore

        Handles paging, sorting and filtering

        At the moment it requires a custom subclass of QueryReadStore
        that implements the necessary mechanics to handle server queries
        the the exported Json RPC 'fetch' method.  Soon it will support
        QueryReadStore itself.
    """
    def __init__(self, *args, **kwargs):
        """
        """

        objects_per_query = kwargs.pop('objects_per_query', None)

        super(ModelQueryStore, self).__init__(*args, **kwargs)

        if objects_per_query is not None:
            self.set_option('objects_per_query', objects_per_query)
        elif not self.has_option('objects_per_query'):
            self.set_option('objects_per_query', 25)

    def filter_objects(self, request, objects, query):
        """ Overridable method used to filter the objects
            based on the query dict.
        """
        return objects

    def sort_objects(self, request, objects, sort_attr, descending):
        """ Overridable method used to sort the objects based
            on the attribute given by sort_attr
        """
        return objects

    def __call__(self, request):
        """
        """
        self.request = request

        # We need the request.GET QueryDict to be mutable.
        query_dict = {}
        for k,v in request.GET.items():
            query_dict[k] = v

        # dojox.data.QueryReadStore only handles sorting by a single field
        sort_attr   = query_dict.pop('sort', None)
        descending  = False
        if sort_attr and sort_attr.startswith('-'):
            descending = True
            sort_attr = sort_attr.lstrip('-')

        # Paginator is 1-indexed
        start_index = int( query_dict.pop('start', 0) ) + 1

        # Calculate the count taking objects_per_query into account
        objects_per_query = self.get_option('objects_per_query')
        count = query_dict.pop('count', objects_per_query)

        # We don't want the client to be able to ask for a million records.
        # They can ask for less, but not more ...
        if count == 'Infinity' or count > objects_per_query:
            count = objects_per_query

        objects = self.filter_objects(request, self.get_option('objects'), query_dict)
        objects = self.sort_objects(request, objects, sort_attr, descending)

        paginator = Paginator(objects, count)

        page_num = 1
        for i in xrange(1, paginator.num_pages + 1):
            if paginator.page(i).start_index() <= start_index <= paginator.page(i).end_index():
                page_num = i
                break

        page = paginator.page(page_num)

        data = self.to_python(objects=page.object_list)
        data['numRows'] = paginator.count
        return data
