/*
 * Copyright (c) 2025 Tectonicus contributors.  All rights reserved.
 *
 * This file is part of Tectonicus. It is subject to the license terms in the LICENSE file found in
 * the top-level directory of this distribution.  The full list of project contributors is contained
 * in the AUTHORS file found in the same location.
 *
 */

package tectonicus.raw;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.jnbt.ByteArrayTag;
import org.jnbt.ByteTag;
import org.jnbt.CompoundTag;
import org.jnbt.IntArrayTag;
import org.jnbt.IntTag;
import org.jnbt.ListTag;
import org.jnbt.LongArrayTag;
import org.jnbt.NBTInputStream;
import org.jnbt.NBTInputStream.Compression;
import org.jnbt.ShortTag;
import org.jnbt.StringTag;
import org.jnbt.Tag;
import tectonicus.BlockIds;
import tectonicus.Minecraft;
import tectonicus.WorldStats;
import tectonicus.blockTypes.Banner.Pattern;
import tectonicus.chunk.ChunkCoord;
import tectonicus.chunk.ChunkData;
import tectonicus.util.FileUtils;
import tectonicus.world.Colors;
import tectonicus.world.Effect;
import tectonicus.world.WorldInfo;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;

import static tectonicus.Version.VERSION_13;
import static tectonicus.Version.VERSION_18;
import static tectonicus.Version.VERSION_UNKNOWN;

public class RawChunk {
	public static final int WIDTH = 16;
	public static final int DEPTH = 16;

	public static final int MC_REGION_HEIGHT = 128;

	public static final int SECTION_WIDTH = 16;
	public static final int SECTION_HEIGHT = 16;
	public static final int SECTION_DEPTH = 16;
        
        public static final int SECTION_SIZE = SECTION_WIDTH * SECTION_HEIGHT * SECTION_DEPTH;

	public static final int MAX_LIGHT = 16;

	private final int maxSections;

	private static final ObjectReader OBJECT_READER = FileUtils.getOBJECT_MAPPER().reader();
	private static final ObjectWriter OBJECT_WRITER = FileUtils.getOBJECT_MAPPER().writer();

	private int[][] biomes;
	private int[][][] biomes3d;

	private Section[] sections;

	private int chunkX, chunkZ;
	private int minSectionY;

	private int dataVersion;
	@Getter
	private boolean fullChunk = true;

	private Map<String, SignEntity> signs;
	private Map<String, FlowerPotEntity> flowerPots;
	private Map<String, SkullEntity> skulls;
	private Map<String, BeaconEntity> beacons;
	private Map<String, BannerEntity> banners;
	private Map<String, BedEntity> beds;
        private Map<String, DecoratedPotEntity> decoratedPots;

	private List<PaintingEntity> paintings;
	private List<PaintingEntity> itemFrames;
	private List<ContainerEntity> chests;
        private List<ArmorStandEntity> armorStands;

	private final Map<String, Object> filterData = new HashMap<>();

	public RawChunk() {
		maxSections = Minecraft.getChunkHeight() / SECTION_HEIGHT;
		clear();
	}

	public RawChunk(File file) throws IOException {
		maxSections = Minecraft.getChunkHeight() / SECTION_HEIGHT;
		init(new ChunkData(Files.readAllBytes(file.toPath()), Compression.Gzip), null, new WorldInfo(VERSION_UNKNOWN, false));
	}

	public RawChunk(ChunkData chunkData, WorldStats worldStats, WorldInfo worldInfo) throws IOException {
		maxSections = Minecraft.getChunkHeight() / SECTION_HEIGHT;
		init(chunkData, worldStats, worldInfo);
	}

	public RawChunk(ChunkData chunkData, ChunkData entityChunkData, WorldStats worldStats, WorldInfo worldInfo) throws IOException {
		this(chunkData, worldStats, worldInfo);

		byte[] chunkBytes = entityChunkData.getBytes();
		try (InputStream in = new ByteArrayInputStream(chunkBytes, 0, chunkBytes.length);
			 NBTInputStream nbtIn = new NBTInputStream(in, entityChunkData.getCompressionType())) {
			Tag tag = nbtIn.readTag();
			if (tag instanceof CompoundTag) {
				parseEntities(NbtUtil.getChild((CompoundTag) tag, "Entities", ListTag.class),
						worldInfo.getVersion().getNumVersion() >= VERSION_18.getNumVersion());
			}
		}
	}

