#
#  Copyright (c) 2008 Helmut Merz helmutm@cy55.de
#
#  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
#

"""
Definition of the concept view classes.

$Id$
"""

from itertools import groupby
from zope import interface, component, schema
from zope.app import zapi
from zope.app.catalog.interfaces import ICatalog
from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
from zope.app.container.contained import ObjectRemovedEvent
from zope.app.form.browser.interfaces import ITerms
from zope.app.form.interfaces import IDisplayWidget
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.app.security.interfaces import IUnauthenticatedPrincipal
from zope.cachedescriptors.property import Lazy
from zope.dottedname.resolve import resolve
from zope.event import notify
from zope.formlib.form import EditForm, FormFields, setUpEditWidgets
from zope.formlib.namedtemplate import NamedTemplate
from zope.interface import implements
from zope.publisher.interfaces import BadRequest
from zope.publisher.interfaces.browser import IBrowserRequest
from zope.schema.interfaces import IIterableSource
from zope.security.proxy import removeSecurityProxy
from zope.traversing.api import getName

from cybertools.browser.action import actions
from cybertools.composer.interfaces import IInstance
from cybertools.composer.schema.interfaces import ISchemaFactory
from cybertools.typology.interfaces import IType, ITypeManager
from cybertools.util.jeep import Jeep
from loops.browser.common import EditForm, BaseView, LoopsTerms, conceptMacrosTemplate
from loops.common import adapted
from loops.concept import Concept, ConceptTypeSourceList, PredicateSourceList
from loops.i18n.browser import I18NView
from loops.interfaces import IConcept, IConceptSchema, ITypeConcept, IResource
from loops import util
from loops.util import _
from loops.versioning.util import getVersion


class ConceptEditForm(EditForm, I18NView):
    """ Classic-style (zope.formlib-based) form for editing concepts.
    """

    #@Lazy  # zope.formlib does not issue a redirect after changes, so that
            # it tries to redisplay the old form even after a type change that
            # changes the set of available attributes. So the typeInterface
            # must be recalculated e.g. after an update of the context object.
    @property
    def typeInterface(self):
        return IType(self.context).typeInterface

    @Lazy
    def title(self):
        return adapted(self.context, self.languageInfo).title

    @property
    def form_fields(self):
        typeInterface = self.typeInterface
        if typeInterface is None:
            fields = FormFields(IConcept)
        elif 'title' in typeInterface:  # new type interface based on ConceptSchema
            f1 = FormFields(IConcept).omit('title', 'description')
            fields = FormFields(typeInterface, f1)
        else:
            fields = FormFields(IConcept, typeInterface)
        return fields

    def setUpWidgets(self, ignore_request=False):
        # TODO: get rid of removeSecurityProxy(): use ConceptSchema in interfaces
        #adapter = removeSecurityProxy(adapted(self.context, self.languageInfo))
        adapter = adapted(self.context, self.languageInfo)
        self.adapters = {self.typeInterface: adapter,
                         IConceptSchema: adapter}
        self.widgets = setUpEditWidgets(
            self.form_fields, self.prefix, self.context, self.request,
            adapters=self.adapters, ignore_request=ignore_request)
        desc = self.widgets.get('description')
        if desc:
            desc.height = 2


class ConceptRelationView(BaseView):
    """ For displaying children and resources lists, used by ConceptView.
    """

    def __init__(self, relation, request, contextIsSecond=False):
        if contextIsSecond:
            self.context = relation.second
            self.other = relation.first
        else:
            self.context = relation.first
            self.other = relation.second
        self.context = getVersion(self.context, request)
        self.predicate = relation.predicate
        self.relation = relation
        self.request = request

    @Lazy
    def adapted(self):
        return adapted(self.context, self.languageInfo)

    @Lazy
    def data(self):
        return self.instance.applyTemplate()

    @Lazy
    def instance(self):
        instance = IInstance(self.adapted)
        instance.template = self.schema
        instance.view = self
        return instance

    @Lazy
    def schema(self):
        ti = self.typeInterface or IConceptSchema
        schemaFactory = component.getAdapter(self.adapted, ISchemaFactory)
        return schemaFactory(ti, manager=self, request=self.request)

    @Lazy
    def title(self):
        return self.adapted.title or getName(self.context)

    @Lazy
    def description(self):
        return self.adapted.description

    @Lazy
    def token(self):
        return ':'.join((self.loopsRoot.getLoopsUri(self.context),
                         self.loopsRoot.getLoopsUri(self.predicate)))

    @Lazy
    def uidToken(self):
        return ':'.join((util.getUidForObject(self.context),
                         util.getUidForObject(self.predicate)))

    @Lazy
    def isProtected(self):
        return getName(self.predicate) == 'hasType'

    @Lazy
    def predicateTitle(self):
        return self.predicate.title

    @Lazy
    def predicateUrl(self):
        return zapi.absoluteURL(self.predicate, self.request)

    @Lazy
    def relevance(self):
        return self.relation.relevance

    @Lazy
    def order(self):
        return self.relation.order


