import json
import yaml
import deepdiff
from enum import Enum
from datetime import datetime
from dust import Datatypes, ValueTypes, Operation, MetaProps, FieldProps, Committed
from importlib import import_module

import threading

_messages_create_message = None
_store_lock = threading.RLock()

UNIT_ENTITY = "entity"
UNIT_ENTITY_META = "entity_meta"
UNIT_ID = 1
UNIT_META_ID = 2

class UnitMeta(MetaProps):
    name = (Datatypes.STRING, ValueTypes.SINGLE, 1, 100)
    id_cnt = (Datatypes.INT, ValueTypes.SINGLE, 2, 101)
    meta_types = (Datatypes.ENTITY, ValueTypes.SET, 3, 102)

class TypeMeta(MetaProps):
    name = (Datatypes.STRING, ValueTypes.SINGLE, 1, 200)
    fields = (Datatypes.ENTITY, ValueTypes.SET, 2, 201)

class MetaField(MetaProps):
    name = (Datatypes.STRING, ValueTypes.SINGLE, 1, 300)
    global_name = (Datatypes.STRING, ValueTypes.SINGLE, 2, 301)
    field_order = (Datatypes.INT, ValueTypes.SINGLE, 3, 302)
    data_type = (Datatypes.STRING, ValueTypes.SINGLE, 4, 303)
    value_type = (Datatypes.STRING, ValueTypes.SINGLE, 5, 304)

class EntityBaseMeta(MetaProps):
    unit = (Datatypes.ENTITY, ValueTypes.SINGLE, 1, 400)
    meta_type = (Datatypes.ENTITY, ValueTypes.SINGLE, 2, 401)
    entity_id = (Datatypes.INT, ValueTypes.SINGLE, 3, 402)
    committed = (Datatypes.STRING, ValueTypes.SINGLE, 4 ,403)

class EntityTypes(FieldProps):
    type_meta = (UNIT_ENTITY_META, TypeMeta, 1)
    _entity_base = (UNIT_ENTITY_META, EntityBaseMeta, 2)
    unit = (UNIT_ENTITY_META, UnitMeta, 3)
    meta_field = (UNIT_ENTITY_META, MetaField, 4)

class EntityJsonSerializer():
    def dump(self, obj, fp):
        if isinstance(obj, Entity):
            fp.write(json.dumps(obj.to_json()).encode())
            fp.write(b"\n")
        elif isinstance(obj, list):
            objs = []
            for e in obj:
                if isinstance(e, Entity):
                    objs.append(e.to_json())
                else:
                    raise ValueError("Only entity object can be serialized!")
            fp.write(json.dumps(objs).encode())
            fp.write(b"\n")
        else:
            raise ValueError("Only entity object can be serialized!")

    def load(self, f):
        try:
            data = json.loads(f.readline().decode())
            if not isinstance(data, list):
                data = [data]
            return Store.from_json(data)
        except:
            raise ValueError("Invalid json data in messagequeue!")

def get_unit_deps_tuple(module_name, unit_name, meta_type_enums):
    module = import_module(module_name)
    unit_name_attr = getattr(module, unit_name)
    meta_type_enums_attr = getattr(module, meta_type_enums)
    dep_func = getattr(module, "get_unit_dependencies", None)
    return (unit_name_attr, meta_type_enums_attr, dep_func)

