package net.minecraft.entity;

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

import org.apache.commons.lang3.ObjectUtils;

import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID;
import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
import net.minecraft.crash.CrashReport;
import net.minecraft.crash.CrashReportCategory;
import net.minecraft.item.ItemStack;
import net.minecraft.network.PacketBuffer;
import net.minecraft.util.BlockPos;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.IChatComponent;
import net.minecraft.util.ReportedException;
import net.minecraft.util.Rotations;

/**
 * +
 * This portion of EaglercraftX contains deobfuscated Minecraft 1.8 source code.
 * 
 * Minecraft 1.8.8 bytecode is (c) 2015 Mojang AB. "Do not distribute!"
 * Mod Coder Pack v9.18 deobfuscation configs are (c) Copyright by the MCP Team
 * 
 * EaglercraftX 1.8 patch files (c) 2022-2024 lax1dude, hoosiertransfer,
 * ayunami2000. All Rights Reserved.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 */
public class DataWatcher {
	private final Entity owner;
	/**
	 * +
	 * When isBlank is true the DataWatcher is not watching any
	 * objects
	 */
	private boolean isBlank = true;
	private final Map<Integer, DataWatcher.WatchableObject> watchedObjects = Maps.newHashMap();
	private boolean objectChanged;

	public DataWatcher(Entity owner) {
		this.owner = owner;
	}

	public <T> void addObject(int id, T object, DataWatcher.Types type) {
		if (id > 31) {
			throw new IllegalArgumentException("Data value id is too big with " + id + "! (Max is " + 31 + ")");
		} else if (this.watchedObjects.containsKey(Integer.valueOf(id))) {
			throw new IllegalArgumentException("Duplicate id value for " + id + "!");
		} else {
			DataWatcher.WatchableObject datawatcher$watchableobject = new DataWatcher.WatchableObject(type, id, object);
			this.watchedObjects.put(Integer.valueOf(id), datawatcher$watchableobject);
			this.isBlank = false;
		}
	}

	/**
	 * +
	 * Add a new object for the DataWatcher to watch, using the
	 * specified data type.
	 */
	public void addObjectByDataType(int id, DataWatcher.Types type) {
		DataWatcher.WatchableObject datawatcher$watchableobject = new DataWatcher.WatchableObject(type, id,
				(Object) null);
		this.watchedObjects.put(Integer.valueOf(id), datawatcher$watchableobject);
		this.isBlank = false;
	}

	/**
	 * +
	 * gets the bytevalue of a watchable object
	 */
	public byte getWatchableObjectByte(int id) {
		return ((Byte) this.getWatchedObject(id).getObject()).byteValue();
	}

	public int getWatchableObjectVarint(int id) {
		return ((Integer) this.getWatchedObject(id).getObject()).intValue();
	}

	public float getWatchableObjectFloat(int id) {
		return ((Float) this.getWatchedObject(id).getObject()).floatValue();
	}

	/**
	 * +
	 * gets a watchable object and returns it as a String
	 */
	public String getWatchableObjectString(int id) {
		return (String) this.getWatchedObject(id).getObject();
	}

	public IChatComponent getWatchableObjectChatComponent(int id) {
		return (IChatComponent) this.getWatchedObject(id).getObject();
	}

	/**
	 * +
	 * Get a watchable object as an ItemStack.
	 */
	public Optional<ItemStack> getWatchableObjectItemStack(int id) {
		return (Optional<ItemStack>) this.getWatchedObject(id).getObject();
	}

	public boolean getWatchableObjectBoolean(int id) {
		return ((Boolean) this.getWatchedObject(id).getObject()).booleanValue();
	}

	public Rotations getWatchableObjectRotations(int id) {
		return (Rotations) this.getWatchedObject(id).getObject();
	}

	public BlockPos getWatchableObjectBlockPos(int id) {
		return (BlockPos) this.getWatchedObject(id).getObject();
	}