class ConceptView(BaseView):

    template = ViewPageTemplateFile('concept_macros.pt')
    childViewFactory = ConceptRelationView

    @Lazy
    def macro(self):
        return self.template.macros['conceptdata']

    @Lazy
    def conceptMacros(self):
        return conceptMacrosTemplate.macros

    def __init__(self, context, request):
        super(ConceptView, self).__init__(context, request)
        cont = self.controller
        if (cont is not None and not IUnauthenticatedPrincipal.providedBy(
                                                    self.request.principal)):
            cont.macros.register('portlet_right', 'parents', title=_(u'Parents'),
                         subMacro=self.template.macros['parents'],
                         priority=20, info=self)

    @Lazy
    def adapted(self):
        return adapted(self.context, self.languageInfo)

    @Lazy
    def title(self):
        return self.adapted.title or getName(self.context)

    @Lazy
    def description(self):
        return self.adapted.description

    def getData(self, omit=('title', 'description')):
        data = self.instance.applyTemplate()
        for k in omit:
            if k in data:
                del data[k]
        return data

    @Lazy
    def data(self):
        return self.getData()

    def getFields(self, omit=('title', 'description')):
        fields = Jeep(self.schema.fields)
        fields.remove(*omit)
        return fields

    @Lazy
    def fields(self):
        return self.getFields()

    @Lazy
    def schema(self):
        ti = self.typeInterface or IConceptSchema
        schemaFactory = component.getAdapter(self.adapted, ISchemaFactory)
        return schemaFactory(ti, manager=self, request=self.request)

    @Lazy
    def instance(self):
        instance = IInstance(self.adapted)
        instance.template = self.schema
        instance.view = self
        return instance

    def getChildren(self, topLevelOnly=True, sort=True):
        cm = self.loopsRoot.getConceptManager()
        hasType = cm.getTypePredicate()
        params = self.params
        criteria = {}
        if params.get('types'):
            criteria['types'] = [cm.get(name) for name in params['types']]
        standard = cm.getDefaultPredicate()
        rels = (self.childViewFactory(r, self.request, contextIsSecond=True)
                for r in self.context.getChildRelations(sort=None))
        if sort:
            rels = sorted(rels, key=lambda r: (r.order, r.title.lower()))
        for r in rels:
            if criteria:
                if not self.checkCriteria(r, criteria):
                    continue
            if topLevelOnly and r.predicate == hasType:
                # only show top-level entries for type instances:
                skip = False
                for parent in r.context.getParents((standard,)):
                    if parent.conceptType == self.context:
                        skip = True
                        break
                if skip:
                    continue
            yield r

    def checkCriteria(self, relation, criteria):
        result = True
        for k, v in criteria.items():
            if k == 'types':
                v = [item for item in v if item is not None]
                result = result and (relation.context.conceptType in v)
        return result

    # Override in subclass to control what is displayd in listings:
    children = getChildren

    def childrenAlphaGroups(self):
        result = Jeep()
        rels = self.getChildren(topLevelOnly=False, sort=False)
        rels = sorted(rels, key=lambda r: r.title.lower())
        for letter, group in groupby(rels, lambda r: r.title.lower()[0]):
            letter = letter.upper()
            result[letter] = list(group)
        return result

    def childrenByType(self):
        result = Jeep()
        rels = self.getChildren(topLevelOnly=False, sort=False)
        rels = sorted(rels, key=lambda r: (r.typeTitle.lower(), r.title.lower()))
        for type, group in groupby(rels, lambda r: r.type):
            typeName = getName(type.typeProvider)
            result[typeName] = list(group)
        return result

    def parents(self):
        rels = sorted(self.context.getParentRelations(),
                      key=(lambda x: x.first.title.lower()))
        for r in rels:
            yield self.childViewFactory(r, self.request)

    def resources(self):
        rels = self.context.getResourceRelations()
        for r in rels:
            #yield self.childViewFactory(r, self.request, contextIsSecond=True)
            from loops.browser.resource import ResourceRelationView
            yield ResourceRelationView(r, self.request, contextIsSecond=True)

    @Lazy
    def view(self):
        context = self.context
        name = self.request.get('loops.viewName') or getattr(self, '_viewName', None)
        if not name:
            ti = IType(context).typeInterface
            # TODO: check the interface (maybe for a base interface IViewProvider)
            #       instead of the viewName attribute:
            if ti and ti != ITypeConcept and 'viewName' in ti:
                typeAdapter = ti(self.context)
                name = typeAdapter.viewName
        if not name:
            tp = IType(context).typeProvider
            if tp:
               name = ITypeConcept(tp).viewName
        if name:
            if '?' in name:
                name, params = name.split('?', 1)
                ann = self.request.annotations.get('loops.view', {})
                ann['params'] = params
                self.request.annotations['loops.view'] = ann
            # ??? Would it make sense to use a somehow restricted interface
            #     that should be provided by the view like IQuery?
            #viewInterface = getattr(typeAdapter, 'viewInterface', None) or IQuery
            adapter = component.queryMultiAdapter((context, self.request),
                                                  name=name)
            if adapter is not None:
                return adapter
        #elif type provides view: use this
        return self

    def clients(self):
        from loops.browser.node import NodeView  # avoid circular import
        for node in self.context.getClients():
            yield NodeView(node, self.request)

    def getActions(self, category='object', page=None, target=None):
        t = IType(self.context)
        actInfo = t.optionsDict.get('action.' + category, '')
        actNames = [n.strip() for n in actInfo.split(',')]
        if actNames:
            return actions.get(category, actNames, view=self, page=page, target=target)
        return []