def compare_entity_to_json_simple(meta_type_enum, entity, json_entity, json_entity_map, compare_sub_entity_fields_map=None, log_prefix=""):
    changed = {}

    compare_sub_entity_fields = {}
    if compare_sub_entity_fields_map:
        compare_sub_entity_fields = compare_sub_entity_fields_map.get(meta_type_enum, {})

    for field in meta_type_enum.fields_enum:
        if field.valuetype == ValueTypes.SINGLE:
            if field.datatype == Datatypes.ENTITY:
                if compare_sub_entity_fields and field in compare_sub_entity_fields:
                    orig_value = entity.access(Operation.GET, None, field)
                    #print(field.name+"-"+str(orig_value)+"-"+str(_entity_map[entity.global_id()][0]))
                    new_value = None
                    new_entity_global_id = json_entity.get(Store.get_global_fieldname(field))
                    if new_entity_global_id:
                        new_value = json_entity_map[new_entity_global_id]
                        #print(str(new_value))
                    if orig_value is None and not new_value is None or not orig_value is None and new_value is None:
                        changed[log_prefix+field.name] = {"orig_value": orig_value, "new_value": new_value}
                    elif not orig_value is None and not new_value is None:
                        changed.update(compare_entity_to_json_simple(compare_sub_entity_fields[field], orig_value, new_value, json_entity_map, compare_sub_entity_fields_map, log_prefix="{}#".format(field.name)))
            else:
                entity_changed = False
                if ( json_entity is None and not entity is None ) or ( not json_entity is None and entity is None ):
                    entity_changed = True
                elif json_entity and entity:
                    value1 = entity.access(Operation.GET, None, field)
                    value2 = json_entity.get(Store.get_global_fieldname(field))
                    if value1 is None and not value2 is None or not value1 is None and value2 is None:
                        entity_changed = True
                    elif value1 and value2:
                        if field.datatype == Datatypes.BOOL:
                            entity_changed = bool(value1) != bool(value2)
                        elif field.datatype == Datatypes.INT:
                            entity_changed = int(value1) != int(value2)
                        elif field.datatype == Datatypes.NUMERIC:
                            entity_changed = abs(float(value1) - float(value2)) > 0.0001
                        elif field.datatype == Datatypes.STRING:
                            entity_changed = str(value1) != str(value2)
                        elif field.datatype == Datatypes.JSON:
                            dd = deepdiff.DeepDiff(value1, value2, ignore_order=True)
                            if dd:
                                entity_changed = True
                        else:
                            entity_changed = value1 != value2
                    
                if entity_changed:
                    changed[log_prefix+field.name] = {"orig_value": entity.access(Operation.GET, None, field), "new_value": json_entity.get(Store.get_global_fieldname(field))}

        elif field.valuetype == ValueTypes.SET or field.valuetype == ValueTypes.LIST or field.valuetype == ValueTypes.MAP:
            if field.datatype != Datatypes.ENTITY:
                iter1 = entity.access(Operation.GET, None, field)
                iter2 = json_entity.get(Store.get_global_fieldname(field))
                if field.valuetype == field.valuetype == ValueTypes.SET and iter2:
                    iter2 = set(iter2)

                dd = deepdiff.DeepDiff(iter1, iter2, ignore_order=True)
                if dd:
                    orig_value = entity.access(Operation.GET, None, field)
                    new_value = json_entity.get(Store.get_global_fieldname(field))

                    if field.valuetype == ValueTypes.SET:
                        if orig_value:
                            orig_value = list(orig_value)
                        if new_value:
                            new_value = list(new_value)
                    changed[log_prefix+field.name] = {"orig_value": orig_value, "new_value": new_value}
            elif field.valuetype == ValueTypes.SET or field.valuetype == ValueTypes.LIST:
                if compare_sub_entity_fields and field in compare_sub_entity_fields:
                    orig_value_global_ids = entity.access(Operation.GET, None, field)
                    new_entity_global_ids = json_entity.get(Store.get_global_fieldname(field))
                    # at this point we only have unordered ids, so we have to compare everything to everything and fail as early as possible
                    if not orig_value_global_ids and new_entity_global_ids or not new_entity_global_ids and orig_value_global_ids or len(orig_value_global_ids) != len(new_entity_global_ids):
                        changed[log_prefix+field.name] = {"orig_list_value": list(orig_value_global_ids), "new_list_value": list(new_entity_global_ids)}
                    else:
                        # Same number of entries
                        no_match_found = False
                        match_to = list(new_entity_global_ids)
                        for orig_value_global_id in orig_value_global_ids:
                            orig_value = Store.access(Operation.GET, None, orig_value_global_id)
                            if orig_value is None:
                                no_match_found = True
                                break
                            else:
                                match_found = False
                                for new_entity_global_id in match_to:
                                    new_value = json_entity_map[new_entity_global_id]
                                    if not new_value is None:
                                        sub_changed = compare_entity_to_json_simple(compare_sub_entity_fields[field], orig_value, new_value, json_entity_map, compare_sub_entity_fields_map, log_prefix="{}#".format(field.name))
                                        if len(sub_changed) == 0:
                                            match_found = True
                                            break
                                if not match_found:
                                    no_match_found = True
                                    break
                        if no_match_found:
                            changed[log_prefix+field.name] = {"orig_list_value": list(orig_value_global_ids), "new_list_value": list(new_entity_global_ids)}

    return changed

