##############################################################################
#
# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
#                    Vincent Pelletier <vincent@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

from Products.ZSQLCatalog.SQLExpression import SQLExpression
from Products.ZSQLCatalog.ColumnMap import ColumnMap
from zLOG import LOG
from Products.ZSQLCatalog.Interface.IEntireQuery import IEntireQuery
from Interface.Verify import verifyClass
from Products.ZSQLCatalog.SQLCatalog import profiler_decorator

class EntireQuery(object):
  """
    This is not a Query subclass, since it does not define a
    registerColumnMap method, and instead does the ColumnMap handling
    internaly.
  """

  __implements__ = IEntireQuery

  column_map = None

  @profiler_decorator
  def __init__(self, query, order_by_list=None, group_by_list=None,
               select_dict=None, limit=None, catalog_table_name=None,
               extra_column_list=None, from_expression=None,
               order_by_override_list=None):
    def default(value):
      if value is None:
        return []
      assert isinstance(value, (tuple, list))
      return value
    def defaultDict(value):
      if value is None:
        return {}
      assert isinstance(value, dict)
      return value
    self.query = query
    self.order_by_list = default(order_by_list)
    self.order_by_override_set = frozenset(default(order_by_override_list))
    self.group_by_list = default(group_by_list)
    self.select_dict = defaultDict(select_dict)
    self.limit = limit
    self.catalog_table_name = catalog_table_name
    self.extra_column_list = default(extra_column_list)
    self.from_expression = from_expression

  def asSearchTextExpression(self, sql_catalog):
    return query.asSearchTextExpression(sql_catalog)

  @profiler_decorator
  def asSQLExpression(self, sql_catalog, only_group_columns):
    column_map = self.column_map
    if column_map is None:
      # XXX: should we provide a way to register column map as a separate mathod or do it here ?
      # Column Map was not built yet, do it.
      self.column_map = column_map = ColumnMap(catalog_table_name=self.catalog_table_name)
      for extra_column in self.extra_column_list:
        table, column = extra_column.replace('`', '').split('.')
        if table != self.catalog_table_name:
          raise ValueError, 'Extra columns must be catalog columns. %r does not follow this rule (catalog=%r, extra_column_list=%r)' % (extra_column, self.catalog_table_name, self.extra_column_list)
        column_map.registerColumn(extra_column)
      for column in self.group_by_list:
        column_map.registerColumn(column)
      for alias, column in self.select_dict.iteritems():
        if column is None:
          column = alias
        else:
          column_map.ignoreColumn(alias)
        column_map.registerColumn(column)
      for override in self.order_by_override_set:
        column_map.ignoreColumn(override)
      for order_by in self.order_by_list:
        assert isinstance(order_by, (tuple, list))
        assert len(order_by)
        column_map.registerColumn(order_by[0])
      self.query.registerColumnMap(sql_catalog, column_map)
      column_map.build(sql_catalog)
      # Replace given group_by_list entries by their mapped representations.
      new_column_list = []
      append = new_column_list.append
      for column in self.group_by_list:
        try:
          append(column_map.asSQLColumn(column))
        except KeyError:
          LOG('EntireQuery', 100, 'Group-by column %r could not be mapped, but is passed through. This use is strongly discouraged.' % (column, ))
          append(column)
      self.group_by_list = new_column_list
      # Build a dictionnary from select_dict aliasing their mapped representations
      self.final_select_dict = select_dict = {}
      for alias, raw_column in self.select_dict.iteritems():
        if raw_column is None:
          column = alias
        else:
          column = raw_column
        try:
          rendered = column_map.asSQLColumn(column)
        except KeyError:
          LOG('EntireQuery', 100, 'Select column %r could not be mapped, but is passed through. This use is strongly discouraged.' % (column, ))
          rendered = column
        select_dict[alias] = rendered
      # Replace given order_by_list entries by their mapped representations.
      new_order_by_list = []
      append = new_order_by_list.append
      for order_by in self.order_by_list:
        column = order_by[0]
        if column in self.order_by_override_set:
          LOG('EntireQuery', 100, 'Order-by column %r is forcibly accepted. This use is strongly discouraged.' % (column, ))
          rendered = column
        else:
          try:
            rendered = column_map.asSQLColumn(column)
          except KeyError:
            LOG('SQLCatalog', 100, 'Order by %r ignored: it could not be mapped to a known column.' % (order_by, ))
            rendered = None
        if rendered is not None:
          if len(order_by) > 1:
            if len(order_by) > 2 and order_by[2] not in (None, ''):
              rendered = 'CAST(%s AS %s)' % (rendered, order_by[2])
            rendered = '%s %s' % (rendered, order_by[1])
          append(rendered)
      self.order_by_list = new_order_by_list
      # generate SQLExpression from query
      sql_expression_list = [self.query.asSQLExpression(sql_catalog, column_map, only_group_columns)]
      # generate join expression based on column_map.getJoinTableAliasList
      append = sql_expression_list.append
      for join_query in column_map.iterJoinQueryList():
        append(join_query.asSQLExpression(sql_catalog, column_map, only_group_columns))
      join_table_list = column_map.getJoinTableAliasList()
      if len(join_table_list):
        # XXX: Is there any special rule to observe when joining tables ?
        # Maybe we could check which column is a primary key instead of
        # hardcoding "uid".
        where_pattern = '`%s`.`uid` = `%%s`.`uid`' % \
          (column_map.getCatalogTableAlias(), )
        # XXX: It would cleaner from completeness point of view to use column
        # mapper to render column, but makes code much more complex to just do
        # a simple text rendering. If there is any reason why we should have
        # those column in the mapper, then we should use the clean way.
        append(SQLExpression(self, where_expression=' AND '.join(
          where_pattern % (x, ) for x in join_table_list
        )))
      self.sql_expression_list = sql_expression_list
    return SQLExpression(
      self,
      table_alias_dict=column_map.getTableAliasDict(),
      from_expression=self.from_expression,
      order_by_list=self.order_by_list,
      group_by_list=self.group_by_list,
      select_dict=self.final_select_dict,
      limit=self.limit,
      where_expression_operator='and',
      sql_expression_list=self.sql_expression_list)

verifyClass(IEntireQuery, EntireQuery)