# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved.
#               Hervé Poulain <herve@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.CMFCore.WorkflowCore import WorkflowException
from Products.ERP5TioSafe.Conduit.TioSafeBaseConduit import TioSafeBaseConduit
from lxml import etree
parser = etree.XMLParser(remove_blank_text=True)
from zLOG import LOG

class ERP5ResourceConduit(TioSafeBaseConduit):
  """
    This is the conduit use to synchonize ERP5 Products
  """
  def __init__(self):
    # Define the object_tag element to add object
    self.xml_object_tag = 'resource'
    self.system_pref = None

  def getObjectAsXML(self, object, domain):
    return object.Resource_asTioSafeXML(context_document=domain)

  def _createContent(self, xml=None, object=None, object_id=None,
      sub_object=None, reset_local_roles=0, reset_workflow=0, simulate=0,
      **kw):
    """
      This is the method calling to create an object
    """
    if object_id is not None:
      if sub_object is None:
        sub_object = object._getOb(object_id, None)
      if sub_object is None: # If so, it does not exist
        portal_type = ''
        portal_type = self.getObjectType(xml)
        # Create a product object
        sub_object, reset_local_roles, reset_workflow = self.constructContent(
            object,
            object_id,
            portal_type,
        )
        # The initialization here is used to define our own base category, we
        # don't want to use the default base categories
        sub_object.edit(
            variation_base_category_list = [],
            optional_variation_base_category_list = [],
            individual_variation_base_category = [],
        )
        # if exist namespace retrieve only the tag
        index = 0
        category_dict = {}
        if xml.nsmap not in [None, {}]:
          index = -1

        # Set the use value
        sub_object.setUse('sale')
        portal = object.getPortalObject()
        # Browse the list to work on categories
        mapping_list = []
        for node in xml.getchildren():

          # Only works on right tags, and no on the comments, ...
          if type(node.tag) is not str:
            continue
          # Build the split list of the tag
          split_tag = node.tag.split('}')
          # Build the tag (without is Namespace)
          tag = node.tag.split('}')[index]
          # Treat sub-element
          if tag == 'category':
            category_xml_value = node.text.encode('utf-8')
            category_split_list = category_xml_value.split('/')
            base_category = category_split_list[0]
            shared_variation = True
            try:
              # Try to access the category
              category = portal.portal_categories.restrictedTraverse(category_xml_value)
            except KeyError:
              # This is an individual variation
              shared_variation = False

            if shared_variation:
              if base_category not in sub_object._baseGetVariationBaseCategoryList():
                base_category_list = sub_object._baseGetVariationBaseCategoryList()
                base_category_list.append(base_category)
                sub_object.setVariationBaseCategoryList(base_category_list)
                self.updateSystemPreference(portal, base_category)

              variation_list = sub_object.getVariationCategoryList()
              if category_xml_value not in variation_list:
                variation_list.append(category_xml_value)
                sub_object.setVariationCategoryList(variation_list)
            else:
              if base_category not in sub_object.getIndividualVariationBaseCategoryList():
                base_category_list = sub_object.getIndividualVariationBaseCategoryList()
                base_category_list.append(base_category)
                sub_object.setIndividualVariationBaseCategoryList(base_category_list)
                self.updateSystemPreference(portal, base_category, True)

              variation_category = "/".join(category_split_list[1:])
              category = sub_object.newContent(
                portal_type='Product Individual Variation',
                title=variation_category,
                variation_base_category=base_category,
                )
            # Store a dict category_name -> category_path to use for mapping later
            category_dict[category_xml_value] = base_category+'/'+category.getRelativeUrl()

          elif tag == "mapping":
            # build mapping list here
            mapping_dict = {'category' : [],}
            for item in node.getchildren():
              split_tag = item.tag.split('}')
              # Build the tag (without is Namespace)
              tag = item.tag.split('}')[index]
              if tag == "category":
                mapping_dict['category'].append(category_dict[item.text.encode('utf-8')])
              else:
                mapping_dict[tag] = item.text.encode('utf-8')
            mapping_list.append(mapping_dict)

      conflict_list = self._createMapping(sub_object, mapping_list)
      if len(conflict_list):
        raise ValueError, "Conflict on creation of resource, should not happen, conflict = %r" %(conflict_list)

      self.newObject(
          object=sub_object,
          xml=xml,
          simulate=simulate,
          reset_local_roles=reset_local_roles,
          reset_workflow=reset_workflow,
      )


      # add to sale supply
      sync_name = self.getIntegrationSite(kw['domain']).getTitle()
      ss = self.getSaleSupply(sync_name, portal)
      if len(ss.searchFolder(resource_title=sub_object.getTitle())) == 0:
        ss.newContent(resource_value=sub_object)

    return sub_object

  def _getMappedPropertyLine(self, resource, prop):
    """
    Return the line, create it if it does not exits
    """
    # XXX-Aurel : Cache this method ?
    mapped_line = None
    for line in resource.contentValues(portal_type='Mapped Property Type'):
      if getattr(line, 'mapped_property', None) == prop:
        mapped_line = line
        break
    if not mapped_line:
      mapped_line = resource.newContent(portal_type='Mapped Property Type',
                                        mapped_property=prop)

    return mapped_line

  def _createMapping(self, resource, mapping_list):
    conflict_list = []
    for mapping in mapping_list:
      category_list = mapping.pop('category')
      base_category_list = [x.split('/', 1)[0] for x in category_list]
      property_list = mapping.keys()
      for prop in property_list:
        line = self._getMappedPropertyLine(resource, prop)
        line.edit(variation_base_category_list=base_category_list,
                  variation_category_list =line.getVariationCategoryList([]) + category_list,)
        line.updateCellRange(base_id=prop)
        # Try to get the cell
        try:
          cell = line.getCell(base_id=prop, *category_list)
        except KeyError:
          cell = None
        if cell is None:
          cell = line.newCell(
            base_id=prop,
            portal_type='Mapped Property Cell',
            *category_list
            )
        # set values on the cell
        cell.setCategoryList(category_list)
        cell.setMembershipCriterionCategoryList(category_list)
        cell.setMembershipCriterionBaseCategoryList(
          line.getVariationBaseCategoryList(),
          )
        cell.setMappedValuePropertyList([prop,])
        # Check possible conflict
        getter_id = "get%s" %(prop.capitalize())
        getter = getattr(cell, getter_id, None)
        if getter:
          current_value = getter()
        else:
          current_value = getattr(cell, prop)
        if current_value and current_value != mapping[prop]:
          conflict_list.append((cell, current_value, mapping[prop]))
          continue
        setter_id = "set%s" %(prop.capitalize())
        setter = getattr(cell, setter_id, None)
        if setter:
          setter(mapping[prop])
        else:
          setattr(cell, prop, mapping[prop])
    return conflict_list

  def getSaleSupply(self, name, portal):
    """
    Retrieve the sale supply for the given synchronization
    If not exist, create it
    """
    if getattr(self, 'sale_supply', None) is None:
      ss_list = portal.sale_supply_module.searchFolder(title=name,
                                                       validation_state='validated')
      if len(ss_list) > 1:
        raise ValueError, "Too many sale supplies, does not know which to choose"
      if len(ss_list) == 0:
        # Create a new one
        ss = portal.sale_supply_module.newContent(title=name)
        ss.validate()
      else:
        ss = ss_list[0].getObject()
      self.sale_supply = ss
    return self.sale_supply

  def updateSystemPreference(self, portal, base_category, individual=False):
    """ Update the system preference according to categories set on products
    so that UI is well configured in the end """
    if self.system_pref is None:
      pref_list = [x for x in portal.portal_preferences.objectValues(portal_type="System Preference")\
                   if x.getPreferenceState()=="global"]
      if len(pref_list) > 1:
        raise ValueError, "Too many system preferences, does not know which to choose"
      elif len(pref_list) == 0:
        pref = portal.portal_preferences.newContent(portal_type="System Preference",
                                             title="default system preference for TioSafe",
                                             priority=1)
        pref.enable()
      else:
        pref = pref_list[0].getObject()
      self.system_pref = pref

    if individual:
      cat_list = self.system_pref.getPreferredProductIndividualVariationBaseCategoryList()
      if base_category not in cat_list:
        cat_list.append(base_category)
        self.system_pref.edit(preferred_product_individual_variation_base_category_list = cat_list)
    else:
      cat_list = self.system_pref.getPreferredProductVariationBaseCategoryList()
      if base_category not in cat_list:
        cat_list.append(base_category)
        self.system_pref.edit(preferred_product_variation_base_category_list = cat_list)

  def afterNewObject(self, object):
    object.validate()
    object.updateLocalRolesOnSecurityGroups()

  def _deleteContent(self, object=None, object_id=None, **kw):
    """ Move the product into "invalidated" state. """
    document = object.product_module._getOb(object_id)
    # dict which provides the list of transition to move into invalidated state
    states_list_dict = {
        'draft': ['validated_action', 'invalidate_action', ],
        'validated': ['invalidate_action', ],
    }
    # move into the "invalidated" state
    current_state = document.getValidationState()
    if current_state in states_list_dict:
      for action in states_list_dict[current_state]:
        try:
          document.portal_workflow.doActionFor(document, action)
        except WorkflowException:
          if current_state == 'draft':
            document.activate().validate()
          document.activate().invalidate()

    # Remove related line from sale supply
    sync_name = self.getIntegrationSite(kw['domain']).getTitle()
    sale_supply_line_list = [x.getObject() for x in
                             object.Base_getRelatedObjectList(portal_type="Sale Supply Line")]
    for sale_supply_line in sale_supply_line_list:
      sale_supply = sale_supply_line.getParentValue()
      if sale_supply.getTitle() == sync_name:
        sale_supply.manage_delObjects(ids=[sale_supply_line.getId(),])

  def editDocument(self, object=None, **kw):
    """
      This is the editDocument method inherit of ERP5Conduit. This method
      is used to save the information of a Product.
    """
    # Map the XML tags to the PropertySheet
    mapping = {
        'title': 'title',
        'reference': 'reference',
        'ean13': 'ean13_code',
        'description': 'description',
    }
    # Translate kw with the PropertySheet
    property = {}
    for k, v in kw.items():
      k = mapping.get(k, k)
      property[k] = v
    object._edit(**property)


  def _getPropertyMappingCell(self, resource, prop, index):
    cell_list = []
    for mapping in resource.contentValues(portal_type="Mapped Property Type"):
      if prop is not None and mapping.mapped_property != prop:
        continue
      else:
        cell_dict = {}
        for cell in mapping.contentValues():
          cat_list = cell.getCategoryList()
          lcat_list = []
          for cat in cat_list:
            base = cat.split('/', 1)[0]
            try:
              cat = resource.getPortalObject().portal_categories.restrictedTraverse(cat).getTitle()
            except KeyError:
              base, path = cat.split('/', 1)
              iv = resource.restrictedTraverse(path)
              cat = iv.getTitle()
            lcat_list.append(base+"/"+cat)
          cell_dict[str(lcat_list)] = cell
        ordered_key_list = cell_dict.keys()
        ordered_key_list.sort()
        cell_key = ordered_key_list[index-1]
        cell_list.append(cell_dict[cell_key])

    return cell_list

  def _updateXupdateUpdate(self, document=None, xml=None, previous_xml=None, **kw):
    """
      This method is called in updateNode and allows to work on the update of
      elements.
    """
    conflict_list = []
    xpath_expression = xml.get('select')
    tag_list = xpath_expression.split('/')

    base_tag = None
    remaining_tag_list = []
    for tag in tag_list:
      if not len(tag) or tag == "resource":
        continue
      elif base_tag is None:
        base_tag = tag
      else:
        remaining_tag_list.append(tag)

    new_value = xml.text
    keyword = {}

    # retrieve the previous xml etree through xpath
    previous_xml = previous_xml.xpath(xpath_expression)
    try:
      previous_value = previous_xml[0].text
    except IndexError:
      raise IndexError, 'Too little or too many value, only one is required for %s' % (
          previous_xml,
      )

    if isinstance(previous_value, unicode):
      previous_value = previous_value.encode('utf-8')

    if isinstance(new_value, unicode):
      new_value = new_value.encode('utf-8')

    # check if it'a work on product or on categories
    if base_tag.startswith('category'):
      # init base category, variation and boolean which check update
      base_category, variation = new_value.split('/', 1)
      old_base_category, old_variation = previous_value.split('/', 1)
      # retrieve the base_categories and the variations
      base_category_list = document.getVariationBaseCategoryList()
      variation_list = document.getVariationCategoryList()

      # about shared and individual variations, it's necessary to check the
      # mapping existency
      shared_variation = True
      try:
        # Try to access the category
        category = document.getPortalObject().portal_categories.restrictedTraverse(new_value)
      except KeyError:
        # This is an individual variation
        shared_variation = False

      # the mapping of an element must only be defined one time
      individual_variation = document.searchFolder(
          portal_type='Product Individual Variation',
          title=old_variation,
          base_category=old_base_category,
      )
      # If this is badly defined, fix the objects
      if len(individual_variation) > 1:
        id_to_remove = []
        for individual in individual_variation[1:]:
          id_to_remove.append(individual.getId())
        document.manage_delObjects(id_to_remove)
      if len(individual_variation) and previous_value in variation_list:
        for individual in individual_variation:
          id_to_remove.append(individual.getId())
        document.manage_delObjects(id_to_remove)

      # Update variation
      if not shared_variation:
        # work on the cases :
        # new = individual variation
        #   old = individual variation -> update
        #   old = shared variation -> remoce shared and add individual

        # Fist check individual base
        if base_category not in document.getIndividualVariationBaseCategoryList():
          base_category_list = document.getIndividualVariationBaseCategoryList()
          base_category_list.append(base_category)
          document.setIndividualVariationBaseCategoryList(base_category_list)
          self.updateSystemPreference(document.getPortalObject(), base_category, True)

        # Then update or add variation
        if len(individual_variation):
          individual_variation = individual_variation[0].getObject()
          individual_variation.setTitle(variation)
          individual_variation.setVariationBaseCategory(base_category)
        else:
          # create the individual variation
          document.newContent(
              portal_type='Product Individual Variation',
              title=variation,
              base_category=base_category,
          )
      else:
        # work on the cases :
        # new = shared variation
        #   old = individual variation -> remove individual and add shared
        #   old = shared variation -> update shared
        if len(individual_variation):
          # remove individual if previous was that
          document.manage_delObjects([individual_variation[0].getId(), ])
        else:
          # remove the shared from the list if it's a shared
          variation_list.remove(previous_value)

        # set the base category and the variations
        if base_category not in document._baseGetVariationBaseCategoryList():
          base_category_list = document._baseGetVariationBaseCategoryList()
          base_category_list.append(base_category)
          document.setVariationBaseCategoryList(base_category_list)
          self.updateSystemPreference(document.getPortalObject(), base_category)

        if new_value not in variation_list:
          variation_list.append(new_value)
          document.setVariationCategoryList(variation_list)
    elif base_tag.startswith('mapping'):
      index_value = int(base_tag[-2]) # because it is tag[index]
      if len(remaining_tag_list) > 1 or not(remaining_tag_list):
        raise NotImplementedError
      tag = remaining_tag_list[0]
      # Retrieve the mapping cell
      cell = self._getPropertyMappingCell(resource=document, prop=tag,
                                          index=index_value)[0]
      getter_id = "get%s" %(tag.capitalize(),)
      getter = getattr(cell, getter_id)
      current_value = getter()
      if isinstance(current_value, unicode):
        current_value = current_value.encode('utf-8')
      if current_value not in [new_value, previous_value]:
        conflict_list.append(self._generateConflict(document.getPhysicalPath(),
                                                    base_tag+'/'+tag,
                                                    etree.tostring(xml, encoding='utf-8'),
                                                    current_value,
                                                    new_value,
                                                    kw['signature']))
      else:
        setter_id = "set%s" %(tag.capitalize(),)
        setter = getattr(cell, setter_id)
        current_value = setter(new_value)
    else:
      # getter used to retrieve the current values and to check conflicts
      getter_value_dict = {
          'title': document.getTitle(),
          'reference': document.getReference(),
          'ean13': document.getEan13Code(),
          'description': document.getDescription(),
      }

      # create and fill a conflict when the integration site value, the erp5
      # value and the previous value are differents
      current_value = getter_value_dict[base_tag]
      if isinstance(current_value, float):
        current_value = '%.6f' % current_value
      if isinstance(current_value, unicode):
        current_value = current_value.encode('utf-8')
      if current_value not in [new_value, previous_value]:
        conflict_list.append(self._generateConflict(document.getPhysicalPath(),
                                                    base_tag,
                                                    etree.tostring(xml, encoding='utf-8'),
                                                    current_value,
                                                    new_value,
                                                    kw['signature']))
      else:
        keyword[base_tag] = new_value
        self.editDocument(object=document, **keyword)

    return conflict_list


  def _updateXupdateDel(self, document=None, xml=None, previous_xml=None, **kw):
    """ This method is called in updateNode and allows to remove elements. """
    conflict_list = []
    base_tag = None
    remaining_tag_list = []
    tag_list = xml.get('select').split('/')
    for tag in tag_list:
      if not len(tag) or tag == "resource":
        continue
      elif base_tag is None:
        base_tag = tag
      else:
        remaining_tag_list.append(tag)
    keyword = {}

    if base_tag.startswith('category'):
      # retrieve the previous xml etree through xpath
      previous_xml = previous_xml.xpath(base_tag)
      try:
        previous_value = previous_xml[0].text
      except IndexError:
        raise IndexError, 'Too little or too many value, only one is required for %s' % (
            previous_xml
        )

      if isinstance(previous_value, unicode):
        previous_value = previous_value.encode('utf-8')

      # boolean which check update
      updated = False
      # check first in shared variations
      shared_variation_list = document.getVariationCategoryList()
      if previous_value in shared_variation_list:
        updated = True
        shared_variation_list.remove(previous_value)
        document.setVariationCategoryList(shared_variation_list)
      # if no update has occured, check in individual variations
      if not updated:
        individual_variation = document.portal_catalog(
            portal_type='Product Individual Variation',
            title=previous_value.split('/', 1)[-1],
            parent_uid=document.getUid(),
        )
        if len(individual_variation) == 1:
          individual_variation = individual_variation[0].getObject()
          document.manage_delObjects([individual_variation.getId(), ])
    elif base_tag.startswith('mapping'):
      index_value = int(base_tag[-2]) # because it is tag[index]
      if not len(remaining_tag_list):
        # We are deleting a cell
        cell_list = self._getPropertyMappingCell(resource=document, prop=None,
                                            index=index_value)
        for cell in cell_list:
          line = cell.getParentValue()
          line.manage_delObjects(cell.getId())
      else:
        # We are deleting a property only
        if len(remaining_tag_list) > 1:
          raise NotImplementedError
        tag = remaining_tag_list[0]
        cell = self._getPropertyMappingCell(resource=document, prop=tag,
                                            index=index_value)[0]
        getter_id = "get%s" %(tag.capitalize(),)
        getter = getattr(cell, getter_id)
        current_value = getter()
        if isinstance(current_value, unicode):
          current_value = current_value.encode('utf-8')
        if current_value not in [new_value, previous_value]:
          conflict_list.append(self._generateConflict(document.getPhysicalPath(),
                                                      base_tag+'/'+tag,
                                                      etree.tostring(xml, encoding='utf-8'),
                                                      current_value,
                                                      new_value,
                                                      kw['signature']))
        else:
          setter_id = "set%s" %(tag.capitalize(),)
          setter = getattr(cell, setter_id)
          current_value = setter(None)
    else:
      keyword[base_tag] = None
      self.editDocument(object=document, **keyword)
    return conflict_list


  def _getCategoryDict(self, resource=None):
    """
    Build a dict title -> path for variation categories of a resource
    """
    category_dict = {}
    for category in resource.getVariationRangeCategoryList(display_base_category=1,
                                                           omit_individual_variation=0):
      base = category.split("/", 1)[0]
      category_title = resource.portal_categories.restrictedTraverse(category).getTitle()
      category_dict[base+"/"+category_title] = category

    return category_dict

  def _updateXupdateInsertOrAdd(self, document=None, xml=None, previous_xml=None, **kw):
    """ This method is called in updateNode and allows to add elements. """
    conflict_list = []
    keyword = {}
    mapping_list = []
    category_dict = self._getCategoryDict(resource=document)
    # browse subnode of the insert and check what will be create
    for subnode in xml.getchildren():
      new_tag = subnode.attrib['name']
      new_value = subnode.text
      if new_tag == 'category':
        # init base category, variation and boolean which check update
        base_category, variation = new_value.split('/', 1)
        updated = False

        # check first in shared variations
        shared_variation_list = document.getVariationCategoryList()
        if new_value not in shared_variation_list:
          # set variation if it's an existing shared variation, else
          # it's an individual
          base_category_object = document.portal_categories[base_category]
          if getattr(base_category_object, variation, None) is not None:
            updated = True
            shared_variation_list.append(new_value)
            document.setVariationCategoryList(shared_variation_list)
        # if no update has occured, check in individual variations
        if not updated and \
            new_value not in document.getVariationCategoryList():
          # individual variation list filtered on base_category
          individual_variation = [individual
              for individual in document.contentValues(
                portal_type='Product Individual Variation',
              )
              if individual.getTitle() == variation and \
                  individual.getVariationBaseCategoryList() == \
                  [base_category, ]
          ]
          if not individual_variation: # empty list
            new_variation = document.newContent(
                portal_type='Product Individual Variation',
                title=variation,
            )
            new_variation.setVariationBaseCategoryList([base_category, ])
      elif new_tag == "mapping":
        # build mapping list here
        mapping_dict = {'category' : [],}
        for item in subnode.getchildren():
          # Build the tag (without is Namespace)
          tag = item.tag
          if tag == "category":
            # Retrieve the category path
            mapping_dict['category'].append(category_dict[item.text.encode('utf-8')])
          else:
            mapping_dict[tag] = item.text.encode('utf-8')
        mapping_list.append(mapping_dict)
      else:
        if len(subnode.getchildren()):
          raise NotImplementedError
        keyword[new_tag] = new_value
        self.editDocument(object=document, **keyword)
      if len(mapping_list):
        conflict_list = self._createMapping(document, mapping_list)
        for cell, current_value, new_value in conflict_list:
          conflict_list.append(self._generateConflict(cell.getPhysicalPath(),
                                                      'mapping',
                                                      etree.tostring(xml, encoding='utf-8'),
                                                      current_value,
                                                      new_value,
                                                      kw['signature']))

    return conflict_list