package electrosphere.net.client.protocol;

import java.util.Arrays;
import java.util.List;

import org.joml.Quaterniond;
import org.joml.Vector3d;

import electrosphere.engine.Globals;
import electrosphere.entity.ClientEntityUtils;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.equip.ClientEquipState;
import electrosphere.entity.state.inventory.InventoryUtils;
import electrosphere.entity.state.physicssync.ClientPhysicsSyncTree;
import electrosphere.entity.types.attach.AttachUtils;
import electrosphere.entity.types.creature.CreatureEquipData.EquippedItem;
import electrosphere.entity.types.creature.CreatureTemplate;
import electrosphere.entity.types.creature.CreatureUtils;
import electrosphere.entity.types.foliage.FoliageUtils;
import electrosphere.entity.types.item.ItemUtils;
import electrosphere.entity.types.object.ObjectUtils;
import electrosphere.game.data.creature.type.CreatureData;
import electrosphere.game.data.creature.type.ViewModelData;
import electrosphere.logger.LoggerInterface;
import electrosphere.net.parser.net.message.EntityMessage;
import electrosphere.net.parser.net.message.EntityMessage.EntityMessageType;
import electrosphere.net.synchronization.transport.StateCollection;
import electrosphere.net.template.ClientProtocolTemplate;
import electrosphere.util.Utilities;

/**
 * Client entity network protocol
 */
public class EntityProtocol implements ClientProtocolTemplate<EntityMessage> {

    //Messages to ignore when complaining about messages that have nonexistant entity associated
    static List<EntityMessageType> idModifyingMessages = Arrays.asList(new EntityMessageType[]{
        EntityMessageType.CREATE,
        EntityMessageType.SPAWNCREATURE,
        EntityMessageType.SPAWNFOLIAGESEED,
        EntityMessageType.SPAWNITEM,
        EntityMessageType.SPAWNOBJECT,
        EntityMessageType.DESTROY,
    });

    @Override
    public EntityMessage handleAsyncMessage(EntityMessage message) {
        return message;
    }

    @Override
    public void handleSyncMessage(EntityMessage message) {
        Globals.profiler.beginCpuSample("EntityProtocol.handleEntityMessage");
        LoggerInterface.loggerNetworking.DEBUG_LOOP("Parse entity message of type " + message.getMessageSubtype());

        //error check
        if(Globals.clientScene != null && Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID()) == null && !idModifyingMessages.contains(message.getMessageSubtype())){
            LoggerInterface.loggerNetworking.WARNING("Client received packet for entity that is not in the client scene!"); 
            Globals.clientSceneWrapper.dumpTranslationLayerStatus();
            Globals.clientSceneWrapper.dumpIdData(message.getentityID());
        }

