package electrosphere.net.server;

import electrosphere.entity.types.creature.CreatureUtils;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.types.item.ItemUtils;
import electrosphere.entity.types.attach.AttachUtils;
import electrosphere.entity.types.collision.CollisionObjUtils;
import electrosphere.logger.LoggerInterface;
import electrosphere.main.Globals;
import electrosphere.main.Main;
import electrosphere.net.NetUtils;
import electrosphere.net.parser.net.message.EntityMessage;
import electrosphere.net.parser.net.message.NetworkMessage;
import electrosphere.net.parser.net.message.PlayerMessage;
import electrosphere.net.parser.net.message.StatusMessage;
import electrosphere.net.parser.net.message.WorldMessage;
import electrosphere.net.parser.net.raw.NetworkParser;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.Socket;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.security.spec.RSAKeyGenParameterSpec;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.crypto.stream.CryptoInputStream;
import org.apache.commons.crypto.stream.CryptoOutputStream;
import org.joml.Vector3f;

/**
 *
 * @author satellite
 */
public class ServerConnectionHandler implements Runnable {
    
    static int playerIdIncrementer = 0;
    
    Socket socket;
//    CryptoInputStream inputStream;
//    CryptoOutputStream outputStream;
    InputStream inputStream;
    OutputStream outputStream;
    boolean initialized;
    NetworkParser networkParser;
    int playerID;
    int playerCharacterID;
    
    ServerProtocol serverProtocol;
    
    public ServerConnectionHandler(Socket socket) {
        this.socket = socket;
        playerID = getPlayerID();
        LoggerInterface.loggerNetworking.INFO("Player ID: " + playerID);
    }

