from django.db import models
import operator
from django.db.models import Q
from ox.text import smartSplit
from ox.django.fields import DictField
from django.core.paginator import Paginator, InvalidPage, EmptyPage
from django.template.loader import render_to_string
from django.contrib.contenttypes.models import ContentType
from insidepages.models import ModuleTab, ModelExtra
from os.path import exists
from sorl.thumbnail import get_thumbnail
from django.template import RequestContext
import datetime

def splitSearch(string):
  ret = []
  for s in smartSplit(string):
    word = s.replace("'", "").replace('"', '')
    ret.append(word)
  return ret

#i see the point of this function, but please clean it up: refer to 'except MultipleObjectsReturned' to see why this is here.
def get_real_ctype(module_name):
  for c in ContentType.objects.filter(model=module_name):
    try:
      if c.model_class().is_itf_model == True:
        return c
    except:
      pass 
  return None



class ItfModel(models.Model):
  '''
  Base models class that all other models that have front-end views inherit from. It handles passing of parameters to generate the list of items on the left, as well as methods (that can be over-ridden) to generate the context to be sent to the template that renders the object. 
  '''
  fts_fields = [] #Fields that should be text-searchable through
  fk_filters = [] #Foreign key fields that should be filterable by
#  related_models = []
  sort_fields = [] #Fields that the model can be sorted by
  hasComments = True #Whether comments should be enabled for this model
  form_names = [] #Names of forms, defined in <app>/forms.py for this model
  title_field = "title" #The db field that corresponds to the 'title' to display
  is_itf_model = True #FIXME: remove this property, one should use isinstance(foo, ItfModel) instead to check if something is_itf_model
  changed = models.DateTimeField(null=True, editable=False) #All models should have a changed timestamp
  created = models.DateTimeField(null=True, editable=False) #All models should have a created timestamp


  #For all models, on save, update their "changed" timestamp - if created, set their created timestamp.
  def save(self, *args, **kwargs):
    ''' On save, update timestamps '''
    if not self.id:
      self.created = datetime.datetime.today()
    self.changed = datetime.datetime.today()
    if self.created == None:
        self.created = self.changed
    super(ItfModel, self).save(*args, **kwargs)

  class Meta:
    abstract = True

  #Should return the id for the object and the title to display in the left hand list
  def list_dict(self):
    return {
        'id': self.pk,
        'title': self.get_title()
    }

  #Data that's used by the autocomplete views
  def autocomplete_dict(self):
    d = self.list_dict()
    d['text'] = self.get_text()
    if not d['text']:
        d['text'] = ''
    if len(d['text']) > 150:
        d['text'] = d['text'][:150] + "..."
    return d

 
  def info_dict(self):
    '''
        Ideally you should not over-ride this - over-ride .get_dict() to pass custom data.
    '''
    d = self.get_dict()
#    d['add_form'] = self.__class__.get_add_form()
    d['forms'] = self.__class__.get_forms()
    d['obj'] = self
    try:
      edit_url = self.get_edit_url()
      d['edit_url'] = edit_url
    except:
      pass
    return d


  #Returns the title for this object, used as title of page, title of object, and by default, title in left hand display. By default, is a field called 'title', but subclasses can over-ride either title field or the get_title method and return anything.
  def get_title(self):
    try:
        return self.get(self.title_field)
    except:
        return self.__unicode__()

  #Returns "text" for this object - like a "description" or "about" field, or nothing - ideally would be over-ridden by subclasses.
  def get_text(self):
    try:
      return self.description
    except:
      try:
        return self.about
      except:
        pass
    return ''

  #Get the ModuleTab object associated with this object.
  def get_tab(self):
    modelextra = self.get_modelextra()
    tab = ModuleTab.objects.filter(model=modelextra)[0]
    return tab

  
  '''
  Get all the forms associated with this class
  '''
  @classmethod
  def get_forms(cls):

    def get_form(form_name):
      app_label = cls._meta.app_label
      module = __import__(app_label + ".forms")
      return module.forms.__getattribute__(form_name)
   
    ret = {}
    for form_name in cls.form_names:
      ret[form_name] = get_form(form_name)
    return ret
    
  @classmethod
  def get_popup_form(cls):
    all_forms = cls.get_forms()
    for k in all_forms.keys():
      if k.startswith("Popup"):
        return all_forms[k]
    return all_forms[0]
         
  '''
  Get the insidepages.models.ModelExtra object associated with this model
  '''
  def get_modelextra(self):
    try:
      ctype = ContentType.objects.get(model=self.__class__._meta.module_name)
    except:#FIXME: ideally catch only MultipleObjectsReturned (figure out where to import that from :/ ) #FUCKING way ugly hack to get clashing model ctype names with django internal models working (get_real_ctypes simply checks, if there are multiple content objects with the same 'module_name', which one is_itf_model, and returns that).
      ctype = get_real_ctype(self.__class__._meta.module_name)
