# Copyright (c) 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import datetime
import numbers
import re
import types

import semantic_version
import six

from glance.common.artifacts import declarative
import glance.common.exception as exc
from glance import i18n


_ = i18n._


class Text(declarative.PropertyDefinition):
    """A text metadata property of arbitrary length

    Maps to TEXT columns in database, does not support sorting or filtering
    """
    ALLOWED_TYPES = (six.string_types,)
    DB_TYPE = 'text'


# noinspection PyAttributeOutsideInit
class String(Text):
    """A string metadata property of limited length

    Maps to VARCHAR columns in database, supports filtering and sorting.
    May have constrains on length and regexp patterns.

    The maximum length is limited to 255 characters
    """

    DB_TYPE = 'string'

    def __init__(self, max_length=255, min_length=0, pattern=None, **kwargs):
        """Defines a String metadata property.

        :param max_length: maximum value length
        :param min_length: minimum value length
        :param pattern: regexp pattern to match
        """
        super(String, self).__init__(**kwargs)

        self.max_length(max_length)
        self.min_length(min_length)
        if pattern:
            self.pattern(pattern)
        # if default and/or allowed_values are specified (in base classes)
        # then we need to validate them against the newly added validators
        self._check_definition()

    def max_length(self, value):
        """Sets the maximum value length"""
        self._max_length = value
        if value is not None:
            if value > 255:
                raise exc.InvalidArtifactTypePropertyDefinition(
                    _('Max string length may not exceed 255 characters'))
            self._add_validator('max_length',
                                lambda v: len(v) <= self._max_length,
                                _('Length  is greater than maximum'))
        else:
            self._remove_validator('max_length')
        self._check_definition()

    def min_length(self, value):
        """Sets the minimum value length"""
        self._min_length = value
        if value is not None:
            if value < 0:
                raise exc.InvalidArtifactTypePropertyDefinition(
                    _('Min string length may not be negative'))

            self._add_validator('min_length',
                                lambda v: len(v) >= self._min_length,
                                _('Length is less than minimum'))
        else:
            self._remove_validator('min_length')
        self._check_definition()

    def pattern(self, value):
        """Sets the regexp pattern to match"""
        self._pattern = value
        if value is not None:
            self._add_validator('pattern',
                                lambda v: re.match(self._pattern,
                                                   v) is not None,
                                _('Does not match pattern'))
        else:
            self._remove_validator('pattern')
        self._check_definition()


class SemVerString(String):
    """A String metadata property matching semver pattern"""

    def __init__(self, **kwargs):
        def validate(value):
            try:
                semantic_version.Version(value, partial=True)
            except ValueError:
                return False
            return True

        super(SemVerString,
              self).__init__(validators=[(validate,
                                         "Invalid semver string")],
                             **kwargs)


# noinspection PyAttributeOutsideInit
class Integer(declarative.PropertyDefinition):
    """An Integer metadata property

    Maps to INT columns in Database, supports filtering and sorting.
    May have constraints on value
    """

    ALLOWED_TYPES = (six.integer_types,)
    DB_TYPE = 'int'

    def __init__(self, min_value=None, max_value=None, **kwargs):
        """Defines an Integer metadata property

        :param min_value: minimum allowed value
        :param max_value: maximum allowed value
        """
        super(Integer, self).__init__(**kwargs)
        if min_value is not None:
            self.min_value(min_value)

        if max_value is not None:
            self.max_value(max_value)

        # if default and/or allowed_values are specified (in base classes)
        # then we need to validate them against the newly added validators
        self._check_definition()

    def min_value(self, value):
        """Sets the minimum allowed value"""
        self._min_value = value
        if value is not None:
            self._add_validator('min_value',
                                lambda v: v >= self._min_value,
                                _('Value is less than minimum'))
        else:
            self._remove_validator('min_value')
        self._check_definition()

    def max_value(self, value):
        """Sets the maximum allowed value"""
        self._max_value = value
        if value is not None:
            self._add_validator('max_value',
                                lambda v: v <= self._max_value,
                                _('Value is greater than maximum'))
        else:
            self._remove_validator('max_value')
        self._check_definition()


