package com.zivilon.cindercore.network;

import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.event.FMLServerStartingEvent;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.Mod.EventHandler;
import cpw.mods.fml.common.gameevent.PlayerEvent;
import cpw.mods.fml.common.network.FMLNetworkEvent;
import cpw.mods.fml.common.network.FMLNetworkEvent.ServerConnectionFromClientEvent;
import cpw.mods.fml.common.network.NetworkRegistry;
import cpw.mods.fml.common.network.handshake.FMLHandshakeMessage;
import cpw.mods.fml.common.network.internal.EntitySpawnHandler;
import cpw.mods.fml.common.network.internal.FMLMessage;
import net.minecraftforge.common.MinecraftForge;

import net.minecraft.entity.DataWatcher;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.init.Blocks;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.block.Block;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.NetHandlerPlayServer;
import net.minecraft.network.Packet;
import net.minecraft.network.play.server.S00PacketKeepAlive;
import net.minecraft.network.play.server.S02PacketChat;
import net.minecraft.network.play.server.S03PacketTimeUpdate;
import net.minecraft.network.play.server.S04PacketEntityEquipment;
import net.minecraft.network.play.server.S05PacketSpawnPosition;
import net.minecraft.network.play.server.S06PacketUpdateHealth;
import net.minecraft.network.play.server.S08PacketPlayerPosLook;
import net.minecraft.network.play.server.S0CPacketSpawnPlayer;
import net.minecraft.network.play.server.S0EPacketSpawnObject;
import net.minecraft.network.play.server.S0FPacketSpawnMob;

import net.minecraft.network.play.server.S12PacketEntityVelocity;
import net.minecraft.network.play.server.S13PacketDestroyEntities;
import net.minecraft.network.play.server.S14PacketEntity;
import net.minecraft.network.play.server.S18PacketEntityTeleport;
import net.minecraft.network.play.server.S19PacketEntityHeadLook;
import net.minecraft.network.play.server.S1BPacketEntityAttach;
import net.minecraft.network.play.server.S1CPacketEntityMetadata;
import net.minecraft.network.play.server.S1FPacketSetExperience;

import net.minecraft.network.play.server.S20PacketEntityProperties;
import net.minecraft.network.play.server.S21PacketChunkData;
import net.minecraft.network.play.server.S22PacketMultiBlockChange;
import net.minecraft.network.play.server.S23PacketBlockChange;
import net.minecraft.network.play.server.S24PacketBlockAction;
import net.minecraft.network.play.server.S26PacketMapChunkBulk;
import net.minecraft.network.play.server.S28PacketEffect;
import net.minecraft.network.play.server.S29PacketSoundEffect;
import net.minecraft.network.play.server.S2FPacketSetSlot;

import net.minecraft.network.play.server.S30PacketWindowItems;
import net.minecraft.network.play.server.S33PacketUpdateSign;
import net.minecraft.network.play.server.S35PacketUpdateTileEntity;
import net.minecraft.network.play.server.S3FPacketCustomPayload;

import net.minecraft.server.MinecraftServer;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.WorldServer;
import net.minecraft.world.World;

import cpw.mods.fml.common.network.internal.FMLProxyPacket;

import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPromise;
import io.netty.buffer.UnpooledUnsafeDirectByteBuf;

import java.io.DataInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.UUID;
import java.util.zip.Inflater;
import java.text.DecimalFormat;


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.zivilon.cindercore.CinderCore;
import com.zivilon.cindercore.interfaces.ICustomChunk;

import com.zivilon.cindercore_modlist.CinderCoreModlist;
import com.zivilon.cindercore.mixins.MixinS0CPacketSpawnPlayer;
import com.zivilon.cindercore.mixins.MixinS0EPacketSpawnObject;
import com.zivilon.cindercore.mixins.MixinS0FPacketSpawnMob;
import com.zivilon.cindercore.mixins.MixinS1CPacketEntityMetadata;
import com.zivilon.cindercore.mixins.MixinS22PacketMultiBlockChange;
import com.zivilon.cindercore.mixins.MixinS23PacketBlockChange;
import com.zivilon.cindercore.mixins.MixinS24PacketBlockAction;
import com.zivilon.cindercore.mixins.MixinS30PacketWindowItems;
import com.zivilon.cindercore.mixins.MixinPlayerManager;
import com.zivilon.cindercore.mixins.MixinWorldServer;
import com.zivilon.cindercore.util.ChunkDeepCopy;

