package net.minecraftforge.fml.common.network.internal;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

import java.io.IOException;
import java.util.List;

import org.apache.logging.log4j.Level;

import com.google.common.base.Throwables;

import net.minecraft.entity.DataWatcher;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.network.PacketBuffer;
import net.minecraft.util.MathHelper;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.ModContainer;
import net.minecraftforge.fml.common.network.ByteBufUtils;
import net.minecraftforge.fml.common.registry.IEntityAdditionalSpawnData;
import net.minecraftforge.fml.common.registry.IThrowableEntity;
import net.minecraftforge.fml.common.registry.EntityRegistry.EntityRegistration;
import net.minecraftforge.fml.relauncher.Side;

public abstract class FMLMessage {
    public static class CompleteHandshake extends FMLMessage {
        Side target;
        public CompleteHandshake() {
        }
        public CompleteHandshake(Side target)
        {
            this.target = target;
        }
        @Override
        void fromBytes(ByteBuf buf)
        {
            target = Side.values()[buf.readByte()];
        }
        @Override
        void toBytes(ByteBuf buf)
        {
            buf.writeByte(target.ordinal());
        }
    }
    public static class OpenGui extends FMLMessage {
        int windowId;
        String modId;
        int modGuiId;
        int x;
        int y;
        int z;

        public OpenGui() {}
        OpenGui(int windowId, String modId, int modGuiId, int x, int y, int z)
        {
            this.windowId = windowId;
            this.modId = modId;
            this.modGuiId = modGuiId;
            this.x = x;
            this.y = y;
            this.z = z;
        }

        @Override
        void toBytes(ByteBuf buf)
        {
            buf.writeInt(windowId);
            ByteBufUtils.writeUTF8String(buf, modId);
            buf.writeInt(modGuiId);
            buf.writeInt(x);
            buf.writeInt(y);
            buf.writeInt(z);
        }

        @Override
        void fromBytes(ByteBuf buf)
        {
            windowId = buf.readInt();
            modId = ByteBufUtils.readUTF8String(buf);
            modGuiId = buf.readInt();
            x = buf.readInt();
            y = buf.readInt();
            z = buf.readInt();
        }
    }

    public abstract static class EntityMessage extends FMLMessage {
        Entity entity;
        int entityId;
        public EntityMessage()
        {

        }
        EntityMessage(Entity entity)
        {
            this.entity = entity;
        }
        @Override
        void toBytes(ByteBuf buf)
        {
            buf.writeInt(entity.getEntityId());
        }

        @Override
        void fromBytes(ByteBuf buf)
        {
            entityId = buf.readInt();
        }
    }

    public static class EntityAdjustMessage extends EntityMessage {
        int serverX;
        int serverY;
        int serverZ;

        public EntityAdjustMessage() {}
        public EntityAdjustMessage(Entity entity, int serverX, int serverY, int serverZ)
        {
            super(entity);
            this.serverX = serverX;
            this.serverY = serverY;
            this.serverZ = serverZ;
        }

        @Override
        void toBytes(ByteBuf buf)
        {
            super.toBytes(buf);
            buf.writeInt(serverX);
            buf.writeInt(serverY);
            buf.writeInt(serverZ);
        }

        @Override
        void fromBytes(ByteBuf buf)
        {
            super.fromBytes(buf);
            serverX = buf.readInt();
            serverY = buf.readInt();
            serverZ = buf.readInt();
        }
    }
    public static class EntitySpawnMessage extends EntityMessage {
        String modId;
        int modEntityTypeId;
        int rawX;
        int rawY;
        int rawZ;
        double scaledX;
        double scaledY;
        double scaledZ;
        float scaledYaw;
        float scaledPitch;
        float scaledHeadYaw;
        int throwerId;
        double speedScaledX;
        double speedScaledY;
        double speedScaledZ;
        @SuppressWarnings("rawtypes")
        List dataWatcherList;
        ByteBuf dataStream;

