/*
 * 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.util;

import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import tectonicus.Block;
import tectonicus.BlockIds;
import tectonicus.BlockTypeRegistry;
import tectonicus.BuildInfo;
import tectonicus.ItemRenderer;
import tectonicus.MemoryMonitor;
import tectonicus.PlayerIconAssembler;
import tectonicus.Portal;
import tectonicus.TileRenderer;
import tectonicus.Version;
import tectonicus.blockregistry.BlockRegistry;
import tectonicus.cache.swap.HddObjectListReader;
import tectonicus.configuration.Configuration;
import tectonicus.configuration.Dimension;
import tectonicus.configuration.ImageFormat;
import tectonicus.configuration.Layer;
import tectonicus.configuration.filter.PlayerFilter;
import tectonicus.configuration.filter.SignFilterType;
import tectonicus.itemmodeldefinitionregistry.ItemModelDefinition;
import tectonicus.itemmodeldefinitionregistry.ItemModelDefinitionRegistry;
import tectonicus.itemregistry.ItemModel;
import tectonicus.itemregistry.ItemRegistry;
import tectonicus.rasteriser.Rasteriser;
import tectonicus.raw.ArmorTrimTag;
import tectonicus.raw.BeaconEntity;
import tectonicus.raw.BedEntity;
import tectonicus.raw.BiomesOld;
import tectonicus.raw.BlockProperties;
import tectonicus.raw.ContainerEntity;
import tectonicus.raw.CustomNameTag;
import tectonicus.raw.DyedColorTag;
import tectonicus.raw.EnchantmentTag;
import tectonicus.raw.EnchantmentsTag;
import tectonicus.raw.Item;
import tectonicus.raw.Player;
import tectonicus.raw.PotionContentsTag;
import tectonicus.raw.StoredEnchantmentsTag;
import tectonicus.texture.TexturePack;
import tectonicus.world.Colors;
import tectonicus.world.Sign;
import tectonicus.world.World;
import tectonicus.world.subset.WorldSubset;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static tectonicus.Version.VERSION_12;
import static tectonicus.Version.VERSION_13;
import static tectonicus.Version.VERSION_16;

@Slf4j
@UtilityClass
public class OutputResourcesUtil {
	public static void outputSigns(File outputFile, File signListFile, tectonicus.configuration.Map map) {
		HddObjectListReader<Sign> signsIn = null;
		try {
			signsIn = new HddObjectListReader<>(signListFile);
			outputSigns(outputFile, signsIn, map);
		} catch (Exception e) {
			log.error("Exception: ", e);
		} finally {
			if (signsIn != null)
				signsIn.close();
		}
	}

	private static void outputSigns(File signFile, HddObjectListReader<Sign> signs, tectonicus.configuration.Map map) throws IOException {
		log.info("Exporting signs to {}", signFile.getAbsolutePath());

		Files.deleteIfExists(signFile.toPath());

		try (JsArrayWriter jsWriter = new JsArrayWriter(signFile, map.getId() + "_signData")) {

			WorldSubset worldSubset = map.getWorldSubset();
			Sign sign = new Sign();
			while (signs.hasNext()) {
				signs.read(sign);
				String message = "\"" + sign.getText(0) + "/n" + sign.getText(1) + "/n" + sign.getText(2) + "/n" + sign.getText(3) + "\"";
				if (map.getSignFilter().getType() == SignFilterType.OBEY)
					message = "\"/nOBEY/n/n\"";

				Map<String, String> signArgs = new HashMap<>();

				final float worldX = sign.getX() + 0.5f;
				final float worldY = sign.getY();
				final float worldZ = sign.getZ() + 0.5f;

				String posStr = "new WorldCoord(" + worldX + ", " + worldY + ", " + worldZ + ")";
				signArgs.put("worldPos", posStr);
				signArgs.put("message", message);
				if (map.getSignFilter().getType() == SignFilterType.OBEY) {
					signArgs.put("text1", "\"\"");
					signArgs.put("text2", "\"OBEY\"");
					signArgs.put("text3", "\"\"");
					signArgs.put("text4", "\"\"");
				} else {
					signArgs.put("text1", "\"" + sign.getText(0) + "\"");
					signArgs.put("text2", "\"" + sign.getText(1) + "\"");
					signArgs.put("text3", "\"" + sign.getText(2) + "\"");
					signArgs.put("text4", "\"" + sign.getText(3) + "\"");
				}

				if (worldSubset.containsBlock(sign.getX(), sign.getZ())) {
					jsWriter.write(signArgs);
				}
			}
		} catch (Exception e) {
			log.error("Exception: ", e);
		}
	}

	public static void outputPlayers(File playersFile, File imagesDir, tectonicus.configuration.Map map, List<Player> players, PlayerIconAssembler playerIconAssembler) {
		try {
			Files.deleteIfExists(playersFile.toPath());
		} catch (IOException e) {
			log.error("Exception: ", e);
		}

		FileUtils.ensureExists(imagesDir);

		log.info("Exporting players to {}", playersFile.getAbsolutePath());

		int numOutput = 0;
		ExecutorService executor = Executors.newCachedThreadPool();
		try (JsArrayWriter jsWriter = new JsArrayWriter(playersFile, map.getId() + "_playerData")) {

			PlayerFilter playerFilter = map.getPlayerFilter();
			WorldSubset worldSubset = map.getWorldSubset();
			for (Player player : players) {
				if (playerFilter.passesFilter(player)) {
					Vector3d position = player.getPosition();
					if (worldSubset.containsBlock(position.x, position.z)) {
						log.debug("\texporting {}", player.getName());

						Map<String, String> args = new HashMap<>();

						Vector3d pos = player.getPosition();
						args.put("name", "\"" + player.getName() + "\"");

						String posStr = "new WorldCoord(" + pos.x + ", " + pos.y + ", " + pos.z + ")";
						args.put("worldPos", posStr);

						args.put("health", "" + player.getHealth());
						args.put("food", "" + player.getFood());
						args.put("air", "" + player.getAir());

						args.put("xpLevel", "" + player.getXpLevel());
						args.put("xpTotal", "" + player.getXpTotal());

						jsWriter.write(args);

						File iconFile = new File(imagesDir, player.getName() + ".png");
						PlayerIconAssembler.WriteIconTask task = playerIconAssembler.new WriteIconTask(player, iconFile);
						executor.submit(task);

						numOutput++;
					}
				}
			}
		} catch (Exception e) {
			log.error("Exception: ", e);
		} finally {
			executor.shutdown();
		}
		log.debug("Exported {} players", numOutput);
	}

	public static void outputBeds(File exportDir, tectonicus.configuration.Map map, List<Player> players, Queue<BedEntity> beds) {
		File bedsFile = new File(exportDir, "beds.js");
		try {
			Files.deleteIfExists(bedsFile.toPath());
		} catch (IOException e) {
			log.error("Exception: ", e);
		}

		log.info("Exporting beds to {}", bedsFile.getAbsolutePath());

		int numOutput = 0;

		try (JsArrayWriter jsWriter = new JsArrayWriter(bedsFile, map.getId() + "_bedData")) {

			if (map.getDimension() == Dimension.OVERWORLD) // Beds only exist in the overworld dimension
			{
				WorldSubset worldSubset = map.getWorldSubset();
				PlayerFilter filter = map.getPlayerFilter();
				for (Player player : players) {
					if (filter.isShowBeds() && filter.passesFilter(player) && player.getSpawnDimension() == Dimension.OVERWORLD && player.getSpawnPosition() != null) {
						Map<String, String> bedArgs = new HashMap<>();

						Vector3l spawn = player.getSpawnPosition();

						if (worldSubset.containsBlock(spawn.x, spawn.z)) {
							log.debug("\texporting {}'s bed", player.getName());

							bedArgs.put("playerName", "\"" + player.getName() + "\"");

							String posStr = "new WorldCoord(" + spawn.x + ", " + spawn.y + ", " + spawn.z + ")";
							bedArgs.put("worldPos", posStr);
							
							if (!beds.isEmpty()) {
								for (BedEntity bed : beds) {
									if (bed.getX() == spawn.x && bed.getY() == spawn.y && bed.getZ() == spawn.z) {
										bedArgs.put("color", "\"" + Colors.byId(bed.getColor()).getName() + "\"");
									}
								}
							} else {
								bedArgs.put("color", "\"red\"");
							}

							jsWriter.write(bedArgs);
							numOutput++;
						}
					}
				}
			}
		} catch (Exception e) {
			log.error("Exception: ", e);
		}

		log.debug("Exported {} beds", numOutput);
	}

	public static void outputRespawnAnchors(File exportDir, tectonicus.configuration.Map map, List<Player> players) {
		File anchorsFile = new File(exportDir, "respawnAnchors.js");
		try {
			Files.deleteIfExists(anchorsFile.toPath());
		} catch (IOException e) {
			log.error("Exception: ", e);
		}

		log.info("Exporting respawn anchors to {}", anchorsFile.getAbsolutePath());

		int numOutput = 0;

		try (JsArrayWriter jsWriter = new JsArrayWriter(anchorsFile, map.getId() + "_respawnAnchorData")) {

			if (map.getDimension() == Dimension.NETHER) // Respawn anchors only work in the nether dimension
			{
				WorldSubset worldSubset = map.getWorldSubset();
				PlayerFilter filter = map.getPlayerFilter();
				for (Player player : players) {
					if (filter.isShowRespawnAnchors() && filter.passesFilter(player) && player.getSpawnDimension() == Dimension.NETHER && player.getSpawnPosition() != null) {
						Map<String, String> anchorArgs = new HashMap<>();

						Vector3l spawn = player.getSpawnPosition();

						if (worldSubset.containsBlock(spawn.x, spawn.z)) {
							log.debug("\texporting {}'s respawn anchor", player.getName());

							anchorArgs.put("playerName", "\"" + player.getName() + "\"");

							String posStr = "new WorldCoord(" + spawn.x + ", " + spawn.y + ", " + spawn.z + ")";
							anchorArgs.put("worldPos", posStr);


							jsWriter.write(anchorArgs);
							numOutput++;
						}
					}
				}
			}
		} catch (Exception e) {
			log.error("Exception: ", e);
		}

		log.debug("Exported {} respawn anchors", numOutput);
	}

	public static List<Portal> outputPortals(File outFile, File portalListFile, tectonicus.configuration.Map map) {
		List<Portal> portals = new ArrayList<>();

		try {
			HddObjectListReader<Portal> portalsIn = new HddObjectListReader<>(portalListFile);
			portals = outputPortals(outFile, portalsIn, map);
			portalsIn.close();
		} catch (Exception e) {
			log.error("Exception: ", e);
		}

		return portals;
	}

	private static List<Portal> outputPortals(File portalFile, HddObjectListReader<Portal> portalPositions, tectonicus.configuration.Map map) throws IOException {
		log.info("Exporting portals...");

		Files.deleteIfExists(portalFile.toPath());

		List<Portal> portals = new ArrayList<>();
		try (JsArrayWriter jsWriter = new JsArrayWriter(portalFile, map.getId() + "_portalData")) {
			if (portalPositions.hasNext()) {
				long prevX;
				long prevY;
				long prevZ;
				long firstX;
				long firstZ;

				Portal portal = new Portal();
				portalPositions.read(portal);
				firstX = portal.getX();
				firstZ = portal.getZ();
				prevX = portal.getX();
				prevY = portal.getY();
				prevZ = portal.getZ();

				while (portalPositions.hasNext()) {
					portalPositions.read(portal);

					//Find the horizontal center portal block location
					if ((portal.getX() == prevX && portal.getZ() == prevZ + 1) || (portal.getX() == prevX + 1 && portal.getZ() == prevZ)) {
						prevX = portal.getX();
						prevY = portal.getY();
						prevZ = portal.getZ();
					} else {
						portals.add(new Portal(prevX + (firstX - prevX) / 2, prevY, prevZ + (firstZ - prevZ) / 2));
						prevX = portal.getX();
						prevY = portal.getY();
						prevZ = portal.getZ();
						firstX = portal.getX();
						firstZ = portal.getZ();
					}
				}
				portals.add(new Portal(portal.getX() + ((firstX - prevX) / 2), portal.getY(), portal.getZ() + (firstZ - prevZ) / 2));

				WorldSubset worldSubset = map.getWorldSubset();
				for (Portal p : portals) {
					final float worldX = p.getX();
					final float worldY = p.getY();
					final float worldZ = p.getZ();

					Map<String, String> portalArgs = new HashMap<>();
					String posStr = "new WorldCoord(" + worldX + ", " + worldY + ", " + worldZ + ")";
					portalArgs.put("worldPos", posStr);

					if (worldSubset.containsBlock(p.getX(), p.getZ())) {
						jsWriter.write(portalArgs);
					}
				}
			}
		} catch (Exception e) {
			log.error("Exception: ", e);
		}

		log.debug("Exported {} portals", portals.size());
		return portals;
	}

	public static void outputViews(File outputFile, File viewsListFile, tectonicus.configuration.Map map) {
		HddObjectListReader<Sign> viewsIn = null;
		try {
			viewsIn = new HddObjectListReader<>(viewsListFile);
			outputViews(outputFile, viewsIn, map);
		} catch (Exception e) {
			log.error("Exception: ", e);
		} finally {
			if (viewsIn != null)
				viewsIn.close();
		}
	}

	private static void outputViews(File viewsFile, HddObjectListReader<Sign> views, tectonicus.configuration.Map map) throws IOException {
		log.info("Exporting views...");

		Files.deleteIfExists(viewsFile.toPath());

		try (JsArrayWriter jsWriter = new JsArrayWriter(viewsFile, map.getId() + "_viewData")) {
			Sign sign = new Sign();
			while (views.hasNext()) {
				views.read(sign);

				Map<String, String> viewArgs = new HashMap<>();

				final float worldX = sign.getX() + 0.5f;
				final float worldY = sign.getY();
				final float worldZ = sign.getZ() + 0.5f;

				String posStr = "new WorldCoord(" + worldX + ", " + worldY + ", " + worldZ + ")";
				viewArgs.put("worldPos", posStr);

				StringBuilder text = new StringBuilder();
				for (int i = 0; i < 4; i++) {
					if (!sign.getText(i).startsWith("#")) {
						text.append(sign.getText(i)).append(" ");
					}
				}

				viewArgs.put("text", "\"" + text.toString().trim() + "\"");

				ImageFormat imageFormat = map.getViewConfig().getImageFormat();
				String filename = map.getId() + "/Views/View_" + sign.getX() + "_" + sign.getY() + "_" + sign.getZ() + "." + imageFormat.getExtension();
				viewArgs.put("imageFile", "\"" + filename + "\"");

				if (map.getWorldSubset().containsBlock(sign.getX(), sign.getZ())) {
					jsWriter.write(viewArgs);
				}
			}
		} catch (Exception e) {
			log.error("Exception: ", e);
		}
	}

	public static void outputChests(File chestFile, tectonicus.configuration.Map map, ConcurrentLinkedQueue<ContainerEntity> chestList) {
		log.info("Exporting chests to {}", chestFile.getAbsolutePath());

		try {
			Files.deleteIfExists(chestFile.toPath());
		} catch (IOException e) {
			log.error("Exception: ", e);
		}

		try (JsArrayWriter jsWriter = new JsArrayWriter(chestFile, map.getId() + "_chestData")) {
			WorldSubset worldSubset = map.getWorldSubset();
			for (ContainerEntity entity : chestList) {
                                if ("left".equals(entity.getType())) {
                                        // Skip left part of large chests (we will merge them with right part and display them as one sigle large chest)
                                        continue;
                                }
                            
				float worldX = entity.getX() + 0.5f;
				float worldY = entity.getY();
				float worldZ = entity.getZ() + 0.5f;
				Map<String, String> chestArgs = new HashMap<>();

                                String items = "[\r\n";
                                for (var item : entity.getItems()) {
                                        items += outputItem(item, false);
                                }
                                if ("right".equals(entity.getType())) {
                                        // Find right part and add its items
                                        int leftX = entity.getX();
                                        int leftY = entity.getY();
                                        int leftZ = entity.getZ();

                                        switch (entity.getFacing()) {
                                                case "east":
                                                        leftZ -= 1;
                                                        break;
                                                case "north":
                                                        leftX -= 1;
                                                        break;
                                                case "south":
                                                        leftX += 1;
                                                        break;
                                                case "west":
                                                        leftZ += 1;
                                                        break;
                                        }
                                        
                                        for (ContainerEntity left : chestList) {
                                                if (left.getX()!=leftX || left.getY()!=leftY || left.getZ()!=leftZ || !"left".equals(left.getType())) {
                                                     continue;
                                                }
                                                for (var item : left.getItems()) {
                                                    items += outputItem(item, true);
                                                }
                                                break;
                                        }
                                }
                                items += "\t\t]";
                                
				chestArgs.put("worldPos", "new WorldCoord(" + worldX + ", " + worldY + ", " + worldZ + ")");
                                chestArgs.put("name", "\"" + entity.getCustomName() + "\"");
                                chestArgs.put("items", items);
                                if ("right".equals(entity.getType())) {
                                        chestArgs.put("large", "true");
                                }

				if (worldSubset.containsBlock(entity.getX(), entity.getZ())) {
					jsWriter.write(chestArgs);
				}
			}
		} catch (Exception e) {
			log.error("Exception: ", e);
		}
	}
	
	public static void outputBeacons(File beaconFile, tectonicus.configuration.Map map, Queue<BeaconEntity> beacons) {
		log.info("Exporting beacons to {}", beaconFile.getAbsolutePath());
		
		try {
			Files.deleteIfExists(beaconFile.toPath());
		} catch (IOException e) {
			log.error("Exception: ", e);
		}
		
		try (JsArrayWriter jsWriter = new JsArrayWriter(beaconFile, map.getId() + "_beaconData")) {
			WorldSubset worldSubset = map.getWorldSubset();
			for (BeaconEntity beacon : beacons) {
				float worldX = beacon.getX() + 0.5f;
				float worldY = beacon.getY();
				float worldZ = beacon.getZ() + 0.5f;
				
				Map<String, String> beaconArgs = new HashMap<>();
				beaconArgs.put("worldPos", "new WorldCoord(" + worldX + ", " + worldY + ", " + worldZ + ")");
				beaconArgs.put("levels", Integer.toString(beacon.getLevels()));
				beaconArgs.put("primaryEffect", "\"" + beacon.getPrimaryEffect().name().toLowerCase() + "\"");
				beaconArgs.put("secondaryEffect", "\"" + beacon.getSecondaryEffect().name().toLowerCase() + "\"");
				
				if (worldSubset.containsBlock(beacon.getX(), beacon.getZ())) {
					jsWriter.write(beaconArgs);
				}
			}
		} catch (Exception e) {
			log.error("Exception: ", e);
		}
	}
        
        private static String outputItem(Item item, Boolean isLeft) {
                String result = "\t\t\t{ id: \"" + item.id + "\", ";

                CustomNameTag customNameTag = item.getComponent(CustomNameTag.class);
                if (customNameTag != null) {
                        if (customNameTag.name != null) {
                                result += "customName: \"" + customNameTag.name + "\", ";
                        }
                }

                DyedColorTag dyedColorTag = item.getComponent(DyedColorTag.class);
                if (dyedColorTag != null) {
                        result += "color: " + dyedColorTag.color + ", ";
                }
                
                int slot = item.slot;
                slot += isLeft ? 3 * 9 : 0;
                result += "count: " + item.count + ", slot: " + slot + ", ";
                
                PotionContentsTag potionContents = item.getComponent(PotionContentsTag.class);
                if (potionContents != null && potionContents.potion != null) {
                        result += "components: { potionContents: { potion: \"" + potionContents.potion + "\" } }, ";
                }
                
                ArmorTrimTag trimTag = item.getComponent(ArmorTrimTag.class);
                if (trimTag != null) {
                        result += "trim: { pattern: \"" + trimTag.pattern + "\", material: \"" + trimTag.material + "\" }, ";
                }
                
                List<EnchantmentTag> enchantments = null;
                
                EnchantmentsTag enchantmentsTag = item.getComponent(EnchantmentsTag.class);
                if (enchantmentsTag != null) {
                        enchantments = enchantmentsTag.enchantments;
                }
                StoredEnchantmentsTag storedEnchantmentsTag = item.getComponent(StoredEnchantmentsTag.class);
                if (storedEnchantmentsTag != null) {
                        enchantments = storedEnchantmentsTag.enchantments;
                }
                
                if (enchantments != null) {
                        result += "enchantments: [";
                        for (var enchantment : enchantments) {
                                result += "{ id: \"" + enchantment.id + "\", level: " + enchantment.level.toString() + " }, ";
                        }
                        result += "], ";
                }

                result += "},\r\n";
                
                return result;
        }

	public static void outputInventoryItemIcons(Configuration args, Rasteriser rasteriser, TexturePack texturePack, BlockTypeRegistry blockTypeRegistry, BlockRegistry blockRegistry, ItemRegistry itemRegistry, ItemModelDefinitionRegistry itemModelDefinitionRegistry) {
		log.info("Rendering icons for inventory items");
		if (texturePack.getVersion().getNumVersion() <= VERSION_12.getNumVersion()) { //A hack to skip rendering icons if the resource pack is for 1.12 or older
			log.info("Skipping icon rendering. Chest items are supported for 1.13 and newer.");
			return;
		}
                
		try {
			ItemRenderer itemRenderer = new ItemRenderer(rasteriser);
			File itemIconDir = new File(args.getOutputDir(), "Images/Items/");
			Files.createDirectories(itemIconDir.toPath());
			
                        for (Map.Entry<String, ItemModelDefinition> entry : itemModelDefinitionRegistry.getModelDefinitions().entrySet()) {
                                final String entryKey = entry.getKey();
				final ItemModelDefinition itemModelDefinition = entry.getValue();
                                File outFile = new File(itemIconDir, entryKey + ".png");

                                System.out.print("\tRendering icon for: " + entryKey + "                    \r"); //prints a carriage return after line
                                log.trace("\tRendering icon for: " + entryKey);

                                String modelName = itemModelDefinition.getModelName();
                                
                                if (modelName == null) {
                                    continue;
                                }
                                
                                if (modelName.startsWith("minecraft:block")) {
                                        itemRenderer.renderInventoryBlockModel(outFile, blockRegistry, texturePack, modelName);
                                }
                                else if (modelName.startsWith("minecraft:item")) {
                                        // We should get entry from itemRegistry and render 2d texture item.
                                        
                                        // We do not need to do anything though, since all items from itemRegistry are iterated through and rendered below.
                                        // This also ensures compatibility with pre-1.21.4 versions
                                }
                        }
                        
			for (Map.Entry<String, ItemModel> entry : itemRegistry.getModels().entrySet()) {
				final String entryKey = entry.getKey();
				final ItemModel itemModel = entry.getValue();
				final ItemModel ultimatePredecessorModel = itemRegistry.findUltimatePredecessor(itemModel);
				File outFile = new File(itemIconDir, entryKey + ".png");
				
				System.out.print("\tRendering icon for: " + entryKey + "                    \r"); //prints a carriage return after line
				log.trace("\tRendering icon for: " + entryKey);
				String modelName = ultimatePredecessorModel.getParent();
				
				if (entryKey.endsWith("_bed")) {
					modelName = "minecraft:" + entryKey;
					itemRenderer.renderBed(outFile, blockTypeRegistry, texturePack, modelName);
					continue;
				}
				
				if (modelName == null) {
					//TODO: multiple models now no longer have parent models because of the new item model definitions
					//System.out.println("Models with no parent: " + entryKey);
					// Do not crash for blocks without parent (Air)
					continue;
				}
				
				// Some items need special handling (shulker boxes, mob heads, banners, etc.)
				if (modelName.endsWith("builtin/entity")) {  //TODO: this doesn't appear to exist anymore with 1.21.4
					modelName = "minecraft:" + entryKey;
					List<Map<String, ArrayList<Float>>> transforms = itemRegistry.getTransformsList(itemModel);
					itemRenderer.renderItem(outFile, blockTypeRegistry, texturePack, modelName, transforms);
					continue;
				}
                                
                                // Items that are just 2d textures
                                if (modelName.endsWith("builtin/generated")) {
                                        final Map<String, String> textures = itemModel.getTextures();
                                        if (textures != null) {
                                                BufferedImage composited = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
                                                for (var layer : textures.entrySet()) {
                                                        if (!layer.getKey().startsWith("layer")) {
                                                                // Ignore particles
                                                                continue;
                                                        }
                                                        final String[] layerTexture = layer.getValue().split(":");
                                                        final String namespace = layerTexture.length == 1 ? "minecraft" : layerTexture[0];
                                                        final String textureId = layerTexture.length == 1 ? layerTexture[0] : layerTexture[1];
                                                        
                                                        BufferedImage texture;
                                                        
                                                        if (!layer.getKey().equals("layer0") && textureId.contains("trim")) {
                                                                String[] trimParts = textureId.split("_trim_");
                                                                
                                                                String trim = "assets/" + namespace + "/textures/" + trimParts[0] + "_trim.png";
                                                                String palette = "assets/" + namespace + "/textures/trims/color_palettes/" + trimParts[1] + ".png";
                                                                String keyPalette = "assets/" + namespace + "/textures/trims/color_palettes/trim_palette.png";
                                                                
                                                                texture = texturePack.loadPalettedTexture(trim, palette, keyPalette);
                                                        } else {
                                                                texture = texturePack.loadTexture("assets/" + namespace + "/textures/" + textureId + ".png");
                                                        }
                                                        
                                                        var block = blockRegistry.getBlockModels().getIfPresent(layer.getValue());
                                                        if (block != null) {
                                                                if (block.getElements().get(0).getFaces().values().iterator().next().isTinted()) {
                                                                        Colour4f tintColor = texturePack.getFoliageColor(BiomesOld.FOREST);
                                                                        for (int y=0; y<texture.getHeight(); y++) {
                                                                                for (int x=0; x<texture.getWidth(); x++) {
                                                                                        Colour4f pixel = new Colour4f(texture.getRGB(x, y));
                                                                                        pixel.multiply(tintColor);
                                                                                        texture.setRGB(x, y, pixel.toArgb());
                                                                                }
                                                                        }
                                                                }
                                                        }
                                                              
                                                        if (layer.getKey().equals("layer0") &&
                                                                (
                                                                    textureId.contains("leather_") ||
                                                                    textureId.contains("potion") ||
                                                                    textureId.contains("tipped_arrow")
                                                                )) {
                                                                // Split the leather armor, potion and tipped arrow icons into base layer and overlay
                                                                // so that the base layer can be coloured in CSS due to the colour not being known at this time.
                                                                writeImage(texture, 16, 16, outFile);
                                                                outFile = new File(args.getOutputDir(), "Images/Items/" + entryKey + "_overlay.png");
                                                        } else {
                                                                composited.getGraphics().drawImage(texture, 0, 0, null);
                                                        }
                                                }
                                                writeImage(composited, 16, 16, outFile);
                                                continue;
                                        }
                                }
                                
                                // Inventory block models are not loaded in the registry because they do not have a block state. Let's load them manually
                                if (modelName.contains("_inventory")) {
                                        var model = blockRegistry.loadModel(modelName, "", new HashMap<>(), null);
                                        itemRenderer.renderInventoryBlockModel(outFile, blockRegistry, texturePack, model);
                                        continue;
                                }
                                