class Store():
    @staticmethod
    def load_unit_types(filenames):
        for filename in filenames:
            with open(filename, "r") as tf:
                loaded__meta_types = yaml.load(tf, Loader=yaml.FullLoader)["types"]

            Store.load_types_from_dict(loaded__meta_types)

    @staticmethod
    def create_unit(unit_name, unit_id):
        entity_map = globals()["_entity_map"]
        enum_map = globals()["_enum_map"]

        if not unit_name in [UNIT_ENTITY, UNIT_ENTITY_META] and not unit_name in enum_map:
            unit = Store.access(Operation.GET, None, UNIT_ENTITY, unit_id, EntityTypes.unit)
            enum_map[unit_name] = unit
            enum_map[unit] = unit_name
            #print("Unit name: {} ({}), global id: {}".format(unit_name, unit.entity_id, unit.global_id()))
            unit.access(Operation.SET, 0, UnitMeta.id_cnt)
            unit.access(Operation.SET, unit_name, UnitMeta.name)
        
        elif not ( UNIT_ENTITY+":2:"+EntityTypes.unit.name ) in entity_map:
            unit = Store._create_entity(None, 1, None)
            unit.unit = unit
            enum_map[UNIT_ENTITY] = unit
            enum_map[unit] = UNIT_ENTITY

            unit_meta = Store._create_entity(unit, 2, None)
            enum_map[UNIT_ENTITY_META] = unit_meta
            enum_map[unit_meta] = UNIT_ENTITY_META

            cnt = 0
            types = {}
            for entity_type in EntityTypes:
                cnt += 1
                et = Store._create_entity(unit_meta, entity_type.id_value, None)
                enum_map[entity_type] = et
                enum_map[et] = entity_type
                if entity_type.id_value > cnt:
                    cnt = entity_type.id_value

            unit.meta_type = enum_map[EntityTypes.unit]
            unit_meta.meta_type = enum_map[EntityTypes.unit]
            enum_map[EntityTypes.unit].meta_type = enum_map[EntityTypes.type_meta]

            Store._add_entity_to_store(unit)
            Store._add_entity_to_store(unit_meta)

            for entity_type in [EntityTypes.type_meta, EntityTypes.unit, EntityTypes._entity_base, EntityTypes.meta_field]:
                et = enum_map[entity_type]
                et.meta_type = enum_map[EntityTypes.type_meta]
                enum_map[et.global_id()] = entity_type
                Store._add_entity_to_store(et)
                et.access(Operation.SET, entity_type.name, TypeMeta.name)

            unit.access(Operation.SET, 2, UnitMeta.id_cnt)
            unit.access(Operation.SET, UNIT_ENTITY, UnitMeta.name)

            unit_meta.access(Operation.SET, cnt, UnitMeta.id_cnt)
            unit_meta.access(Operation.SET, UNIT_ENTITY_META, UnitMeta.name)

            return unit_meta

        else:
            unit = enum_map[unit_name]

        return unit

    @staticmethod
    def _get_base_fields():
        fields = []
        for base_field in EntityBaseMeta:
            fields.append(Store._global_field_name(UNIT_ENTITY_META, EntityTypes._entity_base.name, base_field.name))

        return tuple(fields)

    @staticmethod
    def _create_entity(unit, entity_id, meta_type):
        enum_map = globals()["_enum_map"]

        if unit and not isinstance(unit, Entity):
            unit = enum_map[unit]
        if meta_type and not isinstance(meta_type, Entity):
            meta_type = enum_map[meta_type]

        e = Entity(unit, entity_id, meta_type)

        if unit and meta_type:
            Store._add_entity_to_store(e)

        return e

    @staticmethod
    def _add_entity_to_store(e):
        entity_map = globals()["_entity_map"]

        unit_field, meta_type_field, entity_id_field, committed_field = Store._get_base_fields()

        entity_map[e.global_id()] = ({
            unit_field: e.unit.global_id(), 
            entity_id_field: e.entity_id, 
            meta_type_field: e.meta_type.global_id(),
            committed_field: e.committed.name
        }, e)

    @staticmethod
    def _delete_entity_to_store(e):
        entity_map = globals()["_entity_map"]

        if e.global_id() in entity_map:
            del entity_map[e.global_id()]

    @staticmethod
    def increment_unit_counter(unit, requested_id):
        if isinstance(unit, str):
            unit = globals()["_enum_map"][unit]
        unit_entity_map = globals()["_entity_map"][unit.global_id()][0]

        if requested_id is None:
            return unit.access(Operation.CHANGE, 1, UnitMeta.id_cnt)
        elif not unit.entity_id is None:
            current_cnt = unit_entity_map.get("entity_meta:unit:id_cnt")
            if current_cnt == None or current_cnt < requested_id:
                unit_entity_map["entity_meta:unit:id_cnt"] = requested_id

            return requested_id

    @staticmethod
    def load_types_from_enum(e, unit_meta_id):
        enum_map = globals()["_enum_map"]
        unit_meta_types = {}
        for meta_type in e:
            for field in meta_type.fields_enum:
                global_name = Store._global_field_name(meta_type.unit_name, meta_type.name, field.name)
                enum_map[global_name] = {"datatype": field.value[0], "valuetype": field.value[1], "id": field.id_value, "field_order": field.order_value, "_enum": field}
                enum_map[field] = global_name

        for base_field in EntityBaseMeta:
            global_name = Store._global_field_name(UNIT_ENTITY_META, EntityTypes._entity_base.name, base_field.name)
            enum_map[global_name] = {"datatype": base_field.value[0], "valuetype": base_field.value[1], "id": base_field.id_value, "field_order": base_field.order_value, "_enum": base_field}
            enum_map[base_field] = global_name

        for meta_type in e:
            unit = Store.create_unit(meta_type.unit_name, unit_meta_id)

            if meta_type in enum_map:
                field_meta_type = enum_map[meta_type]
            else:
                cnt = unit.access(Operation.GET, 0, UnitMeta.id_cnt)
                if meta_type.id_value > cnt:
                    unit.access(Operation.SET, meta_type.id_value, UnitMeta.id_cnt)
                field_meta_type = Store.access(Operation.GET, None, meta_type.unit_name, meta_type.id_value, EntityTypes.type_meta)
                field_meta_type.access(Operation.SET, meta_type.name, TypeMeta.name)
                enum_map[meta_type] = field_meta_type
                enum_map[field_meta_type] = meta_type
                enum_map[field_meta_type.global_id()] = meta_type

            field_entities = []
            max_id_value = 0
            for field in meta_type.fields_enum:
                global_name = Store._global_field_name(meta_type.unit_name, meta_type.name, field.name)

                field_entity = Store.access(Operation.GET, None, meta_type.unit_name, field.id_value, EntityTypes.meta_field)
                field_entity.access(Operation.SET, field.name, MetaField.name)
                field_entity.access(Operation.SET, field.order_value, MetaField.field_order)
                field_entity.access(Operation.SET, global_name, MetaField.global_name)
                field_entity.access(Operation.SET, field.datatype.name, MetaField.data_type)
                field_entity.access(Operation.SET, field.valuetype.name, MetaField.value_type)
                field_entities.append(field_entity)

                if field.id_value > max_id_value:
                    max_id_value = field.id_value

            for field_entity in field_entities:
                field_meta_type.access(Operation.ADD, field_entity, TypeMeta.fields)

            if max_id_value > 0:
                cnt = unit.access(Operation.GET, 0, UnitMeta.id_cnt)
                if max_id_value > cnt:
                    unit.access(Operation.SET, max_id_value, UnitMeta.id_cnt)

            unit_meta_types.setdefault(unit, []).append(field_meta_type)

        for unit, field_meta_types in unit_meta_types.items():
            for field_meta_type in field_meta_types:
                unit.access(Operation.ADD, field_meta_type, UnitMeta.meta_types)

    @staticmethod
    def access(operation, value, *path):
        with _store_lock:
            return Store._access(operation, value, *path)

    @staticmethod
    def _access(operation, value, *path):
        entities = globals()["_entity_map"]
        enum_map = globals()["_enum_map"]

        local_ref = None
        last_obj = None
        remaining_path = None
        idx = 0
        last_global_id = None
        last_entity_path = []

        unit = None
        meta_type = None

        if len(path) > 0:
            if operation == Operation.WALK and isinstance(path[0], list):
                last_obj = path[0]
                idx = 1
            else:
                unit, entity_id, meta_type = Entity._resolve_global_id(path[0])
                if entity_id is None:
                    # path is done with 3 parts
                    if len(path) > 2 and ( path[1] is None or isinstance(path[1], int) ) and \
                        ( isinstance( path[2], FieldProps ) or isinstance( path[2], Entity ) ):
                        entity_id = Store.increment_unit_counter(path[0], path[1])
                        unit = path[0]
                        meta_type = path[2]
                        local_ref = Entity._ref(unit, entity_id, meta_type)
                        idx = 3
                else:
                    # found entity id on path[0]
                    local_ref = path[0]
                    idx = 1

        if idx < len(path):
            remaining_path = path[idx:]
        else:
            remaining_path = []

        #print("Access 1: local_ref={}, path={}".format(local_ref, path))
        if local_ref:
            if not local_ref in entities:
                last_obj = Store._create_entity(unit, entity_id, meta_type)
            else:
                last_obj = entities[local_ref][1]
            last_global_id = last_obj.global_id()

        path_length = len(remaining_path)
        if last_obj and path_length == 0 and operation == Operation.GET:
            if _messages_create_message:
                _create_message(MessageType.ENTITY_ACCESS, {"path": last_entity_path, "op": operation}, [last_global_id])
            return last_obj
        else:
            rv = None

            if last_obj is None:
                last_obj = [e[1] for e in entities.values()]
                parent = last_obj

            for last_idx in range(path_length):
                key = remaining_path[last_idx]
                #print("{}: Access 2: key={}".format(last_obj, key))

                if last_obj is None:
                    last_obj = Store._access_data_create_container(parent, remaining_path[last_idx - 1], key)
                elif isinstance(last_obj, Entity):
                    last_global_id = last_obj.global_id()
                    last_entity_path = path[last_idx:]

                if isinstance(last_obj, str):
                    unit, entity_id, meta_type = Entity._resolve_global_id(last_obj)
                    if entity_id:
                        parent = entities[last_obj][1]
                    else:
                        parent = last_obj
                else:
                    parent = last_obj

                last_obj = Store._access_data_get_value(parent, key, None)
                last_idx += 1

            if operation == Operation.SET or operation == Operation.ADD or operation == Operation.CHANGE:
                changed, rv = Store._access_data_set_value(parent, remaining_path[last_idx - 1], value, operation)
                if changed and _messages_create_message:
                    _create_message(MessageType.ENTITY_ACCESS, {"path": last_entity_path, "op": operation}, [last_global_id])

            elif operation == Operation.GET:
                #if _messages_create_message:
                #    _create_message(MessageType.ENTITY_ACCESS, {"path": last_entity_path, "op": operation}, [last_global_id])
                rv = last_obj

            elif operation == Operation.VISIT:
                #if _messages_create_message:
                #    _create_message(MessageType.ENTITY_ACCESS, {"path": last_entity_path, "op": operation}, [last_global_id])
                rv = last_obj

                if callable(value):
                    if isinstance(last_obj, list) or isinstance(last_obj, set):
                        for e in last_obj:
                            value(e)
                    elif isinstance(last_obj, dict):
                        for key, value in last_obj.items():
                            value(key, value)

            elif operation == Operation.WALK:
                stored_entities = set()
                if isinstance(last_obj, list):
                    rv = last_obj
                else:
                    rv = [last_obj]
                Store._walk_entities(rv, stored_entities)

            return rv

    @staticmethod
    def _walk_entities(entities, stored_entities):
        idx = 0
        while True:
            if idx >= len(entities):
                break

            entity = entities[idx]
            idx += 1
                
            meta_type = Store.get_meta_type_enum(entity)
            field_enums = [EntityTypes._entity_base.fields_enum, meta_type.fields_enum]
            for field_enum in field_enums:
                for field in field_enum:
                    if field.datatype == Datatypes.ENTITY:
                        if field.valuetype == ValueTypes.SINGLE:
                            value = entity.access(Operation.GET, None, field)
                            if isinstance(value, Entity) and not value.global_id() in stored_entities:
                                entities.append(value)
                                stored_entities.add(value.global_id())
                        elif field.valuetype in [ValueTypes.LIST, ValueTypes.SET]:
                            values = entity.access(Operation.GET, None, field)
                            if values:
                                for value in values:
                                    if value and not value in stored_entities:
                                        list_entity = Store.access(Operation.GET, None, value)
                                        if list_entity is None:
                                            print(entity.global_id() + " - " + value)
                                        entities.append(list_entity)
                                        stored_entities.add(list_entity.global_id())

    @staticmethod
    def _access_data_create_container(obj, field, key):
        if isinstance(obj, Entity):
            entities = globals()["_entity_map"]
            enum_map = globals()["_enum_map"]

            global_field_name = enum_map[field]
            field_config = enum_map[global_field_name]

            e_map = entities[obj.global_id()][0]
            if field_config["valuetype"] == ValueTypes.SET:
                e_map[global_field_name] = set()
            elif field_config["valuetype"] == ValueTypes.LIST:
                e_map[global_field_name] = []
            
            return e_map[global_field_name]

    @staticmethod
    def _set_uncommitted(e, e_map):
        if e.committed == Committed.SAVED:
            e.committed = Committed.UPDATED

        committed_field = Store._get_base_fields()[3]
        e_map[committed_field] = e.committed.name

        return True

    @staticmethod
    def _access_data_set_value(obj, field, value, operation):
        if isinstance(obj, Entity):
            changed = False

            entities = globals()["_entity_map"]
            enum_map = globals()["_enum_map"]

            e_map = entities[obj.global_id()][0]
            entity = entities[obj.global_id()][1]
            global_field_name = enum_map[field]
            field_config = enum_map[global_field_name]

            if field.datatype == Datatypes.ENTITY and isinstance(value, Entity):
                if field.valuetype == ValueTypes.LIST:
                    changed = Store._set_uncommitted(entity, e_map)
                    e_map.setdefault(global_field_name, []).append(value.global_id())
                elif field.valuetype == ValueTypes.SET:
                    if not global_field_name in e_map or not value.global_id in e_map[global_field_name]:
                        e_map.setdefault(global_field_name, set()).add(value.global_id())
                        changed = Store._set_uncommitted(entity, e_map)
                else:
                    if e_map.get(global_field_name, None) != value: # TODO: change if it shoudl be value.global_id()
                        e_map[global_field_name] = value.global_id()
                        changed = Store._set_uncommitted(entity, e_map)
            else:
                if field.valuetype == ValueTypes.LIST and not isinstance(value, list):
                    changed = Store._set_uncommitted(entity, e_map)
                    e_map.setdefault(global_field_name, []).append(value)
                elif field.valuetype == ValueTypes.SET and not isinstance(value, set):
                    if isinstance(value, list):
                        all_included = False
                        if global_field_name in e_map:
                            all_included = True
                            old_values = e_map[global_field_name]
                            for v in value:
                                all_included = v in old_values
                                if not all_included:
                                    break
                        if not all_included:
                            e_map.setdefault(global_field_name, set()).update(value)
                            changed = Store._set_uncommitted(entity, e_map)
                    else:
                        if not global_field_name in e_map or not value in e_map[global_field_name]:
                            e_map.setdefault(global_field_name, set()).add(value)
                            changed = Store._set_uncommitted(entity, e_map)
                else:
                    if operation == Operation.CHANGE:
                        e_map[global_field_name] += value
                        value = e_map[global_field_name]
                        changed = Store._set_uncommitted(entity, e_map)
                    else:
                        if e_map.get(global_field_name, None) != value:
                            e_map[global_field_name] = value
                            changed = Store._set_uncommitted(entity, e_map)

            return (changed, value)

        return (False, None)

    @staticmethod
    def _access_data_get_value(obj, key, default_value):
        entities = globals()["_entity_map"]
        enum_map = globals()["_enum_map"]

        if isinstance(obj, Entity):
            global_name = enum_map[key]
            e_map = entities[obj.global_id()][0]
            if global_name in e_map:
                unit, entity_id, meta_type = Entity._resolve_global_id(e_map[global_name])
                if entity_id:
                    if not e_map[global_name] in entities:
                        e = Store.access(Operation.GET, None, e_map[global_name])
                        #print(str(e.global_id()))
                    return entities[e_map[global_name]][1]
                else:
                    return e_map[global_name]
        elif isinstance(obj, list):
            if isinstance(key, int) and key < len(obj):
                return obj[key]

        elif isinstance(obj, dict):
            if key in obj:
                return obj[key]

        elif isinstance(obj, set):
            raise Exception("Not possible to request path element on sets.")

        return default_value

    @staticmethod
    def _get_field_config(unit_name, meta_type, field):
        enum_map = globals()["_enum_map"]
        if isinstance(meta_type, Enum) and meta_type in [EntityTypes.type_meta, EntityTypes.meta_field]:
            return enum_map[UNIT_ENTITY_META+":"+meta_type.name+":"+field.name]
        elif isinstance(meta_type, str) and meta_type in [EntityTypes.type_meta.name, EntityTypes.meta_field.name]:
            return enum_map[UNIT_ENTITY_META+":"+meta_type+":"+field]
        elif isinstance(meta_type, Enum) and isinstance(field, Enum):
            return enum_map[unit_name+":"+meta_type.name+":"+field.name]
        else:
            return enum_map[unit_name+":"+meta_type+":"+field]

    @staticmethod
    def _global_field_name(unit, meta_type, field_name):
        if isinstance(unit, Entity) and isinstance(meta_type, Entity):
            return  enum_map[unit]+":"+enum_map[meta_type].name+":"+field_name
        else:
            return "{}:{}:{}".format(unit, meta_type, field_name)

    @staticmethod
    def to_json(*path):
        entities = Store.access(Operation.VISIT, None, *path)
        json_array = []
        for e in entities:
            json_array.append(e.to_json())
        return json_array

    @staticmethod
    def from_json(objects):
        entities = []
        for e_map in objects:
            entities.append(Entity.from_json(e_map))

        return entities

    @staticmethod
    def get_meta_type_enum(entity):
        enum_map = globals()["_enum_map"]
        if isinstance(entity, Entity):
            return enum_map[entity.meta_type]
        elif isinstance(entity, dict):
            _, meta_type_field, _,  _ = Store._get_base_fields()
            return enum_map[entity[meta_type_field]]

    @staticmethod
    def get_global_fieldname(field):
        enum_map = globals()["_enum_map"]
        return enum_map[field]

    @staticmethod
    def global_id(entity):
        if isinstance(entity, Entity):
            return entity.global_id()
        elif isinstance(entity, dict):
            unit_field, meta_type_field, entity_id_field,  _ = Store._get_base_fields()
            unit = Store.access(Operation.GET, None, entity[unit_field])
            meta_type = Store.access(Operation.GET, None, entity[meta_type_field])
            return Entity._ref(unit, entity[entity_id_field], meta_type)

    @staticmethod
    def concat_field_values(entity, fields, separator="#", ext_store=None):
        values = []
        template = separator.join(["{}" for i in range(len(fields))])
        for field in fields:
            if isinstance(entity, Entity):
                if isinstance(field, Enum):
                    values.append(entity.access(Operation.GET, None, field))
                elif isinstance(field, tuple):
                    values.append(entity.access(Operation.GET, None, *field))
            elif isinstance(entity, dict):
                if isinstance(field, Enum):
                    values.append(entity.get(Store.get_global_fieldname(field)))
                elif isinstance(field, tuple):
                    # Only works if except last field all are Entities
                    e = entity
                    v = None
                    for field_element in field:
                        if e and field_element.datatype == Datatypes.ENTITY:
                            e = ext_store.get(e.get(Store.get_global_fieldname(field_element)))
                        else:
                            v = e.get(Store.get_global_fieldname(field_element))
                    values.append(v)

        return template.format(*values)