#    modelextra = ModelExtra.objects.filter(model=ctype)[0]
    modelextra = ctype.modelextra_set.all()[0]
    return modelextra

  '''
  Get absolute URL for this instance
  '''
  def get_absolute_url(self):
    return "%s/?tab=%s&object_id=%d" % (self.get_module().get_absolute_url(), self.get_tab().slug, self.id)

  
  def get_edit_url(self):
    '''
        Get the Edit URL for this object
    '''
    return "%s/%s/%d/edit" % (self.get_module().get_absolute_url(), self.get_tab().slug, self.id)

   
  def get_add_url(self):
    if hasattr(self, 'main_form'):
      return "%s/%s/add" % (self.get_module().get_absolute_url(), self.get_tab().slug,)
    else:
      return None 

  '''
  Get insidepages.models.Module instance for this object
  '''
  def get_module(self):
    tab = self.get_tab()
    if tab:
      return tab.module
    else:
      return None
          


  '''
  Get the main image thumbnail for this object - checks if object has either a field or custom method called 'main_image', then checks for default_images on the ModelExtra for this model.
  '''
  def get_main_image(self, size="142x150"):
    if hasattr(self, 'main_image'):
        main_image_getter = self.main_image
        if type(main_image_getter).__name__ == 'instancemethod':
            imgfield = main_image_getter()
        else:
          imgfield = main_image_getter
    else:
      imgfield = self.get_modelextra().default_image #FIXME!!

    if imgfield is None or imgfield.name == '':
      imgfield = self.get_modelextra().default_image

    if imgfield:
      try:
        thumb =  get_thumbnail(imgfield, size, crop="center").url
      except:
        thumb = ''
    else:
      thumb = ''  # Add default image for site
    return {
        'thumb': thumb
    }

  '''
  def main_image(self):
    return None
  '''

  '''
  The templates for objects are stored following the convention templates/modules/<app_label>/<model_name>.html
  '''
  def get_template_path(self):
    kls = self.__class__
    return  "modules/%s/%s.html" % (kls._meta.app_label, kls._meta.module_name)

  def get_dict(self):
    return self.get(self._get_fields().keys())


  '''
  Renders the html for this object by taking .info_dict() and rendering it to .get_template_path() . Subclasses should not over-ride this - over-ride .get_dict() ideally to pass custom data.
  '''
  def insidepage_dict(self, request):
    context = RequestContext(request, self.info_dict())
    html = render_to_string(self.get_template_path(), context)
    return {
        'url': self.get_absolute_url(),
        'title': self.get_title(),
        'main_image': self.get_main_image(),
        'html': html
    }




  '''
  Filters a queryset to return only objects whose fts_fields contain search term. FIXME: Ideally, this would be handled by the search back-end and not __icontains queries in the db.
  '''
  @classmethod
  def fts(kls, qset, search):
    terms = splitSearch(search)
    qobjects = []
    for t in terms:  
      for f in kls.fts_fields:
        qstring = f + '__icontains'
        qobject = Q(**{qstring:t})
        qobjects.append(qobject)
    return qset.filter(reduce(operator.or_, qobjects)).distinct()


  '''
  Main function to return list of items for this model class - optionally filtered by various parameters
  '''  
  @classmethod
  def get_list(kls, data):
    options = {
      'page': 1,
      'count': 20,
      'search': '',
      'sort': [],
      'range': [0,50],
      'object_id': False
    }
    options.update(data)
    ret = []
    page_no = options['page']
    list_size = options['count']

    #Get the full queryset - defaults to kls.objects.all()
    qset = kls.get_qset()

    #If search term exists, filter by it.
    search = options['search']
    if search != '':
      qset = kls.fts(qset, search) 
   
    
    sort = options['sort']

    #If sort is not an empty array, sort by sort options
    if sort != []:
      for s in sort:
        if s['operator'] == '-':
          operator = '-'
        else:
          operator = ''
        sort = operator + s['key']
        qset = qset.order_by(sort)


    #FIXME: This is a very ugly hack where to make sure the object that is currently selected displays on the current page, we make it the first item and remove it from the qset.
    if options['object_id'] != False and options['object_id'] != '':
        object_id = options['object_id']
        qset = qset.exclude(pk=object_id)
        try:
            obj = kls.objects.get(pk=object_id)
            ret.append(obj.list_dict())
        except:
            pass

    #Another slight hack - if there are 0 results returned, we set hasResults to False and return the entire queryset, telling the template to display a "No Results Found" message and let the user see all the items in the list.
    hasResults = True
    if qset.count() == 0:
        hasResults = False
        qset = kls.get_qset()

    '''
    r0 = options['range'][0]
    r1 = options['range'][1]
    results = qset[r0:r1]
    '''

    #Handle pagination through django Paginator class
    paginator = Paginator(qset, list_size)


    try:
      results = paginator.page(page_no)
    except (EmptyPage, InvalidPage):
      results = paginator.page(paginator.num_pages)
    
    for r in results.object_list:
      d = r.list_dict()
      d['url'] = r.get_absolute_url()
      ret.append(d)


    return {
        'has_next': results.has_next(),
        'has_previous': results.has_previous(),
        'page_no': page_no,
        'has_results': hasResults,
        'num_pages': paginator.num_pages,
        'items': ret
    }

  @classmethod
  def get_qset(kls):
    '''
    Override this method in your model class to define a custom queryset instead of objects.all(), for instance, to always exclude unpublished items.
    '''
    return kls.objects.all()



  '''
  Function to filter a queryset by a foreignkey. NOT IMPLEMENTED.
  eg. fks = {
      'somefield': [1, 5, 7],
      'someotherfield': [3]
      }
  '''
  @classmethod
  def filter_by_fks(kls, qset, fks):
    qobjects = []
    for key in fks.keys():
      field = getField(kls._meta.fields, key)
      if field:
