package common.network;

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

import common.init.ItemRegistry;
import common.item.ItemStack;
import common.net.buffer.ByteBuf;
import common.net.buffer.ByteBufInputStream;
import common.net.buffer.ByteBufOutputStream;
import common.net.handler.codec.DecoderException;
import common.net.handler.codec.EncoderException;
import common.tags.TagObject;
import common.util.BlockPos;

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

	private final ByteBuf buf;

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

	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[] data) {
		this.writeVarInt(data.length);
		this.writeBytes(data);
	}

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

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

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

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

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

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

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

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

	public int readVarInt() {
		int v = 0;
		int c = 0;
		while(true) {
			byte b = this.readByte();
			v |= (b & 127) << c++ * 7;
			if(c > 5)
				throw new RuntimeException("VarInt too big");
			if((b & 128) != 128)
				break;
		}
		return v;
	}
	
	public void writeTag(TagObject tag) {
		if(tag == null) {
			this.writeByte(0);
			return;
		}
		try {
			TagObject.write(tag, new ByteBufOutputStream(this.buf));
		}
		catch(IOException e) {
			throw new EncoderException(e);
		}
	}

	public TagObject readTag() throws IOException {
		int i = this.buf.readerIndex();
		byte b = this.readByte();
		if(b == 0)
			return null;
		this.buf.readerIndex(i);
		return TagObject.read(new ByteBufInputStream(this.buf), 2097152);
	}

	public void writeItemStack(ItemStack stack) {
		if(stack == null) {
			this.writeShort(-1);
			return;
		}
		this.writeShort(ItemRegistry.getIdFromItem(stack.getItem()));
		this.writeVarInt(stack.size);
		this.writeTag(stack.getTagCompound());
	}

	public ItemStack readItemStack() throws IOException {
		int id = this.readShort();
		if(id < 0)
			return null;
		int amt = this.readVarInt();
		ItemStack stack = new ItemStack(ItemRegistry.getItemById(id), amt);
		stack.setTagCompound(this.readTag());
		return stack;
	}

	public void writeString(String str) {
		byte[] data = str.getBytes(UTF_8);
		if(data.length > 32767)
			throw new EncoderException("String too big (was " + str.length() + " bytes encoded, max " + 32767 + ")");
		this.writeVarInt(data.length);
		this.writeBytes(data);
	}

	public String readString(int limit) {
		int len = this.readVarInt();
		if(len > limit * 4)
			throw new DecoderException("The received encoded string buffer length is longer than maximum allowed (" + len + " > " + limit * 4 + ")");
		else if(len < 0)
			throw new DecoderException("The received encoded string buffer length is less than zero! Weird string!");
		byte[] data = new byte[len];
		this.readBytes(data);
		String str = new String(data, UTF_8);
		if(str.length() > limit)
			throw new DecoderException("The received string length is longer than maximum allowed (" + len + " > " + limit + ")");
		return str;
	}

	public void writeBoolean(boolean value) {
		this.buf.writeBoolean(value);
	}

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

	public void writeByte(int value) {
		this.buf.writeByte(value);
	}

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

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

	public void writeShort(int value) {
		this.buf.writeShort(value);
	}

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

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

	public void writeInt(int value) {
		this.buf.writeInt(value);
	}

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

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

	public void writeLong(long value) {
		this.buf.writeLong(value);
	}

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

	public void writeFloat(float value) {
		this.buf.writeFloat(value);
	}

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

	public void writeDouble(double value) {
		this.buf.writeDouble(value);
	}

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

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

	public void writeBytes(byte[] data) {
		this.buf.writeBytes(data);
	}

	public void readBytes(byte[] buffer) {
		this.buf.readBytes(buffer);
	}

	public void writeBytes(byte[] data, int offset, int length) {
		this.buf.writeBytes(data, offset, length);
	}

	public void writeBuffer(ByteBuf buf) {
		this.buf.writeBytes(buf);
	}

	public void writeBuffer(ByteBuf buf, int offset, int length) {
		this.buf.writeBytes(buf, offset, length);
	}

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

	public void ensureWritable(int size) {
		this.buf.ensureWritable(size);
	}

	public void release() {
		this.buf.release();
	}
}