# noinspection PyAttributeOutsideInit
class DateTime(declarative.PropertyDefinition):
    """A DateTime metadata property

    Maps to a DATETIME columns in database.
    Is not supported as Type Specific property, may be used only as Generic one

    May have constraints on value
    """
    ALLOWED_TYPES = (datetime.datetime,)
    DB_TYPE = 'datetime'

    def __init__(self, min_value=None, max_value=None, **kwargs):
        """Defines a DateTime metadata property

        :param min_value: minimum allowed value
        :param max_value: maximum allowed value
        """
        super(DateTime, self).__init__(**kwargs)
        if min_value is not None:
            self.min_value(min_value)

        if max_value is not None:
            self.max_value(max_value)

        # if default and/or allowed_values are specified (in base classes)
        # then we need to validate them against the newly added validators
        self._check_definition()

    def min_value(self, value):
        """Sets the minimum allowed value"""
        self._min_value = value
        if value is not None:
            self._add_validator('min_value',
                                lambda v: v >= self._min_value,
                                _('Value is less than minimum'))
        else:
            self._remove_validator('min_value')
        self._check_definition()

    def max_value(self, value):
        """Sets the maximum allowed value"""
        self._max_value = value
        if value is not None:
            self._add_validator('max_value',
                                lambda v: v <= self._max_value,
                                _('Value is greater than maximum'))
        else:
            self._remove_validator('max_value')
        self._check_definition()


# noinspection PyAttributeOutsideInit
class Numeric(declarative.PropertyDefinition):
    """A Numeric metadata property

    Maps to floating point number columns in Database, supports filtering and
    sorting. May have constraints on value
    """
    ALLOWED_TYPES = numbers.Number
    DB_TYPE = 'numeric'

    def __init__(self, min_value=None, max_value=None, **kwargs):
        """Defines a Numeric metadata property

        :param min_value: minimum allowed value
        :param max_value: maximum allowed value
        """
        super(Numeric, self).__init__(**kwargs)
        if min_value is not None:
            self.min_value(min_value)

        if max_value is not None:
            self.max_value(max_value)

        # if default and/or allowed_values are specified (in base classes)
        # then we need to validate them against the newly added validators
        self._check_definition()

    def min_value(self, value):
        """Sets the minimum allowed value"""
        self._min_value = value
        if value is not None:
            self._add_validator('min_value',
                                lambda v: v >= self._min_value,
                                _('Value is less than minimum'))
        else:
            self._remove_validator('min_value')
        self._check_definition()

    def max_value(self, value):
        """Sets the maximum allowed value"""
        self._max_value = value
        if value is not None:
            self._add_validator('max_value',
                                lambda v: v <= self._max_value,
                                _('Value is greater than maximum'))
        else:
            self._remove_validator('max_value')
        self._check_definition()


class Boolean(declarative.PropertyDefinition):
    """A Boolean metadata property

    Maps to Boolean columns in database. Supports filtering and sorting.
    """
    ALLOWED_TYPES = (types.BooleanType,)
    DB_TYPE = 'bool'


class Array(declarative.ListAttributeDefinition,
            declarative.PropertyDefinition, list):
    """An array metadata property

    May contain elements of any other PropertyDefinition types except Dict and
    Array. Each elements maps to appropriate type of columns in database.
    Preserves order. Allows filtering based on "Array contains Value" semantics

    May specify constrains on types of elements, their amount and uniqueness.
    """
    ALLOWED_ITEM_TYPES = (declarative.PropertyDefinition,)

    def __init__(self, item_type=String(), min_size=0, max_size=None,
                 unique=False, extra_items=True, **kwargs):
        """Defines an Array metadata property

        :param item_type: defines the types of elements in Array. If set to an
        instance of PropertyDefinition then all the elements have to be of that
        type. If set to list of such instances, then the elements on the
        corresponding positions have to be of the appropriate type.
        :param min_size: minimum size of the Array
        :param max_size: maximum size of the Array
        :param unique: if set to true, all the elements in the Array have to be
        unique
        """
        if isinstance(item_type, Array):
            msg = _("Array property can't have item_type=Array")
            raise exc.InvalidArtifactTypePropertyDefinition(msg)
        declarative.ListAttributeDefinition.__init__(self,
                                                     item_type=item_type,
                                                     min_size=min_size,
                                                     max_size=max_size,
                                                     unique=unique)
        declarative.PropertyDefinition.__init__(self, **kwargs)