        Entity newlySpawnedEntity;
        switch(message.getMessageSubtype()){


            //
            //
            //  SPAWNING STUFF IN
            //
            //
            case CREATE:
                LoggerInterface.loggerNetworking.DEBUG(
                    "Spawn ID " + message.getentityID() + " of type " + message.getentityCategory() + " subtype " + message.getentitySubtype() + 
                    " @ " + message.getpositionX() + " " + message.getpositionY() + " " + message.getpositionZ()
                );
                switch(message.getentityCategory()){
                    case 0:
                        // newlySpawnedEntity = CreatureUtils.spawnBasicCreature(message.getentitySubtype());
                        // EntityUtils.getScale(newlySpawnedEntity).set(0.005f);
                        // EntityUtils.getPosition(newlySpawnedEntity).set(message.getpositionX(),message.getpositionY(),message.getpositionZ());
                        // EntityUtils.setEntityID(newlySpawnedEntity, message.getentityID());
                        break;
                    case 1:
                        newlySpawnedEntity = ItemUtils.clientSpawnBasicItem(message.getentitySubtype());
                        EntityUtils.getScale(newlySpawnedEntity).set(0.005f);
                        EntityUtils.getPosition(newlySpawnedEntity).set(message.getpositionX(),message.getpositionY(),message.getpositionZ());
                        Globals.clientSceneWrapper.mapIdToId(newlySpawnedEntity.getId(), message.getentityID());
                        break;
                }
                break;
            case SPAWNCREATURE: {
                LoggerInterface.loggerNetworking.DEBUG("[CLIENT] Spawn Creature " + message.getentityID() + " at " + message.getpositionX() + " " + message.getpositionY() + " " + message.getpositionZ());
                CreatureTemplate template = Utilities.deserialize(message.getcreatureTemplate(), CreatureTemplate.class);
                newlySpawnedEntity = CreatureUtils.clientSpawnBasicCreature(template.getCreatureType(),template);
                ClientEntityUtils.initiallyPositionEntity(
                    newlySpawnedEntity,
                    new Vector3d(message.getpositionX(),message.getpositionY(),message.getpositionZ()),
                    new Quaterniond(message.getrotationX(),message.getrotationY(),message.getrotationZ(),message.getrotationW())
                );
                Globals.clientSceneWrapper.mapIdToId(newlySpawnedEntity.getId(), message.getentityID());
                //if the creature template includes an equip section, spawn all the equipped items
                if(template != null && template.getCreatureEquipData() != null && template.getCreatureEquipData().getSlots() != null){
                    for(String equipSlotId : template.getCreatureEquipData().getSlots()){

                        //add the item to the creature's inventory
                        EquippedItem itemDefinition = template.getCreatureEquipData().getSlotItem(equipSlotId);
                        Entity itemInInventory = InventoryUtils.clientConstructInInventoryItem(newlySpawnedEntity,itemDefinition.getItemType());

                        //equip the item to the slot defined in the template
                        ClientEquipState clientEquipState = ClientEquipState.getEquipState(newlySpawnedEntity);
                        clientEquipState.attemptEquip(itemInInventory, clientEquipState.getEquipPoint(equipSlotId));

                        //map the constructed item to its server id
                        Globals.clientSceneWrapper.mapIdToId(itemInInventory.getId(), itemDefinition.getEntityId());
                    }
                }
                //apply state synchronization if present
                if(template != null && template.getStateCollection() != null && template.getStateCollection().getValues() != null){
                    StateCollection.applyStateCollection(newlySpawnedEntity, template.getStateCollection());
                }
            } break;
            case SPAWNITEM: {
                LoggerInterface.loggerNetworking.DEBUG("[CLIENT] Spawn Item " + message.getentityID() + " at " + message.getpositionX() + " " + message.getpositionY() + " " + message.getpositionZ());
                //spawn item
                String itemType = message.getcreatureTemplate();
                newlySpawnedEntity = ItemUtils.clientSpawnBasicItem(itemType);
                //position
                ClientEntityUtils.initiallyPositionEntity(
                    newlySpawnedEntity,
                    new Vector3d(message.getpositionX(),message.getpositionY(),message.getpositionZ()),
                    new Quaterniond(message.getrotationX(),message.getrotationY(),message.getrotationZ(),message.getrotationW())
                );
                Globals.clientSceneWrapper.mapIdToId(newlySpawnedEntity.getId(), message.getentityID());
            } break;
            case SPAWNFOLIAGESEED: {
                LoggerInterface.loggerNetworking.DEBUG("[CLIENT] Spawn foliage " + message.getentityID() + " at " + message.getpositionX() + " " + message.getpositionY() + " " + message.getpositionZ());
                String type = message.getcreatureTemplate();
                newlySpawnedEntity = FoliageUtils.spawnBasicFoliage(type,message.getfoliageSeed());
                ClientEntityUtils.initiallyPositionEntity(
                    newlySpawnedEntity,
                    new Vector3d(message.getpositionX(),message.getpositionY(),message.getpositionZ()),
                    new Quaterniond(message.getrotationX(),message.getrotationY(),message.getrotationZ(),message.getrotationW())
                );
                Globals.clientSceneWrapper.mapIdToId(newlySpawnedEntity.getId(), message.getentityID());
            } break;
            case SPAWNOBJECT: {
                LoggerInterface.loggerNetworking.DEBUG("[CLIENT] Spawn object " + message.getentityID() + " at " + message.getpositionX() + " " + message.getpositionY() + " " + message.getpositionZ());
                //spawn item
                String objectType = message.getcreatureTemplate();
                newlySpawnedEntity = ObjectUtils.clientSpawnBasicObject(objectType);
                //position
                ClientEntityUtils.initiallyPositionEntity(
                    newlySpawnedEntity,
                    new Vector3d(message.getpositionX(),message.getpositionY(),message.getpositionZ()),
                    new Quaterniond(message.getrotationX(),message.getrotationY(),message.getrotationZ(),message.getrotationW())
                );
                Globals.clientSceneWrapper.mapIdToId(newlySpawnedEntity.getId(), message.getentityID());
            } break;



            //
            //
            // UPDATING PROPERTIES
            //
            //
            case SETPROPERTY: {
                if(Globals.clientSceneWrapper.serverToClientMapContainsId(message.getentityID())){
                    if(message.getpropertyType() == 0){
                        setPlayerEntity(message);
                    }
                } else {
                    //TODO: bounce message
                    LoggerInterface.loggerNetworking.WARNING("Received property packet for entity that does not exist on client!");
                }
            } break;
            case ATTACHENTITYTOENTITY: {
                Entity child = Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID());
                Entity parent = Globals.clientSceneWrapper.getEntityFromServerId(message.gettargetID());
                LoggerInterface.loggerNetworking.DEBUG("Attach " + message.getentityID() + " to " + message.gettargetID() + " on bone " + message.getbone());
                if(child != null && parent != null){
                    AttachUtils.clientAttachEntityToEntityAtBone(
                        parent,
                        child,
                        message.getbone(),
                        new Vector3d(),
                        new Quaterniond()
                    );
                }
            } break;
            case MOVEUPDATE: {
                Entity target = Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID());
                if(target != null){
                    CreatureUtils.clientAttachEntityMessageToMovementTree(target,message);
                }
            } break;
            case ATTACKUPDATE: {
                LoggerInterface.loggerNetworking.WARNING("Received deprecated attack update message!");
            } break;

            case SYNCPHYSICS: {
                Entity entity = Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID());
                if(entity != null && ClientPhysicsSyncTree.hasTree(entity)){
                    ClientPhysicsSyncTree.getTree(entity).setMessage(message);
                }
            } break;


            //
            //
            // DESTROYING AND DESTRUCTING STUFF
            //
            //
            case DESTROY: {
                Entity entity = Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID());
                if(entity != null){
                    EntityUtils.cleanUpEntity(entity);
                }
            } break;


            //
            //
            // TODO
            //
            //
            case UPDATEENTITYVIEWDIR:
            case KILL:
                //to be implemented
                throw new UnsupportedOperationException();
            case STARTATTACK:
                //silently ignore
                break;
        }
        Globals.profiler.endCpuSample();
    }

    /**
     * Sets the player's entity
     * @param message the network message to parse
     */
    static void setPlayerEntity(EntityMessage message){
        Entity target = Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID());
        if(target != null){
            LoggerInterface.loggerNetworking.DEBUG("Set player entity id for entity: " + target.getId() + " to player: " + message.getpropertyValue());
            CreatureUtils.setControllerPlayerId(target, message.getpropertyValue());
            String creatureTypeRaw = CreatureUtils.getType(target);
            CreatureData creatureType = Globals.gameConfigCurrent.getCreatureTypeLoader().getCreature(creatureTypeRaw);
            ViewModelData viewModelData = creatureType.getViewModelData();
            if(Globals.clientPlayer != null && message.getpropertyValue() == Globals.clientPlayer.getId()){
                LoggerInterface.loggerNetworking.DEBUG("Set this player's entity id!");
                Globals.clientCharacterID = message.getentityID();
                Globals.playerEntity = target;
                if(viewModelData != null && viewModelData.getFirstPersonModelPath() != null){
                    Globals.firstPersonEntity = CreatureUtils.clientCreateViewModel(
                        creatureTypeRaw,
                        null
                    );
                }
            } else {
                //setting player id on entity that is not this player's
            }
        } else {
            LoggerInterface.loggerNetworking.WARNING("Tried to set player entity property on an entity that doesn't exist!");
            Globals.clientSceneWrapper.dumpTranslationLayerStatus();
        }
    }

}