	public Optional<BlockPos> getWatchableObjectOptionalBlockPos(int id) {
		return (Optional<BlockPos>) this.getWatchedObject(id).getObject();
	}

	public EnumFacing getWatchableObjectFacing(int id) {
		return (EnumFacing) this.getWatchedObject(id).getObject();
	}

	public Optional<EaglercraftUUID> getWatchableObjectOptionalUniqueId(int id) {
		return (Optional<EaglercraftUUID>) this.getWatchedObject(id).getObject();
	}

	public Optional<IBlockState> getWatchableObjectBlockState(int id) {
		return (Optional<IBlockState>) this.getWatchedObject(id).getObject();
	}

	/**
	 * +
	 * is threadsafe, unless it throws an exception, then
	 */
	private DataWatcher.WatchableObject getWatchedObject(int id) {
		DataWatcher.WatchableObject datawatcher$watchableobject;
		try {
			datawatcher$watchableobject = (DataWatcher.WatchableObject) this.watchedObjects.get(Integer.valueOf(id));
		} catch (Throwable throwable) {
			CrashReport crashreport = CrashReport.makeCrashReport(throwable, "Getting synched entity data");
			CrashReportCategory crashreportcategory = crashreport.makeCategory("Synched entity data");
			crashreportcategory.addCrashSection("Data ID", Integer.valueOf(id));
			throw new ReportedException(crashreport);
		}
		return datawatcher$watchableobject;
	}

	public <T> void updateObject(int id, T newData) {
		DataWatcher.WatchableObject datawatcher$watchableobject = this.getWatchedObject(id);
		if (ObjectUtils.notEqual(newData, datawatcher$watchableobject.getObject())) {
			datawatcher$watchableobject.setObject(newData);
			this.owner.onDataWatcherUpdate(id);
			datawatcher$watchableobject.setWatched(true);
			this.objectChanged = true;
		}

	}

	public void setObjectWatched(int id) {
		this.getWatchedObject(id).watched = true;
		this.objectChanged = true;
	}

	/**
	 * +
	 * true if one or more object was changed
	 */
	public boolean hasObjectChanged() {
		return this.objectChanged;
	}

	/**
	 * +
	 * Writes the list of watched objects (entity attribute of type
	 * {byte, short, int, float, string, ItemStack,
	 * ChunkCoordinates}) to the specified PacketBuffer
	 */
	public static void writeWatchedListToPacketBuffer(List<DataWatcher.WatchableObject> objectsList,
			PacketBuffer buffer) throws IOException {
		if (objectsList != null) {
			for (int i = 0, l = objectsList.size(); i < l; ++i) {
				writeWatchableObjectToPacketBuffer(buffer, objectsList.get(i));
			}
		}

		buffer.writeByte(255);
	}

	public List<DataWatcher.WatchableObject> getChanged() {
		ArrayList arraylist = null;
		if (this.objectChanged) {
			for (DataWatcher.WatchableObject datawatcher$watchableobject : this.watchedObjects.values()) {
				if (datawatcher$watchableobject.isWatched()) {
					datawatcher$watchableobject.setWatched(false);
					if (arraylist == null) {
						arraylist = Lists.newArrayList();
					}

					arraylist.add(datawatcher$watchableobject);
				}
			}
		}

		this.objectChanged = false;
		return arraylist;
	}

	public void writeTo(PacketBuffer buffer) throws IOException {
		for (DataWatcher.WatchableObject datawatcher$watchableobject : this.watchedObjects.values()) {
			writeWatchableObjectToPacketBuffer(buffer, datawatcher$watchableobject);
		}
		buffer.writeByte(255);
	}

	public List<DataWatcher.WatchableObject> getAllWatched() {
		ArrayList arraylist = null;

		for (DataWatcher.WatchableObject datawatcher$watchableobject : this.watchedObjects.values()) {
			if (arraylist == null) {
				arraylist = Lists.newArrayList();
			}

			arraylist.add(datawatcher$watchableobject);
		}

		return arraylist;
	}