class Dict(declarative.DictAttributeDefinition,
           declarative.PropertyDefinition, dict):
    """A dictionary metadata property

    May contain elements of any other PropertyDefinition types except Dict.
    Each elements maps to appropriate type of columns in database. Allows
    filtering and sorting by values of each key except the ones mapping the
    Text fields.

    May specify constrains on types of elements and their amount.
    """
    ALLOWED_PROPERTY_TYPES = (declarative.PropertyDefinition,)

    def __init__(self, properties=String(), min_properties=0,
                 max_properties=None, **kwargs):
        """Defines a dictionary metadata property

        :param properties: defines the types of dictionary values. If set to an
        instance of PropertyDefinition then all the value have to be of that
        type. If set to a dictionary with string keys and values of
        PropertyDefinition type, then the elements mapped by the corresponding
        have have to be of the appropriate type.
        :param min_properties: minimum allowed amount of properties in the dict
        :param max_properties: maximum allowed amount of properties in the dict
        """
        declarative.DictAttributeDefinition. \
            __init__(self,
                     properties=properties,
                     min_properties=min_properties,
                     max_properties=max_properties)
        declarative.PropertyDefinition.__init__(self, **kwargs)


class ArtifactType(declarative.get_declarative_base()):  # noqa
    """A base class for all the Artifact Type definitions

    Defines the Generic metadata properties as attributes.
    """
    id = String(required=True, readonly=True)
    type_name = String(required=True, readonly=True)
    type_version = SemVerString(required=True, readonly=True)
    name = String(required=True, mutable=False)
    version = SemVerString(required=True, mutable=False)
    description = Text()
    tags = Array(unique=True, default=[])
    visibility = String(required=True,
                        allowed_values=["private", "public", "shared",
                                        "community"],
                        default="private")
    state = String(required=True, readonly=True, allowed_values=["creating",
                                                                 "active",
                                                                 "deactivated",
                                                                 "deleted"])
    owner = String(required=True, readonly=True)
    created_at = DateTime(required=True, readonly=True)
    updated_at = DateTime(required=True, readonly=True)
    published_at = DateTime(readonly=True)
    deleted_at = DateTime(readonly=True)

    def __init__(self, **kwargs):
        if "type_name" in kwargs:
            raise exc.InvalidArtifactPropertyValue(
                _("Unable to specify artifact type explicitly"))
        if "type_version" in kwargs:
            raise exc.InvalidArtifactPropertyValue(
                _("Unable to specify artifact type version explicitly"))
        super(ArtifactType,
              self).__init__(type_name=self.metadata.type_name,
                             type_version=self.metadata.type_version, **kwargs)

    def __eq__(self, other):
        if not isinstance(other, ArtifactType):
            return False
        return self.id == other.id

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash(self.id)

    def __is_mutable__(self):
        return self.state == "creating"


class ArtifactReference(declarative.RelationDefinition):
    """An artifact reference definition

    Allows to define constraints by the name and version of target artifact
    """
    ALLOWED_TYPES = ArtifactType

    def __init__(self, type_name=None, type_version=None, **kwargs):
        """Defines an artifact reference

        :param type_name: type name of the target artifact
        :param type_version: type version of the target artifact
        """
        super(ArtifactReference, self).__init__(**kwargs)
        if type_name is not None:
            if isinstance(type_name, types.ListType):
                type_names = list(type_name)
                if type_version is not None:
                    raise exc.InvalidArtifactTypePropertyDefinition(
                        _('Unable to specify version '
                          'if multiple types are possible'))
            else:
                type_names = [type_name]

            def validate_reference(artifact):
                if artifact.type_name not in type_names:
                    return False
                if (type_version is not None and
                   artifact.type_version != type_version):
                    return False
                return True

            self._add_validator('referenced_type',
                                validate_reference,
                                _("Invalid referenced type"))
        elif type_version is not None:
            raise exc.InvalidArtifactTypePropertyDefinition(
                _('Unable to specify version '
                  'if type is not specified'))
        self._check_definition()