class Entity():
    def __init__(self, unit, entity_id, meta_type):
        self.unit = unit
        self.entity_id = entity_id
        self.meta_type = meta_type
        self.committed = Committed.CREATED

    def access(self, operation, value, *path):
        enum_map = globals()["_enum_map"]
        return Store.access(operation, value, enum_map[self.unit], self.entity_id, enum_map[self.meta_type], *path)

    @staticmethod
    def _ref(unit, entity_id, meta_type):
        enum_map = globals()["_enum_map"]
        if isinstance(unit, Entity) and isinstance(meta_type, Entity):
            return  enum_map[unit]+":"+str(entity_id)+":"+enum_map[meta_type].name
        else:
            return  unit+":"+str(entity_id)+":"+meta_type.name

    def global_id(self):
        return  Entity._ref(self.unit, self.entity_id, self.meta_type)

    def _resolve_global_id(value):
        if isinstance(value, str):
            enum_map = globals()["_enum_map"]
            parts = value.split(":")
            entity_id = None
            try:
                entity_id = int(parts[1])
            except:
                pass
            if len(parts) == 3 and not entity_id is None:
                entity = globals()["_entity_map"].get(value, (None, None))[1]
                unit = enum_map.get(parts[0], None)
                if unit and isinstance(unit, Entity) and enum_map[unit.meta_type] == EntityTypes.unit:
                    if entity and entity.meta_type and isinstance(entity.meta_type, Entity) and entity.meta_type in enum_map:
                        return (unit, entity_id, entity.meta_type)
                    elif entity is None:
                        unit_meta = enum_map[unit.access(Operation.GET, None, UnitMeta.name)+"_meta"]
                        for meta_type_global_id in unit_meta.access(Operation.GET, None, UnitMeta.meta_types):
                            meta_type_entity = Store.access(Operation.GET, None, meta_type_global_id)
                            if meta_type_entity.access(Operation.GET, None, TypeMeta.name) == parts[2]:
                                #print("None existant entity, but create it for {}. Type: {}".format(value, meta_type_entity.access(Operation.GET, None, TypeMeta.name)))
                                return (unit, entity_id, meta_type_entity)


        return (None, None, None)

    def get_meta_type_enum(self):
        enum_map = globals()["_enum_map"]
        return _enum_map[self.meta_type]

    def __to_json(self, json_map):
        entity_value_map = globals()["_entity_map"][self.global_id()][0]
        enum_map = globals()["_enum_map"]

        for field, value in entity_value_map.items():
            parts = field.split(":")
            field_config = Store._get_field_config(parts[0], parts[1], parts[2])

            if field_config["valuetype"] in [ValueTypes.SET, ValueTypes.LIST] and ( isinstance(value, list) or isinstance(value, set) ):
                json_map[field] = []
                for element in value:
                    json_map[field].append(element)
            else:
                json_map[field] = value

    def to_json(self):
        json_map = {}
        self.__to_json(json_map)
        return json_map

    def set_committed(self):
        entity_map = globals()["_entity_map"]
        self.committed = Committed.SAVED

        _, _, _, committed_field = Store._get_base_fields()

        entity_map[self.global_id()][0][committed_field] = self.committed.name

    @staticmethod
    def from_json(e_map, force_new_id=False, force_entity_id=None):
        entity_map = globals()["_entity_map"]
        enum_map = globals()["_enum_map"]

        unit_field, meta_type_field, entity_id_field, committed_field = Store._get_base_fields()

        unit_parts = e_map[unit_field].split(":")

        unit = Store.access(Operation.GET, None, UNIT_ENTITY, int(unit_parts[1]), EntityTypes.unit)
        unit_name = unit.access(Operation.GET, None, UnitMeta.name)
        meta_type = enum_map[e_map[meta_type_field]]

        e = None
        if not force_entity_id is None:
            e = Store.access(Operation.GET, None, force_entity_id)

        if e is None:
            if force_new_id:
                entity_id = None
            else:
                entity_id = int(e_map[entity_id_field])

            e = Store.access(Operation.GET, None, unit_name, entity_id, meta_type)

        # Wipe none base fields
        orig_map = entity_map[e.global_id()][0]
        del_fields = []
        for field in orig_map.keys():
            if not field in [unit_field, meta_type_field, entity_id_field, committed_field]:
                del_fields.append(field)
        for field in del_fields:
            del orig_map[field]

        for field, value in e_map.items():
            if not field in [unit_field, meta_type_field, entity_id_field, committed_field]:
                field_parts = field.split(":")
                field_config = Store._get_field_config(field_parts[0], field_parts[1], field_parts[2])
                if field_config["valuetype"] == ValueTypes.SET and isinstance(value, list):
                    e.access(Operation.SET, set(value), field_config["_enum"])
                else:
                    e.access(Operation.SET, value, field_config["_enum"])
            elif field == committed_field:
                if Committed[value] == Committed.SAVED:
                    e.set_committed()
        return e

    def delete(self):
        Store._delete_entity_to_store(self)

