###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) 2018 Luis Teixeira
# - copied & modified from https://github.com/vergl4s/ethereum-mnemonic-utils
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

import os
import pprint
import hashlib
from typing import Dict, Optional, TypeVar
from pathlib import Path

from zlmdb.flatbuffers.reflection.Schema import Schema as _Schema
from zlmdb.flatbuffers.reflection.BaseType import BaseType as _BaseType


# https://stackoverflow.com/a/46064289/884770
T_FbsRepository = TypeVar('T_FbsRepository', bound='FbsRepository')


class FbsType(object):
    """
    Flatbuffers type.

    See: https://github.com/google/flatbuffers/blob/master/reflection/reflection.fbs
    """

    # no type
    None_ = _BaseType.None_

    # ???
    UType = _BaseType.UType

    # scalar types
    Bool = _BaseType.Bool
    Byte = _BaseType.Byte
    UByte = _BaseType.UByte
    Short = _BaseType.Short
    UShort = _BaseType.UShort
    Int = _BaseType.Int
    UInt = _BaseType.UInt
    Long = _BaseType.Long
    ULong = _BaseType.ULong
    Float = _BaseType.Float
    Double = _BaseType.Double
    String = _BaseType.String

    SCALAR_TYPES = [_BaseType.Bool,
                    _BaseType.Byte,
                    _BaseType.UByte,
                    _BaseType.Short,
                    _BaseType.UShort,
                    _BaseType.Int,
                    _BaseType.UInt,
                    _BaseType.Long,
                    _BaseType.ULong,
                    _BaseType.Float,
                    _BaseType.Double,
                    _BaseType.String]

    # structured types
    Vector = _BaseType.Vector
    Obj = _BaseType.Obj
    Union = _BaseType.Union

    STRUCTURED_TYPES = [_BaseType.Vector,
                        _BaseType.Obj,
                        _BaseType.Union]

    FBS2PY = {
        _BaseType.None_: 'type(None)',
        _BaseType.UType: 'int',
        _BaseType.Bool: 'bool',
        _BaseType.Byte: 'bytes',
        _BaseType.UByte: 'int',
        _BaseType.Short: 'int',
        _BaseType.UShort: 'int',
        _BaseType.Int: 'int',
        _BaseType.UInt: 'int',
        _BaseType.Long: 'int',
        _BaseType.ULong: 'int',
        _BaseType.Float: 'float',
        _BaseType.Double: 'float',
        _BaseType.String: 'str',
        _BaseType.Vector: 'List',
        _BaseType.Obj: 'object',
        _BaseType.Union: 'Union',
    }

    FBS2FLAGS = {
        _BaseType.Bool: 'BoolFlags',
        _BaseType.Byte: 'Int8Flags',
        _BaseType.UByte: 'Uint8Flags',
        _BaseType.Short: 'Int16Flags',
        _BaseType.UShort: 'Uint16Flags',
        _BaseType.Int: 'Int32Flags',
        _BaseType.UInt: 'Uint32Flags',
        _BaseType.Long: 'Int64Flags',
        _BaseType.ULong: 'Uint64Flags',
        _BaseType.Float: 'Float32Flags',
        _BaseType.Double: 'Float64Flags',
    }

    FBS2PREPEND = {
        _BaseType.Bool: 'PrependBoolSlot',
        _BaseType.Byte: 'PrependInt8Slot',
        _BaseType.UByte: 'PrependUint8Slot',
        _BaseType.Short: 'PrependInt16Slot',
        _BaseType.UShort: 'PrependUint16Slot',
        _BaseType.Int: 'PrependInt32Slot',
        _BaseType.UInt: 'PrependUint32Slot',
        _BaseType.Long: 'PrependInt64Slot',
        _BaseType.ULong: 'PrependUint64Slot',
        _BaseType.Float: 'PrependFloat32Slot',
        _BaseType.Double: 'PrependFloat64Slot',
    }

    FBS2STR = {
        _BaseType.None_: 'None',
        _BaseType.UType: 'UType',
        _BaseType.Bool: 'Bool',
        _BaseType.Byte: 'Byte',
        _BaseType.UByte: 'UByte',
        _BaseType.Short: 'Short',
        _BaseType.UShort: 'UShort',
        _BaseType.Int: 'Int',
        _BaseType.UInt: 'UInt',
        _BaseType.Long: 'Long',
        _BaseType.ULong: 'ULong',
        _BaseType.Float: 'Float',
        _BaseType.Double: 'Double',
        _BaseType.String: 'String',
        _BaseType.Vector: 'Vector',
        _BaseType.Obj: 'Obj',
        _BaseType.Union: 'Union',
    }

    STR2FBS = {
        'None': _BaseType.None_,
        'UType': _BaseType.UType,
        'Bool': _BaseType.Bool,
        'Byte': _BaseType.Byte,
        'UByte': _BaseType.UByte,
        'Short': _BaseType.Short,
        'UShort': _BaseType.UShort,
        'Int': _BaseType.Int,
        'UInt': _BaseType.UInt,
        'Long': _BaseType.Long,
        'ULong': _BaseType.ULong,
        'Float': _BaseType.Float,
        'Double': _BaseType.Double,
        'String': _BaseType.String,
        'Vector': _BaseType.Vector,
        'Obj': _BaseType.Obj,
        'Union': _BaseType.Union,
    }

    def __init__(self,
                 repository: T_FbsRepository,
                 basetype: int,
                 element: int,
                 index: int,
                 objtype: Optional[str] = None):
        self._repository = repository
        self._basetype = basetype
        self._element = element
        self._index = index
        self._objtype = objtype

    @property
    def basetype(self):
        """
        Flatbuffers base type.

        :return:
        """
        return self._basetype

    @property
    def element(self):
        """
        Only if basetype == Vector or basetype == Array.

        :return:
        """
        return self._element

    @property
    def index(self):
        """
        If basetype == Object, index into "objects".

        :return:
        """
        return self._index

    @property
    def objtype(self):
        """
        If basetype == Object, fully qualified object type name.

        :return:
        """
        return self._objtype

    def map(self, language: str, attrs: Optional[Dict] = None, required: Optional[bool] = True) -> str:
        """

        :param language:
        :return:
        """
        if language == 'python':
            _mapped_type = None

            if self.basetype == FbsType.Vector:
                # vectors of uint8 are mapped to byte strings ..
                if self.element == FbsType.UByte:
                    if attrs and 'uuid' in attrs:
                        _mapped_type = 'uuid.UUID'
                    else:
                        _mapped_type = 'bytes'
                # .. whereas all other vectors are mapped to lists of the same element type
                else:
                    if self.objtype:
                        # FIXME
                        _mapped_type = 'List[{}]'.format(self.objtype.split('.')[-1])
                        # _mapped_type = 'List[{}.{}]'.format(self._repository.render_to_basemodule, self.objtype)
                    else:
                        _mapped_type = 'List[{}]'.format(FbsType.FBS2PY[self.element])

            elif self.basetype == FbsType.Obj:
                if self.objtype:
                    # FIXME
                    _mapped_type = self.objtype.split('.')[-1]
                    # _mapped_type = '{}.{}'.format(self._repository.render_to_basemodule, self.objtype)
                else:
                    _mapped_type = 'List[{}]'.format(FbsType.FBS2PY[self.element])

            elif self.basetype in FbsType.SCALAR_TYPES + [FbsType.UType, FbsType.Union]:
                # FIXME: follow up processing of Unions (UType/Union)
                if self.basetype == FbsType.ULong and attrs and 'timestamp' in attrs:
                    _mapped_type = 'np.datetime64'
                else:
                    _mapped_type = FbsType.FBS2PY[self.basetype]

            else:
                raise NotImplementedError('FIXME: implement mapping of FlatBuffers type "{}" to Python in {}'.format(self.basetype, self.map))

            if required:
                return _mapped_type
            else:
                return 'Optional[{}]'.format(_mapped_type)
        else:
            raise RuntimeError('cannot map FlatBuffers type to target language "{}" in {}'.format(language, self.map))

    def __str__(self):
        return '\n{}\n'.format(pprint.pformat(self.marshal()))

    def marshal(self):
        obj = {
            'basetype': self.FBS2STR.get(self._basetype, None),
            'element': self.FBS2STR.get(self._element, None),
            'index': self._index,
            'objtype': self._objtype,
        }
        return obj