class ConceptConfigureView(ConceptView):

    def update(self):
        request = self.request
        action = request.get('action')
        if action is None:
            return True
        if action == 'create':
            self.createAndAssign()
            return True
        tokens = request.get('tokens', [])
        for token in tokens:
            parts = token.split(':')
            token = parts[0]
            if len(parts) > 1:
                relToken = parts[1]
            concept = self.loopsRoot.loopsTraverse(token)
            if action == 'assign':
                assignAs = request.get('assignAs', 'child')
                predicate = request.get('predicate') or None
                order = int(request.get('order') or 0)
                relevance = float(request.get('relevance') or 1.0)
                if predicate:
                    predicate = removeSecurityProxy(
                                    self.loopsRoot.loopsTraverse(predicate))
                if assignAs == 'child':
                    self.context.assignChild(removeSecurityProxy(concept), predicate,
                                             order, relevance)
                elif assignAs == 'parent':
                    self.context.assignParent(removeSecurityProxy(concept), predicate,
                                             order, relevance)
                elif assignAs == 'resource':
                    self.context.assignResource(removeSecurityProxy(concept), predicate,
                                             order, relevance)
                else:
                    raise(BadRequest, 'Illegal assignAs parameter: %s.' % assignAs)
            elif action == 'remove':
                predicate = self.loopsRoot.loopsTraverse(relToken)
                qualifier = request.get('qualifier')
                if qualifier == 'parents':
                    self.context.deassignParent(concept, [predicate])
                elif qualifier == 'children':
                    self.context.deassignChild(concept, [predicate])
                elif qualifier == 'resources':
                    self.context.deassignResource(concept, [predicate])
                elif qualifier == 'concepts':
                    self.context.deassignConcept(concept, [predicate])
                else:
                    raise(BadRequest, 'Illegal qualifier: %s.' % qualifier)
            else:
                    raise(BadRequest, 'Illegal action: %s.' % action)
        return True

    def createAndAssign(self):
        request = self.request
        name = request.get('create.name')
        if not name:
            raise(BadRequest, 'Empty name.')
        title = request.get('create.title', u'')
        token = self.request.get('create.type')
        type = ITypeManager(self.context).getType(token)
        factory = type.factory
        container = type.defaultContainer
        concept = removeSecurityProxy(factory(title))
        container[name] = concept
        if IConcept.providedBy(concept):
            concept.conceptType = type.typeProvider
        elif IResource.providedBy(concept):
            concept.resourceType = type.typeProvider
        notify(ObjectCreatedEvent(concept))
        notify(ObjectModifiedEvent(concept))
        assignAs = self.request.get('assignAs', 'child')
        predicate = request.get('create.predicate') or None
        if predicate:
            predicate = removeSecurityProxy(
                            self.loopsRoot.loopsTraverse(predicate))
        order = int(request.get('create.order') or 0)
        relevance = float(request.get('create.relevance') or 1.0)
        if assignAs == 'child':
            self.context.assignChild(concept, predicate, order, relevance)
        elif assignAs == 'parent':
            self.context.assignParent(concept, predicate, order, relevance)
        elif assignAs == 'resource':
            self.context.assignResource(concept, predicate, order, relevance)
        elif assignAs == 'concept':
            self.context.assignConcept(concept, predicate, order, relevance)
        else:
            raise(BadRequest, 'Illegal assignAs parameter: %s.' % assignAs)

    def search(self):
        request = self.request
        if request.get('action') != 'search':
            return []
        searchTerm = request.get('searchTerm', None)
        searchType = request.get('searchType', None)
        result = []
        if searchTerm or searchType != 'none':
            criteria = {}
            if searchTerm:
                criteria['loops_title'] = searchTerm
            if searchType:
                if searchType.endswith('*'):
                    start = searchType[:-1]
                    end = start + '\x7f'
                else:
                    start = end = searchType
                criteria['loops_type'] = (start, end)
            cat = component.getUtility(ICatalog)
            result = cat.searchResults(**criteria)
            # TODO: can this be done in a faster way?
            result = [r for r in result if r.getLoopsRoot() == self.loopsRoot]
        else:
            result = self.loopsRoot.getConceptManager().values()
        if searchType == 'none':
            result = [r for r in result if r.conceptType is None]
        return self.viewIterator(result)

    def predicates(self):
        preds = PredicateSourceList(self.context)
        terms = component.getMultiAdapter((preds, self.request), ITerms)
        for pred in preds:
            yield terms.getTerm(pred)


