package common.network;

import java.io.IOException;
import java.nio.charset.Charset;

import common.init.ItemRegistry;
import common.item.ItemStack;
import common.nbt.NBTLoader;
import common.nbt.NBTSizeTracker;
import common.nbt.NBTTagCompound;
import common.util.BlockPos;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.EncoderException;

public class PacketBuffer
{
	private static final Charset UTF_8 = Charset.forName("UTF-8");
	
    private final ByteBuf buf;

    public PacketBuffer(ByteBuf wrapped)
    {
        this.buf = wrapped;
    }

    /**
     * Calculates the number of bytes required to fit the supplied int (0-5) if it were to be read/written using
     * readVarIntFromBuffer or writeVarIntToBuffer
     */
    public static int getVarIntSize(int input)
    {
        for (int i = 1; i < 5; ++i)
        {
            if ((input & -1 << i * 7) == 0)
            {
                return i;
            }
        }

        return 5;
    }

    public void writeByteArray(byte[] array)
    {
        this.writeVarIntToBuffer(array.length);
        this.writeBytes(array);
    }

    public byte[] readByteArray()
    {
        byte[] abyte = new byte[this.readVarIntFromBuffer()];
        this.readBytes(abyte);
        return abyte;
    }

    public BlockPos readBlockPos()
    {
//    	return new BlockPos(this.readInt(), this.readInt(), this.readInt());
        return BlockPos.fromLong(this.readLong());
    }

    public void writeBlockPos(BlockPos pos)
    {
        this.writeLong(pos.toLong());
//        this.writeInt(pos.getX());
//        this.writeInt(pos.getY());
//        this.writeInt(pos.getZ());
    }

//    public Text readChatComponent() throws IOException
//    {
//        return Text.toComponent(this.readNBTTagCompoundFromBuffer());
//    }
//
//    public void writeChatComponent(Text component) throws IOException
//    {
//    	this.writeNBTTagCompoundToBuffer(Text.toNbt(component));
////    	String s = ;
////    	if(s.length() > 32767) {
////    		s = component.getFormattedText();
////    		s = s.length() > 32720 ? s.substring(0, 32720) : s;
////    		s = TextSerializer.toNbt(new TextComponent(s + ChatFormat.GRAY + ChatFormat.ITALIC + " [...]"));
////    	}
////        this.writeString(s);
//    }

    public <T extends Enum<T>> T readEnumValue(Class<T> enumClass)
    {
        return (T)((Enum[])enumClass.getEnumConstants())[this.readVarIntFromBuffer()];
    }

    public <T extends Enum<T>> T readEnumOrNull(Class<T> enumClass)
    {
    	int n = this.readVarIntFromBuffer();
        return n < 0 ? null : (T)((Enum[])enumClass.getEnumConstants())[n];
    }

    public void writeEnumValue(Enum<?> value)
    {
        this.writeVarIntToBuffer(value.ordinal());
    }

    public void writeEnumOrNull(Enum<?> value)
    {
        this.writeVarIntToBuffer(value == null ? -1 : value.ordinal());
    }

    /**
     * Reads a compressed int from the buffer. To do so it maximally reads 5 byte-sized chunks whose most significant
     * bit dictates whether another byte should be read.
     */
    public int readVarIntFromBuffer()
    {
        int i = 0;
        int j = 0;

        while (true)
        {
            byte b0 = this.readByte();
            i |= (b0 & 127) << j++ * 7;

            if (j > 5)
            {
                throw new RuntimeException("VarInt too big");
            }

            if ((b0 & 128) != 128)
            {
                break;
            }
        }

        return i;
    }