class FbsAttribute(object):
    def __init__(self):
        pass

    def __str__(self):
        return ''.format()


class FbsField(object):
    def __init__(self,
                 repository: T_FbsRepository,
                 name: str,
                 type: FbsType,
                 id: int,
                 offset: int,
                 default_int: int,
                 default_real: float,
                 deprecated: bool,
                 required: bool,
                 attrs: Dict[str, FbsAttribute],
                 docs: str):
        self._repository = repository
        self._name = name
        self._type = type
        self._id = id
        self._offset = offset
        self._default_int = default_int
        self._default_real = default_real
        self._deprecated = deprecated
        self._required = required
        self._attrs = attrs
        self._docs = docs

    @property
    def repository(self):
        return self._repository

    @property
    def name(self):
        return self._name

    @property
    def type(self):
        return self._type

    @property
    def id(self):
        return self._id

    @property
    def offset(self):
        return self._offset

    @property
    def default_int(self):
        return self._default_int

    @property
    def default_real(self):
        return self._default_real

    @property
    def deprecated(self):
        return self._deprecated

    @property
    def required(self):
        return self._required

    @property
    def attrs(self):
        return self._attrs

    @property
    def docs(self):
        return self._docs

    def __str__(self):
        return '\n{}\n'.format(pprint.pformat(self.marshal()))

    def marshal(self):
        obj = {
            'name': self._name,
            'type': self._type.marshal() if self._type else None,
            'id': self._id,
            'offset': self._offset,
            'default_int': self._default_int,
            'default_real': self._default_real,
            'deprecated': self._deprecated,
            'required': self._required,
            'attrs': {},
            'docs': self._docs,
        }
        if self._attrs:
            for k, v in self._attrs.items():
                obj['attrs'][k] = v
        return obj