	public void setFilterMetadata(String id, Object data) {
		this.filterData.put(id, data);
	}

	public void removeFilterMetadata(String id) {
		this.filterData.remove(id);
	}

	public Object getFilterMetadata(String id) {
		return this.filterData.get(id);
	}

	private void clear() {
		signs = new HashMap<>();
		flowerPots = new HashMap<>();
		skulls = new HashMap<>();
		beacons = new HashMap<>();
		banners = new HashMap<>();
		beds = new HashMap<>();
                decoratedPots = new HashMap<>();

		paintings = new ArrayList<>();
		itemFrames = new ArrayList<>();
		chests = new ArrayList<>();
                armorStands = new ArrayList<>();

		sections = new Section[maxSections];
	}

	private void init(ChunkData chunkData, WorldStats worldStats, WorldInfo worldInfo) throws IOException {
		clear();

		byte [] chunkBytes = chunkData.getBytes();
		try (InputStream in = new ByteArrayInputStream(chunkBytes, 0, chunkBytes.length);
			 NBTInputStream nbtIn = new NBTInputStream(in, chunkData.getCompressionType())) {
			Tag tag = nbtIn.readTag();
			if (tag instanceof CompoundTag) {
				CompoundTag root = (CompoundTag) tag;

				dataVersion = NbtUtil.getInt(root,"DataVersion", 0);

				CompoundTag level = NbtUtil.getChild(root, "Level", CompoundTag.class);
				if (level != null) { //Chunk was generated by Minecraft 1.17 or lower
					loadStatus(level);
					if (!fullChunk){ //Don't do anything else because we are going to ignore this chunk
						return;
					}
					
					parseChunkPosition(level);

					ListTag sectionsTag = NbtUtil.getChild(level, "Sections", ListTag.class);
					if (sectionsTag != null) {
						// Parse as anvil format
						parseAnvilData(level, sectionsTag, worldStats, worldInfo);
					} else {
						// Parse as McRegion format
						parseMcRegionData(level);
					}

					ListTag entitiesTag = NbtUtil.getChild(level, "Entities", ListTag.class);
					if (entitiesTag != null && !entitiesTag.getValue().isEmpty()) {
						parseEntities(entitiesTag, worldInfo.getVersion().getNumVersion() >= VERSION_18.getNumVersion());
					}

					ListTag tileEntitiesTag = NbtUtil.getChild(level, "TileEntities", ListTag.class);
					if (tileEntitiesTag != null) {
						parseBlockEntities(tileEntitiesTag, worldInfo.getVersion().getNumVersion() >= VERSION_18.getNumVersion());
					}
				}
				else { //Chunk was generated by Minecraft 1.18+
					loadStatus(root);
					if (!fullChunk){ //Don't do anything else because we are going to ignore this chunk
						return;
					}
					
					parseChunkPosition(root);

					//yPos is the minimum or lowest section position in the chunk (-4 in 1.18)
					minSectionY = NbtUtil.getInt(root, "yPos", -4);

					ListTag sectionsTag = NbtUtil.getChild(root, "sections", ListTag.class);
					parseAnvilDataNew(root, sectionsTag, worldStats);

					ListTag blockEntitiesTag = NbtUtil.getChild(root, "block_entities", ListTag.class);
					parseBlockEntities(blockEntitiesTag, true);
				}
			}
		}
	}

	private void loadStatus(CompoundTag root) {
		Optional.ofNullable(NbtUtil.getChild(root, "Status", StringTag.class)).ifPresent(statusTag -> {
			String status = statusTag.getValue();
			if (!status.contains("full")) {
				fullChunk = false;
			}
		});
	}
	
	private void parseChunkPosition(CompoundTag tag) {
		chunkX = NbtUtil.getInt(tag, "xPos", 0);
		chunkZ = NbtUtil.getInt(tag, "zPos", 0);
	}

