from AccessControl import ModuleSecurityInfo
from Acquisition import aq_base
from Interface import Implements
from OFS.CopySupport import CopySource

from Products.CMFCore.utils import getToolByName
from Products.CMFCore import permissions

try:
    from Products.CMFCore.PortalFolder import PortalFolderBase
except ImportError: # catch CMF 1.4 installations here
    from Products.CMFCore.PortalFolder import PortalFolder
    PortalFolderBase = PortalFolder

from Products.Archetypes.ArchetypeTool import listTypes
from Products.Relations.brain import makeBrainAggregate
from Products.Relations.config import RELATIONS_LIBRARY
from zope import interface

modulesec = ModuleSecurityInfo('Products.Relations.utils')

class AllowedTypesByIface:
    """An approach to restrict allowed content types in a container by
    the interfaces they implement.

    Notice that extending this class means surpassing allowed_content_types,
    filter_content_types etc in the FTI, while we are still concerned about
    security."""

    # XXX: This class depends heavily on implementation details in CMF's
    #      PortalFolder. We need to eventually implement our own FTI, which
    #      should give us much cleaner code.

    allowed_interfaces = () # Don't allow anything, subclasses overwrite!

    def allowedContentTypes(self):
        """Redefines CMF PortalFolder's allowedContentTypes."""
        return self._getTIsByInterfaces(self.allowed_interfaces)

    def invokeFactory(self, type_name, id, RESPONSE = None, *args, **kwargs):
        """Invokes the portal_types tool.

        Overrides PortalFolder.invokeFactory."""
        goodType = self._portalTypeImplementsOneOfInterfaces
        if not goodType(type_name, self.allowed_interfaces):
            raise ValueError, "Type %s does not implement any of %s." % \
                  (type_name, self.allowed_interfaces)
        
        pt = getToolByName(self, 'portal_types')
        args = (type_name, self, id, RESPONSE) + args
        # XXX: PloneFolder returns an id. Do we really need to do that, too?
        new_id = pt.constructContent(*args, **kwargs)
        if not new_id: new_id = id
        return new_id

    def _verifyObjectPaste(self, object, validate_src=1):
        """Overrides PortalFolder._verifyObjectPaste."""
        # What we do here is trick PortalFolder._verifyObjectPaste in its check
        # for allowed content types. We make our typeinfo temporarily
        # unavailable.
        pt = getToolByName(self, 'portal_types')
        tmp_name = '%s_TMP' % self.portal_type
        ti = pt.getTypeInfo(self.portal_type)
        pt.manage_delObjects([self.portal_type])
        value = PortalFolderBase._verifyObjectPaste(self, object, validate_src)
        pt._setObject(self.portal_type, ti)
        return value

    def _getTIsByInterfaces(self, ifaces):
        """Returns a list of type info objects of available (AT) portal types
        implementing iface."""
        pt = getToolByName(self, 'portal_types')
        value = []
        for data in listTypes():
            klass = data['klass']
            for iface in self.allowed_interfaces:
                if iface.isImplementedByInstancesOf(klass):
                    ti = pt.getTypeInfo(data['portal_type'])
                    if ti is not None and ti.isConstructionAllowed(self):
                        value.append(ti)
        return value

    def _portalTypeImplementsOneOfInterfaces(self, portal_type, ifaces):
        """Does the given portal_type implement one of the given interfaces?"""
        klass = self._getClassByPortalType(portal_type)
        for iface in ifaces:
            if iface.isImplementedByInstancesOf(klass):
                return 1

    def _getClassByPortalType(self, name):
        for data in listTypes():
            if data['portal_type'] == name:
                return data['klass']
        raise ValueError, "No such portal type: %s" % name
        

def getPortalTypesByInterfaces(context, allowedIfaces):
    """Returns a list of portal type names of which the types implement any
    of the given interfaces.

    Note that allowedIfaces are strings, not Interfaces."""
    value = []
    tool = getToolByName(context, 'archetype_tool')
    for data in tool.listRegisteredTypes():
        klass = data['klass']
        kifaces = klass.__implements__

            
        # We now flatten the interfaces and get their unqualified names,
        # e.g. <Products.FooBar.interfaces.ISpam> becomes "ISpam"
        # we support now both interfaces for zope2 and zope3
        
        kifaces_z2 = [str(iface.__name__)
                   for iface in Implements.flattenInterfaces((kifaces,))]
                   
        kifaces_z3 = [str(iface.__name__)
                   for iface in interface.implementedBy(klass).flattened()]
                   
        kifaces = kifaces_z2 + kifaces_z3
                   
        if [iface for iface in allowedIfaces if iface in kifaces]:
            value.append(data['portal_type'])

    return value

