
# Copyright (c) 2006 Nexedi SARL and Contributors. All Rights Reserved.
#                    Romain Courteaud <romain@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 AccessControl import ClassSecurityInfo

from Products.ERP5Type import Permissions, PropertySheet
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Type.Tool.BaseTool import BaseTool
from Products.ERP5OOo.OOoUtils import CorruptedOOoFile

class Answer(XMLObject):
  """
  A Answer is contained in an Answer Set and related to a Question.
  It holds a value in one of its value fields depending on the value type.
  """
  meta_type = 'ERP5 Answer'
  portal_type = 'Answer'
  isDelivery = ConstantGetter('isDelivery', value=True)

  add_permission = Permissions.AddPortalContent
  isPortalContent = 1
  isRADContent = 1
  content_md5 = None
  category_dict = None
  
  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  # Declarative properties
  property_sheets = (
             PropertySheet.Base,
             PropertySheet.XMLObject,
             PropertySheet.CategoryCore,
             PropertySheet.DublinCore,
	           PropertySheet.AnswerSet,
             PropertySheet.Answer,)

  security.declarePrivate('manage_afterAdd')
  def manage_afterAdd(self, item, container) :
    super( XMLObject, self).manage_afterAdd(item, container)
    item.setIdGenerator('_generatePerDayId')
    BaseTool.inheritedAttribute('manage_afterAdd')(self, item, container)
             
  def getTitle(self):
    """Return the id of the Answer
    """
    return self.getId()

  def documentExists(self):
    question = self.getResourceValue()
    document_list = self.getAggregateValueList()
    if question is None:
      return False
    else:
      if question.getPortalType() != 'Document Question' or len(document_list) == 0 or document_list[0] is None:
        return False
      else:
        return True

  def correctDocument(self):
    """
       Return False if No Document
    """
    if self.documentExists():
      document_list = self.getAggregateValueList()
      document_md5 = document_list[0].getContentMd5()
      if self.content_md5 != document_md5 or self.getValidationState() == 'draft':
        self._setCategoryDict(self.getCategoriesSpreadsheetMapping(base_data = document_list[0].getBaseData()))
        self.validate()
        self.content_md5 = document_md5
      return True
    else:
      return False

  def getContentMd5(self):
    if not self.correctDocument():
      self.content_md5 = ''
    return self.content_md5

  def _setContentMd5(self, value = None):
    if value is None:
      self.getContentMd5()
    else:
      self.content_md5 = value

  def getCategoryDict(self, simulation_mode = False):
    question = self.getResourceValue()
    if question is not None:
      if question.getPortalType() == 'Category Sheet Question':
        return self.category_dict
    if not (simulation_mode or self.correctDocument()):
      self.category_dict = None
    return self.category_dict

  def setCategoryDict(self, cat_dict):
    self._setCategoryDict(cat_dict)

  def _setCategoryDict(self, cat_dict):
    self.category_dict = cat_dict

  def getCategoriesSpreadsheetMapping(self,import_file = None , invalid_spreadsheet_error_handler=None, base_data = ''):

    """Parses a spreadsheet containing categories and returns a mapping.

    `import_file` must be a spreadsheet in a format supported by openoffice

    `invalid_spreadsheet_error_handler` is the callback method that will be called if
    the spreadsheet is invalid. The method must accept one parameter, an
    explanation of the error.
    The error handler can return a boolean, true meaning that the rest of the
    spreadsheet have to be processed, false meaning that the processing should
    stop.
    If no error_callback is given, the default action is to raise a ValueError on
    the first error encountered.

    The returned mapping has the following structure:
      
      { 'base_category_id':
           # list of category info
           ( { 'path': 'bc/1',
               'id': '1',
               'title': 'Title 1' },
             { 'path': 'bc/1/2'
               'id': '2',
               'title': 'Title 2' }, ), }

    This scripts guarantees that the list of category info is sorted in such a
    way that parent always precedes their children.
    """
    from Products.ERP5Type.Message import translateString
    from Products.ERP5OOo.OOoUtils import OOoParser
    parser = OOoParser()
    category_list_spreadsheet_mapping = dict()
    error_list = []

    def default_invalid_spreadsheet_error_handler(error_message):
      raise ValueError(error_message)

    if invalid_spreadsheet_error_handler is None:
      invalid_spreadsheet_error_handler = default_invalid_spreadsheet_error_handler

    # FIXME: we are actually only interested in properties defined on Category
    # type information.
    property_id_list = self.portal_classes.getPropertySheetPropertyIdList()


    def getIDFromString(string=None):
      """
        This function transform a string to a safe and beautiful ID.
        It is used here to create a safe category ID from a string.
        But the code is not really clever...
      """
      if string is None:
        return None
      clean_id = ''
      translation_map = { 'a'  : [u'\xe0', u'\xe3']
                        , 'e'  : [u'\xe9', u'\xe8']
                        , 'i'  : [u'\xed']
                        , 'u'  : [u'\xf9']
                        , '_'  : [' ', '+']
                        , '-'  : ['-', u'\u2013']
                        , 'and': ['&']
                        }
      # Replace odd chars by safe ascii
      string = string.lower()
      string = string.strip()
      for (safe_char, char_list) in translation_map.items():
        for char in char_list:
          string = string.replace(char, safe_char)
      # Exclude all non alphanumeric chars
      for char in string:
        if char.isalnum() or char in translation_map.keys():
          clean_id += char
      # Delete leading and trailing char which are not alpha-numerics
      # This prevent having IDs with starting underscores
      while len(clean_id) > 0 and not clean_id[0].isalnum():
        clean_id = clean_id[1:]
      while len(clean_id) > 0 and not clean_id[-1].isalnum():
        clean_id = clean_id[:-1]

      return clean_id

    # if the file is not an open office format, try to convert it using oood
    # FIXME: use portal_transforms
    content_type = 'unknown'
    if import_file is None or not import_file:
      from Products.ERP5Type.Document import newTempOOoDocument
      tmp_ooo = newTempOOoDocument(self, "_")
      tmp_ooo.edit(data=base_data,
                 content_type=content_type)
      tmp_ooo.convertToBaseFormat()
      ignored, import_file_content = tmp_ooo.convert('sxc')
      parser.openFromString(str(import_file_content))
    else:
      if hasattr(import_file, 'headers'):
        content_type = import_file.headers.get('Content-Type', '')
      if not (content_type.startswith('application/vnd.sun.xml')
         or content_type.startswith('application/vnd.oasis.opendocument')):
        from Products.ERP5Type.Document import newTempOOoDocument
        tmp_ooo = newTempOOoDocument(self, "_")
        tmp_ooo.edit(data=import_file.read(),
                   content_type=content_type)
        tmp_ooo.convertToBaseFormat()
        ignored, import_file_content = tmp_ooo.convert('sxc')
        parser.openFromString(str(import_file_content))
      else:
        parser.openFile(import_file)

    # Extract tables from the speadsheet file
    filename = parser.getFilename()
    spreadsheet_list = parser.getSpreadsheetsMapping(no_empty_lines=True)


    for table_name in spreadsheet_list.keys():
      sheet = spreadsheet_list[table_name]
      if not sheet:
        continue
      # Get the header of the table
      columns_header = sheet[0]
      # Get the mapping to help us know the property according a cell index
      property_map = {}
      column_index = 0
      path_index = 0
      for column in columns_header:
        column_id = getIDFromString(column)
        # This give us the information that the path definition has started
        path_def_started = 'path_0' in property_map.values()
        # The path of the category has started to be expressed
        if column_id == 'path':
          property_map[column_index] = 'path_' + str(path_index)
          path_index += 1
        # The column has no header information
        elif column_id in (None, ''):
          # Are we in the middle of the path definition ?
          # If the path definition has started and not ended
          if path_def_started and path_index is not None:
            property_map[column_index] = 'path_' + str(path_index)
            path_index += 1
          # else : The path definition is not started or is finished, so ignore the column
        # The column has a normal header
        else:
          # If there is a new column with a header and the path definition has
          # started, that seems the path definition has ended
          if 'path_0' in property_map.values():
            path_index == None # FIXME: useless statement, but what was the original intention ??
          property_map[column_index] = column_id.encode('utf8')
        column_index += 1

      # Construct categories data (with absolute path) from table lines
      # The first category is the Base category
      # 1 table = 1 base category
      base_category_name = table_name
      base_category_id = getIDFromString(base_category_name)
      if type(base_category_name) == type(u''):
        base_category_name = base_category_name.encode('utf8')
      if type(base_category_id) == type(u''):
        base_category_id = base_category_id.encode('utf8')
      category_list = category_list_spreadsheet_mapping.setdefault(base_category_id, dict())

      # This path_element_list help us to reconstruct the absolute path
      path_element_list = []
      line_index = 2
      for line in sheet[1:]:
        # Exclude empty lines
        if line.count('') + line.count(None) == len(line):
          continue

        # Prefetch line datas
        line_data = {}
        path_defined = []
        for cell_index, cell in enumerate(line):
          # Get the property corresponding to the cell data
          property_id = property_map[cell_index]
          if cell is not None and cell.strip()=='':
            # empty string is NOT a valid identifier
            cell=None
          line_data[property_id] = cell
          if cell and property_id.startswith('path_'):
            path_defined.append(cell)
            if len(path_defined) > 1:
              invalid_spreadsheet_error_handler(
                  translateString("More that one path is defined in ${table}"
                  " at line ${line}: ${path_defined}",
                      mapping=dict(path_defined=repr(path_defined),
                                   table=table_name,
                                   line=line_index)))

        # Analyse every cell of the line
        category_property_list = {}
        cell_index = 0
        for (property_id, cell_data) in line_data.items():

          # Try to generate a cell id from cell data
          cell_id = getIDFromString(cell_data)
          # Returned cell_id can be None or '' (empty string). Both have different meaning:
          #   None : no data was input by the user.
          #   ''   : data entered by the user, but no good transformation of the string to a safe ID.

          # If the cell_id tranformation return an empty string, and if the cell is a path item,
          # we should try to use other line data to get a safe id.
          if cell_id == '' and property_id.startswith('path_'):
            for alt_id_source in ['id', 'title']:
              if line_data.has_key(alt_id_source):
                cell_id = getIDFromString(line_data[alt_id_source])
                if cell_id not in ('', None):
                  break

          # Ignore empty cells
          if cell_id not in ('', None):
            # Handle normal properties
            if not property_id.startswith('path_'):
              if type(cell_data) == type(u''):
                cell_data = cell_data.encode('utf8')
              category_property_list[property_id] = cell_data
            # Handle 'path' property
            else:
              path_element_id = cell_id
              # Initialize the list of path elements to the cell element
              absolute_path_element_list = [path_element_id,]
              # Get the depth of the current element
              element_depth = int(property_id[5:]) # 5 == len('path_')
              # Get a path element for each depth level to reach the 0-level
              for searched_depth in range(element_depth)[::-1]:
                # Get the first path element that correspond to the searched depth
                for element in path_element_list[::-1]:
                  if element['depth'] == searched_depth:
                    # Element found, add it to the list
                    absolute_path_element_list.append(element['value'])
                    # Get the next depth
                    break
              path = '/'.join([base_category_id,] + absolute_path_element_list[::-1])
              if type(path) == type(u''):
                path = path.encode('utf8')
              category_property_list['path'] = path

              # Save the current raw path item value as title if no title column defined
              if 'title' not in category_property_list.keys():
                clean_title = cell_data.strip()
                # Only set title if it look like a title
                # (i.e. its tranformation to ID is not the same as the original value)
                if clean_title != cell_id:
                  category_property_list['title'] = clean_title

              # Detect invalid IDs
              if path_element_id in property_id_list:
                cont = invalid_spreadsheet_error_handler(
                          translateString("The ID ${id} in ${table} at line ${line} is invalid, it's a reserved property name",
                            mapping=dict(id=path_element_id, table=table_name, line=line_index)))
                if not cont:
                  return 

              # Detect duplicate IDs
              for element in path_element_list[::-1]:
                if element['depth'] != element_depth:
                  break
                if element['value'] == path_element_id:
                  cont = invalid_spreadsheet_error_handler(
                          translateString(
                          "Duplicate ID found in ${table} at line ${line} : ${id}",
                            mapping=dict(id=element['value'], table=table_name, line=line_index)))
                  if not cont:
                    return


              # Detect wrong hierarchy
              if path_element_list:
                current_depth = element_depth
                for element in path_element_list[::-1]:
                  if element['depth'] > current_depth:
                    break # we are now on another branch
                  if element['depth'] == current_depth:
                    continue # we are on the same level
                  elif element['depth'] == (current_depth - 1):
                    current_depth = element['depth']
                    continue # we are on the direct parent (current level - 1)
                  else:
                    cont = invalid_spreadsheet_error_handler(
                            translateString(
                               "Wrong hierarchy found for ID ${id} and depth ${depth} in ${table} at line ${line} ",
                                 mapping=dict(id=path_element_id,
                                           depth=element_depth, table=table_name, line=line_index)))
                    if not cont:
                      return


              # Save the path element
              path_element_list.append({ 'depth': element_depth
                                   , 'value': path_element_id
                                   })

          # Proceed to next cell
          cell_index += 1
        line_index += 1
        if len(category_property_list) > 0 and 'path' in category_property_list.keys():
          category_property_list['line_index'] = line_index
          category_property_list['parent'] = '/'.join(category_property_list['path'].split('/')[0:-1])
          category_list[category_property_list['path']] = category_property_list
    if error_list:
      return {'error_list':error_list}
    else:
      return category_list_spreadsheet_mapping