    public long readVarLong()
    {
        long i = 0L;
        int j = 0;

        while (true)
        {
            byte b0 = this.readByte();
            i |= (long)(b0 & 127) << j++ * 7;

            if (j > 10)
            {
                throw new RuntimeException("VarLong too big");
            }

            if ((b0 & 128) != 128)
            {
                break;
            }
        }

        return i;
    }

//    public void writeUuid(UUID uuid)
//    {
//        this.writeLong(uuid.getMostSignificantBits());
//        this.writeLong(uuid.getLeastSignificantBits());
//    }
//
//    public UUID readUuid()
//    {
//        return new UUID(this.readLong(), this.readLong());
//    }

    /**
     * Writes a compressed int to the buffer. The smallest number of bytes to fit the passed int will be written. Of
     * each such byte only 7 bits will be used to describe the actual value since its most significant bit dictates
     * whether the next byte is part of that same int. Micro-optimization for int values that are expected to have
     * values below 128.
     */
    public void writeVarIntToBuffer(int input)
    {
        while ((input & -128) != 0)
        {
            this.writeByte(input & 127 | 128);
            input >>>= 7;
        }

        this.writeByte(input);
    }

    public void writeVarLong(long value)
    {
        while ((value & -128L) != 0L)
        {
            this.writeByte((int)(value & 127L) | 128);
            value >>>= 7;
        }

        this.writeByte((int)value);
    }

    /**
     * Writes a compressed NBTTagCompound to this buffer
     */
    public void writeNBTTagCompoundToBuffer(NBTTagCompound nbt)
    {
        if (nbt == null)
        {
            this.writeByte(0);
        }
        else
        {
            try
            {
                NBTLoader.write(nbt, new ByteBufOutputStream(this.buf));
            }
            catch (IOException ioexception)
            {
                throw new EncoderException(ioexception);
            }
        }
    }

    /**
     * Reads a compressed NBTTagCompound from this buffer
     */
    public NBTTagCompound readNBTTagCompoundFromBuffer() throws IOException
    {
        int i = this.buf.readerIndex();
        byte b0 = this.readByte();

        if (b0 == 0)
        {
            return null;
        }
        else
        {
            this.buf.readerIndex(i);
            return NBTLoader.read(new ByteBufInputStream(this.buf), new NBTSizeTracker(2097152L));
        }
    }

    /**
     * Writes the ItemStack's ID (short), then size (byte), then damage. (short)
     */
    public void writeItemStackToBuffer(ItemStack stack)
    {
        if (stack == null)
        {
            this.writeShort(-1);
        }
        else
        {
            this.writeShort(ItemRegistry.getIdFromItem(stack.getItem()));
            this.writeVarIntToBuffer(stack.stackSize);
            this.writeShort(stack.getMetadata());
            NBTTagCompound nbttagcompound = null;

            if (stack.getItem().isDamageable() || stack.getItem().getShareTag())
            {
                nbttagcompound = stack.getTagCompound();
            }

            this.writeNBTTagCompoundToBuffer(nbttagcompound);
        }
    }

    /**
     * Reads an ItemStack from this buffer
     */
    public ItemStack readItemStackFromBuffer() throws IOException
    {
        ItemStack itemstack = null;
        int i = this.readShort();

        if (i >= 0)
        {
            int j = this.readVarIntFromBuffer();
            int k = this.readShort();
            itemstack = new ItemStack(ItemRegistry.getItemById(i), j, k);
            itemstack.setTagCompound(this.readNBTTagCompoundFromBuffer());
        }

        return itemstack;
    }

    /**
     * Reads a string from this buffer. Expected parameter is maximum allowed string length. Will throw IOException if
     * string length exceeds this value!
     */
    public String readStringFromBuffer(int maxLength)
    {
        int i = this.readVarIntFromBuffer();

        if (i > maxLength * 4)
        {
            throw new DecoderException("The received encoded string buffer length is longer than maximum allowed (" + i + " > " + maxLength * 4 + ")");
        }
        else if (i < 0)
        {
            throw new DecoderException("The received encoded string buffer length is less than zero! Weird string!");
        }
        else
        {
            byte[] abyte = new byte[i];
            this.readBytes(abyte);
            String s = new String(abyte, UTF_8); // this.readBytes(i).array()

            if (s.length() > maxLength)
            {
                throw new DecoderException("The received string length is longer than maximum allowed (" + i + " > " + maxLength + ")");
            }
            else
            {
                return s;
            }
        }
    }

