# -*- coding: ISO-8859-1 -*-
##############################################################################
# Copyright (c) 2002 Zope Corporation 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.
#
##############################################################################
"""Schema Fields

$Id: _field.py 71131 2006-11-15 08:00:29Z dobe $
"""

__docformat__ = 'restructuredtext'

import re
from datetime import datetime, date, timedelta
from sets import Set as SetType

from zope.interface import classImplements, implements
from zope.interface.interfaces import IInterface, IMethod

from zope.schema.interfaces import IField
from zope.schema.interfaces import IMinMaxLen, IText, ITextLine
from zope.schema.interfaces import ISourceText
from zope.schema.interfaces import IInterfaceField
from zope.schema.interfaces import IBytes, IASCII, IBytesLine, IASCIILine
from zope.schema.interfaces import IBool, IInt, IFloat, IDatetime, IFrozenSet
from zope.schema.interfaces import IChoice, ITuple, IList, ISet, IDict
from zope.schema.interfaces import IPassword, IObject, IDate, ITimedelta
from zope.schema.interfaces import IURI, IId, IFromUnicode
from zope.schema.interfaces import ISource, IBaseVocabulary
from zope.schema.interfaces import IContextSourceBinder

from zope.schema.interfaces import ValidationError, InvalidValue
from zope.schema.interfaces import WrongType, WrongContainedType, NotUnique
from zope.schema.interfaces import SchemaNotProvided, SchemaNotFullyImplemented
from zope.schema.interfaces import InvalidURI, InvalidId, InvalidDottedName
from zope.schema.interfaces import ConstraintNotSatisfied

from zope.schema._bootstrapfields import Field, Container, Iterable, Orderable
from zope.schema._bootstrapfields import Text, TextLine, Bool, Int, Password
from zope.schema._bootstrapfields import MinMaxLen
from zope.schema.fieldproperty import FieldProperty
from zope.schema.vocabulary import getVocabularyRegistry
from zope.schema.vocabulary import VocabularyRegistryError
from zope.schema.vocabulary import SimpleVocabulary


# Fix up bootstrap field types
Field.title       = FieldProperty(IField['title'])
Field.description = FieldProperty(IField['description'])
Field.required    = FieldProperty(IField['required'])
Field.readonly    = FieldProperty(IField['readonly'])
# Default is already taken care of
classImplements(Field, IField)

MinMaxLen.min_length = FieldProperty(IMinMaxLen['min_length'])
MinMaxLen.max_length = FieldProperty(IMinMaxLen['max_length'])

classImplements(Text, IText)
classImplements(TextLine, ITextLine)
classImplements(Password, IPassword)
classImplements(Bool, IBool)
classImplements(Int, IInt)

class SourceText(Text):
    __doc__ = ISourceText.__doc__
    implements(ISourceText)
    _type = unicode

class Bytes(MinMaxLen, Field):
    __doc__ = IBytes.__doc__
    implements(IBytes, IFromUnicode)

    _type = str

    def fromUnicode(self, u):
        """
        >>> b = Bytes(constraint=lambda v: 'x' in v)

        >>> b.fromUnicode(u" foo x.y.z bat")
        ' foo x.y.z bat'
        >>> b.fromUnicode(u" foo y.z bat")
        Traceback (most recent call last):
        ...
        ConstraintNotSatisfied:  foo y.z bat

        """
        v = str(u)
        self.validate(v)
        return v

class ASCII(Bytes):
    __doc__ = IASCII.__doc__
    implements(IASCII)

    def _validate(self, value):
        """
        >>> ascii = ASCII()

        Make sure we accept empty strings:

        >>> empty = ''
        >>> ascii._validate(empty)

        and all kinds of alphanumeric strings:

        >>> alphanumeric = "Bob\'s my 23rd uncle"
        >>> ascii._validate(alphanumeric)

        >>> umlauts = "Köhlerstraße"
        >>> ascii._validate(umlauts)
        Traceback (most recent call last):
        ...
        InvalidValue
        """
        super(ASCII, self)._validate(value)
        if not value:
            return
        if not max(map(ord, value)) < 128:
            raise InvalidValue

class BytesLine(Bytes):
    """A Text field with no newlines."""

    implements(IBytesLine)

    def constraint(self, value):
        # TODO: we should probably use a more general definition of newlines
        return '\n' not in value