def getReferenceableTypes(context):
    """Return a list of portal type strings, with all types in the
    types tool that are referenceable."""
    attool = getToolByName(context, 'archetype_tool')
    pttool = getToolByName(context, 'portal_types')
    attypes = [d['portal_type'] for d in attool.listRegisteredTypes()]
    return [t for t in pttool.listContentTypes() if t in attypes]
    

modulesec.declarePublic('isReferenceable')
def isReferenceable(context):
    if getattr(aq_base(context), 'isReferenceable', None) is not None:
        return True
    else:
        return False

## TODO:
##  resolve permission discrepancies between this method and the action that calls it
##  as a condition
modulesec.declarePublic('adddeleteVocab')
def adddeleteVocab(context, test_only=0, ruleset_ids=None):
    """Make Relations vocabularies by querying all rulesets.

    What we return here is a list of dictionaries with the following
    keys:
    
      - id      the ruleset's id, which is the relationship
      - title   the ruleset's title
      - tuples  a list of tuples, see below.

    Each 'tuples' item is a 2-tuple in the form (aggregated brain,
    selected), where selected is true if there's a reference
    (source=context, target=vocab item, relationship=ruleset.id).

    If test_only=1, I will return true if *any* vocabulary item
    exists. Otherwise I will return a false value.
    """

    value = []

    membership = getToolByName(context, 'portal_membership')
    if not membership.checkPermission(permissions.ModifyPortalContent,
                                      context):
        return []

    if not isReferenceable(context):
        return []

    library = context.relations_library
    refctl = context.reference_catalog
    mtool = context.portal_membership

    if not ruleset_ids:
        rulesets = library.getRulesets()
        if None in rulesets:
            #XXX: to be fixed
            rulesets=[r for r in rulesets if r]
    else:
        rulesets = [library.getRuleset(rid) for rid in ruleset_ids]

    for ruleset in rulesets:
        if not mtool.checkPermission('View', ruleset): continue
        relationship = ruleset.getId()
        vocabulary = ruleset.makeVocabulary(context)
        if vocabulary is None: vocabulary = []
        refs = refctl(sourceUID=context.UID(), relationship=relationship)
        existing_targetUIDs = [r.targetUID for r in refs]
        if not vocabulary and not existing_targetUIDs:
            continue # we don't add an entry
        elif test_only:
            return True

        entry = {'id': relationship,
                 'title': ruleset.Title(),
                 'tuples': []}
        value.append(entry)

        tuples = entry['tuples']

        # first add all vocabulary items to 'tuples' in entry dict
        for item in vocabulary:
            selected = False
            if item.UID in existing_targetUIDs:
                selected = True
            tuples.append((item, selected))

        # now find existing refs that are not part of the vocab, which we add
        vocab_uids = [item.UID for (item, selected) in tuples]
        for uid in existing_targetUIDs:
            if uid not in vocab_uids:
                tuples.append((makeBrainAggregate(context, uid), True))

    return value

modulesec.declarePublic('getRulesetIds')        
def getRulesetIds(library, rulesetnames=[]):
    """
    Get flat list ruleset ids in named relation library subcollections
    """
    ruleset_ids = []
    if not rulesetnames:
        rulesetnames = library.objectIds()
    for name in rulesetnames:
        collection = library[name]
        ruleset_ids += [ruleset.getId() for ruleset in collection.getRulesets()]
    return ruleset_ids

modulesec.declarePublic('getRelations')        
def getRelations(context, rulesetnames=[]):
    """
    Returns all relations, filter by rulesets if needed.
    """
    result = []
    library = getToolByName(context, RELATIONS_LIBRARY)
    ruleset_ids = getRulesetIds(library, rulesetnames=rulesetnames)
    for ref in context.getReferenceImpl():
        if ref.relationship in ruleset_ids:
            ruleset = library.getRuleset(ref.relationship)
            result.append({
                'ruleset': ruleset,
                'reference': ref,
                'targetobj': ref.getTargetObject(),
                'contentobj': ref.getContentObject(),
                'actions': ruleset.listActionsFor(ref)
                })
    return result
