# Source code for fints.fields

    import datetime
    import decimal
    import re
    import warnings

    from fints.types import Container, SegmentSequence, TypedField
    from fints.utils import (
        DocTypeMixin, FieldRenderFormatStringMixin, FixedLengthMixin, Password,
    )

    class DataElementField(DocTypeMixin, TypedField):
        pass

    class ContainerField(TypedField):
        def _check_value(self, value):
            if self.type:
                if not isinstance(value, self.type):
                    raise TypeError("Value {!r} is not of type {!r}".format(value, self.type))
            super()._check_value(value)

        def _default_value(self):
            return self.type()

    class DataElementGroupField(DocTypeMixin, ContainerField):
        pass

    class GenericField(FieldRenderFormatStringMixin, DataElementField):
        type = None
        _FORMAT_STRING = "{}"

        def _parse_value(self, value):
            warnings.warn("Generic field used for type {!r} value {!r}".format(self.type, value))
            return value

    class GenericGroupField(DataElementGroupField):
        type = None

        def _default_value(self):
            if self.type is None:
                return Container()
            else:
                return self.type()

        def _parse_value(self, value):
            if self.type is None:
                warnings.warn("Generic field used for type {!r} value {!r}".format(self.type, value))
            return value

    class TextField(FieldRenderFormatStringMixin, DataElementField):
        type = 'txt'
        _DOC_TYPE = str
        _FORMAT_STRING = "{}"  # FIXME Restrict CRLF

        def _parse_value(self, value): return str(value)

    class AlphanumericField(TextField):
        type = 'an'

    class DTAUSField(DataElementField):
        type = 'dta'

    class NumericField(FieldRenderFormatStringMixin, DataElementField):
        type = 'num'
        _DOC_TYPE = int
        _FORMAT_STRING = "{:d}"

        def _parse_value(self, value):
            _value = str(value)
            if len(_value) > 1 and _value[0] == '0':
                raise ValueError("Leading zeroes not allowed for value of type 'num': {!r}".format(value))
            return int(_value, 10)

    class ZeroPaddedNumericField(NumericField):
        type = ''
        _DOC_TYPE = int

        def __init__(self, *args, **kwargs):
            if not kwargs.get('length', None):
                raise ValueError("ZeroPaddedNumericField needs length argument")
            super().__init__(*args, **kwargs)

        @property
        def _FORMAT_STRING(self):
            return "{:0" + str(self.length) + "d}"

        def _parse_value(self, value):
            _value = str(value)
            return int(_value, 10)

    class DigitsField(FieldRenderFormatStringMixin, DataElementField):
        type = 'dig'
        _DOC_TYPE = str
        _FORMAT_STRING = "{}"

        def _parse_value(self, value):
            _value = str(value)
            if not re.match(r'^\d*$', _value):
                raise TypeError("Only digits allowed for value of type 'dig': {!r}".format(value))
            return _value

    class FloatField(DataElementField):
        type = 'float'
        _DOC_TYPE = float
        _FORMAT_STRING = "{:.12f}"  # Warning: Python's float is not exact!
        # FIXME: Needs test

        def _parse_value(self, value):
            if isinstance(value, float):
                return value

            if isinstance(value, decimal.Decimal):
                value = str(value.normalize()).replace(".", ",")

            _value = str(value)
            if not re.match(r'^(?:0|[1-9]\d*),(?:\d*[1-9]|)$', _value):
                raise TypeError("Only digits and ',' allowed for value of type 'float', no superfluous leading or trailing zeroes allowed: {!r}".format(value))

            return float(_value.replace(",", "."))

        def _render_value(self, value):
            retval = self._FORMAT_STRING.format(value)
            retval = retval.replace('.', ',').rstrip('0')
            self._check_value_length(retval)
            return retval

    class AmountField(FixedLengthMixin, DataElementField):
        type = 'wrt'
        _DOC_TYPE = decimal.Decimal
        _FIXED_LENGTH = [None, None, 15]
        # FIXME Needs test

        def _parse_value(self, value):
            if isinstance(value, float):
                return value

            if isinstance(value, decimal.Decimal):
                return value

            _value = str(value)
            if not re.match(r'^(?:0|[1-9]\d*)(?:,)?(?:\d*[1-9]|)$', _value):
                raise TypeError("Only digits and ',' allowed for value of type 'decimal', no superfluous leading or trailing zeroes allowed: {!r}".format(value))

            return decimal.Decimal(_value.replace(",", "."))

        def _render_value(self, value):
            retval = str(value)
            retval = retval.replace('.', ',').rstrip('0')
            self._check_value_length(retval)
            return retval

    class BinaryField(DataElementField):
        type = 'bin'
        _DOC_TYPE = bytes

        def _render_value(self, value):
            retval = bytes(value)
            self._check_value_length(retval)

            return retval

        def _parse_value(self, value): return bytes(value)

    class IDField(FixedLengthMixin, AlphanumericField):
        type = 'id'
        _DOC_TYPE = str
        _FIXED_LENGTH = [None, None, 30]

    class BooleanField(FixedLengthMixin, AlphanumericField):
        type = 'jn'
        _DOC_TYPE = bool
        _FIXED_LENGTH = [1]

        def _render_value(self, value):
            return "J" if value else "N"

        def _parse_value(self, value):
            if value is None:
                return None
            if value == "J" or value is True:
                return True
            elif value == "N" or value is False:
                return False
            else:
                raise ValueError("Invalid value {!r} for BooleanField".format(value))

    class CodeFieldMixin:
        # FIXME Need tests

        def __init__(self, enum=None, *args, **kwargs):
            if enum:
                self._DOC_TYPE = enum
                self._enum = enum
            else:
                self._enum = None
            super().__init__(*args, **kwargs)

        def _parse_value(self, value):
            retval = super()._parse_value(value)
            if self._enum:
                retval = self._enum(retval)
            return retval

        def _render_value(self, value):
            retval = value
            if self._enum:
                retval = str(value.value)
            return super()._render_value(retval)

        def _inline_doc_comment(self, value):
            retval = super()._inline_doc_comment(value)
            if self._enum:
                addendum = value.__doc__
                if addendum and addendum is not value.__class__.__doc__:
                    if not retval:
                        retval = " # "
                    else:
                        retval = retval + ": "
                    retval = retval + addendum
            return retval

    class CodeField(CodeFieldMixin, AlphanumericField):
        type = 'code'
        _DOC_TYPE = str

    class IntCodeField(CodeFieldMixin, NumericField):
        type = ''
        _DOC_TYPE = int
        _FORMAT_STRING = "{}"

    class CountryField(FixedLengthMixin, DigitsField):
        type = 'ctr'
        _FIXED_LENGTH = [3]

    class CurrencyField(FixedLengthMixin, AlphanumericField):
        type = 'cur'
        _FIXED_LENGTH = [3]

    class DateField(FixedLengthMixin, NumericField):
        type = 'dat'  # FIXME Need test
        _DOC_TYPE = datetime.date
        _FIXED_LENGTH = [8]

        def _parse_value(self, value):
            if isinstance(value, datetime.date):
                return value
            val = super()._parse_value(value)
            val = str(val)
            return datetime.date(int(val[0:4]), int(val[4:6]), int(val[6:8]))

        def _render_value(self, value):
            val = "{:04d}{:02d}{:02d}".format(value.year, value.month, value.day)
            val = int(val)
            return super()._render_value(val)

    class TimeField(FixedLengthMixin, DigitsField):
        type = 'tim'  # FIXME Need test
        _DOC_TYPE = datetime.time
        _FIXED_LENGTH = [6]

        def _parse_value(self, value):
            if isinstance(value, datetime.time):
                return value
            val = super()._parse_value(value)
            return datetime.time(int(val[0:2]), int(val[2:4]), int(val[4:6]))

        def _render_value(self, value):
            val = "{:02d}{:02d}{:02d}".format(value.hour, value.minute, value.second)
            return super()._render_value(val)

    class PasswordField(AlphanumericField):
        type = ''
        _DOC_TYPE = Password

        def _parse_value(self, value):
            return Password(value)

        def _render_value(self, value):
            return str(value)

    class SegmentSequenceField(DataElementField):
        type = 'sf'

        def _parse_value(self, value):
            if isinstance(value, SegmentSequence):
                return value
            else:
                return SegmentSequence(value)

        def _render_value(self, value):
            return value.render_bytes()