	private void parseEntities(ListTag entitiesTag, boolean is118) {
		for (Tag t : entitiesTag.getValue()) {
			if (t instanceof CompoundTag) {
				CompoundTag entity = (CompoundTag) t;

				StringTag idTag = NbtUtil.getChild(entity, "id", StringTag.class);
				String id = idTag.getValue();
				
                                boolean painting = id.endsWith("Painting") || id.equals("minecraft:painting");
				boolean itemFrame = id.equals("ItemFrame") || id.equals("minecraft:item_frame") || id.equals("minecraft:glow_item_frame");
                                boolean armorStand = id.equals("ArmorStand") || id.equals("minecraft:armor_stand");
                                
				if (painting || itemFrame) {
					ByteTag oldDir = NbtUtil.getChild(entity, "Dir", ByteTag.class);
					ByteTag dir = NbtUtil.getChild(entity, "Direction", ByteTag.class);

					if (oldDir != null && dir == null) {
						dir = oldDir;
					}

					boolean is18 = false;
					if (dir == null) {
						dir = NbtUtil.getChild(entity, "Facing", ByteTag.class); //It's been renamed to 'facing', nbtutil automatically checks for the lowercase version
						is18 = true;
					}

					int direction = dir.getValue();  // Have to reverse 0 and 2 for the old Dir tag
					if (oldDir != null && direction == 0) {
						direction = 2;
					} else if (oldDir != null && direction == 2) {
						direction = 0;
					}
					
					int x = 0;
					int y = 0;
					int z = 0;
					
					IntTag xTag = NbtUtil.getChild(entity, "TileX", IntTag.class);
					IntTag yTag = NbtUtil.getChild(entity, "TileY", IntTag.class);
					IntTag zTag = NbtUtil.getChild(entity, "TileZ", IntTag.class);
					
					if (xTag == null) { //probably 1.21.5+
						int[] blockPos = NbtUtil.getChild(entity, "block_pos", IntArrayTag.class).getValue();
						x = blockPos[0];
						y = blockPos[1];
						z = blockPos[2];
					} else { // 1.21.4 and older
						x = xTag.getValue();
						y = yTag.getValue();
						z = zTag.getValue();
						
						if (is18 && direction == 0) {
							z = zTag.getValue() - 1;
						} else if (is18 && direction == 1) {
							x = xTag.getValue() + 1;
						} else if (is18 && direction == 2) {
							z = zTag.getValue() + 1;
						} else if (is18 && direction == 3) {
							x = xTag.getValue() - 1;
						}
					}

					final int localX = x - (chunkX * WIDTH);
					int localY;
					if (is118) {
						localY = y + Math.abs(minSectionY) * SECTION_HEIGHT;
					} else {
						localY = y;
					}
					final int localZ = z - (chunkZ * DEPTH);


					if (painting) {
						StringTag motiveTag = NbtUtil.getChild(entity, "variant", StringTag.class);
						if (motiveTag == null) {
							motiveTag = NbtUtil.getChild(entity, "Motive", StringTag.class);
						}
						paintings.add(new PaintingEntity(x, y, z, localX, localY, localZ, id, motiveTag.getValue(), direction));
					} else if (itemFrame) {
						String item = "";
						Map<String, Tag> map = entity.getValue();
						CompoundTag itemTag = (CompoundTag) map.get("Item");
						if (itemTag != null) {
							ShortTag itemIdTag = NbtUtil.getChild(itemTag, "id", ShortTag.class);
							if (itemIdTag == null) {
								StringTag stringItemIdTag = NbtUtil.getChild(itemTag, "id", StringTag.class);
								item = stringItemIdTag.getValue();
							} else {
								if (itemIdTag.getValue() == 358)
									item = "minecraft:filled_map";
							}
						}

						itemFrames.add(new PaintingEntity(x, y, z, localX, localY, localZ, id, item, direction));
					}
				} else if (armorStand) {
					ListTag posTag = NbtUtil.getChild(entity, "Pos", ListTag.class);
					ListTag rotationTag = NbtUtil.getChild(entity, "Rotation", ListTag.class);
					ByteTag invisibleTag = NbtUtil.getChild(entity, "Invisible", ByteTag.class);
					ByteTag noBasePlateTag = NbtUtil.getChild(entity, "NoBasePlate", ByteTag.class);
					
					if (posTag != null && rotationTag != null && invisibleTag != null && noBasePlateTag != null) {
						List<Tag> pos = posTag.getValue();
						
						int x = (int) Math.round(Math.floor((double) pos.get(0).getValue()));
						int y = (int) Math.round(Math.floor((double) pos.get(1).getValue()));
						int z = (int) Math.round(Math.floor((double) pos.get(2).getValue()));
						
						final int localX = x - (chunkX * WIDTH);
						int localY;
						if (is118) {
							localY = y + Math.abs(minSectionY) * SECTION_HEIGHT;
						} else {
							localY = y;
						}
						final int localZ = z - (chunkZ * DEPTH);
						
						List<Tag> rotation = rotationTag.getValue();
						float yaw = (float) rotation.get(0).getValue();
						
						boolean invisible = invisibleTag.getValue() == 1;
						boolean noBasePlate = noBasePlateTag.getValue() == 1;
						
						Item feetArmor = null;
						Item legsArmor = null;
						Item chestArmor = null;
						Item headArmor = null;
						
						
						Function<CompoundTag, Item> parseArmorItem = (armorItemTag) -> {
							if (armorItemTag != null) {
								StringTag armorIdTag = NbtUtil.getChild(armorItemTag, "id", StringTag.class);
								
								if (armorIdTag != null) {
									List<Object> components = parseComponentsTag(NbtUtil.getChild(armorItemTag, "components", CompoundTag.class));
									if (components == null) {
										// Maybe the item uses pre 1.20.5 format? Try parsing the "tag" tag
										components = parseTagTag(NbtUtil.getChild(armorItemTag, "tag", CompoundTag.class));
									}
									return new Item(armorIdTag.getValue(), components);
								}
							}
							
							return null;
						};
						
						//1.21.5+
						CompoundTag equipment = NbtUtil.getChild(entity, "equipment", CompoundTag.class);
						//1.21.4 and older
						ListTag armorItemsTag = NbtUtil.getChild(entity, "ArmorItems", ListTag.class);
						
						if (equipment != null) {
							feetArmor = parseArmorItem.apply(NbtUtil.getChild(equipment, "feet", CompoundTag.class));
							legsArmor = parseArmorItem.apply(NbtUtil.getChild(equipment, "legs", CompoundTag.class));
							chestArmor = parseArmorItem.apply(NbtUtil.getChild(equipment, "chest", CompoundTag.class));
							headArmor = parseArmorItem.apply(NbtUtil.getChild(equipment, "head", CompoundTag.class));
						} else if (armorItemsTag != null) {
							feetArmor = parseArmorItem.apply(NbtUtil.getChild(armorItemsTag, 0, CompoundTag.class));
							legsArmor = parseArmorItem.apply(NbtUtil.getChild(armorItemsTag, 1, CompoundTag.class));
							chestArmor = parseArmorItem.apply(NbtUtil.getChild(armorItemsTag, 2, CompoundTag.class));
							headArmor = parseArmorItem.apply(NbtUtil.getChild(armorItemsTag, 3, CompoundTag.class));
						}
						
						armorStands.add(new ArmorStandEntity(x, y, z, localX, localY, localZ,
								yaw, invisible, noBasePlate, feetArmor, legsArmor, chestArmor, headArmor));
					}
				}
			}
		}
	}

