# -*- coding: UTF-8 -*-
## Copyright 2009-2012 Luc Saffre
## This file is part of the Lino project.
## Lino is free software; you can redistribute it and/or modify 
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 3 of the License, or
## (at your option) any later version.
## Lino is distributed in the hope that it will be useful, 
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
## GNU General Public License for more details.
## You should have received a copy of the GNU General Public License
## along with Lino; if not, see <http://www.gnu.org/licenses/>.

u"""
>>> DATA = [ 
... ["Belgium", "Eupen", 17000] ,
... ["Belgium", u"Liège", 400000] ,
... ["Belgium", "Raeren", 5000] ,
... ["Estonia", "Tallinn", 400000] ,
... ["Estonia", "Vigala", 1500] ,
... ]


>>> class CitiesAndInhabitants(VirtualTable):
...     column_names = "country city population"
...     @classmethod
...     def get_data_rows(self,ar):
...         return DATA
...
...     @column(label="Country")
...     def country(obj,ar):
...         return obj[0]
...     @column(label="City")
...     def city(obj,ar):
...         return obj[1]
...     @column(label="Population")
...     def city(obj,ar):
...         return obj[2]
...

>>> CitiesAndInhabitants.request().render_to_html()

"""

import logging
logger = logging.getLogger(__name__)

import yaml

#~ from lxml import etree

from django.db import models
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

from django.db.models.query import QuerySet
from django.utils.translation import ugettext_lazy as _
from django.utils import simplejson as json

from lino.core import actors
from lino.core import actions
from lino.core.fields import FakeField
from lino.ui import base
from lino.ui import requests as ext_requests
from lino.utils.config import Configured, load_config_files


class InvalidRequest(Exception):
    pass


class GridConfig(Configured):
  
    def __init__(self,report,data,*args,**kw):
        self.report = report
        self.data = data
        self.label_en = data.get('label')
        self.data.update(label=_(self.label_en))
        super(GridConfig,self).__init__(*args,**kw)
        must_save = self.validate()
        if must_save:
            msg = self.save_config()
            #~ msg = self.save_grid_config()
            logger.debug(msg)
  
    def validate(self):
        """
        Removes unknown columns
        """
        must_save = False
        gc = self.data
        columns = gc['columns']
        col_count = len(columns)
        widths = gc.get('widths',None)
        hiddens = gc.get('hiddens',None)
        if widths is None:
            widths = [None for x in columns]
            gc.update(widths=widths)
        elif col_count != len(widths):
            raise Exception("%d columns, but %d widths" % (col_count,len(widths)))
        if hiddens is None:
            hiddens = [False for x in columns]
            gc.update(hiddens=hiddens)
        elif col_count != len(hiddens):
            raise Exception("%d columns, but %d hiddens" % (col_count,len(hiddens)))
            
        valid_columns = []
        valid_widths = []
        valid_hiddens = []
        for i,colname in enumerate(gc['columns']):
            f = self.report.get_data_elem(colname)
            if f is None:
                logger.debug("Removed unknown column %d (%r). Must save.",i,colname)
                must_save = True
            else:
                valid_columns.append(colname)
                valid_widths.append(widths[i])
                valid_hiddens.append(hiddens[i])
        gc.update(widths=valid_widths)
        gc.update(hiddens=valid_hiddens)
        gc.update(columns=valid_columns)
        return must_save
            
    def unused_write_content(self,f):
        self.data.update(label=self.label_en)
        f.write(yaml.dump(self.data))
        self.data.update(label=_(self.label_en))
        
    def write_content(self,f):
        f.write(yaml.dump(self.data))
        
        




