##############################################################################
#
# Copyright (c) 2002 Zope Foundation 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.
#
##############################################################################
"""Computed attributes based on schema fields
"""

from copy import copy
import sys
import zope.schema
from zope import interface
from zope import event
from zope.schema import interfaces
from zope.schema._bootstrapinterfaces import NO_VALUE

_marker = object()


@interface.implementer(interfaces.IFieldUpdatedEvent)
class FieldUpdatedEvent(object):

    def __init__(self, inst, field, old_value, new_value):
        self.inst = inst
        self.field = field
        self.old_value = old_value
        self.new_value = new_value


class FieldProperty(object):
    """Computed attributes based on schema fields

    Field properties provide default values, data validation and error messages
    based on data found in field meta-data.

    Note that FieldProperties cannot be used with slots. They can only
    be used for attributes stored in instance dictionaries.
    """

    def __init__(self, field, name=None):
        if name is None:
            name = field.__name__

        self.__field = field
        self.__name = name

    def __get__(self, inst, klass):
        if inst is None:
            return self

        value = inst.__dict__.get(self.__name, _marker)
        if value is _marker:
            field = self.__field.bind(inst)
            value = getattr(field, 'default', _marker)
            if value is _marker:
                raise AttributeError(self.__name)

        return value

    def queryValue(self, inst, default):
        value = inst.__dict__.get(self.__name, default)
        if value is default:
            field = self.__field.bind(inst)
            value = getattr(field, 'default', default)
        return value

    def __set__(self, inst, value):
        field = self.__field.bind(inst)
        field.validate(value)
        if field.readonly and self.__name in inst.__dict__:
            raise ValueError(self.__name, 'field is readonly')
        oldvalue = self.queryValue(inst, NO_VALUE)
        inst.__dict__[self.__name] = value
        event.notify(FieldUpdatedEvent(inst, field, oldvalue, value))

    def __getattr__(self, name):
        return getattr(self.__field, name)


def createFieldProperties(schema, omit=[]):
    """Creates a FieldProperty fields in `schema` on the class it is called on.

    schema ... interface those fields should be added to class
    omit ... list of field names to be omitted in creation

    """
    frame = sys._getframe(1)
    for name in zope.schema.getFieldNamesInOrder(schema):
        if name in omit:
            continue
        frame.f_locals[name] = FieldProperty(schema[name])


class FieldPropertyStoredThroughField(object):

    def __init__(self, field, name=None):
        if name is None:
            name = field.__name__

        self.field = copy(field)
        self.field.__name__ = "__st_%s_st" % self.field.__name__
        self.__name = name

    def setValue(self, inst, field, value):
        field.set(inst, value)

    def getValue(self, inst, field):
        return field.query(inst, _marker)

    def queryValue(self, inst, field, default):
        return field.query(inst, default)

    def __getattr__(self, name):
        return getattr(self.field, name)

    def __get__(self, inst, klass):
        if inst is None:
            return self

        field = self.field.bind(inst)
        value = self.getValue(inst, field)
        if value is _marker:
            value = getattr(field, 'default', _marker)
            if value is _marker:
                raise AttributeError(self.__name)

        return value

    def __set__(self, inst, value):
        field = self.field.bind(inst)
        field.validate(value)
        if field.readonly:
            if self.queryValue(inst, field, _marker) is _marker:
                field.readonly = False
                self.setValue(inst, field, value)
                field.readonly = True
                return
            else:
                raise ValueError(self.__name, 'field is readonly')
        oldvalue = self.queryValue(inst, field, NO_VALUE)
        self.setValue(inst, field, value)
        event.notify(FieldUpdatedEvent(inst, self.field, oldvalue, value))