def parse_attr(obj):
    attrs = {}
    for j in range(obj.AttributesLength()):
        fbs_attr = obj.Attributes(j)
        attr_key = fbs_attr.Key()
        if attr_key:
            attr_key = attr_key.decode('utf8')
        attr_value = fbs_attr.Value()
        if attr_value:
            attr_value = attr_value.decode('utf8')
        assert attr_key not in attrs
        attrs[attr_key] = attr_value
    return attrs


def parse_docs(obj):
    docs = []
    for j in range(obj.DocumentationLength()):
        doc_line = obj.Documentation(j)
        if doc_line:
            doc_line = doc_line.decode('utf8')
            docs.append(doc_line)
    docs = '\n'.join(docs).strip()
    return docs


def parse_fields(repository, obj, objs_lst=None):
    fields = {}
    fields_by_id = {}
    for j in range(obj.FieldsLength()):
        fbs_field = obj.Fields(j)

        field_name = fbs_field.Name()
        if field_name:
            field_name = field_name.decode('utf8')

        field_id = int(fbs_field.Id())
        fbs_field_type = fbs_field.Type()

        _objtype = None
        if fbs_field_type.Index() >= 0:
            _obj = objs_lst[fbs_field_type.Index()]
            _objtype = _obj.name

        field_type = FbsType(repository=repository,
                             basetype=fbs_field_type.BaseType(),
                             element=fbs_field_type.Element(),
                             index=fbs_field_type.Index(),
                             objtype=_objtype)
        field = FbsField(repository=repository,
                         name=field_name,
                         type=field_type,
                         id=field_id,
                         offset=fbs_field.Offset(),
                         default_int=fbs_field.DefaultInteger(),
                         default_real=fbs_field.DefaultReal(),
                         deprecated=fbs_field.Deprecated(),
                         required=fbs_field.Required(),
                         attrs=parse_attr(fbs_field),
                         docs=parse_docs(fbs_field))
        assert field_name not in fields, 'field "{}" with id "{}" already in fields {}'.format(field_name, field_id, sorted(fields.keys()))
        fields[field_name] = field
        assert field_id not in fields_by_id, 'field "{}" with id " {}" already in fields {}'.format(field_name, field_id, sorted(fields.keys()))
        fields_by_id[field_id] = field_name
    res = []
    for _, value in sorted(fields_by_id.items()):
        res.append(value)
    fields_by_id = res
    return fields, fields_by_id