#        rel_class = field.related.parent_model
        for i in fks[key]:
          qobject = Q(**{field.name: i})
          qobjects.append(qobject)
    return qset.filter(reduce(operator.or_, qobjects))               


  '''
    Some fairly meta functions to iterate through model fields, etc. - was written when trying to write a custom model.to_json() kindof function that 'just worked' for all field types - right now they're not used, but maybe useful in the future.
  '''

  @classmethod
  def get_fk_objects(kls):
    ret = {}
    for f in kls.fk_filters:
      ret[f] = []
      field = getField(kls._meta.fields, f)
      rel_class = field.related.parent_model
      for o in rel_class.objects.all():
        ret[f].append({
          'id': o.id,
          'title': unicode(o)
        })  
    return ret


  def get(self, props):
    typ = type(props)
    if typ == list:
      ret = {}
      for p in props:
        ret[p] = self._get_property(p)
      return ret
    elif typ == str:
      return self._get_property(props)
    else:
      return False

  def _get_property(self, prop):
    fields = self._get_fields()
    if prop in fields.keys():
      field_type = fields[prop]
      if field_type in ["TextField", "CharField", "IntegerField"]:
        return self.__getattribute__(prop)
      elif field_type == "ImageField":
        imagefield = self.__getattribute__(prop)
        return imagefield.url if imagefield.name != '' else ''
      elif field_type == "ForeignKey":
        return self._get_fk(prop)
      elif field_type == "ManyToMany":
        return self._get_m2m(prop)
    elif prop.lower() in self.related_models:
      return self._get_related(prop)
    else:
      try:
        val = self.__getattribute__(prop)
        if type(val).__name__ == 'instancemethod' and prop in self.getters:
          return val()
        else:
          return val
      except:
        return False #FIXME

  def _get_fk(self, prop):
    prop = prop.replace("_id", "")
    return self.__getattribute__(prop).get_dict()

#  def _get_related_objects(self, fk_field):
  def _get_m2m(self, prop):
    ret = []
    for o in self.__getattribute__(prop).all():
      ret.append(o.get_dict())
    return ret
    
  def _get_related(self, prop):
    attr = prop.lower() + "_set"
    ret = []
    for o in self.__getattribute__(attr).all():
      ret.append(o.get_dict())
    return ret

  @classmethod
  def _get_fields(kls):
    ret = {}
    for f in kls._meta.fields:
      ret[f.get_attname()] = type(f).__name__
    return ret    




def getField(fields, name):
  for f in fields:
    if f.name == name:
      return f
  return False



#def site_config():
#    with open(settings.SITE_CONFIG) as f:
#        site_config = json.load(f)
#        site_config['keys'] = {}
#        for key in site_config['itemKeys']:
#            site_config['keys'][key['id']] = key
#        site_config['_findKeys'] = {}
#        for key in site_config['findKeys']:
#            site_config['_findKeys'][key['id']] = key
#    return site_config


#  #TODO: Get rid off and use .get_forms() instead
#  @classmethod
#  def get_add_form(cls):
#    try:
#      formStr = cls.add_form
##      cls = default_tab.model_class()        
#      app_label = cls._meta.app_label
#      module = __import__(app_label + ".forms")
#      add_form = module.forms.__getattribute__(formStr)
#      return add_form
#    except:
#      return None

