##############################################################################
#
# Copyright (c) 2010 Nexedi SA and Contributors. All Rights Reserved.
#                    Leonardo Rochael Almeida <leonardo@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.
#
##############################################################################

# TODO:
# * collapse of parentheses around chains of inner-joins
# * indentation on rendering

SQL_LIST_SEPARATOR = ', '
SQL_SELECT_ALIAS_FORMAT = '%s AS `%s`'

from Products.ZSQLCatalog.Query.SQLQuery import SQLQuery

def escapeTable(table):
  return "`%s`" % table.replace('`', r'\`')

class TableDefinition(object):
  """Base class for all TableDefinition objects. Used for
  typechecking (which should become interface checking later) and
  for dumping common code (if there is any).

  TableDefinition objects describe the table aliasing and joining on
  the "FROM" expression of an SQL query. It is supposed to be
  decoded by an SQLExpressionObject into a string.
  """

  def checkTableAliases(self, current_aliases=None):
    """Check that aliases defined in this table definition don't try
    to alias different tables to the same name.

    Add all aliases defined to the current_aliases mapping if it is passed in.
    """
    if current_aliases is None:
      current_aliases = {}
    self._checkTableAliases(current_aliases)

  def _checkTableAliases(self, current_aliases):
    raise NotImplementedError('should be implemented by subclasses')

  def render(self):
    raise NotImplementedError('should be implemented by subclasses')

  def getJoinConditionQueryList(self):
    """Return a list of SQLQuery objects containing all conditions
    used in this table definition.

    This is a deprecated method that is here only to accomodate the
    fact that not all RelatedKey methods have been migrated.
    """
    query_list = []
    self._extendJoinConditionQueryList(query_list)
    return query_list

  def _extendJoinConditionQueryList(self, query_list):
    raise NotImplementedError('should be implemented by subclasses')

  def getSuperSet(self, other):
    """Checks if this TableDefinition is a subset of the other table
    definition or vice-versa. Return which one is the superset.

    Returns whichever is the superset of the other or None
    """
    raise NotImplementedError('should be implemented by subclasses')

class PlaceHolderTableDefinition(TableDefinition):
  """Table Definition that simply holds an inner table definition and
  delegates to it the rendering.

  This object can be used when you need to change a table definition
  in the future, (like replacing a simple table aliasing with an inner
  join) but don't know who is going to be holding a reference to the
  original table definition to replace it.
  """

  def __init__(self, table_definition=None):
    self.table_definition = table_definition

  replace = __init__

  def __repr__(self):
    return '<%s for %r>' % (self.__class__.__name__, self.table_definition)

  def _checkTableAliases(self, current_aliases):
    assert self.table_definition is not None, "table definition wasn't set"
    return self.table_definition._checkTableAliases(current_aliases)

  def render(self):
    assert self.table_definition is not None, "table definition wasn't set"
    return self.table_definition.render()

  def _extendJoinConditionQueryList(self, query_list):
    # XXX _extendJoinConditionQueryList
    #assert self.table_definition is not None, "table definition wasn't set"
    if self.table_definition is not None:
      return self.table_definition._extendJoinConditionQueryList(query_list)

  def getSuperSet(self, other):
    assert self.table_definition is not None, "table definition wasn't set"
    return self.table_definition.getSuperSet(other)

class TableAlias(TableDefinition):
  """Definition of a table alias as a FROM expression"""

  def __init__(self, table, alias=None):
    self.table = table
    self.alias = alias or table

  def _checkTableAliases(self, current_aliases):
    #table_name = current_aliases.setdefault(self.alias, self.table)
    table_name = current_aliases.get(self.alias)
    if table_name is None:
      current_aliases[self.alias] = self.table
      return
    if table_name != self.table:
      message = ("Attempted to alias both %r and %r to %r" %
                 (table_name, self.table, self.alias,))
    else:
      message = ("Attempted to  alias %r to %r more than once" %
                 (self.table, self.alias,))
    raise ValueError(message)

  def render(self):
    """Render this table definition into an actual FROM expression"""
    return SQL_SELECT_ALIAS_FORMAT % (self.table, self.alias)

  def __repr__(self):
    return '<%s %r AS %r>' % (self.__class__.__name__, self.table, self.alias)

  def _extendJoinConditionQueryList(self, query_list):
    pass

  def __eq__(self, other):
    return (isinstance(other, TableAlias) and
            self.table == other.table and 
            self.alias == other.alias)

  def getSuperSet(self, other):
    """A TableAlias is a subset of another table Alias if either:
     - the other is an equivalent TableAlias
     - the other is an InnerJoin where the left-side is an equivalent TableAlias
    """
    if isinstance(other, TableAlias) and self == other:
      # we're just like the other guy, we could return self or other
      return self
    # delegate the rest of the job to InnerJoin
    return other.getSuperSet(self)