	/**
	 * +
	 * Writes a watchable object (entity attribute of type {byte,
	 * short, int, float, string, ItemStack, ChunkCoordinates}) to
	 * the specified PacketBuffer
	 */
	private static void writeWatchableObjectToPacketBuffer(PacketBuffer buffer, DataWatcher.WatchableObject object)
			throws IOException {
		buffer.writeByte(object.getDataValueId());
		buffer.writeVarIntToBuffer(object.getObjectType().ordinal());
		switch (object.getObjectType()) {
			case BYTE:
				buffer.writeByte(((Byte) object.getObject()).byteValue());
				break;
			case VARINT:
				buffer.writeVarIntToBuffer(((Integer) object.getObject()).intValue());
				break;
			case FLOAT:
				buffer.writeFloat(((Float) object.getObject()).floatValue());
				break;
			case STRING:
				buffer.writeString((String) object.getObject());
				break;
			case CHAT_COMPONENT:
				buffer.writeChatComponent((IChatComponent) object.getObject());
				break;
			case OPTIONAL_ITEM_STACK:
				buffer.writeItemStackToBuffer(((Optional<ItemStack>) object.getObject()).orNull());
				break;
			case BOOLEAN:
				buffer.writeBoolean((Boolean) object.getObject());
				break;
			case ROTATIONS:
				Rotations rotations = (Rotations) object.getObject();
				buffer.writeFloat(rotations.getX());
				buffer.writeFloat(rotations.getY());
				buffer.writeFloat(rotations.getZ());
				break;
			case BLOCK_POS:
				buffer.writeBlockPos((BlockPos) object.getObject());
				break;
			case OPTIONAL_BLOCK_POS:
				Optional<BlockPos> optional = (Optional<BlockPos>) object.getObject();
				buffer.writeBoolean(optional.isPresent());

				if (optional.isPresent()) {
					buffer.writeBlockPos(optional.get());
				}
				break;
			case FACING:
				buffer.writeEnumValue((EnumFacing) object.getObject());
				break;
			case OPTIONAL_UNIQUE_ID:
				Optional<EaglercraftUUID> optional1 = (Optional<EaglercraftUUID>) object.getObject();
				buffer.writeBoolean(optional1.isPresent());

				if (optional1.isPresent()) {
					buffer.writeUuid(optional1.get());
				}
				break;
			case OPTIONAL_BLOCK_STATE:
				Optional<IBlockState> optional2 = (Optional<IBlockState>) object.getObject();
				if (optional2.isPresent()) {
					buffer.writeVarIntToBuffer(Block.getStateId(optional2.get()));
				} else {
					buffer.writeVarIntToBuffer(0);
				}
				break;
		}
	}