	private void parseBlockEntities(ListTag blockEntitiesTag, boolean is118) throws JsonProcessingException {
		for (Tag t : blockEntitiesTag.getValue()) {
			if (t instanceof CompoundTag) {
				CompoundTag entity = (CompoundTag) t;

				StringTag idTag = NbtUtil.getChild(entity, "id", StringTag.class);
				IntTag xTag = NbtUtil.getChild(entity, "x", IntTag.class);
				IntTag yTag = NbtUtil.getChild(entity, "y", IntTag.class);
				IntTag zTag = NbtUtil.getChild(entity, "z", IntTag.class);

				if (idTag != null && xTag != null && yTag != null && zTag != null) {
					final String id = idTag.getValue();

					final int x = xTag.getValue();
					final int y = yTag.getValue();
					final int z = zTag.getValue();

					final int localX = x - (chunkX * WIDTH);
					int localY;
					if (is118) {
						localY = y + Math.abs(minSectionY) * SECTION_HEIGHT;
					} else {
						localY = y;
					}
					final int localZ = z - (chunkZ * DEPTH);

					if (id.equals("Sign") || id.equals("minecraft:sign") || id.equals("minecraft:hanging_sign")) {
						List<String> textLines = new ArrayList<>();
						String color = "black";

                                                CompoundTag frontText = NbtUtil.getChild(entity, "front_text", CompoundTag.class);
                                                CompoundTag backText = NbtUtil.getChild(entity, "back_text", CompoundTag.class);

                                                Consumer<Function<Integer, String>> parseSignText = (getText) -> {
                                                        for (int i = 0; i < 4; i++) {
                                                                String text = getText.apply(i);

                                                                if (!StringUtils.isEmpty(text) && FileUtils.isJSONValid(text))  // 1.9 sign text
                                                                {
                                                                        textLines.add(textFromJSON(text));
                                                                } else if (!StringUtils.isBlank(text)) // 1.8 or older sign text
                                                                {
                                                                        text = text.replaceAll("^\"|\"$", ""); //This removes begin and end double quotes
                                                                        try {
                                                                                textLines.add(OBJECT_WRITER.writeValueAsString(text).replaceAll("^\"|\"$", ""));
                                                                        }
                                                                        catch (JsonProcessingException e) {
                                                                                throw new RuntimeException(e);
                                                                        }
                                                                } else {
                                                                        textLines.add("");
                                                                }
                                                        }
                                                };
                                                
                                                if (frontText == null) {
                                                        // Front and back text not found. This is a pre 1.20 sign. Fall back to old processing.
                                                        parseSignText.accept((i) -> {
                                                                return NbtUtil.getChild(entity, "Text" + (i + 1), StringTag.class).getValue();
                                                        });
                                                        
                                                        StringTag colorTag = NbtUtil.getChild(entity, "Color", StringTag.class);
                                                        if (colorTag != null) {
                                                                color = colorTag.getValue();
                                                        }
                                                } else {
                                                        // Process 1.20 or newer sign
                                                        ListTag frontMessages = NbtUtil.getChild(frontText, "messages", ListTag.class);
                                                        ListTag backMessages = NbtUtil.getChild(backText, "messages", ListTag.class);
                                                        
                                                        parseSignText.accept((i) -> {
                                                                if (NbtUtil.getChild(frontMessages, i, StringTag.class) == null) {
                                                                        return null;
                                                                } else {
                                                                        return NbtUtil.getChild(frontMessages, i, StringTag.class).getValue();
                                                                }
                                                        });
                                                        parseSignText.accept((i) -> {
                                                                return NbtUtil.getChild(backMessages, i, StringTag.class).getValue();
                                                        });
                                                        
                                                        StringTag colorTag = NbtUtil.getChild(frontText, "color", StringTag.class);
                                                        if (colorTag != null) {
                                                                color = colorTag.getValue();
                                                        }
                                                }

						Integer data = null;
						BlockProperties properties = null;
						if (dataVersion >= VERSION_13.getDataVersion()) {
							properties = getBlockState(localX, localY, localZ);
						} else {
							data = getBlockData(localX, localY, localZ);
						}
                                                
						signs.put(createKey(localX, localY, localZ), new SignEntity(x, y, z, localX, localY, localZ,
								textLines.get(0), textLines.get(1), textLines.get(2), textLines.get(3), data, properties, color));
					} else if (id.equals("FlowerPot") || id.equals("minecraft:flower_pot")) {
						IntTag dataTag = NbtUtil.getChild(entity, "Data", IntTag.class);
						IntTag itemTag = NbtUtil.getChild(entity, "Item", IntTag.class);
						final int item;
						if (itemTag == null) {
							StringTag stringIdTag = NbtUtil.getChild(entity, "Item", StringTag.class);
							if (stringIdTag.getValue().equals("minecraft:sapling"))
								item = 6;
							else if (stringIdTag.getValue().equals("minecraft:red_flower"))
								item = 38;
							else
								item = 0;
						} else {
							item = itemTag.getValue();
						}

						flowerPots.put(createKey(localX, localY, localZ), new FlowerPotEntity(x, y, z, localX, localY, localZ, item, dataTag.getValue()));
					} else if (id.equals("Skull") || id.equals("minecraft:skull")) {
						ByteTag skullType = NbtUtil.getChild(entity, "SkullType", ByteTag.class);
						ByteTag rot = NbtUtil.getChild(entity, "Rot", ByteTag.class);

						StringTag nameTag;
						StringTag playerId;
						String name = "";
						String uuid = "";
						String textureURL = "";
						StringTag extraType = NbtUtil.getChild(entity, "ExtraType", StringTag.class);
						CompoundTag profile = NbtUtil.getChild(entity, "profile", CompoundTag.class); //SkullOwner changed to profile in 1.20.5
						if (profile == null) {  //1.16 - 1.20.5
							profile = NbtUtil.getChild(entity, "SkullOwner", CompoundTag.class);
						}
						if (profile == null) { //older than 1.16
							profile = NbtUtil.getChild(entity, "Owner", CompoundTag.class);
						}
						
						if (profile != null) {
							nameTag = NbtUtil.getChild(profile, "Name", StringTag.class);
							if (nameTag != null) {
								name = nameTag.getValue();
							}
							playerId = NbtUtil.getChild(profile, "Id", StringTag.class);
							if (playerId == null) { //1.16
								IntArrayTag idArrayTag = NbtUtil.getChild(profile, "Id", IntArrayTag.class);
								if (idArrayTag != null) {
									int[] integerIdArray = idArrayTag.getValue();
									StringBuilder sb = new StringBuilder();
									for (int integerId : integerIdArray) {
										sb.append(Integer.toHexString(integerId));
									}
									uuid = sb.toString();
								}
							} else {
								uuid = playerId.getValue().replace("-", "");
							}

							// Get skin URL
							CompoundTag properties = NbtUtil.getChild(profile, "Properties", CompoundTag.class);
							ListTag propertiesList = NbtUtil.getChild(profile, "properties", ListTag.class); //NBT structure changed in 1.20.5
							if (properties != null) {
								propertiesList = NbtUtil.getChild(properties, "textures", ListTag.class);
							}
							if(propertiesList != null) {
								CompoundTag textureInfo = NbtUtil.getChild(propertiesList, 0, CompoundTag.class);
								StringTag value = NbtUtil.getChild(textureInfo, "Value", StringTag.class);
								byte[] decoded = Base64.getDecoder().decode(value.getValue());
								JsonNode node = OBJECT_READER.readTree(new String(decoded, StandardCharsets.UTF_8));
								JsonNode skin = node.get("textures").get("SKIN");
								if (skin != null) {
									textureURL = skin.get("url").asText();
								}
							}
						} else if (extraType != null && !(extraType.getValue().isEmpty())) {
							name = uuid = extraType.getValue();
							textureURL = "http://www.minecraft.net/skin/" + extraType.getValue() + ".png";
						}

						if (skullType != null) {
							skulls.put(createKey(localX, localY, localZ), new SkullEntity(x, y, z, localX, localY, localZ, skullType.getValue(), rot.getValue(), name, uuid, textureURL));
						} else { // 1.13+ no longer has SkullType or Rot tags
							skulls.put(createKey(localX, localY, localZ), new SkullEntity(x, y, z, localX, localY, localZ, name, uuid, textureURL));
						}
					} else if (id.equals("Beacon") || id.equals("minecraft:beacon")) {
						IntTag levels = NbtUtil.getChild(entity, "Levels", IntTag.class);
						
						StringTag primaryEffectString = NbtUtil.getChild(entity, "primary_effect", StringTag.class);
						IntTag primaryEffectInt = NbtUtil.getChild(entity, "Primary", IntTag.class);
						Effect primaryEffect = Effect.NONE;
						if (primaryEffectString != null) {
							primaryEffect = Effect.byName(primaryEffectString.getValue().replace("minecraft:", ""));
						} else if(primaryEffectInt != null) {
							primaryEffect = Effect.byId(primaryEffectInt.getValue());
						}
						
						StringTag secondaryEffectString = NbtUtil.getChild(entity, "secondary_effect", StringTag.class);
						IntTag secondaryEffectInt = NbtUtil.getChild(entity, "Secondary", IntTag.class);
						Effect secondaryEffect = Effect.NONE;
						if (secondaryEffectString != null) {
							secondaryEffect = Effect.byName(secondaryEffectString.getValue().replace("minecraft:", ""));
						} else if(secondaryEffectInt != null) {
							int secondary = secondaryEffectInt.getValue();
							secondaryEffect = Effect.byId(secondaryEffectInt.getValue());
						}
						
						beacons.put(createKey(localX, localY, localZ), new BeaconEntity(x, y, z, localX, localY, localZ, levels.getValue(), primaryEffect, secondaryEffect));
					} else if (id.equals("Banner") || id.equals("minecraft:banner")) {
						IntTag base = NbtUtil.getChild(entity, "Base", IntTag.class);
						Integer baseVal = null;
						if (base != null)
							baseVal = 15 - base.getValue();

						ListTag patternList = NbtUtil.getChild(entity, "Patterns", ListTag.class);

						List<Pattern> patterns = new ArrayList<>();

						int numPatterns = 0;
						if (patternList != null)
							numPatterns = patternList.getValue().size();
						if (numPatterns > 0) {
							for (int i = 0; i < numPatterns; i++) {
								CompoundTag p = NbtUtil.getChild(patternList, i, CompoundTag.class);
								StringTag pattern = NbtUtil.getChild(p, "Pattern", StringTag.class);
								IntTag colorInt = NbtUtil.getChild(p, "Color", IntTag.class);
								StringTag colorStr = NbtUtil.getChild(p, "color", StringTag.class); //1.20.5 switched color to String tag
								Colors color = Colors.WHITE;
								if (colorStr != null) { //1.20.5+
									color = Colors.byName(colorStr.getValue());
								} else if (colorInt != null) {
									if (baseVal == null) { //1.13 - 1.20.4
										color = Colors.byId(colorInt.getValue());
									} else { //1.8 - 1.12
										color = Colors.byId(15-colorInt.getValue());
									}
								}
								
								patterns.add(new Pattern(pattern.getValue(), color));
							}
						}
						banners.put(createKey(localX, localY, localZ), new BannerEntity(x, y, z, localX, localY, localZ, baseVal, patterns));
					} else if (id.equals("Chest") || id.equals("minecraft:chest") || id.equals("minecraft:shulker_box") || id.equals("minecraft:barrel")) {
                                                BlockProperties blockState = getBlockState(localX, localY, localZ);
                                                String facing = null;
                                                String type = null;
                                                if (blockState!=null) {
                                                        facing = blockState.get("facing");
                                                        type = blockState.get("type");
                                                }
                                            
						final StringTag customName = NbtUtil.getChild(entity, "CustomName", StringTag.class);
						String name;
						if (customName == null) {
                                                        switch (id) {
                                                                case "minecraft:barrel":
                                                                        name = "Barrel";
                                                                        break;
                                                                case "minecraft:shulker_box":
                                                                        name = "Shulker Box";
                                                                        break;
                                                                default:
                                                                        name = "left".equals(type) || "right".equals(type) ? "Large Chest" : "Chest";;
                                                                        break;
                                                        }
                                                } else {
                                                        name = customName.getValue();
                                                        name = name.replaceAll("\"", ""); // Replace " characters
                                                }

						final StringTag lock = NbtUtil.getChild(entity, "Lock", StringTag.class);
						String lockStr = "";
						if (lock != null)
							lockStr = lock.getValue();

						final StringTag lootTable = NbtUtil.getChild(entity, "LootTable", StringTag.class);
						boolean unopenedChest = lootTable != null;
                                                
                                                final List<Item> items = new ArrayList<>();
                                                final ListTag itemsTag = NbtUtil.getChild(entity, "Items", ListTag.class);
                                                if (itemsTag != null) {
                                                        for (var i : itemsTag.getValue()) {
                                                                if (i instanceof CompoundTag)
                                                                {
                                                                        final CompoundTag itemTag = (CompoundTag)i;

                                                                        final StringTag itemIdTag = NbtUtil.getChild(itemTag, "id", StringTag.class);
                                                                        final ByteTag itemSlotTag = NbtUtil.getChild(itemTag, "Slot", ByteTag.class);
                                                                        
                                                                        if (itemIdTag != null && itemSlotTag != null)
                                                                        {
                                                                                final ByteTag itemCountByteTag = NbtUtil.getChild(itemTag, "Count", ByteTag.class);
                                                                                final IntTag itemCountIntTag = NbtUtil.getChild(itemTag, "Count", IntTag.class); //1.20.5 changed to using IntTag for item count
                                                                                int itemCount = 0;
                                                                                if (itemCountIntTag != null) {
                                                                                        itemCount = itemCountIntTag.getValue();
                                                                                } else if (itemCountByteTag != null) {
                                                                                        itemCount = itemCountByteTag.getValue();
