{% if is_first_by_category %}
##
## object types
##

{% endif %}
{% if render_imports %}
import uuid
import pprint
from typing import Dict, List, Optional

from autobahn.wamp.request import Publication, Subscription, Registration

import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()

{% endif %}


class {{ metadata.classname }}(object):
    """
    {{ metadata.docs }}
    """
    __slots__ = ['_tab', {% for field in metadata.fields_by_id %}'_{{ field.name }}', {% endfor %}]

    def __init__(self, {% for field in metadata.fields_by_id %}{{ field.name }}: {{ field.type.map('python', field.attrs, required=False, objtype_as_string=True) }} = None, {% endfor %}):
        # the underlying FlatBuffers vtable
        self._tab = None

        {% for field in metadata.fields_by_id %}
        # {{ field.docs }}
        self._{{ field.name }}: {{ field.type.map('python', field.attrs, required=False, objtype_as_string=True) }} = {{ field.name }}

        {% endfor %}

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False
        {% for field in metadata.fields_by_id %}
        if other.{{ field.name }} != self.{{ field.name }}:
            return False
        {% endfor %}
        return True

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

    {% for field in metadata.fields_by_id %}
    @property
    def {{ field.name }}(self) -> {{ field.type.map('python', field.attrs, required=False, objtype_as_string=True) }}:
        """
        {{ field.docs }}
        """
        if self._{{ field.name }} is None and self._tab:
            o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset({{ field.offset }}))

            {% if field.type.map('python', field.attrs, True) == 'str' %}
            # access type "string" attribute:
            value = ''
            if o != 0:
                _value = self._tab.String(o + self._tab.Pos)
                if _value is not None:
                    value = _value.decode('utf8')

            {% elif field.type.map('python', field.attrs, True) == 'bytes' %}
            # access type "bytes" attribute:
            value = b''
            if o != 0:
                _off = self._tab.Vector(o)
                _len = self._tab.VectorLen(o)
                _value = memoryview(self._tab.Bytes)[_off:_off + _len]
                if _value is not None:
                    value = _value

            {% elif field.type.map('python', field.attrs, True) in ['int', 'float', 'double'] %}
            # access type "int|float|double" attribute:
            value = 0
            if o != 0:
                _value = self._tab.Get(flatbuffers.number_types.{{ FbsType.FBS2FLAGS[field.type.basetype] }}, o + self._tab.Pos)
                if _value is not None:
                    value = _value

            {% elif field.type.map('python', field.attrs, True) == 'bool' %}
            # access type "bool" attribute:
            value = False
            if o != 0:
                _value = self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)
                if _value is not None:
                    value = _value

            {% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
            # access type "uuid.UUID" attribute:
            value = uuid.UUID(bytes=b'\x00' * 16)
            if o != 0:
                _off = self._tab.Vector(o)
                _len = self._tab.VectorLen(o)
                _value = memoryview(self._tab.Bytes)[_off:_off + _len]
                if _value is not None:
                    value = uuid.UUID(bytes=bytes(_value))

            {% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
            # access type "np.datetime64" attribute:
            value = np.datetime64(0, 'ns')
            if o != 0:
                _value = self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
                if value is not None:
                    value = np.datetime64(_value, 'ns')

            {% elif field.type.basetype == FbsType.Vector %}
            # access type "Vector" attribute:
            value = []
            if o != 0:
                _start_off = self._tab.Vector(o)
                _len = self._tab.VectorLen(o)
                for j in range(_len):
                    _off = _start_off + flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4
                    _off = self._tab.Indirect(_off)
                    {% if False and field.type.element == FbsType.Obj %}
                    _value = {{ field.type.element.split('.')[-1] }}.cast(self._tab.Bytes, _off)
                    {% else %}
                    # FIXME [8]
                    _value = {{ field.type.element }}()
                    {% endif %}
                    value.append(_value)

            {% elif field.type.basetype == FbsType.Obj %}
            # access type "Object" attribute:

            {% if field.type.objtype %}
            value = {{ field.type.objtype.split('.')[-1] }}()
            if o != 0:
                _off = self._tab.Indirect(o + self._tab.Pos)
                value = {{ field.type.objtype.split('.')[-1] }}.cast(self._tab.Bytes, _off)
            {% else %}
            # FIXME [9]: objtype of field "{{ field.name }}" is None
            value = ''
            {% endif %}

            {% else %}
            # FIXME [5]
            raise NotImplementedError('implement processing [5] of FlatBuffers type "{}"'.format({{ field.type.map('python', field.attrs, True) }}))
            {% endif %}
            assert value is not None
            self._{{ field.name }} = value
        return self._{{ field.name }}

    @{{ field.name }}.setter
    def {{ field.name }}(self, value: {{ field.type.map('python', field.attrs, required=False, objtype_as_string=True) }}):
        if value is not None:
            self._{{ field.name }} = value
        else:
            {% if field.type.map('python', field.attrs, True) == 'str' %}
            # set default value on type "string" attribute:
            self._{{ field.name }} = ''
            {% elif field.type.map('python', field.attrs, True) == 'bytes' %}
            # set default value on type "bytes" attribute:
            self._{{ field.name }} = b''
            {% elif field.type.map('python', field.attrs, True) in ['int', 'float', 'double'] %}
            # set default value on type "int|float|double" attribute:
            self._{{ field.name }} = 0
            {% elif field.type.map('python', field.attrs, True) == 'bool' %}
            # set default value on type "bool" attribute:
            self._{{ field.name }} = False
            {% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
            # set default value on type "uuid.UUID" attribute:
            self._{{ field.name }} = uuid.UUID(bytes=b'\x00' * 16)
            {% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
            # set default value on type "np.datetime64" attribute:
            self._{{ field.name }} = np.datetime64(0, 'ns')
            # set default value on type "List" attribute:
            {% elif field.type.basetype == FbsType.Vector %}
            self._{{ field.name }} = []
            # set default value on type "Object" attribute:
            {% elif field.type.basetype == FbsType.Obj %}
            self._{{ field.name }} = {{ field.type.map('python', field.attrs, True) }}()
            {% else %}
            # FIXME [6]
            raise NotImplementedError('implement processing [2] of FlatBuffers type "{}", basetype {}'.format({{ field.type.map('python', field.attrs, True) }}, {{ field.type.basetype }}))
            {% endif %}

    {% endfor %}

    @staticmethod
    def parse(data: Dict) -> '{{ metadata.classname }}':
        """
        Parse generic, native language object into a typed, native language object.

        :param data: Generic native language object to parse, e.g. output of ``cbor2.loads``.

        :returns: Typed object of this class.
        """
        # FIXME
        # for key in data.keys():
        #     assert key in {{ metadata.fields.keys() }}
        obj = {{ metadata.classname }}()
        {% for field in metadata.fields_by_id %}
        if '{{ field.name }}' in data:
            {% if field.type.map('python', field.attrs, True) == 'str' %}
            assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == str), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
            obj.{{ field.name }} = data['{{ field.name }}']

            {% elif field.type.map('python', field.attrs, True) == 'bytes' %}
            assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == bytes), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
            obj.{{ field.name }} = data['{{ field.name }}']

            {% elif field.type.map('python', field.attrs, True) == 'int' %}
            assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == int), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
            obj.{{ field.name }} = data['{{ field.name }}']

            {% elif field.type.map('python', field.attrs, True) == 'float' %}
            assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == float), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
            obj.{{ field.name }} = data['{{ field.name }}']

            {% elif field.type.map('python', field.attrs, True) == 'bool' %}
            assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == bool), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
            obj.{{ field.name }} = data['{{ field.name }}']

            {% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
            assert (data['{{ field.name }}'] is None or (type(data['{{ field.name }}']) == bytes and len(data['{{ field.name }}']) == 16)), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
            if data['{{ field.name }}'] is not None:
                obj.{{ field.name }} = uuid.UUID(bytes=data['{{ field.name }}'])
            else:
                obj.{{ field.name }} = None

            {% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
            assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == int), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
            if data['{{ field.name }}'] is not None:
                obj.{{ field.name }} = np.datetime64(data['{{ field.name }}'], 'ns')
            else:
                obj.{{ field.name }} = np.datetime64(0, 'ns')

            {% elif field.type.basetype == FbsType.Vector %}
            assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == list), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
            _value = []
            for v in data['{{ field.name }}']:
            {% if False and field.type.element == FbsType.Obj %}
                # FIXME
                _value.append({{ field.type.objtype.split('.')[-1] }}.parse(v))
            {% else %}
                _value.append(v)
            {% endif %}
            obj.{{ field.name }} = _value

            {% elif field.type.basetype == FbsType.Obj %}
            assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == dict), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
            _value = {{ field.type.map('python', field.attrs, True) }}.parse(data['{{ field.name }}'])
            obj.{{ field.name }} = _value

            {% else %}
            # FIXME [3]
            raise NotImplementedError('implement processing [3] of FlatBuffers type "{}"'.format({{ field.type.map('python', field.attrs, True) }}))
            {% endif %}
        {% endfor %}
        return obj

    def marshal(self) -> Dict:
        """
        Marshal all data contained in this typed native object into a generic object.

        :returns: Generic object that can be serialized to bytes using e.g. ``cbor2.dumps``.
        """
        obj = {
            {% for field in metadata.fields_by_id %}

            {% if field.type.map('python', field.attrs, True) in ['str', 'bytes', 'int', 'long', 'float', 'double', 'bool'] %}
            '{{ field.name }}': self.{{ field.name }},

            {% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
            '{{ field.name }}': self.{{ field.name }}.bytes if self.{{ field.name }} is not None else None,

            {% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
            '{{ field.name }}': int(self.{{ field.name }}) if self.{{ field.name }} is not None else None,

            {% elif field.type.basetype == FbsType.Vector %}
            {% if field.type.element == FbsType.Obj %}
            '{{ field.name }}': [o.marshal() for o in self.{{ field.name }}] if self.{{ field.name }} is not None else None,
            {% else %}
            '{{ field.name }}': self.{{ field.name }},
            {% endif %}

            {% elif field.type.basetype == FbsType.Obj %}
            '{{ field.name }}': self.{{ field.name }}.marshal() if self.{{ field.name }} is not None else None,

            {% else %}
            # FIXME [4]: implement processing [4] of FlatBuffers type "{{ field.type | string }}" (Python type "{{ field.type.map('python', field.attrs, True) }}")
            {% endif %}
            {% endfor %}
        }
        return obj

    def __str__(self) -> str:
        """
        Return string representation of this object, suitable for e.g. logging.

        :returns: String representation of this object.
        """
        return '\n{}\n'.format(pprint.pformat(self.marshal()))

    @staticmethod
    def cast(buf: bytes, offset: int = 0) -> '{{ metadata.classname }}':
        """
        Cast a FlatBuffers raw input buffer as a typed object of this class.

        :param buf: The raw input buffer to cast.
        :param offset: Offset into raw buffer from which to cast flatbuffers from.

        :returns: New native object that wraps the FlatBuffers raw buffer.
        """
        n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
        x = {{ metadata.classname }}()
        x._tab = flatbuffers.table.Table(buf, n + offset)
        return x

    def build(self, builder):
        """
        Build a FlatBuffers raw output buffer from this typed object.

        :returns: Constructs the FlatBuffers using the builder and
            returns ``builder.EndObject()``.
        """
        # first, write all string|bytes|etc typed attribute values (in order) to the buffer
        {% for field in metadata.fields_by_id %}
        {% if field.type.map('python', field.attrs, True) in ['str', 'bytes'] %}
        _{{ field.name }} = self.{{ field.name }}
        if _{{ field.name }}:
            _{{ field.name }} = builder.CreateString(_{{ field.name }})
        {% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
        _{{ field.name }} = self.{{ field.name }}.bytes if self.{{ field.name }} else None
        if _{{ field.name }}:
            _{{ field.name }} = builder.CreateString(_{{ field.name }})
        {% else %}
        {% endif %}
        {% endfor %}
        # now start a new object in the buffer and write the actual object attributes (in field
        # order) to the buffer
        builder.StartObject({{ metadata.fields_by_id|length }})

        {% for field in metadata.fields_by_id %}

        {% if field.type.map('python', field.attrs, True) in ['str', 'bytes', 'uuid.UUID'] %}
        if _{{ field.name }}:
            builder.PrependUOffsetTRelativeSlot({{ field.id }}, flatbuffers.number_types.UOffsetTFlags.py_type(_{{ field.name }}), 0)

        {% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
        if self.{{ field.name }}:
            builder.PrependUint64Slot({{ field.id }}, int(self.{{ field.name }}), 0)

        {% elif field.type.map('python', field.attrs, True) in ['bool', 'int', 'float'] %}
        if self.{{ field.name }}:
            builder.{{ FbsType.FBS2PREPEND[field.type.basetype] }}({{ field.id }}, self.{{ field.name }}, 0)

        {% else %}
        # FIXME [1]
        # raise NotImplementedError('implement builder [1] for type "{}"'.format({{ field.type.map('python', field.attrs, True) }}))

        {% endif %}
        {% endfor %}

        return builder.EndObject()
