import os.path
import sys
from copy import deepcopy
from DateTime import DateTime
from StringIO import StringIO

from zope.interface import implements

from Products.Archetypes import PloneMessageFactory as _
from Products.Archetypes.interfaces import IArchetypeTool
from Products.Archetypes.interfaces import IExtensibleMetadata
from Products.Archetypes.interfaces.base import IBaseObject
from Products.Archetypes.interfaces.referenceable import IReferenceable
from Products.Archetypes.interfaces.ITemplateMixin import ITemplateMixin
from Products.Archetypes.ClassGen import generateClass
from Products.Archetypes.ClassGen import generateCtor
from Products.Archetypes.ClassGen import generateZMICtor
from Products.Archetypes.SQLStorageConfig import SQLStorageConfig
from Products.Archetypes.config import TOOL_NAME
from Products.Archetypes.config import UID_CATALOG
from Products.Archetypes.config import HAS_GRAPHVIZ
from Products.Archetypes.config import _www
from Products.Archetypes.log import log
from Products.Archetypes.utils import findDict
from Products.Archetypes.utils import DisplayList
from Products.Archetypes.utils import mapply
from Products.Archetypes.Renderer import renderer

from Products.CMFCore import permissions
from Products.CMFCore.ActionProviderBase import ActionProviderBase
from Products.CMFCore.TypesTool import FactoryTypeInformation
from Products.CMFCore.utils import getToolByName
from Products.CMFCore.utils import UniqueObject
from Products.CMFCore.interfaces import ICatalogTool
from Products.CMFCore.ActionInformation import ActionInformation
from Products.CMFCore.Expression import Expression

from AccessControl import ClassSecurityInfo
from Acquisition import ImplicitAcquisitionWrapper
from App.class_init import InitializeClass
from Persistence import PersistentMapping
from OFS.Folder import Folder
from Products.ZCatalog.interfaces import IZCatalog
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from ZODB.POSException import ConflictError
import transaction


class BoundPageTemplateFile(PageTemplateFile):

    def __init__(self, *args, **kw):
        self._extra = kw['extra']
        del kw['extra']
        args = (self,) + args
        mapply(PageTemplateFile.__init__, *args, **kw)

    def pt_render(self, source=False, extra_context={}):
        options = extra_context.get('options', {})
        options.update(self._extra)
        extra_context['options'] = options
        return PageTemplateFile.pt_render(self, source, extra_context)

_skins = os.path.join(os.path.dirname(__file__), 'skins')
_zmi = os.path.join(_www, 'zmi')
document_icon = os.path.join(_zmi, 'icons', 'document_icon.gif')
folder_icon = os.path.join(_zmi, 'icons', 'folder_icon.gif')

# This is the template that we produce our custom types from
base_factory_type_information = (
    {'id': 'Archetype',
     'content_icon': 'document_icon.gif',
     'meta_type': 'Archetype',
     'description': ('Archetype for flexible types'),
     'product': 'Unknown Package',
     'factory': 'addContent',
     'immediate_view': 'base_edit',
     'global_allow': True,
     'filter_content_types': False,
     'allow_discussion': False,
     'fti_meta_type': FactoryTypeInformation.meta_type,
     'aliases': {'(Default)': 'base_view',
                 'view': '(Default)',
                 'index.html': '(Default)',
                 'edit': 'base_edit',
                 'properties': 'base_metadata',
                 'gethtml': '',
                 'mkdir': '',
                 },
     'actions': (
         {'id': 'view',
          'title': 'View',
          'action': Expression('string:${object_url}/view'),
          'permissions': (permissions.View,),
          },

         {'id': 'edit',
          'title': 'Edit',
          'action': Expression('string:${object_url}/edit'),
          'permissions': (permissions.ModifyPortalContent,),
          'condition': Expression('not:object/@@plone_lock_info/is_locked_for_current_user')
          },

         {'id': 'metadata',
          'title': 'Properties',
          'action': Expression('string:${object_url}/properties'),
          'permissions': (permissions.ModifyPortalContent,),
          },

     ),
     }, )