class TableRequest(actions.ActionRequest):
    """
    An :class:`action request <lino.core.actions.ActionRequest>` 
    on a :class:`Table`.
    """
    
    master_instance = None
    master = None
    
    #~ instance = None
    extra = None
    title = None
    #~ layout = None
    
    limit = None
    offset = None
    #~ create_rows = None
    
    def __init__(self,ui,actor,request=None,action=None,**kw):
        if not (isinstance(actor,type) and issubclass(actor,AbstractTable)):
            raise Exception("Expected an AbstractTable subclass, got %r" % actor)
        actions.ActionRequest.__init__(self,ui,actor,request,action,**kw)
        #~ self.setup(*args,**kw)
        self.data_iterator = self.get_data_iterator()
        self.sliced_data_iterator = self.data_iterator
        if self.offset is not None:
            self.sliced_data_iterator = self.sliced_data_iterator[self.offset:]
        if self.limit is not None:
            self.sliced_data_iterator = self.sliced_data_iterator[:self.limit]
        
        
    def parse_req(self,request,rqdata,**kw):
        #~ logger.info("20120710 %s.parse_req()",self.actor)
        #~ rh = self.ah
        master = kw.get('master',self.actor.master)
        if master is ContentType or master is models.Model:
            mt = rqdata.get(ext_requests.URL_PARAM_MASTER_TYPE)
            try:
                master = kw['master'] = ContentType.objects.get(pk=mt).model_class()
            except ContentType.DoesNotExist,e:
                pass
                #~ master is None
                #~ raise ContentType.DoesNotExist("ContentType %r does not exist." % mt)
                
            #~ print kw
        if master is not None and not kw.has_key('master_instance'):
            pk = rqdata.get(ext_requests.URL_PARAM_MASTER_PK,None)
            #~ print '20100406a', self.actor,URL_PARAM_MASTER_PK,"=",pk
            #~ if pk in ('', '-99999'):
            if pk == '':
                pk = None
            if pk is None:
                kw['master_instance'] = None
            else:
                try:
                    kw['master_instance'] = master.objects.get(pk=pk)
                except ValueError,e:
                    raise Exception("Invalid primary key %r for %s",pk,master.__name__)
                except master.DoesNotExist,e:
                    # todo: ReportRequest should become a subclass of Dialog and this exception should call dlg.error()
                    raise Exception("There's no %s with primary key %r" % (master.__name__,pk))
            #~ print '20100212', self #, kw['master_instance']
        #~ print '20100406b', self.actor,kw
        
        if settings.LINO.use_filterRow:
            exclude = dict()
            for f in self.ah.store.fields:
                if f.field:
                    filterOption = rqdata.get('filter[%s_filterOption]' % f.field.name)
                    if filterOption == 'empty':
                        kw[f.field.name + "__isnull"] = True
                    elif filterOption == 'notempty':
                        kw[f.field.name + "__isnull"] = False
                    else:
                        filterValue = rqdata.get('filter[%s]' % f.field.name)
                        if filterValue:
                            if not filterOption: filterOption = 'contains'
                            if filterOption == 'contains':
                                kw[f.field.name + "__icontains"] = filterValue
                            elif filterOption == 'doesnotcontain':
                                exclude[f.field.name + "__icontains"] = filterValue
                            else:
                                print "unknown filterOption %r" % filterOption
            if len(exclude):
                kw.update(exclude=exclude)
                
        if settings.LINO.use_gridfilters:
            filter = rqdata.get(ext_requests.URL_PARAM_GRIDFILTER,None)
            if filter is not None:
                filter = json.loads(filter)
                kw['gridfilters'] = [ext_requests.dict2kw(flt) for flt in filter]
                
        kw = actions.ActionRequest.parse_req(self,request,rqdata,**kw)
        #~ raise Exception("20120121 %s.parse_req(%s)" % (self,kw))
        
        #~ kw.update(self.report.known_values)
        #~ for fieldname, default in self.report.known_values.items():
            #~ v = request.REQUEST.get(fieldname,None)
            #~ if v is not None:
                #~ kw[fieldname] = v
                
        quick_search = rqdata.get(ext_requests.URL_PARAM_FILTER,None)
        if quick_search:
            kw.update(quick_search=quick_search)
            
        requesting_panel = rqdata.get(ext_requests.URL_PARAM_REQUESTING_PANEL,None)
        if requesting_panel:
            kw.update(requesting_panel=requesting_panel)
            
        sort = rqdata.get(ext_requests.URL_PARAM_SORT,None)
        if sort:
            #~ self.sort_column = sort
            sort_dir = rqdata.get(ext_requests.URL_PARAM_SORTDIR,'ASC')
            if sort_dir == 'DESC':
                sort = '-' + sort
                #~ self.sort_direction = 'DESC'
            kw.update(order_by=[sort])
        
                
        offset = rqdata.get(ext_requests.URL_PARAM_START,None)
        if offset:
            kw.update(offset=int(offset))
        limit = rqdata.get(ext_requests.URL_PARAM_LIMIT,None)
        if limit:
            kw.update(limit=int(limit))
        
        
        #~ kw.update(param_values=request.REQUEST.getlist(ext_requests.URL_PARAM_PARAM_VALUES))
        
        #~ def parse_param(fld,request,kv):
            #~ v = request.REQUEST.get(fld.name,None)
            #~ if v is not None:
                #~ kv[fld.name] = v
            
        #~ kv = kw.get('known_values',{})
        #~ for fld in self.report.params:
            #~ parse_param(fld,request,kv)
        #~ if kv:
            #~ kw.update(known_values=kv)
        
        return self.actor.parse_req(request,rqdata,**kw)
        
            
    def setup(self,
            quick_search=None,
            order_by=None,
            offset=None,limit=None,
            master=None,
            title=None,
            master_instance=None,
            master_id=None,
            #~ layout=None,
            filter=None,
            #~ create_rows=None,
            gridfilters=None,
            exclude=None,
            requesting_panel=None,
            extra=None,
            **kw):
            
        self.quick_search = quick_search
        self.requesting_panel = requesting_panel
        self.order_by = order_by
        
            
        #~ logger.info("20120519 %s.setup()",self)
        self.filter = filter
        self.gridfilters = gridfilters
        self.exclude = exclude or self.actor.exclude
        self.extra = extra

        if master is None:
            master = self.actor.master
            # master might still be None
        self.master = master
        
        if title is not None:
            self.title = title
            
        if master_id is not None:
            assert master_instance is None
            master_instance = self.master.objects.get(pk=master_id)
            
        self.master_instance = master_instance
        
        #~ AbstractTableRequest.setup(self,**kw)
        
        """
        Table.page_length is not a default value for ReportRequest.limit
        For example CSVReportRequest wants all rows.
        """
        self.page_length = self.actor.page_length
        
        #~ logger.info("20120121 %s.setup() done",self)
        
        actions.ActionRequest.setup(self,**kw)
        
        """
        20120519 : outbox.MyOutbox had no phantom record when called from menu.
        When called by permalink it had. This was get_create_kw was called before 
        Actor.setup_request() which sets the master_instance.
        """
        self.actor.setup_request(self)
        
        self.create_kw = self.actor.get_create_kw(self.master_instance)
        
        if offset is not None:
            self.offset = offset
            
        if limit is not None:
            self.limit = limit
        
    
    def table2xhtml(self):
        return self.ui.table2xhtml(self)
        
    def get_title(self):
        if self.title is not None:  
            return self.title
        return self.actor.get_title(self)
        
        
        
    def get_status(self,ui,**kw):
        kw = actions.ActionRequest.get_status(self,ui,**kw)
        #~ bp = kw.setdefault('base_params',{})
        bp = kw['base_params']
        if self.quick_search:
            bp[ext_requests.URL_PARAM_FILTER] = self.quick_search
            
        if self.known_values:
            #~ kv = dict()
            for k,v in self.known_values.items():
                if self.actor.known_values.get(k,None) != v:
                    bp[k] = v
            #~ kw.update(known_values = kv)
                
            #~ kw[ext_requests.URL_PARAM_KNOWN_VALUES] = self.known_values
            
        #~ if self.report.__class__.__name__ == 'MyPersonsByGroup':
            #~ print 20111223, self.known_values
        if self.master_instance is not None:
            if self.master is not None:
                bp[ext_requests.URL_PARAM_MASTER_PK] = self.master_instance.pk
                mt = ContentType.objects.get_for_model(self.master_instance.__class__).pk
                bp[ext_requests.URL_PARAM_MASTER_TYPE] = mt
            else:
                bp[ext_requests.URL_PARAM_MASTER_PK] = self.master_instance
                
        return kw
        
        

    @classmethod
    def get_row_by_pk(self,pk):
        return self.data_iterator[int(pk) - 1]
        
    def get_data_iterator(self):
        if self.actor.get_data_rows:
            l = []
            rows = self.actor.get_data_rows(self)
            for row in rows:
                if len(l) > 300:
                    raise Exception("20120521 More than 300 items in %s" % 
                        unicode(rows))
                #~ l.append(row)
                group = self.actor.group_from_row(row)
                group.process_row(l,row)
            return l
        return self.actor.get_request_queryset(self)
        
    def get_total_count(self):
        """
        Calling `len()` on a QuerySet will execute the whole SELECT.
        See :doc:`/blog/2012/0124`
        """
        if isinstance(self.data_iterator,QuerySet):
            return self.data_iterator.count()
        return len(self.data_iterator)
        

    def __iter__(self):
        return self.data_iterator.__iter__()
        
    def __str__(self):
        #~ kw = dict(actor=str(self.actor))
        kw = dict()
        if self.master_instance is not None:
            kw.update(master_instance=self.master_instance.pk)
        if self.filter is not None:
            kw.update(filter=str(self.filter))
        if self.known_values:
            kw.update(known_values=self.known_values)
        #~ return self.__class__.__name__ + '(%s)' % kw
        return self.__class__.__name__ + ' '+str(self.action)+'(%s)' % kw







        


