#
#  Copyright (c) 2014 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
#

"""
Utilities for managing version informations.
"""

from BTrees.OOBTree import OOBTree
from zope import component
from zope.component import adapts
from zope.interface import implements, Attribute
from zope.cachedescriptors.property import Lazy
from zope.schema.interfaces import IField
from zope.traversing.api import getName, getParent

from cybertools.meta.interfaces import IOptions
from cybertools.stateful.interfaces import IStateful
from cybertools.text.mimetypes import extensions
from cybertools.typology.interfaces import IType
from loops.common import adapted, baseObject
from loops.interfaces import IResource, IExternalFile
from loops.versioning.interfaces import IVersionable


_not_found = object()
attrPattern = '__version_%s__'


class VersionableResource(object):
    """ An adapter that enables a resource to store version information.
    """

    implements(IVersionable)
    adapts(IResource)

    def __init__(self, context):
        self.context = context
        self.__parent__ = context

    def getVersioningAttribute(self, attr, default):
        attrName = attrPattern % attr
        value = getattr(self.context, attrName, _not_found)
        if value is _not_found:
            return default
        return value

    def setVersioningAttribute(self, attr, value):
        attrName = attrPattern % attr
        setattr(self.context, attrName, value)

    def initVersions(self):
        attrName = attrPattern % 'versions'
        value = getattr(self.context, attrName, _not_found)
        if value is _not_found:
            versions = OOBTree()
            versionId = '.'.join('1' for x in self.versionLevels)
            #versions['1.1'] = self.context
            versions[versionId] = self.context
            setattr(self.context, attrName, versions)

    @Lazy
    def versionLevels(self):
        options = IOptions(self.master.getLoopsRoot()).useVersioning
        if options:
            if isinstance(options, list):
                return options
            return ['major', 'minor']
        return []

    @Lazy
    def versionNumbers(self):
        #return self.getVersioningAttribute('versionNumbers', (1, 1))
        return self.getVersioningAttribute('versionNumbers',
                                    tuple(1 for x in self.versionLevels))

    @Lazy
    def variantIds(self):
        return self.getVersioningAttribute('variantIds', ())

    @Lazy
    def versionId(self):
        versionPart = '.'.join(str(n) for n in self.versionNumbers)
        return '_'.join([versionPart] + list(self.variantIds))

    @Lazy
    def master(self):
        return self.getVersioningAttribute('master', self.context)

    @Lazy
    def versionableMaster(self):
        """ The adapted master... """
        return IVersionable(self.master)

    @Lazy
    def parent(self):
        return self.getVersioningAttribute('parent', None)

    @Lazy
    def comment(self):
        return self.getVersioningAttribute('comment', u'')

    @property
    def versions(self):
        return self.versionableMaster.getVersioningAttribute('versions', {})

    @property
    def currentVersion(self):
        return self.versionableMaster.getVersioningAttribute('currentVersion', self.master)

    @property
    def releasedVersion(self):
        m = self.versionableMaster
        return self.versionableMaster.getVersioningAttribute('releasedVersion', None)

    def getNotVersioned(self):
        m = self.versionableMaster
        return self.versionableMaster.getVersioningAttribute('notVersioned', False)
    def setNotVersioned(self, value):
        self.versionableMaster.setVersioningAttribute('notVersioned', bool(value))
    notVersioned = property(getNotVersioned, setNotVersioned)

    def createVersionObject(self, versionNumbers, variantIds, comment=u''):
        versionableMaster = self.versionableMaster
        versionableMaster.initVersions()
        context = self.context
        # create object
        cls = context.__class__
        obj = cls()
        # set versioning attributes of new object
        versionableObj = IVersionable(obj)
        versionableObj.setVersioningAttribute('versionNumbers', tuple(versionNumbers))
        versionableObj.setVersioningAttribute('variantIds', variantIds)
        versionableObj.setVersioningAttribute('master', self.master)
        versionableObj.setVersioningAttribute('parent', context)
        versionableObj.setVersioningAttribute('comment', comment)
        # generate name for new object, register in parent
        versionId = versionableObj.versionId
        name = self.generateName(getName(self.master),
                                 extensions.get(context.contentType, ''),
                                 versionId)
        getParent(context)[name] = obj
        # record version on master
        self.versions[versionableObj.versionId] = obj
        # set resource attributes
        ti = IType(context).typeInterface
        attrs = set((ti and list(ti) or [])
                    + ['title', 'description', 'data', 'contentType'])
        adaptedContext = ti and ti(context) or context
        adaptedObj = ti and ti(obj) or obj
        if 'context' in attrs:
            attrs.remove('context')
        if IExternalFile.providedBy(adaptedObj):
            for name in ('data', 'externalAddress',):
                attrs.remove(name)
        for attr in attrs:
            value = getattr(adaptedContext, attr)
            if not callable(value):
                setattr(adaptedObj, attr, value)
        if IExternalFile.providedBy(adaptedObj):
            adaptedObj.storageParams = adaptedContext.storageParams
            adaptedObj.storageName = adaptedContext.storageName
            extAddr = adaptedContext.externalAddress
            newExtAddr = self.generateName(adapted(self.master).externalAddress,
                                 extensions.get(context.contentType, ''),
                                 versionId)
            adaptedObj.externalAddress = newExtAddr
            adaptedObj.data = adaptedContext.data
            adaptedObj.externalAddress = newExtAddr # trigger post-processing
        self.setStates(obj)
        return obj

    def createVersion(self, level=1, comment=u''):
        # get the new version numbers
        vn = list(IVersionable(self.currentVersion).versionNumbers)
        while len(vn) <= level:
            vn.append(1)
        vn[level] += 1
        for l in range(level+1, len(vn)):
            # reset lower levels
            vn[l] = 1
        # create new object
        obj = self.createVersionObject(vn, self.variantIds, comment)
        # set attributes of the master version
        self.versionableMaster.setVersioningAttribute('currentVersion', obj)
        return obj

    def generateName(self, name, ext, versionId):
        if ext:
            if ext and name.endswith(ext):
                name = name[:-len(ext)]
        else:
            parts = name.rsplit('.', 1)
            if len(parts) == 2 and len(parts[1]) <= 4:
                name, ext = parts
        #elif len(name) > 3 and name[-4] == '.':
        #    ext = name[-4:]
        #    name = name[:-4]
        if not ext.startswith('.'):
            ext = '.' + ext
        return name + '_' + versionId + ext

    def setStates(self, obj):
        options = IOptions(self.master.getLoopsRoot()).organize.stateful.resource
        if options:
            for name in options:
                stf = component.getAdapter(self.context, IStateful, name=name)
                stfn = component.getAdapter(obj, IStateful, name=name)
                stfn.state = stf.state


def cleanupVersions(context, event):
    """ Called upon deletion of a resource or concept.
    """
    vContext = IVersionable(context, None)
    if vContext is None:
        return
    #rm = context.getLoopsRoot().getResourceManager()
    rm = getParent(context)
    if context == vContext.master:
        toBeDeleted = []
        for v in vContext.versions.values():
            if v != context:
                toBeDeleted.append(getName(v))
        for name in toBeDeleted:
            # TODO: notify(ObjectRemovedEvent(rm[name]))
            del rm[name]
    else:
        vId = vContext.versionId
        vMaster = vContext.versionableMaster
        if vId in vMaster.versions:
            del vMaster.versions[vId]
        if vMaster.getVersioningAttribute('currentVersion', None) == context:
            newCurrent = sorted(vMaster.versions.items())[-1][1]
            vMaster.setVersioningAttribute('currentVersion', newCurrent)