def fixActionsForType(portal_type, typesTool):
    if 'actions' in portal_type.installMode:
        typeInfo = getattr(typesTool, portal_type.portal_type, None)
        if typeInfo is None:
            return
        if hasattr(portal_type, 'actions'):
            # Look for each action we define in portal_type.actions in
            # typeInfo.action replacing it if its there and just
            # adding it if not
            # rr: this is now trial-and-error programming
            # I really don't know what's going on here
            # most importantly I don't know why the default
            # actions are not set in some cases :-(
            # (maybe they are removed afterwards sometimes???)
            # if getattr(portal_type,'include_default_actions', True):
            if True:
                default = [ActionInformation(**action) for action in
                           base_factory_type_information[0]['actions']]
                next = list(typeInfo._actions)
                all = next + default
                new = [a.clone() for a in all]
            else:
                # If no standard actions are wished don't display them
                new = []

            for action in portal_type.actions:
                # DM: "Expression" derives from "Persistent" and
                # we must not put persistent objects into class attributes.
                # Thus, copy "action"
                action = action.copy()
                hits = [a for a in new if a.id == action['id']]

                # Change action and condition into expressions, if
                # they are still strings
                if 'action' in action and \
                        type(action['action']) in (type(''), type(u'')):
                    action['action'] = Expression(action['action'])
                if 'condition' in action and \
                        type(action['condition']) in (type(''), type(u'')):
                    action['condition'] = Expression(action['condition'])
                if 'name' in action:
                    action['title'] = action['name']
                    del action['name']
                if hits:
                    hits[0].__dict__.update(action)
                else:
                    new.append(ActionInformation(**action))

            typeInfo._actions = tuple(new)
            typeInfo._p_changed = True

        if hasattr(portal_type, 'factory_type_information'):
            typeInfo.__dict__.update(portal_type.factory_type_information)
            typeInfo._p_changed = True


def modify_fti(fti, klass, pkg_name):
    fti[0]['id'] = klass.__name__
    fti[0]['meta_type'] = klass.meta_type
    fti[0]['description'] = klass.__doc__
    fti[0]['factory'] = 'add%s' % klass.__name__
    fti[0]['product'] = pkg_name

    if hasattr(klass, 'content_icon'):
        fti[0]['content_icon'] = klass.content_icon

    if hasattr(klass, 'global_allow'):
        fti[0]['global_allow'] = klass.global_allow

    if hasattr(klass, 'allow_discussion'):
        fti[0]['allow_discussion'] = klass.allow_discussion

    if hasattr(klass, 'allowed_content_types'):
        allowed = klass.allowed_content_types
        fti[0]['allowed_content_types'] = allowed
        fti[0]['filter_content_types'] = allowed and True or False

    if hasattr(klass, 'filter_content_types'):
        fti[0]['filter_content_types'] = klass.filter_content_types

    if hasattr(klass, 'immediate_view'):
        fti[0]['immediate_view'] = klass.immediate_view

    if not IExtensibleMetadata.implementedBy(klass):
        refs = findDict(fti[0]['actions'], 'id', 'metadata')
        refs['visible'] = False

    # Set folder_listing to 'view' if the class implements ITemplateMixin
    if not ITemplateMixin.implementedBy(klass):
        actions = []
        for action in fti[0]['actions']:
            if action['id'] != 'folderlisting':
                actions.append(action)
            else:
                action['action'] = 'string:${folder_url}/view'
                actions.append(action)
        fti[0]['actions'] = tuple(actions)

    # Remove folderlisting action from non folderish content types
    if not getattr(klass, 'isPrincipiaFolderish', None):
        actions = []
        for action in fti[0]['actions']:
            if action['id'] != 'folderlisting':
                actions.append(action)
        fti[0]['actions'] = tuple(actions)

    # CMF 1.5 method aliases
    if getattr(klass, 'aliases', None):
        aliases = klass.aliases
        if not isinstance(aliases, dict):
            raise TypeError, "Invalid type for method aliases in class %s" % klass
        for required in ('(Default)', 'view',):
            if required not in aliases:
                raise ValueError, "Alias %s is required but not provied by %s" % (
                                  required, klass)
        fti[0]['aliases'] = aliases

    # Dynamic View FTI support
    if getattr(klass, 'default_view', False):
        default_view = klass.default_view
        if not isinstance(default_view, basestring):
            raise TypeError, "Invalid type for default view in class %s" % klass
        fti[0]['default_view'] = default_view
        fti[0]['view_methods'] = (default_view, )

        if getattr(klass, 'suppl_views', False):
            suppl_views = klass.suppl_views
            if not isinstance(suppl_views, (list, tuple)):
                raise TypeError, "Invalid type for suppl views in class %s" % klass
            if not default_view in suppl_views:
                suppl_views = suppl_views + (default_view, )
            fti[0]['view_methods'] = suppl_views
    if getattr(klass, '_at_fti_meta_type', False):
        fti[0]['fti_meta_type'] = klass._at_fti_meta_type
    else:
        if fti[0].get('fti_meta_type', False):
            klass._at_fti_meta_type = fti[0]['fti_meta_type']
        else:
            fti[0]['fti_meta_type'] = FactoryTypeInformation.meta_type