class TableHandle(base.Handle): 
  
    _layouts = None
    
    def __init__(self,ui,actor):
        self.actor = actor
        base.Handle.__init__(self,ui)
  
    def __str__(self):
        return str(self.actor) + 'Handle'
            
    def setup_layouts(self):
        if self._layouts is not None:
            return
        self._layouts = [ self.list_layout ] 
              
    def get_actor_url(self,*args,**kw):
        return self.ui.get_actor_url(self.actor,*args,**kw)
        
    def submit_elems(self):
        return []
        
    def get_list_layout(self):
        self.setup_layouts()
        return self._layouts[0]
        
    def get_columns(self):
        lh = self.get_list_layout()
        #~ print 20110315, layout._main.columns
        return lh.main.columns
        
    def get_slaves(self):
        return [ sl.get_handle(self.ui) for sl in self.actor._slaves ]
            
        

class Group(object):
  
    def __init__(self):
        self.sums = []
        
    def process_row(self,collector,row):
        collector.append(row)

    #~ def add_to_table(self,table):
        #~ self.table = table
        #~ for col in table.computed_columns.values():




class AbstractTable(actors.Actor):
    """
    Base class for :class:`Table` and `VirtualTable`.
    
    An AbstractTable is the definition of a tabular data view, 
    usually displayed in a Grid (but it's up to the user 
    interface to decide how to implement this).
    
    The `column_names` attribute defines the "horizontal layout".
    The "vertical layout" is some iterable.
    """
    _handle_class = TableHandle
    
    #~ field = None
    
    column_names = '*'
    """
    A string that describes the list of columns of this table.
    """
    
    group_by = None
    """
    A list of field names that define the groups of rows in this table.
    Each group can have her own header and/or total lines.
    """
    
    custom_groups = []
    """
    Used internally to store :class:`groups <Group>` defined by this Table.
    """
    
    get_data_rows = None
    """
    Custom tables must define a class method of this name which 
    will be called with a TableRequest object and which is expected
    to return or yield the list of "rows"::
    
        @classmethod
        def get_data_rows(self,ar):
            ...
            yield somerow
            
    Model tables may also define such a method in case they need local filtering.
    
    """
    
    #~ column_defaults = {}
    #~ """
    #~ A dictionary of default parameters for :class:`computed columns <ComputedColumn>` on this table.
    #~ """
    
    #~ hide_columns = None
    hidden_columns = frozenset()
    form_class = None
    help_url = None
    #master_instance = None
    
    page_length = 20
    """
    Number of rows to display per page.
    """
    
    cell_edit = True 
    """
    `True` to use ExtJS CellSelectionModel, `False` to use RowSelectionModel.
    """
    
    #~ date_format = lino.DATE_FORMAT_EXTJS
    #~ boolean_texts = boolean_texts
    boolean_texts = boolean_texts = (_('Yes'),_('No'),' ')
    
    #~ show_prev_next = True
    show_detail_navigator = False
    """
    Whether a Detail view on a row of this table should feature a navigator
    """
    
    default_group = Group()
    
    
    #~ default_action = GridEdit
    default_layout = 0
    
    typo_check = True
    """
    True means that Lino shoud issue a warning if a subclass 
    defines any attribute that did not exist in the base class.
    Usually such a warning means that there is something wrong.
    """
    
    #~ url = None
    
    #~ use_layouts = True
    
    #~ button_label = None
    
    slave_grid_format = 'grid'
    """
    How to display this table when it is a slave in a detail window. 
    `grid` (default) to render as a grid. 
    `summary` to render a summary in a HtmlBoxPanel.
    `html` to render plain html a HtmlBoxPanel.
    Example: :class:`links.LinksByController`
    """
    
    grid_configs = []
    """
    Will be filled during :meth:`lino.core.table.Table.do_setup`. 
    """
    
    order_by = None
    
    filter = None
    """
    If specified, this must be a `models.Q` object
    (not a dict of (fieldname -> value) pairs)
    which will be used as a filter.
    
    Unlike :attr:`known_values`, this can use the full range of 
    Django's `field lookup methods 
    <https://docs.djangoproject.com/en/dev/topics/db/queries/#field-lookups>`_
    
    Note that if the user can create rows in a filtered table, 
    you should make sure that new records satisfy your filter condition 
    by default, otherwise you can get surprising behaviour if the user 
    creates a new row.
    If your filter consists of simple static values on some known field, 
    then you'll prefer to use :attr:`known_values` instead of :attr:`filter.`
    """
    exclude = None
    
    
    extra = None
    """
    Examples::
    
      extra = dict(select=dict(lower_name='lower(name)'))
      # (or if you prefer:) 
      # extra = {'select':{'lower_name':'lower(name)'},'order_by'=['lower_name']}
    
    List of SQL functions and which RDBMS supports them:
    http://en.wikibooks.org/wiki/SQL_Dialects_Reference/Functions_and_expressions/String_functions
    
    """
    
    
    def __init__(self,*args,**kw):
        raise NotImplementedError("20120104")
    
    @classmethod
    def spawn(cls,suffix,**kw):
        kw['app_label'] = cls.app_label
        return type(cls.__name__+str(suffix),(cls,),kw)
        
          
    @classmethod
    def parse_req(self,request,rqdata,**kw):
        return kw
    
        
    @classmethod
    def class_init(self):
        if self.default_action is None:
            self.default_action = actions.GridEdit()
    
        if self.get_data_rows is not None:
            self.show_detail_navigator = False
            
        if self.editable is None:
            self.editable = (self.get_data_rows is None)
        super(AbstractTable,self).class_init()
      
      
        
      
    @classmethod
    def do_setup(self):
      
        self.setup_columns()
        
        super(AbstractTable,self).do_setup()
        
        self.grid_configs = []
        
        def loader(content,cd,filename):
            data = yaml.load(content)
            gc = GridConfig(self,data,filename,cd)
            self.grid_configs.append(gc)
            
        load_config_files(loader,'%s.*gc' % self)
        
        #~ self.default_action = self.add_action(actions.GridEdit())
        #~ self.setup_detail_layouts()
        #~ self.set_actions([])
        #~ self.add_action(self.default_action)
        #~ self.setup_actions()
        #~ if self.default_action.actor != self:
            #~ raise Exception("20120103 %r.do_setup() : default.action.actor is %r" % (
              #~ self,self.default_action.actor))
                
        #~ if self.button_label is None:
            #~ self.button_label = self.label
            
        
    @classmethod
    def setup_columns(self):
        pass
        
    @classmethod
    def get_column_names(self,ar):
        """
        Dynamic tables must subclass this method
        """
        return self.column_names
        
    #~ @classmethod
    #~ def add_column(self,*args,**kw):
        #~ """
        #~ Use this from an overridden `before_ui_handle` method to 
        #~ dynamically define computed columns to this table.
        #~ """
        #~ return self._add_column(ComputedColumn(*args,**kw))
        
    #~ @classmethod
    #~ def _add_column(self,col):
        #~ col.add_to_table(self)
        #~ # make sure we don't add it to an inherited `computed_columns`:
        #~ self.computed_columns = dict(self.computed_columns)
        #~ self.computed_columns[col.name] = col
        #~ return col
      
    @classmethod
    def group_from_row(self,row):
        return self.default_group
    
    @classmethod
    def wildcard_data_elems(self):
        for cc in self.virtual_fields.values():
            yield cc
        #~ return []
        
    #~ @classmethod
    #~ def get_detail(self):
        #~ return None
        
    @classmethod
    def get_row_permission(self,obj,user,state,action):
        if self.get_data_rows:
            return action.readonly
        return True
        
        
    @classmethod
    def save_grid_config(self,index,data):
        if len(self.grid_configs) == 0:
            gc = GridConfig(self,data,'%s.gc' % self)
            self.grid_configs.append(gc)
        else:
            gc = self.grid_configs[index]
        gc.data = data
        gc.validate()
        #~ self.grid_configs[index] = gc
        return gc.save_config()
        #~ filename = self.get_grid_config_file(gc)
        #~ f = open(filename,'w')
        #~ f.write("# Generated file. Delete it to restore default configuration.\n")
        #~ d = dict(grid_configs=self.grid_configs)
        #~ f.write(yaml.dump(d))
        #~ f.close()
        #~ return "Grid Config has been saved to %s" % filename
    
    @classmethod
    def slave_as_html_meth(self,ui):
        """
        Creates and returns the method to be used when 
        :attr:`AbstractTable.slave_grid_format` is `html`.
        """
        def meth(master,ar):
            ar = self.request(ui,request=ar.request,
                action=self.default_action,master_instance=master)
            ar.renderer = ui.ext_renderer
            #~ s = ui.table2xhtml(ar).tostring()
            return ui.table2xhtml(ar)
            #~ s = etree.tostring(ui.table2xhtml(ar))
            #~ return s
        return meth

    @classmethod
    def get_create_kw(self,master_instance,**kw):
        return self.get_filter_kw(master_instance,**kw)
        
    @classmethod
    def get_filter_kw(self,master_instance,**kw):
        """
        Return a dict with the "master keywords" for this table 
        and a given `master_instance`.
        """
        if self.master is None:
            pass
            # master_instance may be e.g. a lino.core.actions.EmptyTableRow
            # UsersWithClients as "slave" of the "table" Home
            
            #~ if master_instance is not None:
                #~ raise Exception("Unexpected master %r for table %s" % (master_instance,self.actor_id))
            #~ assert master_instance is None, "Table %s doesn't accept a master" % self.actor_id
        elif self.master is models.Model:
            pass
        elif isinstance(self.master_field,generic.GenericForeignKey):
        #~ elif self.master is ContentType:
            #~ print 20110415
            if master_instance is None:
                """
                20120222 : here was only `pass`, and the two other lines
                were uncommented. don't remember why I commented them out.
                But it caused all tasks to appear in UploadsByController of 
                an insert window for uploads.
                """
                #~ pass
                kw[self.master_field.ct_field] = None
                kw[self.master_field.fk_field] = None
            else:
                ct = ContentType.objects.get_for_model(master_instance.__class__)
                kw[self.master_field.ct_field] = ct
                kw[self.master_field.fk_field] = master_instance.pk
        elif self.master_field is not None:
            if master_instance is None:
                if not self.master_field.null:
                    #~ logger.info('20120519 %s.get_filter_kw()--> None',self)
                    return # cannot add rows to this report
            elif not isinstance(master_instance,self.master):
                raise Exception("%r is not a %s (master_key is %r)" % (
                  master_instance,self.master.__name__,self.master_field))
            kw[self.master_field.name] = master_instance
            
        #~ logger.info('20120519 %s.get_filter_kw(%r) --> %r',self,master_instance,kw)
        return kw
        


class VirtualTable(AbstractTable):
    """
    An :class:`AbstractTable` that works on an 
    volatile (non persistent) list of rows.
    By nature it cannot have database fields, only virtual fields.
    """
    
    @classmethod
    def request(cls,ui=None,request=None,action=None,**kw):
        self = cls
        if action is None:
            action = self.default_action
        #~ return VirtualTableRequest(ui,self,request,action,**kw)
        return TableRequest(ui,self,request,action,**kw)