import net.minecraft.server.management.PlayerManager;

public class PacketInterceptor extends ChannelDuplexHandler {
	public static boolean debug = false;
	public UUID uuid;

	public PacketInterceptor(UUID player_uuid) {
		this.uuid = player_uuid;
	}

	@Override
	public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
		if (msg instanceof S30PacketWindowItems) {
		    S30PacketWindowItems packet = (S30PacketWindowItems) msg;
		    handle_window_items(packet, uuid);
		}
		if (msg instanceof S2FPacketSetSlot) {
		    S2FPacketSetSlot packet = (S2FPacketSetSlot) msg;
		    handle_set_slot(packet, uuid);
		}

		if (msg instanceof S26PacketMapChunkBulk) {
		    S26PacketMapChunkBulk packet = (S26PacketMapChunkBulk) msg;
		    List<Chunk> chunks = handle_map_chunk_bulk(packet, uuid);
		    if (chunks != null) {
	            msg = new S26PacketMapChunkBulk(chunks);
	        }
		}
		if (msg instanceof S21PacketChunkData) {
		    S21PacketChunkData packet = (S21PacketChunkData) msg;
		    Chunk chunk = handle_chunk_data(packet, uuid);
		    if (chunk != null) {
	            msg = new S21PacketChunkData(chunk, packet.func_149274_i(), 0);
	        }
		}
		if (msg instanceof S04PacketEntityEquipment) {
		    S04PacketEntityEquipment packet = (S04PacketEntityEquipment) msg;
		    handle_entity_equipment(packet, uuid);
		}
		if (msg instanceof S0FPacketSpawnMob) {
		    S0FPacketSpawnMob packet = (S0FPacketSpawnMob) msg;
		    handle_spawn_mob(packet, uuid);
		}
		if (msg instanceof S0EPacketSpawnObject) {
		    S0EPacketSpawnObject packet = (S0EPacketSpawnObject) msg;
		    handle_spawn_object(packet, uuid);
		}
		if (msg instanceof S23PacketBlockChange) {
	        S23PacketBlockChange packet = (S23PacketBlockChange) msg;
            msg = handle_block_change(packet, uuid);
	    }
		if (msg instanceof S1CPacketEntityMetadata) {
		    S1CPacketEntityMetadata packet = (S1CPacketEntityMetadata) msg;
		    packet = handle_entity_metadata(packet, uuid);
		    msg = packet;
	    }
		if (msg instanceof S0CPacketSpawnPlayer) {
		    S0CPacketSpawnPlayer packet = (S0CPacketSpawnPlayer) msg;
		    handle_spawn_player(packet, uuid);
		}
		if (msg instanceof S22PacketMultiBlockChange) {
		    S22PacketMultiBlockChange packet = (S22PacketMultiBlockChange) msg;
		    handle_multi_block_change(packet, uuid);
		    msg = packet;
		}
		if (msg instanceof S24PacketBlockAction) {
		    S24PacketBlockAction packet = (S24PacketBlockAction) msg;
		    handle_block_action(packet, uuid);
		}
	    super.write(ctx, msg, promise);
	}

	public S1CPacketEntityMetadata handle_entity_metadata(S1CPacketEntityMetadata packet, UUID uuid) {
		// Rebuild the packet because it is a shared reference and otherwise changes propagate to other clients too
	    Integer entity_id = ((MixinS1CPacketEntityMetadata)packet).get_entity_id();
	    if (entity_id == null) return packet;
	    Entity entity = getPlayerByUUID(uuid).worldObj.getEntityByID(entity_id);
	    if (entity == null) return packet;
	    DataWatcher data_watcher = entity.getDataWatcher();
	    if (data_watcher == null) return packet;

	    // Apply any changes to this new packet instead of old packet
	    S1CPacketEntityMetadata new_packet = new S1CPacketEntityMetadata(entity_id, data_watcher, true);

	    List<DataWatcher.WatchableObject> originalMetadataReference = ((MixinS1CPacketEntityMetadata)new_packet).get_metadata();
	    // Create new List object to dereference it from possible shared packets. Objects within are still shared references but List itself is not.
		List<DataWatcher.WatchableObject> originalMetadata = new ArrayList<>();
        for (DataWatcher.WatchableObject watchableObject : originalMetadataReference) {
            originalMetadata.add(watchableObject);
        }

	    for (int i = 0; i < originalMetadata.size(); i++) {
	        DataWatcher.WatchableObject watchableObject = originalMetadata.get(i);
	        Object obj = watchableObject.getObject();

	        process_custom_entity_metadata(entity, i, originalMetadata, uuid); // Contents of originalMetadata are likely shared references here

	        if (obj instanceof ItemStack) {
	            ItemStack originalItem = (ItemStack) obj;
		        ItemStack modifiedItem = new ItemStack(originalItem.getItem(), originalItem.stackSize, originalItem.getItemDamage());
		        process_custom_item_datavalue(modifiedItem, uuid);
	            if (originalItem != null && (CinderCore.getItemFallbackMap(uuid).containsKey(Item.getIdFromItem(originalItem.getItem()))) || originalItem.getItemDamage() != modifiedItem.getItemDamage()) {
	                Integer fallbackItemId = CinderCore.getItemFallbackMap(uuid).get(Item.getIdFromItem(originalItem.getItem()));
	                if (fallbackItemId != null) modifiedItem.func_150996_a(Item.getItemById(fallbackItemId));
                    // Copy the NBT data from the original item, if present
				    if (originalItem.hasTagCompound()) {
				        NBTTagCompound originalTagCompound = originalItem.getTagCompound();
				        NBTTagCompound modifiedTagCompound = (NBTTagCompound) originalTagCompound.copy();
				        
				        // Recursively process the NBT to handle nested items
				        process_item_nbt(modifiedTagCompound, uuid);
				        
				        modifiedItem.setTagCompound(modifiedTagCompound);
				    }

	                // Construct a new WatchableObject with the modified item
	                DataWatcher.WatchableObject modifiedWatchableObject = new DataWatcher.WatchableObject(watchableObject.getObjectType(), watchableObject.getDataValueId(), modifiedItem);
	                
	                // Replace the object at the same position
	                originalMetadata.set(i, modifiedWatchableObject);
	            }
	        }
	    }
	    ((MixinS1CPacketEntityMetadata)new_packet).set_metadata(originalMetadata);	    
	    return new_packet;
	}

	public void handle_block_action(S24PacketBlockAction packet, UUID uuid) {
        EntityPlayerMP player = getPlayerByUUID(uuid);
        Block block = ((MixinS24PacketBlockAction)packet).get_block();
        Block processed_block = process_block(block, uuid);
        ((MixinS24PacketBlockAction)packet).set_block(processed_block);
	}

	public S23PacketBlockChange handle_block_change(S23PacketBlockChange packet, UUID uuid) {
        EntityPlayerMP player = getPlayerByUUID(uuid);
        if (player != null) {
            World world = player.worldObj;
            int x = ((MixinS23PacketBlockChange)packet).get_x();
            int y = ((MixinS23PacketBlockChange)packet).get_y();
            int z = ((MixinS23PacketBlockChange)packet).get_z();
            Block block = process_block(((MixinS23PacketBlockChange)packet).get_block(), uuid);

            S23PacketBlockChange new_packet = new S23PacketBlockChange(x, y, z, world);
            ((MixinS23PacketBlockChange)new_packet).set_block(block);
            return new_packet;
        }
    	return packet;
	}

	public void handle_multi_block_change(S22PacketMultiBlockChange packet, UUID uuid) {
	    byte[] blockDataArray = ((MixinS22PacketMultiBlockChange)packet).get_block_data_array();

	    DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream(blockDataArray));

	    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
	    DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);

	    try {
	        while (dataInputStream.available() > 0) {
	            short positionShort = dataInputStream.readShort();
	            short blockStateShort = dataInputStream.readShort();

	            int blockId = (blockStateShort >> 4) & 4095; // Extract block ID
	            int metadata = blockStateShort & 15; // Extract metadata

	            // Apply fallback logic to block ID
	            if (CinderCore.getItemFallbackMap(uuid).containsKey(blockId)) {
	                blockId = CinderCore.getItemFallbackMap(uuid).get(blockId);
	            }

	            dataOutputStream.writeShort(positionShort);

	            short newBlockStateShort = (short)((blockId << 4) | metadata);
	            dataOutputStream.writeShort(newBlockStateShort);
	        }

	        byte[] modifiedBlockDataArray = byteArrayOutputStream.toByteArray();
	        ((MixinS22PacketMultiBlockChange)packet).set_block_data_array(modifiedBlockDataArray);
	    } catch (IOException e) {
	    }
	}

	public void handle_spawn_player(S0CPacketSpawnPlayer packet, UUID uuid) {
        int item_id = ((MixinS0CPacketSpawnPlayer)packet).get_item_ID();
        if (CinderCore.getItemFallbackMap(uuid).containsKey(item_id)) {
        	((MixinS0CPacketSpawnPlayer)packet).set_item_ID(CinderCore.getItemFallbackMap(uuid).get(item_id));
        }
	}

    public Block process_block(Block block, UUID uuid) {
    	Map<Integer, Integer> fallbacks = CinderCore.getItemFallbackMap(uuid);
    	int originalBlockId = Block.getIdFromBlock(block);
		if (fallbacks.containsKey(originalBlockId)) {
	        int fallbackBlockId = fallbacks.get(originalBlockId);
	        Block fallbackBlock = Block.getBlockById(fallbackBlockId);
			return fallbackBlock;
	    }
	    return block;
    }

    public int process_entity(int entity_ID, UUID uuid) {
    	Map<Integer, Integer> fallbacks = CinderCore.getEntityFallbackMap(uuid);
        if (fallbacks.containsKey(entity_ID)) {
            entity_ID = fallbacks.get(entity_ID);
        }
        return entity_ID;
    }


    public void handle_spawn_mob(S0FPacketSpawnMob packet, UUID uuid) {
        int entity_ID = ((MixinS0FPacketSpawnMob)packet).get_id();
        ((MixinS0FPacketSpawnMob)packet).set_id(process_entity(entity_ID, uuid));
    }
    public void handle_spawn_object(S0EPacketSpawnObject packet, UUID uuid) {
        int entity_type = ((MixinS0EPacketSpawnObject)packet).get_id();
        ((MixinS0EPacketSpawnObject)packet).set_id(process_entity(entity_type, uuid));
        if (entity_type == 70) { // Test if entity is EntityFallingBlock
        	int item_id = ((MixinS0EPacketSpawnObject)packet).get_item_id();
        	if (CinderCore.getItemFallbackMap(uuid).containsKey(item_id)) {
	            int fallback_id = CinderCore.getItemFallbackMap(uuid).get(item_id);
	            ((MixinS0EPacketSpawnObject)packet).set_item_id(fallback_id);
	        }
        }
    }

	public List<Chunk> handle_map_chunk_bulk(S26PacketMapChunkBulk packet, UUID uuid) {
		List<Chunk> modified_chunks = null;
        List<Chunk> chunks = new ArrayList<>();
        boolean isChunkModified = false;
        Integer chunk_count = packet.func_149254_d();
        EntityPlayerMP player = getPlayerByUUID(uuid);
        World world;
        if (player != null) {
	        world = player.worldObj;
        } else {
        	world = (World)getPlayerWorldByUUID(uuid);
        	if (world == null) {
        		world = MinecraftServer.getServer().worldServers[0]; // Default to overworld because bad data is probably better than crash
        		System.out.println("Defaulting to Overworld to save from crash");
    		}
        }

	    for (int i = 0; i < chunk_count; i++) { // Get number of chunks and run once for each of them
	        Integer x = packet.func_149255_a(i); // Get chunk X coordinate      
	        Integer z = packet.func_149253_b(i); // Get chunk Z coordinate

	        // Original in-world chunk. DO NOT MODIFY
			Chunk originalChunk = world.getChunkFromChunkCoords(x, z);
			Chunk chunk = ChunkDeepCopy.deepCopyChunk(originalChunk);

	        chunks.add(chunk);
	    }
	    for (int i = 0; i < packet.func_149254_d(); i++) {
	        if (process_chunk(chunks.get(i), uuid)) {
				isChunkModified = true;
			}
	    }
	    if (isChunkModified == true) {
	    	modified_chunks = chunks;
	    }
	    return modified_chunks;
	}

    public Chunk handle_chunk_data(S21PacketChunkData packet, UUID uuid) {
        Chunk modified_chunk = null; // Create null return value that can be replaced with chunk if modified
        int x = packet.func_149273_e(); // Get chunk X coordinate      
        int z = packet.func_149271_f(); // Get chunk Z coordinate
        EntityPlayerMP player = getPlayerByUUID(uuid);
        World world;
        if (player != null) {
	        world = player.worldObj;
        } else {
        	world = (World)getPlayerWorldByUUID(uuid);
        	if (world == null) {
        		world = MinecraftServer.getServer().worldServers[0]; // Default to overworld because bad data is probably better than crash
        		System.out.println("Defaulting to Overworld to save from crash");
    		}
        }
		Chunk originalChunk = world.getChunkFromChunkCoords(x, z);

		Chunk chunk = ChunkDeepCopy.deepCopyChunk(originalChunk);

        if (process_chunk(chunk, uuid)) {
            modified_chunk = chunk;            
        }
        return modified_chunk;
    }



	public void handle_window_items(S30PacketWindowItems packet, UUID uuid) {
	    try {
	        ItemStack[] items = packet.func_148910_d();
	        if (items != null) {
		        boolean modified = false;
		        for (int i = 0; i < items.length; i++) {
		            if (items[i] != null) {
		                process_itemstack(items[i], uuid);
		            }
		        }
	        }
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
	}


	public void handle_set_slot(S2FPacketSetSlot packet, UUID uuid) {
	    try {
	        ItemStack item = packet.func_149174_e();

	        if (item != null) {
	            process_itemstack(item, uuid);
	        }
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
	}

	public void handle_entity_equipment(S04PacketEntityEquipment packet, UUID uuid) {
	    ItemStack item = packet.func_149390_c();
	    if (item != null) {
	        process_itemstack(item, uuid);
	    }
	}
	
    public void process_itemstack(ItemStack item, UUID uuid) {	
        Integer item_id = Item.getIdFromItem(item.getItem());
        if (item_id == null) {
        	System.out.println("Item ID is null!");
        	return;
        }
        if (CinderCore.getItemFallbackMap(uuid).containsKey(item_id)) {
            Item fallback_item = Item.getItemById(CinderCore.getItemFallbackMap(uuid).get(item_id));
            item.func_150996_a(fallback_item);
        }
        process_custom_item_datavalue(item, uuid);
        if (item.hasTagCompound()) {
	        process_item_nbt(item.getTagCompound(), uuid);
	    }
    }

    public void process_custom_item_datavalue(ItemStack item, UUID uuid) {
    	// Inject to HEAD with mixins if your mod needs custom handling
    }
    public int process_custom_item_datavalue_int(int datavalue, String item_name, UUID uuid) {
    	// Inject to HEAD with mixins if your mod needs custom handling
    	return datavalue;
    }
    public static void process_custom_entity(Entity entity, UUID uuid) {
    	// Inject to HEAD with mixins if your mod needs custom handling
    }
    public void process_custom_entity_metadata(Entity entity, int i, List<DataWatcher.WatchableObject> originalMetadata, UUID uuid) {
    	// Inject to HEAD with mixins if your mod needs custom handling
    }

	public void process_item_nbt(NBTBase nbt, UUID uuid) {
	    if (nbt instanceof NBTTagCompound) {
	        NBTTagCompound compound = (NBTTagCompound) nbt;
	        // Check if this compound is an item by looking for 'id' and 'Count'
	        if (compound.hasKey("id", 99) && compound.hasKey("Count", 99)) {
	            int item_id = compound.getInteger("id");
	            if (CinderCore.getItemFallbackMap(uuid).containsKey(item_id)) {
	                int fallback_item_id = CinderCore.getItemFallbackMap(uuid).get(item_id);
	                compound.setInteger("id", fallback_item_id);
	            }
	            if (compound.hasKey("Damage")) {
	            	int datavalue = compound.getInteger("Damage");
	            	String item_type = Item.getItemById(item_id).getUnlocalizedName();
	            	compound.setInteger("Damage", process_custom_item_datavalue_int(datavalue, item_type, uuid));
	            }
	        }

	        Set<String> keySet = (Set<String>) compound.func_150296_c();
			for (String key : keySet) {
			    NBTBase inner_nbt = compound.getTag(key);
			    process_item_nbt(inner_nbt, uuid);
			}

	    } else if (nbt instanceof NBTTagList) {
	        NBTTagList list = (NBTTagList) nbt;
	        for (int i = 0; i < list.tagCount(); i++) {
	            NBTBase inner_nbt = list.getCompoundTagAt(i);
	            process_item_nbt(inner_nbt, uuid);
	        }
	    }
	}

	public boolean process_chunk(Chunk chunk, UUID uuid) {
		boolean isModified = false;
	    for (int x = 0; x < 16; x++) {
	        for (int z = 0; z < 16; z++) {
	            for (int y = 0; y < 256; y++) {
	                Block block = chunk.getBlock(x, y, z);
	                int blockId = Block.getIdFromBlock(block);

	                if (CinderCore.getItemFallbackMap(uuid).containsKey(blockId)) {
	                	isModified = true;
	                    int fallbackId = CinderCore.getItemFallbackMap(uuid).get(blockId);
	                    Block fallbackBlock = Block.getBlockById(fallbackId);
	                    int metadata = chunk.getBlockMetadata(x, y, z);

	                    chunk.func_150807_a(x, y, z, fallbackBlock, metadata);
	                }
	            }
	        }
	    }
	    return isModified;
	}



    public static EntityPlayerMP getPlayerByUUID(UUID playerUUID) {
        MinecraftServer server = MinecraftServer.getServer();
        if (server == null) {
            return null;
        }

        for (Object obj : server.getConfigurationManager().playerEntityList) {
    	    if (obj instanceof EntityPlayerMP) {
			    EntityPlayerMP player = (EntityPlayerMP) obj;
	            if (player.getUniqueID().equals(playerUUID)) {
	                return player;
	            }
            } else {
            	System.err.println("[CinderCore] Player list having non-players, WHAT?!?!?!");
            }
        }
        return null; // Player not found or not online
    }

	public static World getPlayerWorldByUUID(UUID uuid) {
	    WorldServer[] worlds = MinecraftServer.getServer().worldServers;
	    for (WorldServer world : worlds) {
	    	PlayerManager pm = ((MixinWorldServer)world).getPlayerManager();
	    	List players = ((MixinPlayerManager)pm).getPlayers();
	    	for (Object obj : players) {
	    		EntityPlayerMP player = (EntityPlayerMP)obj;
	    		if (uuid.equals(player.getUniqueID())) {
	    			return world;
	    		}
	    	}
	    }
	    return null;
	}
}