	/**
	 * +
	 * Reads a list of watched objects (entity attribute of type
	 * {byte, short, int, float, string, ItemStack,
	 * ChunkCoordinates}) from the supplied PacketBuffer
	 */
	public static List<DataWatcher.WatchableObject> readWatchedListFromPacketBuffer(PacketBuffer buffer)
			throws IOException {
		ArrayList arraylist = null;
		int i;

		while ((i = buffer.readUnsignedByte()) != 255) {
			if (arraylist == null) {
				arraylist = Lists.newArrayList();
			}

			DataWatcher.Types type = buffer.readEnumValue(DataWatcher.Types.class);
			DataWatcher.WatchableObject watchableobject = null;

			switch (type) {
				case BYTE:
					watchableobject = new DataWatcher.WatchableObject(type, i, Byte.valueOf(buffer.readByte()));
					break;
				case VARINT:
					watchableobject = new DataWatcher.WatchableObject(type, i,
							Integer.valueOf(buffer.readVarIntFromBuffer()));
					break;
				case FLOAT:
					watchableobject = new DataWatcher.WatchableObject(type, i, Float.valueOf(buffer.readFloat()));
					break;
				case STRING:
					watchableobject = new DataWatcher.WatchableObject(type, i, buffer.readStringFromBuffer(32767));
					break;
				case CHAT_COMPONENT:
					watchableobject = new DataWatcher.WatchableObject(type, i, buffer.readChatComponent());
					break;
				case OPTIONAL_ITEM_STACK:
					watchableobject = new DataWatcher.WatchableObject(type, i,
							Optional.fromNullable(buffer.readItemStackFromBuffer()));
					break;
				case BOOLEAN:
					watchableobject = new DataWatcher.WatchableObject(type, i, Boolean.valueOf(buffer.readBoolean()));
					break;
				case ROTATIONS:
					float f = buffer.readFloat();
					float f1 = buffer.readFloat();
					float f2 = buffer.readFloat();
					watchableobject = new DataWatcher.WatchableObject(type, i, new Rotations(f, f1, f2));
					break;
				case BLOCK_POS:
					watchableobject = new DataWatcher.WatchableObject(type, i, buffer.readBlockPos());
					break;
				case OPTIONAL_BLOCK_POS:
					watchableobject = new DataWatcher.WatchableObject(type, i,
							!buffer.readBoolean() ? Optional.<BlockPos>absent() : Optional.of(buffer.readBlockPos()));
					break;
				case FACING:
					watchableobject = new DataWatcher.WatchableObject(type, i, buffer.readEnumValue(EnumFacing.class));
					break;
				case OPTIONAL_UNIQUE_ID:
					watchableobject = new DataWatcher.WatchableObject(type, i,
							!buffer.readBoolean() ? Optional.<EaglercraftUUID>absent()
									: Optional.of(buffer.readUuid()));
					break;
				case OPTIONAL_BLOCK_STATE:
					int j = buffer.readVarIntFromBuffer();
					watchableobject = new DataWatcher.WatchableObject(type, i,
							j == 0 ? Optional.<IBlockState>absent() : Optional.of(Block.getStateById(j)));
			}

			arraylist.add(watchableobject);
		}

		return arraylist;
	}

	public void updateWatchedObjectsFromList(List<DataWatcher.WatchableObject> parList) {

		for (int i = 0, l = parList.size(); i < l; ++i) {
			DataWatcher.WatchableObject datawatcher$watchableobject = parList.get(i);
			DataWatcher.WatchableObject datawatcher$watchableobject1 = (DataWatcher.WatchableObject) this.watchedObjects
					.get(Integer.valueOf(datawatcher$watchableobject.getDataValueId()));
			if (datawatcher$watchableobject1 != null) {
				datawatcher$watchableobject1.setObject(datawatcher$watchableobject.getObject());
				this.owner.onDataWatcherUpdate(datawatcher$watchableobject.getDataValueId());
			}
		}

		this.objectChanged = true;
	}

	public boolean getIsBlank() {
		return this.isBlank;
	}

	public void func_111144_e() {
		this.objectChanged = false;
	}

	public enum Types {
		BYTE, VARINT, FLOAT, STRING, CHAT_COMPONENT, OPTIONAL_ITEM_STACK, BOOLEAN, ROTATIONS, BLOCK_POS,
		OPTIONAL_BLOCK_POS,
		FACING,
		OPTIONAL_UNIQUE_ID, OPTIONAL_BLOCK_STATE;
	};

	public static class WatchableObject {
		private final DataWatcher.Types objectType;
		private final int dataValueId;
		private Object watchedObject;
		private boolean watched;

		public WatchableObject(DataWatcher.Types type, int id, Object object) {
			this.dataValueId = id;
			this.watchedObject = object;
			this.objectType = type;
			this.watched = true;
		}

		public int getDataValueId() {
			return this.dataValueId;
		}

		public void setObject(Object object) {
			this.watchedObject = object;
		}

		public Object getObject() {
			return this.watchedObject;
		}

		public DataWatcher.Types getObjectType() {
			return this.objectType;
		}

		public boolean isWatched() {
			return this.watched;
		}

		public void setWatched(boolean watched) {
			this.watched = watched;
		}
	}
}