JOIN_FORMAT = """
  (
    %(left)s 
  %(join)s
    %(right)s
  ON 
    %(condition)s
  )
""".strip()

class Join(TableDefinition):

  JOIN_TYPE = None

  def __init__(self, left_tabledef, right_tabledef, condition):
    assert self.JOIN_TYPE, ('Join must be subclassed and self.JOIN_TYPE '
                            'must be defined.')
    assert isinstance(left_tabledef, (TableDefinition, None.__class__))
    assert isinstance(right_tabledef, (TableDefinition, None.__class__))
    self.left_tabledef = left_tabledef
    self.right_tabledef = right_tabledef
    # perhaps assert condition is an SQLExpression?
    self.condition = condition

  def render(self):
    """Render the join as an actual FROM expression, delegating
    the rendering of each table to its own object.
    """
    assert None not in (self.left_tabledef, self.right_tabledef, self.condition)
    return JOIN_FORMAT % dict(left=self.left_tabledef.render(),
                              right=self.right_tabledef.render(),
                              join=self.JOIN_TYPE,
                              condition=self.condition)

  def _checkTableAliases(self, current_aliases):
    self.left_tabledef._checkTableAliases(current_aliases)
    self.right_tabledef._checkTableAliases(current_aliases)

  def __repr__(self):
    return '<%s of %r and %r on %r>' % (self.__class__.__name__,
                                        self.left_tabledef,
                                        self.right_tabledef,
                                        self.condition)

  def _extendJoinConditionQueryList(self, query_list):
    self.left_tabledef._extendJoinConditionQueryList(query_list)
    self.right_tabledef._extendJoinConditionQueryList(query_list)
    query_list.append(SQLQuery(self.condition))

  def getSuperSet(self, other):
    return None

class InnerJoin(Join):
  """Definition of an inner-join as a FROM expression"""

  JOIN_TYPE = "INNER JOIN"

  def getSuperSet(self, other):
    """This InnerJoin is a superset of another TableDefinition if either:

     - other is a TableAlias (or None) equal to our
       left_side. I.e. "other" is at the end of it's inner-join chain.

     - other is an InnerJoin, and it's left-side is equal to our
       left-side (both TableAliases or None), and our right-side is a
       super-set of it's right-side.
    """
    if self.left_tabledef == other:
      # other and left-side are both None or matching TableAliases
      return self
    if (isinstance(other, InnerJoin) and
        self.left_tabledef == other.left_tabledef):
      # our left-sides match. If one of our right sides is a superset of the
      # other right side, then we found the superset
      sub_superset = self.right_tabledef.getSuperSet(other.right_tabledef)
      if sub_superset is self.right_tabledef:
        return self
      elif sub_superset is other.right_tabledef:
        return other
      return None
    return None

class LeftJoin(InnerJoin):
  """Definition of a left-join as a FROM expression"""
  
  JOIN_TYPE = "LEFT JOIN"

  def _extendJoinConditionQueryList(self, query_list):
    """ The condition from a left-join cannot be meaningfully
    extracted to be used in an implicit Inner Join, as is done when a
    query contains a related key that is not formatted to separate the
    join conditions for each related table."""
    raise RuntimeError("Attempted to collapse table definition for implicit "
                       "inner join, but this table definition contains a Left "
                       "Join: %r" % self)

class LegacyTableDefinition(TableDefinition):
  """Table Definition used when a from_expression is passed explicitly.
  Mostly used for manual left-join definitions. Deprecated
  """

  def __init__(self, from_expression, table_alias_map):
    self.from_expression = from_expression
    self.table_alias_map = table_alias_map

  def render(self):
    from_expression_dict = self.from_expression
    table_alias_map = self.table_alias_map
    from_expression = SQL_LIST_SEPARATOR.join(
      from_expression_dict.get(alias, '`%s` AS `%s`' % (table, alias))
      for alias, table in table_alias_map.iteritems())
    return from_expression