def parse_calls(repository, svc_obj, objs_lst=None):
    calls = {}
    calls_by_id = {}
    for j in range(svc_obj.CallsLength()):
        fbs_call = svc_obj.Calls(j)

        call_name = fbs_call.Name()
        if call_name:
            call_name = call_name.decode('utf8')

        # FIXME: schema reflection.RPCCall lacks "Id" (!)
        # call_id = int(fbs_call.Id())
        call_id = j

        fbs_call_req = fbs_call.Request()
        call_req_name = fbs_call_req.Name()
        if call_req_name:
            call_req_name = call_req_name.decode('utf8')
        call_req_is_struct = fbs_call_req.IsStruct()
        call_req_min_align = fbs_call_req.Minalign()
        call_req_bytesize = fbs_call_req.Bytesize()
        call_req_docs = parse_docs(fbs_call_req)
        call_req_attrs = parse_attr(fbs_call_req)
        call_req_fields, call_fields_by_id = parse_fields(repository, fbs_call_req, objs_lst=objs_lst)
        call_req = FbsObject(repository=repository,
                             name=call_req_name,
                             fields=call_req_fields,
                             fields_by_id=call_fields_by_id,
                             is_struct=call_req_is_struct,
                             min_align=call_req_min_align,
                             bytesize=call_req_bytesize,
                             attrs=call_req_attrs,
                             docs=call_req_docs)

        fbs_call_resp = fbs_call.Response()
        call_resp_name = fbs_call_resp.Name()
        if call_resp_name:
            call_resp_name = call_resp_name.decode('utf8')
        call_resp_is_struct = fbs_call_resp.IsStruct()
        call_resp_min_align = fbs_call_resp.Minalign()
        call_resp_bytesize = fbs_call_resp.Bytesize()
        call_resp_docs = parse_docs(fbs_call_resp)
        call_resp_attrs = parse_attr(fbs_call_resp)
        call_resp_fields, call_resp_fields_by_id = parse_fields(repository, fbs_call_resp, objs_lst=objs_lst)
        call_resp = FbsObject(repository=repository,
                              name=call_resp_name,
                              fields=call_resp_fields,
                              fields_by_id=call_resp_fields_by_id,
                              is_struct=call_resp_is_struct,
                              min_align=call_resp_min_align,
                              bytesize=call_resp_bytesize,
                              attrs=call_resp_attrs,
                              docs=call_resp_docs)

        call_docs = parse_docs(fbs_call)
        call_attrs = parse_attr(fbs_call)
        call = FbsRPCCall(repository,
                          name=call_name,
                          id=call_id,
                          request=call_req,
                          response=call_resp,
                          docs=call_docs,
                          attrs=call_attrs)

        assert call_name not in calls, 'call "{}" with id "{}" already in calls {}'.format(call_name, call_id, sorted(calls.keys()))
        calls[call_name] = call
        assert call_id not in calls_by_id, 'call "{}" with id " {}" already in calls {}'.format(call_name, call_id, sorted(calls.keys()))
        calls_by_id[call_id] = call_name

    res = []
    for _, value in sorted(calls_by_id.items()):
        res.append(value)
    calls_by_id = res
    return calls, calls_by_id