    public PacketBuffer writeString(String string)
    {
        byte[] abyte = string.getBytes(UTF_8);

        if (abyte.length > 32767)
        {
            throw new EncoderException("String too big (was " + string.length() + " bytes encoded, max " + 32767 + ")");
        }
        else
        {
            this.writeVarIntToBuffer(abyte.length);
            this.writeBytes(abyte);
            return this;
        }
    }

    public int readableBytes()
    {
        return this.buf.readableBytes();
    }

    public ByteBuf ensureWritable(int p_ensureWritable_1_)
    {
        return this.buf.ensureWritable(p_ensureWritable_1_);
    }

    public boolean readBoolean()
    {
        return this.buf.readBoolean();
    }

    public byte readByte()
    {
        return this.buf.readByte();
    }

    public short readUnsignedByte()
    {
        return this.buf.readUnsignedByte();
    }

    public short readShort()
    {
        return this.buf.readShort();
    }

    public int readUnsignedShort()
    {
        return this.buf.readUnsignedShort();
    }

    public int readInt()
    {
        return this.buf.readInt();
    }

    public long readUnsignedInt()
    {
        return this.buf.readUnsignedInt();
    }

    public long readLong()
    {
        return this.buf.readLong();
    }

    public float readFloat()
    {
        return this.buf.readFloat();
    }

    public double readDouble()
    {
        return this.buf.readDouble();
    }

    public ByteBuf readBytes(int p_readBytes_1_)
    {
        return this.buf.readBytes(p_readBytes_1_);
    }

    public ByteBuf readBytes(byte[] p_readBytes_1_)
    {
        return this.buf.readBytes(p_readBytes_1_);
    }

    public ByteBuf writeBoolean(boolean p_writeBoolean_1_)
    {
        return this.buf.writeBoolean(p_writeBoolean_1_);
    }

    public ByteBuf writeByte(int p_writeByte_1_)
    {
        return this.buf.writeByte(p_writeByte_1_);
    }

    public ByteBuf writeShort(int p_writeShort_1_)
    {
        return this.buf.writeShort(p_writeShort_1_);
    }

    public ByteBuf writeInt(int p_writeInt_1_)
    {
        return this.buf.writeInt(p_writeInt_1_);
    }

    public ByteBuf writeLong(long p_writeLong_1_)
    {
        return this.buf.writeLong(p_writeLong_1_);
    }

    public ByteBuf writeFloat(float p_writeFloat_1_)
    {
        return this.buf.writeFloat(p_writeFloat_1_);
    }

    public ByteBuf writeDouble(double p_writeDouble_1_)
    {
        return this.buf.writeDouble(p_writeDouble_1_);
    }

    public ByteBuf writeBytes(ByteBuf p_writeBytes_1_)
    {
        return this.buf.writeBytes(p_writeBytes_1_);
    }

    public ByteBuf writeBytes(ByteBuf p_writeBytes_1_, int p_writeBytes_2_, int p_writeBytes_3_)
    {
        return this.buf.writeBytes(p_writeBytes_1_, p_writeBytes_2_, p_writeBytes_3_);
    }

    public ByteBuf writeBytes(byte[] p_writeBytes_1_)
    {
        return this.buf.writeBytes(p_writeBytes_1_);
    }

    public ByteBuf writeBytes(byte[] p_writeBytes_1_, int p_writeBytes_2_, int p_writeBytes_3_)
    {
        return this.buf.writeBytes(p_writeBytes_1_, p_writeBytes_2_, p_writeBytes_3_);
    }

    public boolean release()
    {
        return this.buf.release();
    }
}