class ASCIILine(ASCII):
    __doc__ = IASCIILine.__doc__

    implements(IASCIILine)

    def constraint(self, value):
        # TODO: we should probably use a more general definition of newlines
        return '\n' not in value

class Float(Orderable, Field):
    __doc__ = IFloat.__doc__
    implements(IFloat, IFromUnicode)
    _type = float

    def __init__(self, *args, **kw):
        super(Float, self).__init__(*args, **kw)

    def fromUnicode(self, u):
        """
        >>> f = Float()
        >>> f.fromUnicode("1.25")
        1.25
        >>> f.fromUnicode("1.25.6")
        Traceback (most recent call last):
        ...
        ValueError: invalid literal for float(): 1.25.6
        """
        v = float(u)
        self.validate(v)
        return v

class Datetime(Orderable, Field):
    __doc__ = IDatetime.__doc__
    implements(IDatetime)
    _type = datetime

    def __init__(self, *args, **kw):
        super(Datetime, self).__init__(*args, **kw)

class Date(Orderable, Field):
    __doc__ = IDate.__doc__
    implements(IDate)
    _type = date

    def _validate(self, value):
        super(Date, self)._validate(value)
        if isinstance(value, datetime):
            raise WrongType(value, self._type)

class Timedelta(Orderable, Field):
    __doc__ = ITimedelta.__doc__
    implements(ITimedelta)
    _type = timedelta

class Choice(Field):
    """Choice fields can have a value found in a constant or dynamic set of
    values given by the field definition.
    """
    implements(IChoice)

    def __init__(self, values=None, vocabulary=None, source=None, **kw):
        """Initialize object."""
        if vocabulary is not None:
            assert (isinstance(vocabulary, basestring)
                    or IBaseVocabulary.providedBy(vocabulary))
            assert source is None, (
                "You cannot specify both source and vocabulary.")
        elif source is not None:
            vocabulary = source

        assert not (values is None and vocabulary is None), (
               "You must specify either values or vocabulary.")
        assert values is None or vocabulary is None, (
               "You cannot specify both values and vocabulary.")

        self.vocabulary = None
        self.vocabularyName = None
        if values is not None:
            self.vocabulary = SimpleVocabulary.fromValues(values)
        elif isinstance(vocabulary, (unicode, str)):
            self.vocabularyName = vocabulary
        else:
            assert (ISource.providedBy(vocabulary) or 
                    IContextSourceBinder.providedBy(vocabulary))
            self.vocabulary = vocabulary
        # Before a default value is checked, it is validated. However, a
        # named vocabulary is usually not complete when these fields are
        # initialized. Therefore signal the validation method to ignore
        # default value checks during initialization of a Choice tied to a
        # registered vocabulary.
        self._init_field = bool(self.vocabularyName)
        super(Choice, self).__init__(**kw)
        self._init_field = False

    source = property(lambda self: self.vocabulary)

    def bind(self, object):
        """See zope.schema._bootstrapinterfaces.IField."""
        clone = super(Choice, self).bind(object)
        # get registered vocabulary if needed:
        if IContextSourceBinder.providedBy(self.vocabulary):
            clone.vocabulary = self.vocabulary(object)
            assert ISource.providedBy(clone.vocabulary)
        elif clone.vocabulary is None and self.vocabularyName is not None:
            vr = getVocabularyRegistry()
            clone.vocabulary = vr.get(object, self.vocabularyName)
            assert ISource.providedBy(clone.vocabulary)

        return clone

    def fromUnicode(self, str):
        """
        >>> from vocabulary import SimpleVocabulary
        >>> t = Choice(
        ...     vocabulary=SimpleVocabulary.fromValues([u'foo',u'bar']))
        >>> t.fromUnicode(u"baz")
        Traceback (most recent call last):
        ...
        ConstraintNotSatisfied: baz
        >>> t.fromUnicode(u"foo")
        u'foo'
        """
        self.validate(str)
        return str

    def _validate(self, value):
        # Pass all validations during initialization
        if self._init_field:
            return
        super(Choice, self)._validate(value)
        vocabulary = self.vocabulary
        if vocabulary is None:
            vr = getVocabularyRegistry()
            try:
                vocabulary = vr.get(None, self.vocabularyName)
            except VocabularyRegistryError:
                raise ValueError("Can't validate value without vocabulary")
        if value not in vocabulary:
            raise ConstraintNotSatisfied(value)