class FbsObject(object):
    def __init__(self,
                 repository: T_FbsRepository,
                 name: str,
                 fields: Dict[str, FbsField],
                 fields_by_id: Dict[int, str],
                 is_struct: bool,
                 min_align: int,
                 bytesize: int,
                 attrs: Dict[str, FbsAttribute],
                 docs: str):
        self._repository = repository
        self._name = name
        self._fields = fields
        self._fields_by_id = fields_by_id
        self._is_struct = is_struct
        self._min_align = min_align
        self._bytesize = bytesize
        self._attrs = attrs
        self._docs = docs

    def map(self, language: str) -> str:
        if language == 'python':
            klass = self._name.split('.')[-1]
            return klass
        else:
            raise NotImplementedError()

    def map_import(self, language: str) -> str:
        if language == 'python':
            base = self._name.split('.')[-2]
            klass = self._name.split('.')[-1]
            return 'from {} import {}'.format(base, klass)
        else:
            raise NotImplementedError()

    @property
    def repository(self):
        return self._repository

    @property
    def name(self):
        return self._name

    @property
    def fields(self):
        return self._fields

    @property
    def fields_by_id(self):
        return self._fields_by_id

    @property
    def is_struct(self):
        return self._is_struct

    @property
    def min_align(self):
        return self._min_align

    @property
    def bytesize(self):
        return self._bytesize

    @property
    def attrs(self):
        return self._attrs

    @property
    def docs(self):
        return self._docs

    def __str__(self):
        return '\n{}\n'.format(pprint.pformat(self.marshal()))

    def marshal(self):
        obj = {
            'name': self._name,
            'fields': {},
            'is_struct': self._is_struct,
            'min_align': self._min_align,
            'bytesize': self._bytesize,
            'attrs': {},
            'docs': self._docs,
        }
        if self._fields:
            for k, v in self._fields.items():
                obj['fields'][k] = v.marshal() if v else None
        if self._attrs:
            for k, v in self._attrs.items():
                obj['attrs'][k] = v
        return obj

    @staticmethod
    def parse(repository, fbs_obj, objs_lst=None):
        obj_name = fbs_obj.Name()
        if obj_name:
            obj_name = obj_name.decode('utf8')
        obj_docs = parse_docs(fbs_obj)
        obj_attrs = parse_attr(fbs_obj)
        obj_fields, obj_fields_by_id = parse_fields(repository, fbs_obj, objs_lst=objs_lst)
        obj = FbsObject(repository=repository,
                        name=obj_name,
                        fields=obj_fields,
                        fields_by_id=obj_fields_by_id,
                        is_struct=fbs_obj.IsStruct(),
                        min_align=fbs_obj.Minalign(),
                        bytesize=fbs_obj.Bytesize(),
                        attrs=obj_attrs,
                        docs=obj_docs)
        return obj


class FbsRPCCall(object):
    def __init__(self,
                 repository: T_FbsRepository,
                 name: str,
                 id: int,
                 request: FbsObject,
                 response: FbsObject,
                 docs: str,
                 attrs: Dict[str, FbsAttribute]):
        self._repository = repository
        self._name = name
        self._id = id
        self._request = request
        self._response = response
        self._docs = docs
        self._attrs = attrs

    @property
    def repository(self):
        return self._repository

    @property
    def name(self):
        return self._name

    @property
    def id(self):
        return self._id

    @property
    def request(self):
        return self._request

    @property
    def response(self):
        return self._response

    @property
    def docs(self):
        return self._docs

    @property
    def attrs(self):
        return self._attrs

    def __str__(self):
        return '\n{}\n'.format(pprint.pformat(self.marshal()))

    def marshal(self):
        obj = {
            'name': self._name,
            'request': self._request.marshal() if self._request else None,
            'response': self._response.marshal() if self._response else None,
            'attrs': {},
            'docs': self._docs,
        }
        if self._attrs:
            for k, v in self._attrs.items():
                obj['attrs'][k] = v
        return obj