def process_types(types, pkg_name):
    content_types = ()
    constructors = ()
    ftis = ()

    for rti in types:
        typeName = rti['name']
        klass = rti['klass']
        module = rti['module']

        if hasattr(module, 'factory_type_information'):
            fti = module.factory_type_information
        else:
            fti = deepcopy(base_factory_type_information)
            modify_fti(fti, klass, pkg_name)

        # Add a callback to modifty the fti
        if hasattr(module, 'modify_fti'):
            module.modify_fti(fti[0])
        else:
            m = None
            for k in klass.__bases__:
                base_module = sys.modules[k.__module__]
                if hasattr(base_module, 'modify_fti'):
                    m = base_module
                    break
            if m is not None:
                m.modify_fti(fti[0])

        ctor = getattr(module, 'add%s' % typeName, None)
        if ctor is None:
            ctor = generateCtor(typeName, module)

        content_types += (klass,)
        constructors += (ctor,)
        ftis += fti

    return content_types, constructors, ftis


_types = {}


def registerType(klass, package):
    # Registering a class results in classgen doing its thing
    # Set up accessor/mutators and sane meta/portal_type
    generateClass(klass)

    data = {
        'klass': klass,
        'name': klass.__name__,
        'identifier': klass.meta_type.capitalize().replace(' ', '_'),
        'meta_type': klass.meta_type,
        'portal_type': klass.portal_type,
        'package': package,
        'module': sys.modules[klass.__module__],
        'schema': klass.schema,
        'signature': klass.schema.signature(),
    }

    key = '%s.%s' % (package, data['meta_type'])
    if key in _types.keys():
        existing = _types[key]
        existing_name = '%s.%s' % (
            existing['module'].__name__, existing['name'])
        override_name = '%s.%s' % (data['module'].__name__, data['name'])
        log('ArchetypesTool: Trying to register "%s" which '
            'has already been registered.  The new type %s '
            'is going to override %s' % (key, override_name, existing_name))
    _types[key] = data


def fixAfterRenameType(context, old_portal_type, new_portal_type):
    """Helper method to fix some vars after renaming a type in portal_types

    It will raise an IndexError if called with a nonexisting old_portal_type.
    If you like to swallow the error please use a try/except block in your own
    code and do NOT 'fix' this method.
    """
    at_tool = getToolByName(context, TOOL_NAME)
    __traceback_info__ = (context, old_portal_type, new_portal_type,
                          [t['portal_type'] for t in _types.values()])
    # Will fail if old portal type wasn't registered (DO 'FIX' THE INDEX
    # ERROR!)
    old_type = [t for t in _types.values()
                if t['portal_type'] == old_portal_type][0]

    # Rename portal type
    old_type['portal_type'] = new_portal_type

    # Copy old templates to new portal name without references
    old_templates = at_tool._templates.get(old_portal_type)
    at_tool._templates[new_portal_type] = deepcopy(old_templates)


def registerClasses(context, package, types=None):
    registered = listTypes(package)
    if types is not None:
        registered = filter(lambda t: t['meta_type'] in types, registered)
    for t in registered:
        module = t['module']
        typeName = t['name']
        meta_type = t['meta_type']
        portal_type = t['portal_type']
        klass = t['klass']
        ctorName = 'manage_add%s' % typeName
        constructor = getattr(module, ctorName, None)
        if constructor is None:
            constructor = generateZMICtor(typeName, module)
        addFormName = 'manage_add%sForm' % typeName
        setattr(module, addFormName,
                BoundPageTemplateFile('base_add.pt', _zmi,
                                      __name__=addFormName,
                                      extra={'constructor': ctorName,
                                             'type': meta_type,
                                             'package': package,
                                             'portal_type': portal_type}
                                      ))
        editFormName = 'manage_edit%sForm' % typeName
        setattr(klass, editFormName,
                BoundPageTemplateFile('base_edit.pt', _zmi,
                                      __name__=editFormName,
                                      extra={'handler': 'processForm',
                                             'type': meta_type,
                                             'package': package,
                                             'portal_type': portal_type}
                                      ))

        position = False
        for item in klass.manage_options:
            if item['label'] != 'Contents':
                continue
            position += 1
        folderish = getattr(klass, 'isPrincipiaFolderish', position)
        options = list(klass.manage_options)
        options.insert(position, {'label': 'Edit',
                                  'action': editFormName
                                  })
        klass.manage_options = tuple(options)
        generatedForm = getattr(module, addFormName)
        icon = folderish and folder_icon or document_icon
        if 'content_icon' in klass.__dict__:
            icon = klass.content_icon
        elif hasattr(klass, 'factory_type_information'):
            factory_type_information = klass.factory_type_information
            if 'content_icon' in factory_type_information:
                icon = factory_type_information['content_icon']

        context.registerClass(
            t['klass'],
            constructors=(generatedForm, constructor),
            visibility=None,
            icon=icon
        )


def listTypes(package=None):
    values = _types.values()
    if package:
        values = [v for v in values if v['package'] == package]

    return values