class InterfaceField(Field):
    __doc__ = IInterfaceField.__doc__
    implements(IInterfaceField)

    def _validate(self, value):
        super(InterfaceField, self)._validate(value)
        if not IInterface.providedBy(value):
            raise WrongType("An interface is required")

def _validate_sequence(value_type, value, errors=None):
    """Validates a sequence value.

    Returns a list of validation errors generated during the validation. If
    no errors are generated, returns an empty list.

    value_type is a field. value is the sequence being validated. errors is
    an optional list of errors that will be prepended to the return value.

    To illustrate, we'll use a text value type. All values must be unicode.

            >>> field = TextLine(required=True)

        To validate a sequence of various values:

            >>> errors = _validate_sequence(field, ('foo', u'bar', 1))
            >>> errors
            [foo <type 'unicode'>, 1 <type 'unicode'>]

        The only valid value in the sequence is the second item. The others
        generated errors.

        We can use the optional errors argument to collect additional errors
        for a new sequence:

        >>> errors = _validate_sequence(field, (2, u'baz'), errors)
        >>> errors
        [foo <type 'unicode'>, 1 <type 'unicode'>, 2 <type 'unicode'>]

    """
    if errors is None:
        errors = []
    if value_type is None:
        return errors
    for item in value:
        try:
            value_type.validate(item)
        except ValidationError, error:
            errors.append(error)
    return errors

def _validate_uniqueness(value):
    temp_values = []
    for item in value:
        if item in temp_values:
            raise NotUnique(item)

        temp_values.append(item)

class AbstractCollection(MinMaxLen, Iterable):
    value_type = None
    unique = False

    def __init__(self, value_type=None, unique=False, **kw):
        super(AbstractCollection, self).__init__(**kw)
        # whine if value_type is not a field
        if value_type is not None and not IField.providedBy(value_type):
            raise ValueError("'value_type' must be field instance.")
        self.value_type = value_type
        self.unique = unique

    def bind(self, object):
        """See zope.schema._bootstrapinterfaces.IField."""
        clone = super(AbstractCollection, self).bind(object)
        # binding value_type is necessary for choices with named vocabularies,
        # and possibly also for other fields.
        if clone.value_type is not None:
           clone.value_type = clone.value_type.bind(object)
        return clone

    def _validate(self, value):
        super(AbstractCollection, self)._validate(value)
        errors = _validate_sequence(self.value_type, value)
        if errors:
            raise WrongContainedType(errors)
        if self.unique:
            _validate_uniqueness(value)

class Tuple(AbstractCollection):
    """A field representing a Tuple."""
    implements(ITuple)
    _type = tuple

class List(AbstractCollection):
    """A field representing a List."""
    implements(IList)
    _type = list

class Set(AbstractCollection):
    """A field representing a set."""
    implements(ISet)
    _type = SetType, set
    def __init__(self, **kw):
        if 'unique' in kw: # set members are always unique
            raise TypeError(
                "__init__() got an unexpected keyword argument 'unique'")
        super(Set, self).__init__(unique=True, **kw)

class FrozenSet(AbstractCollection):
    implements(IFrozenSet)
    _type = frozenset
    def __init__(self, **kw):
        if 'unique' in kw: # set members are always unique
            raise TypeError(
                "__init__() got an unexpected keyword argument 'unique'")
        super(FrozenSet, self).__init__(unique=True, **kw)

def _validate_fields(schema, value, errors=None):
    if errors is None:
        errors = []
    for name in schema.names(all=True):
        if not IMethod.providedBy(schema[name]):
            try:
                attribute = schema[name]
                if IField.providedBy(attribute):
                    # validate attributes that are fields
                    attribute.validate(getattr(value, name))
            except ValidationError, error:
                errors.append(error)
            except AttributeError, error:
                # property for the given name is not implemented
                errors.append(SchemaNotFullyImplemented(error))
    return errors