class ArtifactReferenceList(declarative.ListAttributeDefinition,
                            declarative.RelationDefinition, list):
    """A list of Artifact References

    Allows to define a collection of references to other artifacts, each
    optionally constrained by type name and type version
    """
    ALLOWED_ITEM_TYPES = (ArtifactReference,)

    def __init__(self, references=ArtifactReference(), min_size=0,
                 max_size=None, **kwargs):
        if isinstance(references, types.ListType):
            raise exc.InvalidArtifactTypePropertyDefinition(
                _("Invalid reference list specification"))
        declarative.RelationDefinition.__init__(self, **kwargs)
        declarative.ListAttributeDefinition.__init__(self,
                                                     item_type=references,
                                                     min_size=min_size,
                                                     max_size=max_size,
                                                     unique=True,
                                                     default=[]
                                                     if min_size == 0 else
                                                     None)


class Blob(object):
    """A Binary object being part of the Artifact"""
    def __init__(self, size=0, locations=None, checksum=None, item_key=None):
        """Initializes a new Binary Object for an Artifact

        :param size: the size of Binary Data
        :param locations: a list of data locations in backing stores
        :param checksum: a checksum for the data
        """
        if locations is None:
            locations = []
        self.size = size
        self.checksum = checksum
        self.locations = locations
        self.item_key = item_key

    def to_dict(self):
        return {
            "size": self.size,
            "checksum": self.checksum,
        }


class BinaryObject(declarative.BlobDefinition, Blob):
    """A definition of BinaryObject binding

    Adds a BinaryObject to an Artifact Type, optionally constrained by file
    size and amount of locations
    """
    ALLOWED_TYPES = (Blob,)

    def __init__(self,
                 max_file_size=None,
                 min_file_size=None,
                 min_locations=None,
                 max_locations=None,
                 **kwargs):
        """Defines a binary object as part of Artifact Type
        :param max_file_size: maximum size of the associate Blob
        :param min_file_size: minimum size of the associated Blob
        :param min_locations: minimum number of locations in the associated
        Blob
        :param max_locations: maximum number of locations in the associated
        Blob
        """
        super(BinaryObject, self).__init__(default=None, readonly=False,
                                           mutable=False, **kwargs)
        self._max_file_size = max_file_size
        self._min_file_size = min_file_size
        self._min_locations = min_locations
        self._max_locations = max_locations

        self._add_validator('size_not_empty',
                            lambda v: v.size is not None,
                            _('Blob size is not set'))
        if max_file_size:
            self._add_validator('max_size',
                                lambda v: v.size <= self._max_file_size,
                                _("File too large"))
        if min_file_size:
            self._add_validator('min_size',
                                lambda v: v.size >= self._min_file_size,
                                _("File too small"))
        if min_locations:
            self._add_validator('min_locations',
                                lambda v: len(
                                    v.locations) >= self._min_locations,
                                _("Too few locations"))
        if max_locations:
            self._add_validator(
                'max_locations',
                lambda v: len(v.locations) <= self._max_locations,
                _("Too many locations"))


class BinaryObjectList(declarative.ListAttributeDefinition,
                       declarative.BlobDefinition, list):
    """A definition of binding to the list of BinaryObject

    Adds a list of BinaryObject's to an artifact type, optionally constrained
    by the number of objects in the list and their uniqueness

    """
    ALLOWED_ITEM_TYPES = (BinaryObject,)

    def __init__(self, objects=BinaryObject(), min_count=0, max_count=None,
                 **kwargs):
        declarative.BlobDefinition.__init__(self, **kwargs)
        declarative.ListAttributeDefinition.__init__(self,
                                                     item_type=objects,
                                                     min_size=min_count,
                                                     max_size=max_count,
                                                     unique=True)
        self.default = [] if min_count == 0 else None