class FbsService(object):
    def __init__(self,
                 repository: T_FbsRepository,
                 name: str,
                 calls: Dict[str, FbsRPCCall],
                 calls_by_id: Dict[int, str],
                 attrs: Dict[str, FbsAttribute],
                 docs: str):
        self._repository = repository
        self._name = name
        self._calls = calls
        self._calls_by_id = calls_by_id
        self._attrs = attrs
        self._docs = docs

    @property
    def repository(self):
        return self._repository

    @property
    def name(self):
        return self._name

    @property
    def calls(self):
        return self._calls

    @property
    def calls_by_id(self):
        return self._calls_by_id

    @property
    def attrs(self):
        return self._attrs

    @property
    def docs(self):
        return self._docs

    def __str__(self):
        return '\n{}\n'.format(pprint.pformat(self.marshal()))

    def marshal(self):
        obj = {
            'name': self._name,
            'calls': {},
            'attrs': {},
            'docs': self._docs,
        }
        if self._calls:
            for k, v in self._calls.items():
                obj['calls'][k] = v.marshal()
        if self._attrs:
            for k, v in self._attrs.items():
                obj['attrs'][k] = v
        return obj


class FbsEnumValue(object):
    def __init__(self, repository, name, value, docs):
        self._repository = repository
        self._name = name
        self._value = value
        self._attrs = {}
        self._docs = docs

    @property
    def repository(self):
        return self._repository

    @property
    def name(self):
        return self._name

    @property
    def value(self):
        return self._value

    @property
    def attrs(self):
        return self._attrs

    @property
    def docs(self):
        return self._docs

    def __str__(self):
        return '\n{}\n'.format(pprint.pformat(self.marshal()))

    def marshal(self):
        obj = {
            'name': self._name,
            'attrs': self._attrs,
            'docs': self._docs,
            'value': self._value,
        }
        if self._attrs:
            for k, v in self._attrs.items():
                obj['attrs'][k] = v
        return obj


class FbsEnum(object):
    """
    FlatBuffers enum type.
    """
    def __init__(self,
                 repository: T_FbsRepository,
                 name: str,
                 values: Dict[str, FbsEnumValue],
                 is_union: bool,
                 underlying_type: int,
                 attrs: Dict[str, FbsAttribute],
                 docs: str):
        self._repository = repository
        self._name = name
        self._values = values
        self._is_union = is_union

        # zlmdb.flatbuffers.reflection.Type.Type
        self._underlying_type = underlying_type
        self._attrs = attrs
        self._docs = docs

    @property
    def repository(self):
        return self._repository

    @property
    def name(self):
        return self._name

    @property
    def values(self):
        return self._values

    @property
    def is_union(self):
        return self._is_union

    @property
    def underlying_type(self):
        return self._underlying_type

    @property
    def attrs(self):
        return self._attrs

    @property
    def docs(self):
        return self._docs

    def __str__(self):
        return '\n{}\n'.format(pprint.pformat(self.marshal()))

    def marshal(self):
        obj = {
            'name': self._name,
            'values': {},
            'is_union': self._is_union,
            'underlying_type': FbsType.FBS2STR.get(self._underlying_type, None),
            'attrs': {},
            'docs': self._docs,
        }
        if self._values:
            for k, v in self._values.items():
                obj['values'][k] = v.marshal()
        if self._attrs:
            for k, v in self._attrs.items():
                obj['attrs'][k] = v
        return obj