class Object(Field):
    __doc__ = IObject.__doc__
    implements(IObject)

    def __init__(self, schema, **kw):
        if not IInterface.providedBy(schema):
            raise WrongType

        self.schema = schema
        super(Object, self).__init__(**kw)

    def _validate(self, value):
        super(Object, self)._validate(value)

        # schema has to be provided by value
        if not self.schema.providedBy(value):
            raise SchemaNotProvided

        # check the value against schema
        errors = _validate_fields(self.schema, value)
        if errors:
            raise WrongContainedType(errors)


class Dict(MinMaxLen, Iterable):
    """A field representing a Dict."""
    implements(IDict)
    _type = dict
    key_type   = None
    value_type = None

    def __init__(self, key_type=None, value_type=None, **kw):
        super(Dict, self).__init__(**kw)
        # whine if key_type or value_type is not a field
        if key_type is not None and not IField.providedBy(key_type):
            raise ValueError("'key_type' must be field instance.")
        if value_type is not None and not IField.providedBy(value_type):
            raise ValueError("'value_type' must be field instance.")
        self.key_type = key_type
        self.value_type = value_type

    def _validate(self, value):
        super(Dict, self)._validate(value)
        errors = []
        try:
            if self.value_type:
                errors = _validate_sequence(self.value_type, value.values(),
                                            errors)
            errors = _validate_sequence(self.key_type, value, errors)

            if errors:
                raise WrongContainedType(errors)

        finally:
            errors = None

    def bind(self, object):
        """See zope.schema._bootstrapinterfaces.IField."""
        clone = super(Dict, self).bind(object)
        # binding value_type is necessary for choices with named vocabularies,
        # and possibly also for other fields.
        if clone.key_type is not None:
           clone.key_type = clone.key_type.bind(object)
        if clone.value_type is not None:
           clone.value_type = clone.value_type.bind(object)
        return clone


_isuri = re.compile(
    r"[a-zA-z0-9+.-]+:"   # scheme
    r"\S*$"               # non space (should be pickier)
    ).match

class URI(BytesLine):
    """URI schema field
    """

    implements(IURI, IFromUnicode)

    def _validate(self, value):
        """
        >>> uri = URI(__name__='test')
        >>> uri.validate("http://www.python.org/foo/bar")
        >>> uri.validate("DAV:")
        >>> uri.validate("www.python.org/foo/bar")
        Traceback (most recent call last):
        ...
        InvalidURI: www.python.org/foo/bar
        """

        super(URI, self)._validate(value)
        if _isuri(value):
            return

        raise InvalidURI(value)

    def fromUnicode(self, value):
        """
        >>> uri = URI(__name__='test')
        >>> uri.fromUnicode("http://www.python.org/foo/bar")
        'http://www.python.org/foo/bar'
        >>> uri.fromUnicode("          http://www.python.org/foo/bar")
        'http://www.python.org/foo/bar'
        >>> uri.fromUnicode("      \\n    http://www.python.org/foo/bar\\n")
        'http://www.python.org/foo/bar'
        >>> uri.fromUnicode("http://www.python.org/ foo/bar")
        Traceback (most recent call last):
        ...
        InvalidURI: http://www.python.org/ foo/bar
        """
        v = str(value.strip())
        self.validate(v)
        return v


_isdotted = re.compile(
    r"([a-zA-Z][a-zA-z0-9_]*)"
    r"([.][a-zA-Z][a-zA-z0-9_]*)*"
    r"$" # use the whole line
    ).match

class Id(BytesLine):
    """Id field

    Values of id fields must be either uris or dotted names.
    """

    implements(IId, IFromUnicode)

    def _validate(self, value):
        """
        >>> id = Id(__name__='test')
        >>> id.validate("http://www.python.org/foo/bar")
        >>> id.validate("zope.app.content")
        >>> id.validate("zope.app.content/a")
        Traceback (most recent call last):
        ...
        InvalidId: zope.app.content/a
        >>> id.validate("http://zope.app.content x y")
        Traceback (most recent call last):
        ...
        InvalidId: http://zope.app.content x y
        """
        super(Id, self)._validate(value)
        if _isuri(value):
            return
        if _isdotted(value) and "." in value:
            return

        raise InvalidId(value)

    def fromUnicode(self, value):
        """
        >>> id = Id(__name__='test')
        >>> id.fromUnicode("http://www.python.org/foo/bar")
        'http://www.python.org/foo/bar'
        >>> id.fromUnicode(u" http://www.python.org/foo/bar ")
        'http://www.python.org/foo/bar'
        >>> id.fromUnicode("http://www.python.org/ foo/bar")
        Traceback (most recent call last):
        ...
        InvalidId: http://www.python.org/ foo/bar
        >>> id.fromUnicode("      \\n x.y.z \\n")
        'x.y.z'

        """
        v = str(value.strip())
        self.validate(v)
        return v