    @Override
    public void run() {
        LoggerInterface.loggerNetworking.INFO("ServerConnectionHandler start");
        initialized = false;
        try {
            socket.setSoTimeout(100);
        } catch (SocketException ex) {
            ex.printStackTrace();
        }
            //        final SecretKeySpec key = new SecretKeySpec(("1234567890123456").getBytes(),"AES");
//        final Properties properties = new Properties();
//        final RSAKeyGenParameterSpec spec = new RSAKeyGenParameterSpec(4096, BigInteger.probablePrime(4000, new Random()));
//        try {
//            inputStream = new CryptoInputStream("AES/ECB/PKCS5Padding",properties,socket.getInputStream(),key,spec);
//        } catch (IOException ex) {
//            ex.printStackTrace();
//            System.exit(1);
//        }
//        try {
//            outputStream = new CryptoOutputStream("AES/ECB/PKCS5Padding",properties,socket.getOutputStream(),key,spec);
//        } catch (IOException ex) {
//            ex.printStackTrace();
//            System.exit(1);
//        }
        try {
            inputStream = socket.getInputStream();
            outputStream = socket.getOutputStream();
            networkParser = new NetworkParser(inputStream,outputStream);
            serverProtocol = new ServerProtocol(this);
        } catch (IOException ex) {
            ex.printStackTrace();
            System.exit(1);
        }
        Player newPlayerObject = new Player(this);
        Globals.playerManager.addPlayer(newPlayerObject);
        //spawn player in world
        Entity newPlayerCharacter = CreatureUtils.spawnBasicCreature("Human");
        playerCharacterID = newPlayerCharacter.getId();
        CollisionObjUtils.positionCharacter(newPlayerCharacter, new Vector3f(Globals.spawnPoint.x,Globals.spawnPoint.y,Globals.spawnPoint.z));
        //attach player object to player character
        newPlayerObject.setPlayerEntity(newPlayerCharacter);
        newPlayerObject.setWorldX(Globals.serverWorldData.convertRealToChunkSpace(Globals.spawnPoint.x));
        newPlayerObject.setWorldY(Globals.serverWorldData.convertRealToChunkSpace(Globals.spawnPoint.z));
        Globals.dataCellManager.addPlayer(newPlayerObject);
        Globals.dataCellManager.movePlayer(newPlayerObject, Globals.serverWorldData.convertRealToChunkSpace(Globals.spawnPoint.x), Globals.serverWorldData.convertRealToChunkSpace(Globals.spawnPoint.z));
        //spawn player sword
//        Entity sword = ItemUtils.spawnBasicItem("Katana");
//        AttachUtils.attachEntityToEntityAtBone(newPlayerCharacter, sword, "Bone.031");
        //set controller id
        CreatureUtils.setControllerPlayerId(newPlayerCharacter, playerID);
        if(Globals.RUN_SERVER && Main.playerId == -1){
            Globals.playerCharacter = newPlayerCharacter;
            Globals.serverPlayer = newPlayerObject;
        }
        //world metadata
        networkParser.addOutgoingMessage(
                WorldMessage.constructMetadataMessage(
                    Globals.serverWorldData.getWorldSizeDiscrete(),
                    Globals.serverWorldData.getDynamicInterpolationRatio(),
                    Globals.serverWorldData.getRandomDampener(),
                    (int)Globals.serverWorldData.getWorldBoundMin().x,
                    (int)Globals.serverWorldData.getWorldBoundMin().z,
                    (int)Globals.serverWorldData.getWorldBoundMax().x,
                    (int)Globals.serverWorldData.getWorldBoundMax().z
                )
        );
        //set client initial discrete position
        networkParser.addOutgoingMessage(
                PlayerMessage.constructSetInitialDiscretePositionMessage(
                        Globals.serverWorldData.convertRealToChunkSpace(Globals.spawnPoint.x),
                        Globals.serverWorldData.convertRealToChunkSpace(Globals.spawnPoint.z)
                        )
        );
        //send spawn point
        networkParser.addOutgoingMessage(
                WorldMessage.constructSpawnPositionMessage(Globals.spawnPoint.x, Globals.spawnPoint.z)
        );
        //tell them what player stats they are
        networkParser.addOutgoingMessage(PlayerMessage.constructSet_IDMessage(playerID));
        //figure out what chunk they're in
        
        //queue messages for that chunk
        if(Globals.RUN_SERVER && Main.playerId == -1){
            
        } else {
            for(Entity currentEntity : Globals.entityManager.getMoveable()){
                networkParser.addOutgoingMessage(
                        EntityMessage.constructCreateMessage(
                                currentEntity.getId(),
                                0, //0 for creatures
                                CreatureUtils.getType(currentEntity),
                                EntityUtils.getPosition(currentEntity).x,
                                EntityUtils.getPosition(currentEntity).y,
                                EntityUtils.getPosition(currentEntity).z
                        )
                );
                if(CreatureUtils.isCreature(currentEntity)){
                    if(CreatureUtils.hasControllerPlayerId(currentEntity)){
                        LoggerInterface.loggerNetworking.INFO("Sending controller packets");
                        networkParser.addOutgoingMessage(NetUtils.createSetCreatureControllerIdEntityMessage(currentEntity));
                    }
                }
            }
            for(Entity currentEntity : Globals.entityManager.getItemEntities()){
                networkParser.addOutgoingMessage(
                            EntityMessage.constructCreateMessage(
                            currentEntity.getId(),
                            1, //1 for items
                            ItemUtils.getType(currentEntity),
                            EntityUtils.getPosition(currentEntity).x,
                            EntityUtils.getPosition(currentEntity).y,
                            EntityUtils.getPosition(currentEntity).z
                        )
                );
                if(AttachUtils.isAttached(currentEntity)){
                    networkParser.addOutgoingMessage(
                            EntityMessage.constructattachEntityToEntityMessage(
                                    currentEntity.getId(),
                                    AttachUtils.getTargetBone(currentEntity),
                                    AttachUtils.getParent(currentEntity).getId()
                            )
                    );
                }
            }
        }
        
        //let client know it's ready
        networkParser.addOutgoingMessage(StatusMessage.constructReadyMessage(1));

        initialized = true;
        while(Main.isRunning()){
            //attempt poll incoming messages
            networkParser.readMessagesIn();
            //ponder incoming messages
            while(networkParser.hasIncomingMessaage()){
                NetworkMessage message = networkParser.popIncomingMessage();
                serverProtocol.handleMessage(message);
            }
            //push outgoing message
            networkParser.pushMessagesOut();
            try {
                //sleep
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }
    
    
    
    
    
    static int getPlayerID(){
        playerIdIncrementer++;
        return playerIdIncrementer;
    }
    
    public void addMessagetoOutgoingQueue(NetworkMessage message){
        switch(message.getType()){
            case ENTITY_MESSAGE:
                switch(((EntityMessage)message).getMessageSubtype()){
                    case MOVEUPDATE:
                        if(((EntityMessage)message).getentityID()==playerCharacterID){
                            //basically don't send the message if this is the player's character and it's a move update
                        } else {
                            networkParser.addOutgoingMessage(message);
                        }
                        break;
                    case ATTACKUPDATE:
                        if(((EntityMessage)message).getentityID()==playerCharacterID){
                            //basically don't send the message if this is the player's character and it's a move update
                        } else {
                            networkParser.addOutgoingMessage(message);
                        }
                        break;
                    default:
                        networkParser.addOutgoingMessage(message);
                        break;
                }
                break;
            default:
                networkParser.addOutgoingMessage(message);
                break;
        }
    }
}
