#-*- coding: utf8
import odml.dtypes as dtypes
import odml.base as base
import odml.format as format

import string
from odml.tools.doc_inherit import inherit_docstring, allow_inherit_docstring


class Value(base._baseobj):
    pass


@allow_inherit_docstring
class BaseValue(base.baseobject, Value):
    """
    An odML value

    data
        mandatory (unless value is set). It's the content itself.
        (see the data and value attributes of this object)

    uncertainty (optional)
        an estimation of the value's uncertainty.

    unit (optional)
        the value's unit

    dtype (optional)
        the data type of the value

    reference (optional)
        an external reference number (e.g. entry in a database)

    filename (optional)
        the default file name which should be used when saving the object

    encoder (encoder)
        binary content must be encoded into ascii to be included in odML files.
        Currently supported is only base64

    checksum (optional)
        if binary content is directly included or if the URL of an external file is given,
        a checksum entry can be used to validate the file's identity, integrity.
        Use this element to indicate the algorithm and the checksum in the format algorithm$checksum
        e.g. crc32$b84892a2 or md5$d41d8cd98f00b204e9800998ecf8427e

    definition (optional)
        additional comments on the value of the property

    value
        mandatory (unless data is set). It's the string representation of the value.

    TODO: comment
    """

    _format = format.Value

    def __init__(self, data=None, uncertainty=None, unit=None, dtype=None, definition=None, reference=None,
                 filename=None, encoder=None, checksum=None, comment=None, value=None):
        if data is None and value is None:
            raise TypeError("either data or value has to be set")
        if data is not None and value is not None:
            raise TypeError("only one of data or value can be set")

        self._property = None
        self._unit = unit
        self._uncertainty = uncertainty
        self._dtype = dtype
        self._definition = definition
        self._reference = reference
        self._filename = filename
        self._comment = comment
        self._encoder = encoder

        if value is not None:
            # assign value directly (through property would raise a change-event)
            self._value = dtypes.get(value, self._dtype, self._encoder)
        elif data is not None:
            if dtype is None:
                self._dtype = dtypes.infer_dtype(data)
            self._value = data

        self._checksum_type = None
        if checksum is not None:
            self.checksum = checksum

    def __repr__(self):
        if self._dtype:
            return "<%s %s>" % (str(self._dtype), self.get_display())
        return "<%s>" % str(self._value)

    @property
    def parent(self):
        """the property containing this value"""
        return self._property

    @property
    def data(self):
        """
        used to access the raw data of the value
        (i.e. a datetime-object if dtype is "datetime")
        see also the value attribute
        """
        return self._value

    @data.setter
    def data(self, new_value):
        self._value = new_value

    @property
    def value(self):
        """
        used to access typed data of the value as a string.
        Use data to access the raw type, i.e.:

        >>> v = Value(1, type="float")
        >>> v.data
        1.0
        >>> v.data = 1.5
        >>> v.value
        "1.5"
        >>> v.value = 2
        >>> v.data
        2.0
        """
        return dtypes.set(self._value, self._dtype, self._encoder)

    @value.setter
    def value(self, new_string):
        self._value = dtypes.get(new_string, self._dtype, self._encoder)

    @property
    def dtype(self):
        """
        the data type of the value

        If the data type is changed, it is tried, to convert the value to the new type.

        If this doesn't work, the change is refused.
        This behaviour can be overridden by directly accessing the *_dtype* attribute
        and adjusting the *data* attribute manually.
        """
        return self._dtype

    @dtype.setter
    def dtype(self, new_type):
        # check if this is a valid type
        if not dtypes.valid_type(new_type):
            raise AttributeError("'%s' is not a valid type." % new_type)
        # we convert the value if possible
        old_type = self._dtype
        old_value = dtypes.set(self._value, self._dtype, self._encoder)
        try:
            new_value = dtypes.get(old_value, new_type, self._encoder)
        except:
            # cannot convert, try the other way around
            try:
                old_value = dtypes.set(self._value, new_type, self._encoder)
                new_value = dtypes.get(old_value, new_type, self._encoder)
            except:
                #doesn't work either, therefore refuse
                raise ValueError("cannot convert '%s' from '%s' to '%s'" % (self.value, old_type, new_type))
        self._value = new_value
        self._dtype = new_type

    @property
    def encoder(self):
        """
        the encoding of binary data

        changing the encoding also converts the data
        """
        if self._dtype == "binary":
            return self._encoder
        return None

    @encoder.setter
    def encoder(self, encoding):
        if not self._dtype == "binary":
            raise AttributeError("attribute 'encoding' can only be set for binary types, not for '%s'" % self._dtype)

        if not encoding:
            encoding = None

        if not dtypes.valid_encoding(encoding):
            raise AttributeError("'%s' is not a valid encoding" % encoding)

        # no need to cast anything here, because the encoding
        # effects only the display, not the representation
        self._encoder = encoding

    @property
    def uncertainty(self):
        return self._uncertainty

    @uncertainty.setter
    def uncertainty(self, new_value):
        self._uncertainty = new_value

    @property
    def unit(self):
        return self._unit

    @unit.setter
    def unit(self, new_value):
        self._unit = new_value

    @property
    def reference(self):
        return self._reference

    @reference.setter
    def reference(self, new_value):
        self._reference = new_value

    @property
    def definition(self):
        return self._definition

    @definition.setter
    def definition(self, new_value):
        self._definition = new_value

    @property
    def comment(self):
        return self._comment

    @comment.setter
    def comment(self, new_value):
        self._comment = new_value

    @property
    def filename(self):
        return self._filename

    @filename.setter
    def filename(self, new_value):
        self._filename = new_value

    @property
    def checksum(self):
        if not self._dtype == "binary":
            return None
        cs_type = self._checksum_type
        if cs_type is None:
            cs_type = "crc32"
        return "%s$%s" % (cs_type, self.calculate_checksum(cs_type))

    @checksum.setter
    def checksum(self, value):
        if not self._dtype == "binary":
            raise AttributeError("attribute 'checksum' can only be set for binary types, not for '%s'" % self._dtype)

        data = value.split("$", 1)
        if not dtypes.valid_checksum_type(data[0]):
            raise AttributeError("unsupported checksum type '%s'" % data[0])
        self._checksum_type = data[0]

    def calculate_checksum(self, cs_type):
        """
        returns the checksum for the data of this Value-object

        *cs_type* is the checksum mechanism (e.g. 'crc32' or 'md5')
        """
        return dtypes.calculate_checksum(self._value, cs_type)

    def can_display(self, text=None, max_length=-1):
        """
        return whether the content of this can be safely displayed in the gui
        """
        if text is None:
            text = self._value
        if text is None:
            return True

        if max_length != -1 and len(text) > max_length:
            return False

        if self._dtype == "binary":
            unprintable = filter(lambda x: x not in string.printable, text)
            if self._encoder is None and len(unprintable) > 0:
                return False

        if "\n" in text or "\t" in text:
            return False

        return True

    def get_display(self, max_length=-1):
        """
        return a textual representation that can be used for display

        typically takes the first line (max *max_length* chars) and adds '…'
        """
        text = self.value
        if self.can_display(text, max_length):
            return text

        text = text.split("\n")[0]
        if max_length != -1:
            text = text[:max_length]
        if self.can_display(text, max_length):
            return text + u'…'

        return "(%d bytes)" % len(self._value)

#    def can_edit(self, max_length=-1)
#        if not self.can_display(self): return False
#
#        return True
#
    @inherit_docstring
    def reorder(self, new_index):
        return self._reorder(self.parent.values, new_index)

    def clone(self):
        obj = super(BaseValue, self).clone()
        obj._property = None
        return obj

    @inherit_docstring
    def get_terminology_equivalent(self):
        prop = self._property.get_terminology_equivalent()

        if prop is None:
            return None

        for val in prop:
            if val == self:  # TODO: shouldn't we take the index or st.?
                return val

        return None