class DottedName(BytesLine):
    """Dotted name field.

    Values of DottedName fields must be Python-style dotted names.
    """

    def __init__(self, *args, **kw):
        """
        >>> DottedName(min_dots=-1)
        Traceback (most recent call last):
        ...
        ValueError: min_dots cannot be less than zero

        >>> DottedName(max_dots=-1)
        Traceback (most recent call last):
        ...
        ValueError: max_dots cannot be less than min_dots

        >>> DottedName(max_dots=1, min_dots=2)
        Traceback (most recent call last):
        ...
        ValueError: max_dots cannot be less than min_dots

        >>> dotted_name = DottedName(max_dots=1, min_dots=1)

        >>> dotted_name = DottedName(max_dots=1)
        >>> dotted_name.min_dots
        0

        >>> dotted_name = DottedName(min_dots=1)
        >>> dotted_name.max_dots
        >>> dotted_name.min_dots
        1
        """
        self.min_dots = int(kw.pop("min_dots", 0))
        if self.min_dots < 0:
            raise ValueError("min_dots cannot be less than zero")
        self.max_dots = kw.pop("max_dots", None)
        if self.max_dots is not None:
            self.max_dots = int(self.max_dots)
            if self.max_dots < self.min_dots:
                raise ValueError("max_dots cannot be less than min_dots")
        super(DottedName, self).__init__(*args, **kw)

    def _validate(self, value):
        """
        >>> dotted_name = DottedName(__name__='test')
        >>> dotted_name.validate("a.b.c")
        >>> dotted_name.validate("a")
        >>> dotted_name.validate("   a")
        Traceback (most recent call last):
        ...
        InvalidDottedName:    a

        >>> dotted_name = DottedName(__name__='test', min_dots=1)
        >>> dotted_name.validate('a.b')
        >>> dotted_name.validate('a.b.c.d')
        >>> dotted_name.validate('a')
        Traceback (most recent call last):
        ...
        InvalidDottedName: ('too few dots; 1 required', 'a')

        >>> dotted_name = DottedName(__name__='test', max_dots=0)
        >>> dotted_name.validate('a')
        >>> dotted_name.validate('a.b')
        Traceback (most recent call last):
        ...
        InvalidDottedName: ('too many dots; no more than 0 allowed', 'a.b')

        >>> dotted_name = DottedName(__name__='test', max_dots=2)
        >>> dotted_name.validate('a')
        >>> dotted_name.validate('a.b')
        >>> dotted_name.validate('a.b.c')
        >>> dotted_name.validate('a.b.c.d')
        Traceback (most recent call last):
        ...
        InvalidDottedName: ('too many dots; no more than 2 allowed', 'a.b.c.d')

        >>> dotted_name = DottedName(__name__='test', max_dots=1, min_dots=1)
        >>> dotted_name.validate('a.b')
        >>> dotted_name.validate('a')
        Traceback (most recent call last):
        ...
        InvalidDottedName: ('too few dots; 1 required', 'a')
        >>> dotted_name.validate('a.b.c')
        Traceback (most recent call last):
        ...
        InvalidDottedName: ('too many dots; no more than 1 allowed', 'a.b.c')

        """
        super(DottedName, self)._validate(value)
        if not _isdotted(value):
            raise InvalidDottedName(value)
        dots = value.count(".")
        if dots < self.min_dots:
            raise InvalidDottedName("too few dots; %d required" % self.min_dots,
                                    value)
        if self.max_dots is not None and dots > self.max_dots:
            raise InvalidDottedName("too many dots; no more than %d allowed" %
                                    self.max_dots, value)

    def fromUnicode(self, value):
        v = str(value.strip())
        self.validate(v)
        return v
