# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility 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
# guarantees 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
##############################################################################

import zope.interface
from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, interfaces

class RuleMixin:
  """
  Provides generic methods and helper methods to implement
  IRule and 
  """
  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  # Declarative interfaces
  zope.interface.implements(interfaces.IRule,
                            interfaces.IMovementCollectionUpdater,)

  # Implementation of IRule
  def constructNewAppliedRule(self, context, id=None, 
                              activate_kw=None, **kw):
    """
    Create a new applied rule in the context.

    An applied rule is an instanciation of a Rule. The applied rule is
    linked to the Rule through the `specialise` relation. The newly
    created rule should thus point to self.

    context -- usually, a parent simulation movement of the
               newly created applied rule

    activate_kw -- activity parameters, required to control
                   activity constraints

    kw -- XXX-JPS probably wrong interface specification
    """
    portal_types = getToolByName(self, 'portal_types')
    if id is None:
      id = context.generateNewId()
    if getattr(aq_base(context), id, None) is None:
      context.newContent(id=id,
                         portal_type='Applied Rule',
                         specialise_value=self,
                         activate_kw=activate_kw)
    return context.get(id)

  def expand(self, applied_rule, **kw):
    """
    Expand this applied rule to create new documents inside the
    applied rule.

    At expand time, we must replace or compensate certain
    properties. However, if some properties were overwriten
    by a decision (ie. a resource if changed), then we
    should not try to compensate such a decision.
    """
    # Update movements
    #  NOTE-JPS: it is OK to make rounding a standard parameter of rules
    #            altough rounding in simulation is not recommended at all
    self.updateMovementCollection(applied_rule, movement_generator=self._geMovementGenerator())
    # And forward expand
    for movement in applied_rule.getMovementList():
      movement.expand(**kw)      

  # Implementation of IMovementCollectionUpdater
  def getMovementCollectionDiff(self, context, rounding=False, movement_generator=None):
    """
    Return a IMovementCollectionDiff by comparing movements
    the list of movements of context and the list of movements
    generated by movement_generator on context.

    context -- an IMovementCollection usually, possibly
               an IMovementList or an IMovement

    movement_generator -- an optional IMovementGenerator
                          (if not specified, a context implicit 
                          IMovementGenerator will be used)
    """
    # We suppose here that we have an IMovementCollection in hand
    decision_movement_list = context.getMovementList()
    prevision_movement_list = movement_generator(self._geMovementGeneratorContext(),
            movement_list=self._geMovementGeneratorMovementList(), rounding=rounding)

    # Get divergence testers
    tester_list = self._getMatchingTesterList()
    if len(tester_list) == 0:
      raise ValueError("It is not possible to match movements without divergence testers")

    # Create small groups of movements per hash keys
    decision_movement_dict = {}
    for movement in decision_movement_list:
      tester_key = []
      for tester in tester_list:
        if tester.test(movement):
          tester_key.append(tester.generateHashKey(movement))
        else:
          tester_key.append(None)
      tester_key = tuple(tester_key)
      decision_movement_dict.setdefaults(tester_key, []).append(movement)
    prevision_movement_dict = {}
    for movement in prevision_movement_list:
      tester_key = []
      for tester in tester_list:
        if tester.test(movement):
          tester_key.append(tester.generateHashKey(movement))
        else:
          tester_key.append(None)
      tester_key = tuple(tester_key)
      prevision_movement_dict.setdefaults(tester_key, []).append(movement)

    # Build the diff
    movement_collection_diff = self._buildMovementCollectionDiff(tester_list,
                                                                 decision_movement_dict,
                                                                 prevision_movement_dict,)

    # Return result
    return movement_collection_diff
                  
  def updateMovementCollection(self, context, rounding=False, movement_generator=None):
    """
    Invoke getMovementCollectionDiff and update context with 
    the resulting IMovementCollectionDiff.

    context -- an IMovementCollection usually, possibly
               an IMovementList or an IMovement

    movement_generator -- an optional IMovementGenerator
                          (if not specified, a context implicit 
                          IMovementGenerator will be used)
    """
    movement_diff = self.getMovementCollectionDiff(context, 
                 rounding=rounding, movement_generator=movement_generator)

    # Apply Diff
    for movement in movement_diff.getDeletableMovementList():
      movement.getParentValue().deleteContent(movement.getId())
    for movement in movement_diff.getUpdatableMovementList():
      kw = movement_diff.getMovementPropertyDict(movement)
      movement.edit(**kw)
    for movement in movement_diff.getNewMovementList():
      # This cas is easy, cause it is an applied rule
      kw = movement_diff.getMovementPropertyDict(movement)
      movement = context.newContent(portal_type='Simulation Movement')
      movement.edit(**kw)
      
  # Placeholder for methods to override
  def _geMovementGenerator(self):
    """
    Return the movement generator to use in the expand process
    """
    raise NotImplementedError

  def _geMovementGeneratorContext(self):
    """
    Return the movement generator context to use for expand
    """
    raise NotImplementedError

  def _geMovementGeneratorMovementList(self):
    """
    Return the movement lists to provide to the movement generator
    """
    raise NotImplementedError

  def _getMatchingTesterList(self):
    """
    Return the applicable divergence testers which must 
    be used to match movements and build the diff (ie.
    not all divergence testers of the Rule)
    """
    raise NotImplementedError

  def _getQuantityTesterList(self):
    """
    Return the applicable divergence testers which must 
    be used to match movements and build the diff (ie.
    not all divergence testers of the Rule)
    """
    raise NotImplementedError

  def _buildMovementCollectionDiff(self, tester_list,
                     decision_movement_dict, prevision_movement_dict):
    """
    Build the diff of movements, based on the rule policy
    which must take into account for example the risk
    of theft. This is the most important method to override.

    NOTE: XXX-JPS there is probably a way to make parts of this generic
    and make implementation even easier and simpler.
    """
    raise NotImplementedError
    # Sample implementation bellow
    def _compare(tester_list, prevision_movement, decision_movement):
      for tester in tester_list:
        if not tester.compare(prevision_movement, decision_movement):
          return False
      return True

    # Find movements to add or compensate (all updates go through compensate)
    new_movement_list = []
    compensation_movement_list = []
    for tester_key in prevision_movement_dict.keys():
      if decision_movement_dict.has_key(tester_key):
        # We have found now a small group of movements with same key
        #  we can now start comparing
        for prevision_movement in prevision_movement_dict[tester_key]:
          prevision_quantity = prevision_movement.getQuantity()
          decision_quantity = 0.0
          compare_movement = None
          for decision_movement in decision_movement_dict[tester_key]:
            if  _compare(tester_list, prevision_movement, decision_movement):
              decision_quantity += decision_movement.getQuantity()
              compare_movement = decision_movement
          # Test globaly
          if compare_movement is None:
            new_movement_list.append(prevision_movement)
          else:
            # Build a fake movement with total quantity
            compare_movement = compare_movement.asContext(quantity=decision_quantity)
            # And compare it to the expected quantity
            if not _compare(self._getQuantityTesterList(), prevision_movement, compare_movement):
              # Find any movement which is no frozen and update
              updated_movement = None
              for decision_movement in decision_movement_dict[tester_key]:
                if not decision_movement.isFrozen():
                  updated_movement = decision_movement
                  decision_movement.setQuantity(prevision_quantity-decision_quantity)
                  break
              if updated_movement is not None:
                updatable_list.append(decision_movement, {})
              else:
                # Else compensate
                compensation_movement_list.append(compare_movement.asContext(
                                     quantity=prevision_quantity-decision_quantity))
      else;
        new_movement_list.extend(prevision_movement_dict[tester_key])

    # Find movements to delete or cancel by compensating
    for tester_key in decision_movement_dict.keys():
      if prevision_movement_dict.has_key(tester_key):
        for movement in decision_movement_dict[tester_key]:
          quantity = 0
          for decision_movement in decision_movement_dict[tester_key]:
            pass
      else:
        # The movement is no longer present
        for movement in decision_movement_dict[tester_key]:
          