        public EntitySpawnMessage() {}
        public EntitySpawnMessage(EntityRegistration er, Entity entity, ModContainer modContainer)
        {
            super(entity);
            modId = modContainer.getModId();
            modEntityTypeId = er.getModEntityId();
        }
        @Override
        void toBytes(ByteBuf buf)
        {
            super.toBytes(buf);
            ByteBufUtils.writeUTF8String(buf, modId);
            buf.writeInt(modEntityTypeId);
            // posX, posY, posZ
            buf.writeInt(MathHelper.floor_double(entity.posX * 32D));
            buf.writeInt(MathHelper.floor_double(entity.posY * 32D));
            buf.writeInt(MathHelper.floor_double(entity.posZ * 32D));
            // yaw, pitch
            buf.writeByte((byte)(entity.rotationYaw * 256.0F / 360.0F));
            buf.writeByte((byte) (entity.rotationPitch * 256.0F / 360.0F));
            // head yaw
            if (entity instanceof EntityLivingBase)
            {
                buf.writeByte((byte) (((EntityLivingBase)entity).rotationYawHead * 256.0F / 360.0F));
            }
            else
            {
                buf.writeByte(0);
            }
            ByteBuf tmpBuf = Unpooled.buffer();
            PacketBuffer pb = new PacketBuffer(tmpBuf);
            try
            {
                entity.getDataWatcher().writeTo(pb);
            } catch (IOException e)
            {
                FMLLog.log(Level.FATAL,e,"Encountered fatal exception trying to send entity spawn data watchers");
                throw Throwables.propagate(e);
            }
            buf.writeBytes(tmpBuf);

            if (entity instanceof IThrowableEntity)
            {
                Entity owner = ((IThrowableEntity)entity).getThrower();
                buf.writeInt(owner == null ? entity.getEntityId() : owner.getEntityId());
                double maxVel = 3.9D;
                double mX = entity.motionX;
                double mY = entity.motionY;
                double mZ = entity.motionZ;
                if (mX < -maxVel) mX = -maxVel;
                if (mY < -maxVel) mY = -maxVel;
                if (mZ < -maxVel) mZ = -maxVel;
                if (mX >  maxVel) mX =  maxVel;
                if (mY >  maxVel) mY =  maxVel;
                if (mZ >  maxVel) mZ =  maxVel;
                buf.writeInt((int)(mX * 8000D));
                buf.writeInt((int)(mY * 8000D));
                buf.writeInt((int)(mZ * 8000D));
            }
            else
            {
                buf.writeInt(0);
            }
            if (entity instanceof IEntityAdditionalSpawnData)
            {
                tmpBuf = Unpooled.buffer();
                ((IEntityAdditionalSpawnData)entity).writeSpawnData(tmpBuf);
                buf.writeBytes(tmpBuf);
            }
        }
        @Override
        void fromBytes(ByteBuf dat)
        {
            super.fromBytes(dat);
            modId = ByteBufUtils.readUTF8String(dat);
            modEntityTypeId = dat.readInt();
            rawX = dat.readInt();
            rawY = dat.readInt();
            rawZ = dat.readInt();
            scaledX = rawX / 32D;
            scaledY = rawY / 32D;
            scaledZ = rawZ / 32D;
            scaledYaw = dat.readByte() * 360F / 256F;
            scaledPitch = dat.readByte() * 360F / 256F;
            scaledHeadYaw = dat.readByte() * 360F / 256F;
            try
            {
                dataWatcherList = DataWatcher.readWatchedListFromPacketBuffer(new PacketBuffer(dat));
            } catch (IOException e)
            {
                FMLLog.log(Level.FATAL, e, "There was a critical error decoding the datawatcher stream for a mod entity.");
                throw Throwables.propagate(e);
            }

            throwerId = dat.readInt();
            if (throwerId != 0)
            {
                speedScaledX = dat.readInt() / 8000D;
                speedScaledY = dat.readInt() / 8000D;
                speedScaledZ = dat.readInt() / 8000D;
            }
            this.dataStream = dat;
        }
    }
    abstract void toBytes(ByteBuf buf);
    abstract void fromBytes(ByteBuf buf);
}