/*
 * This file is part of SpoutcraftPlugin.
 *
 * Copyright (c) 2011 SpoutcraftDev <http://spoutcraft.org//>
 * SpoutcraftPlugin is licensed under the GNU Lesser General Public License.
 *
 * SpoutcraftPlugin is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * SpoutcraftPlugin is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.getspout.spout.block.mcblock;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import net.minecraft.server.v1_6_R3.Block;
import net.minecraft.server.v1_6_R3.BlockButtonAbstract;
import net.minecraft.server.v1_6_R3.BlockDiodeAbstract;
import net.minecraft.server.v1_6_R3.BlockFurnace;
import net.minecraft.server.v1_6_R3.BlockHalfTransparant;
import net.minecraft.server.v1_6_R3.BlockMinecartTrack;
import net.minecraft.server.v1_6_R3.BlockPressurePlateAbstract;
import net.minecraft.server.v1_6_R3.BlockPressurePlateBinary;
import net.minecraft.server.v1_6_R3.BlockPumpkin;
import net.minecraft.server.v1_6_R3.BlockRedstoneOre;
import net.minecraft.server.v1_6_R3.BlockRedstoneTorch;
import net.minecraft.server.v1_6_R3.BlockStem;
import net.minecraft.server.v1_6_R3.BlockStepAbstract;
import net.minecraft.server.v1_6_R3.Entity;
import net.minecraft.server.v1_6_R3.EntityHuman;
import net.minecraft.server.v1_6_R3.EntityPlayer;
import net.minecraft.server.v1_6_R3.EnumMobType;
import net.minecraft.server.v1_6_R3.IBlockAccess;
import net.minecraft.server.v1_6_R3.Material;
import net.minecraft.server.v1_6_R3.StepSound;
import net.minecraft.server.v1_6_R3.World;
import org.getspout.spout.block.SpoutCraftBlock;
import org.getspout.spoutapi.SpoutManager;
import org.getspout.spoutapi.block.SpoutBlock;
import org.getspout.spoutapi.inventory.SpoutItemStack;
import org.getspout.spoutapi.material.CustomItem;
import org.getspout.spoutapi.material.MaterialData;
import org.getspout.spoutapi.material.Tool;
import org.getspout.spoutapi.player.SpoutPlayer;

public final class CustomMCBlock implements MethodInterceptor {
	private final Block wrapped;
	private CustomMCBlock(Block toWrap) {
		this.wrapped = toWrap;
	}

	public Block getWrapped() {
		return wrapped;
	}

	private org.getspout.spoutapi.material.CustomBlock getCustomBlock(World world, int x, int y, int z) {
		short[] customIds = SpoutManager.getChunkDataManager().getCustomBlockIds(world.getWorld(), x >> 4, z >> 4);
		if (customIds != null) {
			int index = getIndex(x, y, z);
			short id = customIds[index];
			return MaterialData.getCustomBlock(id);
		}
		return null;
	}

	protected static int getIndex(int x, int y, int z) {
		return (x & 0xF) << 0xC | (z & 0xF) << 0x8 | (y & 0xFF);
	}

	@SuppressWarnings("rawtypes")
	private static Block createProxy(Block parent) {
		Enhancer enc = new Enhancer();
		enc.setSuperclass(parent.getClass());
		enc.setInterfaces(new Class[] { WrappedMCBlock.class });
		enc.setCallback(new CustomMCBlock(parent));
		enc.setClassLoader(CustomMCBlock.class.getClassLoader());

		Constructor<?>[] constructors = parent.getClass().getDeclaredConstructors();
		BlockConstructor use = null;
		for (Constructor<?> c : constructors) {
			c.setAccessible(true);
			for (BlockConstructor type : BlockConstructor.values()) {
				if (Arrays.deepEquals(c.getParameterTypes(), type.constructor)) {
					use = type;
					break;
				}
			}
		}

		if (use == null) {
			System.err.println("Unable to find matching constructor for class: " + parent.getClass().getName());
			for (Constructor<?> c : constructors) {
				System.err.println("    " + Arrays.toString(c.getParameterTypes()));
			}
			return null;
		}

		try {
			Block proxy;
			switch(use) {
				case None:
					proxy = (Block) enc.create(); break;
				case Id:
					proxy = (Block) enc.create(use.constructor, new Object[] {parent.id}); break;
				case IdAndStep:
					{
						boolean field2;
						if (parent instanceof BlockStepAbstract) {
							field2 = ((Boolean)getField(BlockStepAbstract.class, parent, "a")).booleanValue();
						} else if (parent instanceof BlockFurnace) {
							field2 = ((Boolean)getField(parent, "b")).booleanValue();
						} else if (parent instanceof BlockRedstoneTorch) {
							field2 = ((Boolean)getField(parent, "isOn")).booleanValue();
						} else if (parent instanceof BlockDiodeAbstract) {
							field2 = ((Boolean)getField(BlockDiodeAbstract.class, parent, "a")).booleanValue();
						} else {
							field2 = ((Boolean)getField(parent, "a")).booleanValue();
						}
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, field2});
					}
					break;
				case IdAndMaterial:
					proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, parent.material}); break;
				case IdAndTexture:
					proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, (Integer)getField(parent, "a")}); break;
				case IdTextureAndMaterial:
					proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, parent.material}); break;
				case IdMaterialAndFlag:
					proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, parent.material, false}); break;
				case IdAndName:
					{
						String name = (String) getField(parent, "a");
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, name});
					}
					break;
				case IdNameAndMaterial:
					{
						String name = (String) getField(parent, "a");
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, name, parent.material});
					}
					break;
				case IdNameMaterialAndDrop:
					{
						String name = (String) getField(BlockPressurePlateAbstract.class, parent, "a");
						int field4 = (Integer) getField(parent, "a");
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, name, parent.material, field4});
					}
					break;
				case IdBlockAndOther:
					{
						Block field2 = ((Block)getField(parent, "b"));
						int field3 = ((Integer)getField(parent, "c"));
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, field2, field3});
					}
					break;
				case IdMaterialAndDrop:
					{
						int field3 = ((Integer)getField(parent, "b"));
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, parent.material, field3});
					}
					break;
				case SignBlock:
					{
						Class field2 = ((Class)getField(parent, "a"));
						boolean field3 = ((Boolean)getField(parent, "b"));
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, field2, field3});
					}
					break;
				case IdTextureAndTicks:
					{
						boolean field3;
						if (parent instanceof BlockMinecartTrack || parent instanceof BlockRedstoneOre || parent instanceof BlockButtonAbstract || parent instanceof BlockPumpkin) {
							field3 = ((Boolean)getField(parent, "a")).booleanValue();
						} else {
							field3 = ((Boolean)getField(parent, "isTicking")).booleanValue();
						}
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, field3});
					}
					break;
				case PressurePlate:
					{
						EnumMobType field3 = ((EnumMobType)getField(parent, "a"));
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, field3, parent.material});
					}
					break;
				case HugeMushroom:
					{
						int field4 = ((Integer)getField(parent, "a"));
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, parent.material, field4});
					}
					break;
				case BlockStem:
					{
						Block field2;
						if (parent instanceof BlockStem) {
							field2 = ((Block)getField(parent, "blockFruit"));
						} else {
							field2 = Block.COBBLESTONE;
						}
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, field2});
					}
					break;
				case IdTextureMaterialAndTransparent:
					{
						boolean field4 = ((Boolean)getField(BlockHalfTransparant.class, parent, "a")).booleanValue();
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, parent.material, field4});
					}
					break;
				case IdTextureDataMaterialAndDrops:
					{
						int field3 = (Integer) getField(parent, "a");
						boolean field5 = ((Boolean)getField(parent, "b")).booleanValue();
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, field3, parent.material, field5});
					}
					break;
				case IdNameMaterialAndMobType:
					{
						String name = (String) getField(BlockPressurePlateAbstract.class, parent, "a");
						EnumMobType mobs = (EnumMobType) getField(BlockPressurePlateBinary.class, parent, "a");
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, name, parent.material, mobs});
					}
					break;
				case IdStringStringMaterialAndFlag:
					{
						String field2 = (String) getField(parent, "a");
						String field3 = (String) getField(parent, "c");
						boolean field5 = (Boolean) getField(parent, "b");
						proxy = (Block) enc.create(use.constructor, new Object[] {parent.id, field2, field3, parent.material, field5});
					}
					break;
				default: throw new IllegalStateException("Unknown type " +  use);
			}
			// Restore Parent StepSound
			if (proxy !=null) {
				proxy.stepSound = parent.stepSound;
			}			
			return proxy;
		} catch (RuntimeException e) {
			System.err.println("Error creating : " + parent.getClass().getName() + " with constructor: " + use.name());
			e.printStackTrace();
			return null;
		}
	}

	public static void replaceBlocks() {
		//Store proper lighting values for opacity
		final int[] lightOpacity = new int[Block.lightBlock.length];
		System.arraycopy(Block.lightBlock, 0, lightOpacity, 0, lightOpacity.length);
		//Store...whatever t is
		final boolean[] sArray = new boolean[Block.t.length];
		System.arraycopy(Block.t, 0, sArray, 0, Block.t.length);
		//Store...whatever v is
		final boolean[] uArray = new boolean[Block.v.length];
		System.arraycopy(Block.v, 0, sArray, 0, Block.v.length);
		//Store...whatever x is
		final boolean[] wArray = new boolean[Block.x.length];
		System.arraycopy(Block.x, 0, wArray, 0, Block.x.length);
		//Store proper lighting values for emission
		final int[] lightEmission = new int[Block.lightEmission.length];
		System.arraycopy(Block.lightEmission, 0, lightEmission, 0, Block.lightEmission.length);

		for (int i = 0; i < Block.byId.length; i++) {
			if (Block.byId[i] != null) {
				Block parent = Block.byId[i];
				Block.byId[i] = null;

				float strength = 0;
				try {
					final Field field = getField(parent.getClass(), "strength");
					field.setAccessible(true);
					strength = field.getFloat(parent);
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				} catch (NoSuchFieldException e) {
					e.printStackTrace();
				}

				float durability = 0;
				try {
					final Field field = getField(parent.getClass(), "durability");
					field.setAccessible(true);
					durability = field.getFloat(parent);
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				} catch (NoSuchFieldException e) {
					e.printStackTrace();
				}

				try {
					Block fake  = createProxy(parent);

					if (fake != null) {
						Block.byId[i] = fake;
					} else {
						Block.byId[i] = parent;
					}
					final Field strengthField = getField(fake.getClass(), "strength");
					strengthField.setAccessible(true);
					strengthField.setFloat(fake, strength);

					final Field durabilityField = getField(fake.getClass(), "durability");
					durabilityField.setAccessible(true);
					durabilityField.setFloat(fake, durability);

				} catch (Throwable t) {
					System.err.println("Error replacing : " + parent.getClass().getName());
					t.printStackTrace();
					Block.byId[i] = parent;
				}
			}
		}
		//Fix values
		System.arraycopy(lightOpacity, 0, Block.lightBlock, 0, lightOpacity.length);
		//System.arraycopy(sArray, 0, Block.t, 0, sArray.length);
		//System.arraycopy(uArray, 0, Block.v, 0, uArray.length);
		//System.arraycopy(wArray, 0, Block.x, 0, wArray.length);
		System.arraycopy(lightEmission, 0, Block.lightEmission, 0, lightEmission.length);
	}

	@SuppressWarnings("rawtypes")
	private static enum BlockConstructor {
		None(new Class[0]),
		Id(new Class[] {int.class}),
		IdAndStep(new Class[] {int.class, boolean.class}),
		IdAndMaterial(new Class[] {int.class, Material.class}),
		IdAndTexture(new Class[] {int.class, int.class}),
		IdAndName(new Class[] {int.class, String.class}),
		IdMaterialAndDrop(new Class[] {int.class, Material.class, int.class}),
		IdNameAndMaterial(new Class[] {int.class, String.class, Material.class}),
		IdNameMaterialAndDrop(new Class[] {int.class, String.class, Material.class, int.class}),
		IdMaterialAndFlag(new Class[] {int.class, Material.class, boolean.class}),
		IdBlockAndOther(new Class[] {int.class, Block.class, int.class}),
		SignBlock(new Class[] {int.class, Class.class, boolean.class}),
		PressurePlate(new Class[] {int.class, int.class, EnumMobType.class, Material.class}),
		HugeMushroom(new Class[] {int.class, Material.class, int.class, int.class}),
		BlockStem(new Class[] {int.class, Block.class}),
		IdTextureAndTicks(new Class[] {int.class, int.class, boolean.class}),
		IdTextureAndMaterial(new Class[] {int.class, int.class, Material.class}),
		IdTextureMaterialAndTransparent(new Class[] {int.class, int.class, Material.class, boolean.class}),
		IdTextureDataMaterialAndDrops(new Class[] {int.class, int.class, int.class, Material.class, boolean.class}),
		IdNameMaterialAndMobType(new Class[]{int.class, String.class, Material.class, EnumMobType.class}),
		IdStringStringMaterialAndFlag(new Class[] {int.class, String.class, String.class, Material.class, boolean.class});

		private final Class[] constructor;
		BlockConstructor(Class[] constructor) {
			this.constructor = constructor;
		}
		;
	}

	// TODO This causes redstone issues with glass (allows power through glass, vanilla does not) but it can't
	// Be solved from a plugin. Implemented workaround: Don't update glass unless non-opaque custom blocks are on the server.
	public static void updateGlass() {
		// Allow placement of blocks on glass
		try {
			Field field = Material.SHATTERABLE.getClass().getDeclaredField("J");
			field.setAccessible(true);
			field.setBoolean(Material.SHATTERABLE, false);
		} catch (Exception e) {
			e.printStackTrace();
		}
		Block.lightBlock[Block.GLASS.id] = 0;
	}

	public static void resetBlocks() {
		for (int i = 0; i < Block.byId.length; i++) {
			if (Block.byId[i] != null) {
				Block parent = Block.byId[i];
				if (parent instanceof WrappedMCBlock) {
					WrappedMCBlock wrapped = (WrappedMCBlock) parent;
					Block.byId[i] = null;
					Block.byId[i] = wrapped.getWrapped();
				}
			}
		}
	}

	private static Object getField(Block parent, String fieldName) {
		return getField(parent.getClass(), parent, fieldName);
	}

	private static Object getField(Class<?> clazz, Block parent, String fieldName) {
		try {
			Field field = clazz.getDeclaredField(fieldName);
			field.setAccessible(true);
			return field.get(parent);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings("rawtypes")
	private static Method findRecursive(Class<?> clazz, String name, Class[] args) {
		for (Method m : clazz.getDeclaredMethods()) {
			if (m.getName().equals(name)) {
				if (Arrays.deepEquals(m.getParameterTypes(), args)) {
					return m;
				}
			}
		}
		if (clazz.getSuperclass() != null) {
			return findRecursive(clazz.getSuperclass(), name, args);
		}
		return null;
	}

	private final ConcurrentHashMap<Method, Method> fastMethodCache = new ConcurrentHashMap<Method, Method>(100);
	@Override
	public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
		Method m = fastMethodCache.get(method);
		if (m == null) {
			// Special cases
			if (method.getName().equals("getWrapped")) {
				return getWrapped();
			} else if (method.getName().equals("setHardness")) {
				Field f = Block.class.getDeclaredField("strength");
				f.setAccessible(true);
				f.set(wrapped, args[0]);
				return null;
			} else {
				m = findRecursive(wrapped.getClass(), method.getName(), method.getParameterTypes());
		        if (m != null) {
		        	m.setAccessible(true);
		        	fastMethodCache.put(method, m);
		        } else {
		        	System.err.println("Unable to find method : " + method.getName() + " with: " + Arrays.toString(method.getParameterTypes()));	 
		        	return null;
				}
			}
		}
		// Overridden methods
		if (method.getName().equals("a") && Arrays.deepEquals(method.getParameterTypes(), new Class[] {IBlockAccess.class, int.class, int.class, int.class, int.class})) {
			World world = (World) args[0];
			int x = (Integer) args[1];
			int y = (Integer) args[2];
			int z = (Integer) args[3];
			int face = (Integer) args[4];

			org.getspout.spoutapi.material.CustomBlock block = getCustomBlock(world, x, y, z);
			if (block != null) {
				return block.isProvidingPowerTo(world.getWorld(), x, y, z, org.bukkit.craftbukkit.v1_6_R3.block.CraftBlock.notchToBlockFace(face));
			}
		} else if (method.getName().equals("c") && Arrays.deepEquals(method.getParameterTypes(), new Class[] {World.class, int.class, int.class, int.class, int.class})) {
			World world = (World) args[0];
			int x = (Integer) args[1];
			int y = (Integer) args[2];
			int z = (Integer) args[3];
			int face = (Integer) args[4];
			org.getspout.spoutapi.material.CustomBlock block = getCustomBlock(world, x, y, z);
			if (block != null) {
				return block.isProvidingPowerTo(world.getWorld(), x, y, z, org.bukkit.craftbukkit.v1_6_R3.block.CraftBlock.notchToBlockFace(face));
			}
		} else if (method.getName().equals("b") && Arrays.deepEquals(method.getParameterTypes(), new Class[] {World.class, int.class, int.class, int.class, Entity.class})) {
			World world = (World) args[0];
			int x = (Integer) args[1];
			int y = (Integer) args[2];
			int z = (Integer) args[3];
			Entity e = (Entity) args[4];
			org.getspout.spoutapi.material.CustomBlock block = getCustomBlock(world, x, y, z);
			if (block != null) {
				block.onEntityMoveAt(world.getWorld(), x, y, z, e.getBukkitEntity());
				return null;
			}
		} else if (method.getName().equals("interact")) {
			World world = (World) args[0];
			int x = (Integer) args[1];
			int y = (Integer) args[2];
			int z = (Integer) args[3];
			Entity human = (Entity) args[4];
			org.getspout.spoutapi.material.CustomBlock block = getCustomBlock(world, x, y, z);
			if (block != null && human instanceof EntityPlayer) {
				return block.onBlockInteract(world.getWorld(), x, y, z, ((SpoutPlayer) human.getBukkitEntity()));
			}
		} else if (method.getName().equals("getDamage")) {
			EntityHuman human = (EntityHuman) args[0];
			World world = (World) args[1];
			int x = (Integer) args[2];
			int y = (Integer) args[3];
			int z = (Integer) args[4];
			org.getspout.spoutapi.material.CustomBlock block = getCustomBlock(world, x, y, z);
			if (block != null) {
				if (block instanceof org.getspout.spoutapi.material.CustomBlock) {
					SpoutPlayer player = (SpoutPlayer) human.getBukkitEntity();
					float def;
					SpoutItemStack inHand = player.getItemInHand() == null ? null : new SpoutItemStack(player.getItemInHand());
					org.getspout.spoutapi.material.Material item = inHand.getMaterial();

					float hardness = block.getHardness();
					if (hardness <= 0F) {
						return m.invoke(wrapped, args);
					}

					def = (!human.a(wrapped) ? 1.0F / hardness / 100.0F : human.a(wrapped, false) / hardness / 30.0F); //TODO EntityHuman.a(Block, boolean) appears to not make any use of the flag variable...

					if (!(item instanceof CustomItem)) {
						return def;
					}

					if (!(item instanceof Tool)) {
						return def;
					}

					Tool tool = (Tool) item;

					float modifier = tool.getStrengthModifier(block);

					return modifier / hardness / (modifier > 1F ? 30F : 100F);
				}
			}
		} else if (method.getName().equals("remove")) {
			World world = (World) args[0];
			int x = (Integer) args[1];
			int y = (Integer) args[2];
			int z = (Integer) args[3];
			org.getspout.spoutapi.material.CustomBlock block = getCustomBlock(world, x, y, z);
			if (block != null) {
				block.onBlockDestroyed(world.getWorld(), x, y, z);
			}
		} else if (method.getName().equals("doPhysics")) {
			World world = (World) args[0];
			int x = (Integer) args[1];
			int y = (Integer) args[2];
			int z = (Integer) args[3];
			int face = (Integer) args[4];
			org.getspout.spoutapi.material.CustomBlock block = getCustomBlock(world, x, y, z);
			if (block != null) {
				block.onNeighborBlockChange(world.getWorld(), x, y, z, face);
			}
		}
			return m.invoke(wrapped, args);
	}

	private static Field getField(Class clazz, String fieldName) throws NoSuchFieldException {
		try {
			return clazz.getDeclaredField(fieldName);
		} catch (NoSuchFieldException e) {
			Class superClass = clazz.getSuperclass();
			if (superClass == null) {
				throw e;
			} else {
				return getField(superClass, fieldName);
			}
		}
	}
}