_entity_map = {}
_enum_map = {}

Store.load_types_from_enum(EntityTypes, UNIT_META_ID)
_messages_module = import_module("dust.messages")
MessageType = getattr(_messages_module, "MessageType")
MessageTypes = getattr(_messages_module, "MessageTypes")
_messages_create_message = getattr(_messages_module, "create_message")
_UNIT_MESSAGE = _messages_module.UNIT_MESSAGES

def _create_message(message_type, message_params, entities):
    if _messages_create_message and message_params["op"] in [Operation.ADD, Operation.SET, Operation.DEL, Operation.CHANGE]:
        params = {}
        if entities and not entities[0] is None:
            enum_map = globals()["_enum_map"]
            unit, _, _ = Entity._resolve_global_id(entities[0])
            unit_name = enum_map[unit]
            if unit_name in [_UNIT_MESSAGE, UNIT_ENTITY] or unit_name.endswith("_meta"):
                return

        params["op"] = message_params["op"].name
        params["path"] = []
        for path_element in message_params["path"]:
            if isinstance(path_element, Enum):
                params["path"].append(path_element.name)
            elif isinstance(path_element, Entity):
                params["path"].append(path_element.global_id())

        _messages_create_message(message_type, params, entities)

if __name__ == '__main__':
    print(json.dumps(_entity_map, indent=4))
    print(str(_meta_ref))
