##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
# Copyright (c) 2006 Nexedi SARL and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################

from Products.CMFCore import Skinnable
from Products.CMFCore.Skinnable import SKINDATA, SkinnableObjectManager

from thread import get_ident
from zLOG import LOG, WARNING, DEBUG
from Acquisition import aq_base

"""
  This patch modifies the way CMF Portal Skins gets a skin by its name from
  the right skin folder. This way, the access complexity is O(1), and not O(n)
  (n was the number of skin folders in skin selection list) any more.

  XXX: the resolve/ignore dicts used in
  CMFCoreSkinnableSkinnableObjectManager___getattr__
  implies that it's not possible to get skins from multiple skin selections
  during the same request.
"""

def _initializeCache(skin_tool, skin_folder_id_list):
    skin_list = {}
    for skin_folder_id in skin_folder_id_list[::-1]:
      try:
        skin_folder = getattr(skin_tool, skin_folder_id)
      except AttributeError:
        LOG(__name__, WARNING, 'Skin folder %s is in selection list'
                               ' but does not exist.' % skin_folder_id)
      else:
        skin_list.update(dict.fromkeys(skin_folder.objectIds(), skin_folder_id))
    return skin_list

def CMFCoreSkinnableSkinnableObjectManager_initializeCache(self):
  '''
    Initialize the cache on portal skins.
  '''
  portal_skins = getattr(self, 'portal_skins', None)
  if portal_skins is None:
    return
  portal_skins = portal_skins.aq_base
  skin_selection_mapping = {}
  for selection_name, skin_folder_id_string in portal_skins._getSelections().iteritems():
    skin_selection_mapping[selection_name] = _initializeCache(portal_skins,
      skin_folder_id_string.split(','))
  portal_skins._v_skin_location_list = skin_selection_mapping
  return skin_selection_mapping

Skinnable.SkinnableObjectManager.initializeCache = CMFCoreSkinnableSkinnableObjectManager_initializeCache

def skinResolve(self, selection, name):
  try:
    portal_skins = aq_base(self.portal_skins)
  except AttributeError:
    raise AttributeError, name
  try:
    skin_selection_mapping = portal_skins._v_skin_location_list
    reset = False
  except AttributeError:
    LOG(__name__, DEBUG, 'Initial skin cache fill.'
        ' This should not happen often. Current thread id:%X' % get_ident())
    skin_selection_mapping = self.initializeCache()
    reset = True
  while True:
    try:
      skin_folder_id = skin_selection_mapping[selection][name]
    except KeyError:
      if selection in skin_selection_mapping or \
         isinstance(selection, basestring):
        return
      skin_list = portal_skins._getSelections()[selection[0]].split(',')
      skin_selection_mapping[selection] = skin_list = _initializeCache(
        portal_skins, skin_list[1+skin_list.index(selection[1]):])
      try:
        skin_folder_id = skin_list[name]
      except KeyError:
        return
      reset = True
    try:
      return aq_base(getattr(getattr(portal_skins, skin_folder_id), name))
    except AttributeError:
      if reset:
        return
      # We cannot find a document referenced in the cache, so reset it.
      skin_selection_mapping = self.initializeCache()
      reset = True

def CMFCoreSkinnableSkinnableObjectManager___getattr__(self, name):
  '''
    Looks for the name in an object with wrappers that only reach
    up to the root skins folder.
    This should be fast, flexible, and predictable.
  '''
  if name[:1] != '_' and name[:3] != 'aq_':
    skin_info = SKINDATA.get(get_ident())
    if skin_info is not None:
      _, skin_selection_name, ignore, resolve = skin_info
      try:
        return resolve[name]
      except KeyError:
        if name not in ignore:
          object = skinResolve(self, skin_selection_name, name)
          if object is not None:
            resolve[name] = object
            return object
          ignore[name] = None
  raise AttributeError(name)

def CMFCoreSkinnableSkinnableObjectManager_changeSkin(self, skinname, REQUEST=None):
  '''
    Change the current skin.

    Can be called manually, allowing the user to change
    skins in the middle of a request.

    Patched not to call getSkin.
  '''
  if skinname is None:
    sfn = self.getSkinsFolderName()
    if sfn is not None:
      sf = getattr(self, sfn, None)
      if sf is not None:
        skinname = sf.getDefaultSkin()
  tid = get_ident()
  SKINDATA[tid] = (None, skinname, {'portal_skins': None}, {})
  if REQUEST is None:
    REQUEST = getattr(self, 'REQUEST', None)
  if REQUEST is not None:
    REQUEST._hold(SkinDataCleanup(tid, SKINDATA[tid]))

def CMFCoreSkinnableSkinnableObjectManager_getSkin(self, name=None):
  """
    Replacement for original getSkin which makes obvious possible remaining
    calls.
    FIXME: Which exception should be raised here ?
  """
  raise Exception, 'This method must not be called when new caching system is applied.'

Skinnable.SkinnableObjectManager.__getattr__ = CMFCoreSkinnableSkinnableObjectManager___getattr__
Skinnable.SkinnableObjectManager.changeSkin = CMFCoreSkinnableSkinnableObjectManager_changeSkin
Skinnable.SkinnableObjectManager.getSkin = CMFCoreSkinnableSkinnableObjectManager_getSkin

# Some original attributes from SkinnableObjectManager are explicitely set as
# value on PortalObjectBase. They must be updated there too, otherwise
# patching is incompletely available at ERP5Site class level.
from Products.CMFCore.PortalObject import PortalObjectBase
PortalObjectBase.__getattr__ = CMFCoreSkinnableSkinnableObjectManager___getattr__

# Redefine SkinDataCleanup completely.
# This class is designed to remove entries from SKINDATA dictionnary, to avoid
# keeping references to persistent objects besides transaction boundaries.
# This cleanup is triggered by deletion of SkinDataCleanup instance, which
# happens after corresponding REQUEST instance is deleted (because of '_hold'
# mechanism).
# But because of the lag which exists between REQUEST deletion and
# SkinDataCleanup deletion (due to garbage collection), it sometimes deletes a
# new SKINDATA entry, unrelated to the intended one. This leaves a system with
# no SKINDATA entry for current thread, leading to errors.
# A case where it's easy to trigger such error is CMFActivity's REQUEST
# separation mechanism, where one request is created for each single activity.

class SkinDataCleanup:
  def __init__(self, tid, skindata):
    self.tid = tid
    self.skindata_id = self.hashSkinData(skindata)

  def __del__(self):
    tid = self.tid
    if SKINDATA is None:
      return
    skindata = SKINDATA.get(tid)
    if skindata is not None:
      if self.hashSkinData(skindata) == self.skindata_id:
        try:
          # Entry might have already disapeared. Ignore.
          del SKINDATA[tid]
        except KeyError:
          pass

  def hashSkinData(self, skindata):
    return id(skindata)