class FbsSchema(object):
    """
    """
    def __init__(self,
                 repository: T_FbsRepository,
                 file_name: str,
                 file_sha256: str,
                 file_size: int,
                 file_ident: str,
                 file_ext: str,
                 root_table: FbsObject,
                 root: _Schema,
                 objs: Dict[str, FbsObject],
                 enums: Dict[str, FbsEnum],
                 services: Dict[str, FbsService]):
        """

        :param repository:
        :param file_name:
        :param file_sha256:
        :param file_size:
        :param file_ident:
        :param file_ext:
        :param root_table:
        :param root:
        :param objs:
        :param enums:
        :param services:
        """
        self._repository = repository
        self._file_name = file_name
        self._file_sha256 = file_sha256
        self._file_size = file_size
        self._file_ident = file_ident
        self._file_ext = file_ext
        self._root_table = root_table
        self._root = root
        self._objs = objs
        self._enums = enums
        self._services = services

    @property
    def repository(self):
        return self._repository

    @property
    def file_name(self):
        return self._file_name

    @property
    def file_sha256(self):
        return self._file_sha256

    @property
    def file_size(self):
        return self._file_size

    @property
    def file_ident(self):
        return self._file_ident

    @property
    def file_ext(self):
        return self._file_ext

    @property
    def root_table(self):
        return self._root_table

    @property
    def root(self):
        return self._root

    @property
    def objs(self):
        return self._objs

    @property
    def enums(self):
        return self._enums

    @property
    def services(self):
        return self._services

    def __str__(self):
        return '\n{}\n'.format(pprint.pformat(self.marshal(), width=255))

    def marshal(self) -> Dict[str, object]:
        """

        :return:
        """
        obj = {
            'schema': {
                'ident': self._file_ident,
                'ext': self._file_ext,
                'name': os.path.basename(self._file_name) if self._file_name else None,
                'sha256': self._file_sha256,
                'size': self._file_size,
                'objects': len(self._objs),
                'enums': len(self._enums),
                'services': len(self._services),
            },
            'root_table': self._root_table.marshal() if self._root_table else None,
            'enums': {},
            'objects': {},
            'services': {},
        }
        if self._enums:
            for k, v in self._enums.items():
                obj['enums'][k] = v.marshal()
        if self._objs:
            for k, v in self._objs.items():
                obj['objects'][k] = v.marshal()
        if self._services:
            for k, v in self._services.items():
                obj['services'][k] = v.marshal()
        return obj

    @staticmethod
    def load(repository, filename) -> object:
        """

        :param filename:
        :return:
        """
        if not os.path.isfile(filename):
            raise RuntimeError('cannot open schema file {}'.format(filename))
        with open(filename, 'rb') as fd:
            data = fd.read()

        print('processing schema {} ({} bytes) ..'.format(filename, len(data)))
        root = _Schema.GetRootAsSchema(data, 0)

        file_ident = root.FileIdent()
        if file_ident is not None:
            file_ident = file_ident.decode('utf8')

        file_ext = root.FileExt()
        if file_ext is not None:
            file_ext = file_ext.decode('utf8')

        root_table = root.RootTable()
        if root_table is not None:
            root_table = FbsObject.parse(repository, root_table)

        objs = {}
        objs_lst = []
        services = {}
        enums = {}

        for i in range(root.EnumsLength()):
            fbs_enum = root.Enums(i)

            enum_name = fbs_enum.Name()
            if enum_name:
                enum_name = enum_name.decode('utf8')

            enum_underlying_type = fbs_enum.UnderlyingType()

            enum_values = {}
            for j in range(fbs_enum.ValuesLength()):
                fbs_enum_value = fbs_enum.Values(j)
                enum_value_name = fbs_enum_value.Name()
                if enum_value_name:
                    enum_value_name = enum_value_name.decode('utf8')
                enum_value_value = fbs_enum_value.Value()
                enum_value_docs = parse_docs(fbs_enum_value)
                enum_value = FbsEnumValue(repository=repository,
                                          name=enum_value_name,
                                          value=enum_value_value,
                                          docs=enum_value_docs)
                assert enum_value_name not in enum_values
                enum_values[enum_value_name] = enum_value

            enum = FbsEnum(repository=repository,
                           name=enum_name,
                           values=enum_values,
                           is_union=fbs_enum.IsUnion(),
                           underlying_type=enum_underlying_type,
                           attrs=parse_attr(fbs_enum),
                           docs=parse_docs(fbs_enum))
            assert enum_name not in enums
            enums[enum_name] = enum

        for i in range(root.ObjectsLength()):
            fbs_obj = root.Objects(i)
            obj = FbsObject.parse(repository, fbs_obj, objs_lst=objs_lst)
            assert obj.name not in objs
            objs[obj.name] = obj
            objs_lst.append(obj)

        for i in range(root.ServicesLength()):
            svc_obj = root.Services(i)

            svc_name = svc_obj.Name()
            if svc_name:
                svc_name = svc_name.decode('utf8')

            docs = parse_docs(svc_obj)
            attrs = parse_attr(svc_obj)
            calls, calls_by_id = parse_calls(repository, svc_obj, objs_lst=objs_lst)

            service = FbsService(repository=repository,
                                 name=svc_name,
                                 calls=calls,
                                 calls_by_id=calls_by_id,
                                 attrs=attrs,
                                 docs=docs)
            assert svc_name not in services
            services[svc_name] = service

        m = hashlib.sha256()
        m.update(data)

        schema = FbsSchema(repository=repository,
                           file_name=filename,
                           file_size=len(data),
                           file_sha256=m.hexdigest(),
                           file_ident=file_ident,
                           file_ext=file_ext,
                           root_table=root_table,
                           root=root,
                           objs=objs,
                           enums=enums,
                           services=services)
        return schema