def getType(name, package):
    key = '%s.%s' % (package, name)
    return _types[key]


class WidgetWrapper:
    """Wrapper used for drawing widgets without an instance.

    E.g.: for a search form.
    """
    security = ClassSecurityInfo()
    security.declareObjectPublic()

    def __init__(self, **args):
        self._args = args

    def __call__(self):
        __traceback_info__ = self._args
        return renderer.render(**self._args)

last_load = DateTime()


class ArchetypeTool(UniqueObject, ActionProviderBase,
                    SQLStorageConfig, Folder):
    """Archetypes tool, manage aspects of Archetype instances.
    """
    id = TOOL_NAME
    meta_type = TOOL_NAME.title().replace('_', ' ')

    implements(IArchetypeTool)

    isPrincipiaFolderish = True  # Show up in the ZMI

    security = ClassSecurityInfo()

    meta_types = all_meta_types = ()

    manage_options = (
        (
            {'label': 'Types',
             'action': 'manage_debugForm',
             },

            {'label': 'Catalogs',
             'action': 'manage_catalogs',
             },

            {'label': 'Templates',
             'action': 'manage_templateForm',
             },

            {'label': 'UIDs',
             'action': 'manage_uids',
             },

            {'label': 'Update Schema',
             'action': 'manage_updateSchemaForm',
             },

            {'label': 'Migration',
             'action': 'manage_migrationForm',
             },

        ) + SQLStorageConfig.manage_options
    )

    security.declareProtected(permissions.ManagePortal,
                              'manage_uids')
    manage_uids = PageTemplateFile('viewContents', _www)
    security.declareProtected(permissions.ManagePortal,
                              'manage_templateForm')
    manage_templateForm = PageTemplateFile('manageTemplates', _www)
    security.declareProtected(permissions.ManagePortal,
                              'manage_debugForm')
    manage_debugForm = PageTemplateFile('generateDebug', _www)
    security.declareProtected(permissions.ManagePortal,
                              'manage_updateSchemaForm')
    manage_updateSchemaForm = PageTemplateFile('updateSchemaForm', _www)
    security.declareProtected(permissions.ManagePortal,
                              'manage_migrationForm')
    manage_migrationForm = PageTemplateFile('migrationForm', _www)
    security.declareProtected(permissions.ManagePortal,
                              'manage_dumpSchemaForm')
    manage_dumpSchemaForm = PageTemplateFile('schema', _www)
    security.declareProtected(permissions.ManagePortal,
                              'manage_catalogs')
    manage_catalogs = PageTemplateFile('manage_catalogs', _www)

    def __init__(self):
        self._schemas = PersistentMapping()
        self._templates = PersistentMapping()
        self._registeredTemplates = PersistentMapping()
        # meta_type -> [names of CatalogTools]
        self.catalog_map = PersistentMapping()
        self.catalog_map['Reference'] = []  # References not in portal_catalog
        # DM (avoid persistency bug): "_types" now maps known schemas to
        # signatures
        self._types = {}

    security.declareProtected(permissions.ManagePortal,
                              'manage_dumpSchema')

    def manage_dumpSchema(self, REQUEST=None):
        """XML Dump Schema of passed in class.
        """
        from Products.Archetypes.Schema import getSchemata
        package = REQUEST.get('package', '')
        type_name = REQUEST.get('type_name', '')
        spec = self.getTypeSpec(package, type_name)
        type = self.lookupType(package, type_name)
        options = {}
        options['classname'] = spec
        options['schematas'] = getSchemata(type['klass'])
        REQUEST.RESPONSE.setHeader('Content-Type', 'text/xml')
        return self.manage_dumpSchemaForm(**options)

    # Template Management
    # Views can be pretty generic by iterating the schema so we don't
    # register by type anymore, we just create per site selection
    # lists
    #
    # We keep two lists, all register templates and their
    # names/titles and the mapping of type to template bindings both
    # are persistent
    security.declareProtected(permissions.ManagePortal,
                              'registerTemplate')

    def registerTemplate(self, template, name=None):
        # Lookup the template by name
        if not name:
            obj = self.unrestrictedTraverse(template, None)
            try:
                name = obj.title_or_id()
            except:
                name = template

        self._registeredTemplates[template] = name

    security.declareProtected(permissions.View, 'lookupTemplates')

    def lookupTemplates(self, instance_or_portaltype=None):
        # Lookup templates by giving an instance or a portal_type.
        # Returns a DisplayList.
        results = []
        if not isinstance(instance_or_portaltype, basestring):
            portal_type = instance_or_portaltype.getTypeInfo().getId()
        else:
            portal_type = instance_or_portaltype
        try:
            templates = self._templates[portal_type]
        except KeyError:
            return DisplayList()
            # XXX Look this up in the types tool later
            # self._templates[instance] = ['base_view',]
            # templates = self._templates[instance]
        for t in templates:
            results.append((t, self._registeredTemplates[t]))

        return DisplayList(results).sortedByValue()

    security.declareProtected(permissions.View, 'listTemplates')

    def listTemplates(self):
        # Lists all the templates.
        return DisplayList(self._registeredTemplates.items()).sortedByValue()

    security.declareProtected(permissions.ManagePortal, 'bindTemplate')

    def bindTemplate(self, portal_type, templateList):
        # Creates binding between a type and its associated views.
        self._templates[portal_type] = templateList

    security.declareProtected(permissions.ManagePortal,
                              'manage_templates')

    def manage_templates(self, REQUEST=None):
        """Sets all the template/type mappings.
        """
        prefix = 'template_names_'
        for key in REQUEST.form.keys():
            if key.startswith(prefix):
                k = key[len(prefix):]
                v = REQUEST.form.get(key)
                self.bindTemplate(k, v)

        add = REQUEST.get('addTemplate')
        name = REQUEST.get('newTemplate')
        if add and name:
            self.registerTemplate(name)

        return REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_templateForm')

    security.declareProtected(permissions.View, 'typeImplementsInterfaces')

    def typeImplementsInterfaces(self, type, interfaces):
        # Checks if an type uses one of the given interfaces.
        if isinstance(type, dict) and 'klass' in type:
            type = type['klass']
        for iface in interfaces:
            res = iface.implementedBy(type)
            if res:
                return True
        return False

    security.declareProtected(permissions.View, 'isTemplateEnabled')

    def isTemplateEnabled(self, type):
        # Checks if an type uses ITemplateMixin.
        return self.typeImplementsInterfaces(type, [ITemplateMixin])

    security.declareProtected(
        permissions.View, 'listTemplateEnabledPortalTypes')

    def listTemplateEnabledPortalTypes(self):
        # Return a list of portal_types with ITemplateMixin
        return self.listPortalTypesWithInterfaces([ITemplateMixin])

    security.declareProtected(
        permissions.View, 'listPortalTypesWithInterfaces')

    def listPortalTypesWithInterfaces(self, ifaces):
        """Returns a list of ftis of which the types implement one of
        the given interfaces.  Only returns AT types.

        Get a list of FTIs of types implementing IReferenceable:
        >>> portal = layer['portal']
        >>> from Products.Archetypes.config import TOOL_NAME
        >>> tool = getToolByName(portal, TOOL_NAME)
        >>> ftis = tool.listPortalTypesWithInterfaces([IReferenceable])

        Sort the type ids and print them:
        >>> type_ids = [fti.getId() for fti in ftis]
        >>> type_ids.sort()
        >>> type_ids
        ['ATBIFolder', 'Collection', 'ComplexType', ...]
        """
        pt = getToolByName(self, 'portal_types')
        value = []
        for data in listTypes():
            klass = data['klass']
            for iface in ifaces:
                if iface.implementedBy(klass):
                    ti = pt.getTypeInfo(data['portal_type'])
                    if ti is not None:
                        value.append(ti)
        return value

    # Type/Schema Management
    security.declareProtected(permissions.View, 'listRegisteredTypes')

    def listRegisteredTypes(self, inProject=False, portalTypes=False):
        # Return the list of sorted types.

        def type_sort(a, b):
            v = cmp(a['package'], b['package'])
            if v != False:
                return v

            c = cmp(a['klass'].__class__.__name__,
                    b['klass'].__class__.__name__)

            if c == False:
                return cmp(a['package'], b['package'])
            return c

        values = listTypes()
        values.sort(type_sort)

        if inProject:
            # portal_type can change (as it does after ATCT-migration), so we
            # need to check against the content_meta_type of each type-info
            ttool = getToolByName(self, 'portal_types')
            types = [ti.Metatype() for ti in ttool.listTypeInfo()]
            if portalTypes:
                values = [v for v in values if v['portal_type'] in types]
            else:
                values = [v for v in values if v['meta_type'] in types]

        return values

    security.declareProtected(permissions.View, 'getTypeSpec')

    def getTypeSpec(self, package, type):
        t = self.lookupType(package, type)
        module = t['klass'].__module__
        klass = t['name']
        return '%s.%s' % (module, klass)

    security.declareProtected(permissions.View, 'listTypes')

    def listTypes(self, package=None, type=None):
        # Just the class.
        if type is None:
            return [t['klass'] for t in listTypes(package)]
        else:
            return [getType(type, package)['klass']]

    security.declareProtected(permissions.View, 'lookupType')

    def lookupType(self, package, type):
        types = self.listRegisteredTypes()
        for t in types:
            if t['package'] != package:
                continue
            if t['meta_type'] == type:
                # We have to return the schema wrapped into the acquisition of
                # something to allow access. Otherwise we will end up with:
                # Your user account is defined outside the context of the object
                # being accessed.
                t['schema'] = ImplicitAcquisitionWrapper(t['schema'], self)
                return t
        return None

    security.declareProtected(permissions.ManagePortal,
                              'manage_installType')

    def manage_installType(self, typeName, package=None,
                           uninstall=None, REQUEST=None):
        """Un/Install a type TTW.
        """
        typesTool = getToolByName(self, 'portal_types')
        try:
            typesTool._delObject(typeName)
        except (ConflictError, KeyboardInterrupt):
            raise
        except:  # XXX bare exception
            pass
        if uninstall is not None:
            if REQUEST:
                return REQUEST.RESPONSE.redirect(self.absolute_url() +
                                                 '/manage_debugForm')
            return

        typeinfo_name = '%s: %s' % (package, typeName)

        # We want to run the process/modify_fti code which might not
        # have been called
        typeDesc = getType(typeName, package)
        process_types([typeDesc], package)
        klass = typeDesc['klass']

        # get the meta type of the FTI from the class, use the default FTI as
        # default
        fti_meta_type = getattr(klass, '_at_fti_meta_type', None)
        if fti_meta_type in (None, 'simple item'):
            fti_meta_type = FactoryTypeInformation.meta_type

        typesTool.manage_addTypeInformation(fti_meta_type,
                                            id=typeName,
                                            typeinfo_name=typeinfo_name)
        t = getattr(typesTool, typeName, None)
        if t:
            t.title = getattr(klass, 'archetype_name',
                              typeDesc['portal_type'])

        # and update the actions as needed
        fixActionsForType(klass, typesTool)

        if REQUEST:
            return REQUEST.RESPONSE.redirect(self.absolute_url() +
                                             '/manage_debugForm')

    security.declarePublic('getSearchWidgets')

    def getSearchWidgets(self, package=None, type=None,
                         context=None, nosort=None):
        # Empty widgets for searching.
        return self.getWidgets(package=package, type=type,
                               context=context, mode='search', nosort=nosort)

    security.declarePublic('getWidgets')

    def getWidgets(self, instance=None,
                   package=None, type=None,
                   context=None, mode='edit',
                   fields=None, schemata=None, nosort=None):
        # Empty widgets for standalone rendering.
        widgets = []
        w_keys = {}
        context = context is not None and context or self
        instances = instance is not None and [instance] or []
        f_names = fields
        if not instances:
            for t in self.listTypes(package, type):
                instance = t('fake_instance')
                instance._at_is_fake_instance = True
                wrapped = instance.__of__(context)
                wrapped.initializeArchetype()
                instances.append(wrapped)
        for instance in instances:
            if schemata is not None:
                schema = instance.Schemata()[schemata].copy()
            else:
                schema = instance.Schema().copy()
            fields = schema.fields()
            if mode == 'search':
                # Include only fields which have an index
                # XXX duplicate fieldnames may break this,
                # as different widgets with the same name
                # on different schemas means only the first
                # one found will be used
                indexes = self.portal_catalog.indexes()
                fields = [f for f in fields
                          if (f.accessor and
                              not f.accessor in w_keys
                              and f.accessor in indexes)]
            if f_names is not None:
                fields = filter(lambda f: f.getName() in f_names, fields)
            for field in fields:
                widget = field.widget
                field_name = field.getName()
                accessor = field.getAccessor(instance)
                if mode == 'search':
                    field.required = False
                    field.addable = False  # for ReferenceField
                    if not isinstance(field.vocabulary, DisplayList):
                        field.vocabulary = field.Vocabulary(instance)
                    if '' not in field.vocabulary.keys():
                        field.vocabulary = DisplayList([('', _(u'at_search_any', default=u'<any>'))]) + \
                            field.vocabulary
                    widget.populate = False
                    field_name = field.accessor
                    # accessor must be a method which doesn't take an argument
                    # this lambda is facking an accessor
                    accessor = lambda: field.getDefault(instance)

                w_keys[field_name] = None
                widgets.append((field_name, WidgetWrapper(
                    field_name=field_name,
                    mode=mode,
                    widget=widget,
                    instance=instance,
                    field=field,
                    accessor=accessor)))
        if mode == 'search' and nosort == None:
            widgets.sort()
        return [widget for name, widget in widgets]

    security.declarePrivate('_rawEnum')

    def _rawEnum(self, callback, *args, **kwargs):
        # Finds all object to check if they are 'referenceable'.
        catalog = getToolByName(self, 'portal_catalog')
        brains = catalog(dict(id=[]))
        for b in brains:
            o = b.getObject()
            if o is not None:
                if IBaseObject.providedBy(o):
                    callback(o, *args, **kwargs)
            else:
                log('no object for brain: %s:%s' % (b, b.getURL()))

    security.declareProtected(permissions.View, 'enum')

    def enum(self, callback, *args, **kwargs):
        catalog = getToolByName(self, UID_CATALOG)
        keys = catalog.uniqueValuesFor('UID')
        for uid in keys:
            o = self.getObject(uid)
            if o:
                callback(o, *args, **kwargs)
            else:
                log('No object for %s' % uid)

    security.declareProtected(permissions.View, 'Content')

    def Content(self):
        # Return a list of all the content ids.
        catalog = getToolByName(self, UID_CATALOG)
        keys = catalog.uniqueValuesFor('UID')
        results = catalog(dict(UID=keys))
        return results

    # Management Forms
    security.declareProtected(permissions.ManagePortal,
                              'manage_doGenerate')

    def manage_doGenerate(self, sids=(), REQUEST=None):
        """(Re)generate types.
        """
        schemas = []
        for sid in sids:
            schemas.append(self.getSchema(sid))

        for s in schemas:
            s.generate()

        if REQUEST:
            return REQUEST.RESPONSE.redirect(self.absolute_url() +
                                             '/manage_workspace')

    security.declareProtected(permissions.ManagePortal,
                              'manage_inspect')

    def manage_inspect(self, UID, REQUEST=None):
        """Dump some things about an object hook in the debugger for now.
        """
        object = self.getObject(UID)
        log("uid: %s, schema: %s" % (object, object.Schema()))

        return REQUEST.RESPONSE.redirect(self.absolute_url() +
                                         '/manage_uids')

    security.declareProtected(permissions.ManagePortal,
                              'manage_reindex')

    def manage_reindex(self, REQUEST=None):
        """Assign UIDs to all basecontent objects.
        """

        def _index(object, archetype_tool):
            archetype_tool.registerContent(object)

        self._rawEnum(_index, self)

        return REQUEST.RESPONSE.redirect(self.absolute_url() +
                                         '/manage_uids')

    security.declareProtected(permissions.ManagePortal, 'index')
    index = manage_reindex

    def _listAllTypes(self):
        # List all types -- either currently known or known to us.
        allTypes = _types.copy()
        allTypes.update(self._types)
        return allTypes.keys()

    security.declareProtected(permissions.ManagePortal,
                              'getChangedSchema')

    def getChangedSchema(self):
        # Returns a list of tuples indicating which schema have changed.
        # Tuples have the form (schema, changed).
        list = []
        currentTypes = _types
        ourTypes = self._types
        modified = False
        keys = self._listAllTypes()
        keys.sort()
        for t in keys:
            if t not in ourTypes:
                # Add it
                ourTypes[t] = currentTypes[t]['signature']
                modified = True
                list.append((t, 0))
            elif t not in currentTypes:
                # Huh: what shall we do? We remove it -- this might be wrong!
                del ourTypes[t]
                modified = True
                # We do not add an entry because we cannot update
                # these objects (having no longer type information for them)
            else:
                list.append((t, ourTypes[t] != currentTypes[t]['signature']))
        if modified:
            self._p_changed = True
        return list

    security.declareProtected(permissions.ManagePortal,
                              'manage_updateSchema')

    def manage_updateSchema(self, REQUEST=None, update_all=None,
                            remove_instance_schemas=None):
        """Make sure all objects' schema are up to date.
        """
        out = StringIO()
        print >> out, 'Updating schema...'

        update_types = []
        if REQUEST is None:
            # DM (avoid persistency bug): avoid code duplication
            update_types = [ti[0] for ti in self.getChangedSchema() if ti[1]]
        else:
            # DM (avoid persistency bug):
            for t in self._listAllTypes():
                if REQUEST.form.get(t, False):
                    update_types.append(t)
            update_all = REQUEST.form.get('update_all', False)
            remove_instance_schemas = REQUEST.form.get(
                'remove_instance_schemas', False)

        # XXX: Enter this block only when there are types to update!
        if update_types:
            # Use the catalog's ZopeFindAndApply method to walk through
            # all objects in the portal.  This works much better than
            # relying on the catalog to find objects, because an object
            # may be uncatalogable because of its schema, and then you
            # can't update it if you require that it be in the catalog.
            catalog = getToolByName(self, 'portal_catalog')
            portal = getToolByName(self, 'portal_url').getPortalObject()
            meta_types = [_types[t]['meta_type'] for t in update_types]
            if remove_instance_schemas:
                func_update_changed = self._removeSchemaAndUpdateChangedObject
                func_update_all = self._removeSchemaAndUpdateObject
            else:
                func_update_changed = self._updateChangedObject
                func_update_all = self._updateObject
            if update_all:
                catalog.ZopeFindAndApply(portal, obj_metatypes=meta_types,
                                         search_sub=True, apply_func=func_update_all)
            else:
                catalog.ZopeFindAndApply(portal, obj_metatypes=meta_types,
                                         search_sub=True, apply_func=func_update_changed)
            for t in update_types:
                self._types[t] = _types[t]['signature']
            self._p_changed = True

        print >> out, 'Done.'
        return out.getvalue()

    # A counter to ensure that in a given interval a subtransaction
    # commit is done.
    subtransactioncounter = 0

    def _updateObject(self, o, path, remove_instance_schemas=None):
        o._updateSchema(remove_instance_schemas=remove_instance_schemas)
        # Subtransactions to avoid eating up RAM when used inside a
        # 'ZopeFindAndApply' like in manage_updateSchema
        self.subtransactioncounter += 1
        # Only every 250 objects a sub-commit, otherwise it eats up all
        # diskspace
        if not self.subtransactioncounter % 250:
            transaction.savepoint(optimistic=True)

    def _updateChangedObject(self, o, path):
        if not o._isSchemaCurrent():
            self._updateObject(o, path)

    def _removeSchemaAndUpdateObject(self, o, path):
        self._updateObject(o, path, remove_instance_schemas=True)

    def _removeSchemaAndUpdateChangedObject(self, o, path):
        if not o._isSchemaCurrent():
            self._removeSchemaAndUpdateObject(o, path)

    security.declareProtected(permissions.ManagePortal,
                              'manage_updateSchema')

    def manage_migrate(self, REQUEST=None):
        """Run Extensions.migrations.migrate.
        """
        from Products.Archetypes.Extensions.migrations import migrate
        out = migrate(self)
        self.manage_updateSchema()
        return out

    # Catalog management
    security.declareProtected(permissions.View,
                              'listCatalogs')

    def listCatalogs(self):
        # Show the catalog mapping.
        return self.catalog_map

    security.declareProtected(permissions.ManagePortal,
                              'manage_updateCatalogs')

    def manage_updateCatalogs(self, REQUEST=None):
        """Set the catalog map for meta_type to include the list
        catalog_names.
        """
        prefix = 'catalog_names_'
        for key in REQUEST.form.keys():
            if key.startswith(prefix):
                k = key[len(prefix):]
                v = REQUEST.form.get(key)
                self.setCatalogsByType(k, v)

        return REQUEST.RESPONSE.redirect(self.absolute_url() +
                                         '/manage_catalogs')

    security.declareProtected(permissions.ManagePortal,
                              'setCatalogsByType')

    def setCatalogsByType(self, portal_type, catalogList):
        # Associate catalogList with meta_type. (unfortunately not portal_type).
        # catalogList is a list of strings with the ids of the catalogs.
        # Each catalog is has to be a tool, means unique in site root.
        self.catalog_map[portal_type] = catalogList

    security.declareProtected(permissions.View, 'getCatalogsByType')

    def getCatalogsByType(self, portal_type):
        # Return the catalog objects assoicated with a given type.
        catalogs = []
        catalog_map = getattr(self, 'catalog_map', None)
        if catalog_map is not None:
            names = self.catalog_map.get(portal_type, ['portal_catalog'])
        else:
            names = ['portal_catalog']
        portal = getToolByName(self, 'portal_url').getPortalObject()
        for name in names:
            try:
                catalogs.append(getToolByName(portal, name))
            except (ConflictError, KeyboardInterrupt):
                raise
            except Exception, E:
                log('No tool %s' % name, E)
                pass
        return catalogs

    security.declareProtected(permissions.View, 'getCatalogsInSite')

    def getCatalogsInSite(self):
        # Return a list of ids for objects implementing ZCatalog.
        portal = getToolByName(self, 'portal_url').getPortalObject()
        res = []
        for object in portal.objectValues():
            if ICatalogTool.providedBy(object):
                res.append(object.getId())
                continue
            if IZCatalog.providedBy(object):
                res.append(object.getId())
                continue

        res.sort()

        return res

    security.declareProtected(permissions.View, 'visibleLookup')

    def visibleLookup(self, field, vis_key, vis_value='visible'):
        # Checks the value of a specific key in the field widget's
        # 'visible' dictionary.
        # Returns True or False so it can be used within a lambda as
        # the predicate for a filterFields call.
        vis_dict = field.widget.visible
        value = ''
        if vis_key in vis_dict:
            value = field.widget.visible[vis_key]
        if value == vis_value:
            return True
        else:
            return False

    def has_graphviz(self):
        # Runtime check for graphviz, used in condition on tab.
        return HAS_GRAPHVIZ

InitializeClass(ArchetypeTool)