class FbsRepository(object):
    """
    """

    def __init__(self, render_to_basemodule):
        self._render_to_basemodule = render_to_basemodule

        self._schemata = {}

        # Dict[str, FbsObject]
        self._objs = {}

        # Dict[str, FbsEnum]
        self._enums = {}

        # Dict[str, FbsService]
        self._services = {}

    def summary(self, keys=False):
        if keys:
            return {
                'schemata': sorted(self._schemata.keys()),
                'objs': sorted(self._objs.keys()),
                'enums': sorted(self._enums.keys()),
                'services': sorted(self._services.keys()),
            }
        else:
            return {
                'schemata': len(self._schemata),
                'objs': len(self._objs),
                'enums': len(self._enums),
                'services': len(self._services),
            }

    @property
    def render_to_basemodule(self):
        return self._render_to_basemodule

    @property
    def objs(self):
        return self._objs

    @property
    def enums(self):
        return self._enums

    @property
    def services(self):
        return self._services

    def load(self, dirname) -> object:
        if not os.path.isdir(dirname):
            raise RuntimeError('cannot open schema directory {}'.format(dirname))

        found = []
        for path in Path(dirname).rglob('*.bfbs'):
            fn = os.path.abspath(os.path.join(dirname, path.name))
            if fn not in self._schemata:
                found.append(fn)
            else:
                print('duplicate schema: {} already loaded'.format(fn))

        # iterate over all schema files found
        for fn in found:
            # load this schema file
            schema = FbsSchema.load(self, fn)

            # add enum types
            for enum in schema.enums.values():
                if enum.name in self._enums:
                    print('duplicate enum for name "{}"'.format(enum.name))
                else:
                    self._enums[enum.name] = enum

            # add object types
            for obj in schema.objs.values():
                if obj.name in self._objs:
                    print('duplicate object for name "{}"'.format(obj.name))
                else:
                    self._objs[obj.name] = obj

            # add service definitions ("APIs")
            for svc in schema.services.values():
                if svc.name in self._services:
                    print('duplicate service for name "{}"'.format(svc.name))
                else:
                    self._services[svc.name] = svc

            self._schemata[fn] = schema
