/*
 * Copyright (c) 2016-2017, Adam <Adam@sigterm.info>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package net.runelite.cache;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.imageio.ImageIO;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import net.runelite.cache.definitions.AreaDefinition;
import net.runelite.cache.definitions.FontDefinition;
import net.runelite.cache.definitions.ObjectDefinition;
import net.runelite.cache.definitions.OverlayDefinition;
import net.runelite.cache.definitions.SpriteDefinition;
import net.runelite.cache.definitions.UnderlayDefinition;
import net.runelite.cache.definitions.WorldMapElementDefinition;
import net.runelite.cache.definitions.loaders.OverlayLoader;
import net.runelite.cache.definitions.loaders.SpriteLoader;
import net.runelite.cache.definitions.loaders.UnderlayLoader;
import net.runelite.cache.fs.Archive;
import net.runelite.cache.fs.ArchiveFiles;
import net.runelite.cache.fs.FSFile;
import net.runelite.cache.fs.Index;
import net.runelite.cache.fs.Storage;
import net.runelite.cache.fs.Store;
import net.runelite.cache.item.RSTextureProvider;
import net.runelite.cache.models.JagexColor;
import net.runelite.cache.region.Location;
import net.runelite.cache.region.Position;
import net.runelite.cache.region.Region;
import net.runelite.cache.region.RegionLoader;
import net.runelite.cache.util.BigBufferedImage;
import net.runelite.cache.util.KeyProvider;
import net.runelite.cache.util.XteaKeyManager;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import com.google.gson.Gson;

@Slf4j
@Accessors(chain = true)
public class MapImageDumper
{
    private static String outputDirectory;
    private static final int MAP_SCALE = 4; // this squared is the number of pixels per map square
    private static final int BLEND = 5; // number of surrounding tiles for ground blending

    private static int[] colorPalette = JagexColor.createPalette(JagexColor.BRIGHTNESS_MIN);

    private static int[][] TILE_SHAPE_2D = new int[][]{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1}, {1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0}, {0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1}, {0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0}, {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1}, {1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1}};
    private static int[][] TILE_ROTATION_2D = new int[][]{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, {12, 8, 4, 0, 13, 9, 5, 1, 14, 10, 6, 2, 15, 11, 7, 3}, {15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}, {3, 7, 11, 15, 2, 6, 10, 14, 1, 5, 9, 13, 0, 4, 8, 12}};

    private final int wallColor = (238 + (int) (random() * 20.0D) - 10 << 16) + (238 + (int) (random() * 20.0D) - 10 << 8) + (238 + (int) (random() * 20.0D) - 10);
    private final int doorColor = 238 + (int) (random() * 20.0D) - 10 << 16;

    private final Store store;

    private final Map<Integer, UnderlayDefinition> underlays = new HashMap<>();
    private final Map<Integer, OverlayDefinition> overlays = new HashMap<>();
    private SpriteDefinition[] mapDecorations;

    private final RegionLoader regionLoader;
    private final AreaManager areas;
    private final SpriteManager sprites;
    private final FontManager fonts;
    private final WorldMapManager worldMapManager;
    private RSTextureProvider rsTextureProvider;
    private final ObjectManager objectManager;

    @Getter
    @Setter
    private boolean labelRegions;

    @Getter
    @Setter
    private boolean outlineRegions;

    @Getter
    @Setter
    private boolean renderMap = true;

    @Getter
    @Setter
    private boolean renderObjects = true;

    @Getter
    @Setter
    private boolean renderIcons = true;

    @Getter
    @Setter
    private boolean renderLabels = true;

    @Getter
    @Setter
    private boolean transparency = true;

    @Getter
    @Setter
    private boolean lowMemory = false;

    public MapImageDumper(Store store, KeyProvider keyProvider)
    {
        this(store, new RegionLoader(store, keyProvider));
    }

    public MapImageDumper(Store store, RegionLoader regionLoader)
    {
        this.store = store;
        this.regionLoader = regionLoader;
        this.areas = new AreaManager(store);
        this.sprites = new SpriteManager(store);
        this.fonts = new FontManager(store);
        this.worldMapManager = new WorldMapManager(store);
        this.objectManager = new ObjectManager(store);
    }

    public static void main(String[] args) throws IOException
    {
        Options options = new Options();
        options.addOption(Option.builder().longOpt("cachedir").hasArg().required().build());
        options.addOption(Option.builder().longOpt("xteapath").hasArg().required().build());
        options.addOption(Option.builder().longOpt("outputdir").hasArg().required().build());

        CommandLineParser parser = new DefaultParser();
        CommandLine cmd;
        try
            {
                cmd = parser.parse(options, args);
            }
        catch (ParseException ex)
            {
                System.err.println("Error parsing command line options: " + ex.getMessage());
                System.exit(-1);
                return;
            }

        final String cacheDirectory = cmd.getOptionValue("cachedir");
        final String xteaJSONPath = cmd.getOptionValue("xteapath");
        outputDirectory = cmd.getOptionValue("outputdir");

        XteaKeyManager xteaKeyManager = new XteaKeyManager();
        try (FileInputStream fin = new FileInputStream(xteaJSONPath))
            {
                xteaKeyManager.loadKeys(fin);
            }

        File base = new File(cacheDirectory);
        File outDir = new File(outputDirectory);
        outDir.mkdirs();

        try (Store store = new Store(base))
            {
                store.load();

                MapImageDumper dumper = new MapImageDumper(store, xteaKeyManager);
                dumper.load();

                for (int i = 0; i < Region.Z; ++i)
                    {
                        BufferedImage image = dumper.drawMap(i);

                        File imageFile = new File(outDir, "img-" + i + ".png");

                        ImageIO.write(image, "png", imageFile);
                        log.info("Wrote image {}", imageFile);
                    }
            }
    }

    protected double random()
    {
        // the client would use a random value here, but we prefer determinism
        return 0.5;
    }

    public MapImageDumper setBrightness(double brightness)
    {
        colorPalette = JagexColor.createPalette(brightness);
        return this;
    }

    public MapImageDumper load() throws IOException
    {
        loadUnderlays(store);
        loadOverlays(store);
        objectManager.load();

        TextureManager textureManager = new TextureManager(store);
        textureManager.load();
        rsTextureProvider = new RSTextureProvider(textureManager, sprites);

        loadRegions();
        areas.load();
        sprites.load();
        loadSprites();
        fonts.load();
        worldMapManager.load();

        return this;
    }

    public BufferedImage drawMap(int z)
    {
        int minX = regionLoader.getLowestX().getBaseX();
        int minY = regionLoader.getLowestY().getBaseY();

        int maxX = regionLoader.getHighestX().getBaseX() + Region.X;
        int maxY = regionLoader.getHighestY().getBaseY() + Region.Y;

        int dimX = maxX - minX;
        int dimY = maxY - minY;

        int pixelsX = dimX * MAP_SCALE;
        int pixelsY = dimY * MAP_SCALE;

        log.info("Map image dimensions: {}px x {}px, {}px per map square ({} MB). Max memory: {}mb", pixelsX, pixelsY,
                 MAP_SCALE, (pixelsX * pixelsY * 3 / 1024 / 1024),
                 Runtime.getRuntime().maxMemory() / 1024L / 1024L);

        BufferedImage image;
        if (lowMemory)
            {
                image = BigBufferedImage.create(pixelsX, pixelsY, transparency ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);
            }
        else
            {
                image = new BufferedImage(pixelsX, pixelsY, transparency ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);
            }

        drawMap(image, z);
        drawObjects(image, z);
        dumpMapIcons(z);
        // drawMapIcons(image, z);
        // drawMapLabels(image, z);

        return image;
    }

    private void drawNeighborObjects(BufferedImage image, int rx, int ry, int dx, int dy, int z)
    {
        Region neighbor = regionLoader.findRegionForRegionCoordinates(rx + dx, ry + dy);
        if (neighbor == null)
            {
                return;
            }

        drawObjects(image, Region.X * dx, Region.Y * -dy, neighbor, z);
    }

    public BufferedImage drawRegion(Region region, int z)
    {
        int pixelsX = Region.X * MAP_SCALE;
        int pixelsY = Region.Y * MAP_SCALE;

        BufferedImage image = new BufferedImage(pixelsX, pixelsY, transparency ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);

        drawMap(image, 0, 0, z, region);

        drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), -1, -1, z);
        drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), -1, 0, z);
        drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), -1, 1, z);
        drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), 0, -1, z);
        drawObjects(image, 0, 0, region, z);
        drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), 0, 1, z);
        drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), 1, -1, z);
        drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), 1, 0, z);
        drawNeighborObjects(image, region.getRegionX(), region.getRegionY(), 1, 1, z);
        drawMapIcons(image, 0, 0, region, z);

        return image;
    }

    private void drawMap(BufferedImage image, int drawBaseX, int drawBaseY, int z, Region region)
    {
        if (!renderMap)
            {
                return;
            }

        int[][][] map = new int[4][][];

        for (int x = 0; x < Region.X; ++x)
            {
                for (int y = 0; y < Region.Y; ++y)
                    {
                        boolean isBridge = (region.getTileSetting(1, x, Region.Y - y - 1) & 2) != 0;
                        int tileZ = z + (isBridge ? 1 : 0);
                        if (tileZ >= Region.Z)
                            {
                                continue;
                            }

                        int tileSetting = region.getTileSetting(z, x, Region.Y - y - 1);
                        if ((tileSetting & 24) == 0)
                            {
                                if (z == 0 && isBridge)
                                    {
                                        drawTile(image, map, region, drawBaseX, drawBaseY, 0, x, y);
                                    }
                                drawTile(image, map, region, drawBaseX, drawBaseY, tileZ, x, y);
                            }

                        if (tileZ < 3)
                            {
                                int upTileSetting = region.getTileSetting(z + 1, x, Region.Y - y - 1);
                                if ((upTileSetting & 8) != 0)
                                    {
                                        drawTile(image, map, region, drawBaseX, drawBaseY, tileZ + 1, x, y);
                                    }
                            }
                    }
            }
    }

    private void drawMap(BufferedImage image, int z)
    {
        for (Region region : regionLoader.getRegions())
            {
                int baseX = region.getBaseX();
                int baseY = region.getBaseY();

                // to pixel X
                int drawBaseX = baseX - regionLoader.getLowestX().getBaseX();

                // to pixel Y. top most y is 0, but the top most
                // region has the greatest y, so invert
                int drawBaseY = regionLoader.getHighestY().getBaseY() - baseY;

                drawMap(image, drawBaseX, drawBaseY, z, region);
            }
    }

    private void drawTile(BufferedImage to, int[][][] planes, Region region, int drawBaseX, int drawBaseY, int z, int x, int y)
    {
        int[][] pixels = planes[z];

        if (pixels == null)
            {
                pixels = planes[z] = new int[Region.X * MAP_SCALE][Region.Y * MAP_SCALE];
                drawMap(pixels, region, z);
            }

        for (int i = 0; i < MAP_SCALE; ++i)
            {
                for (int j = 0; j < MAP_SCALE; ++j)
                    {
                        int argb = pixels[x * MAP_SCALE + i][y * MAP_SCALE + j];
                        if (argb != 0)
                            {
                                to.setRGB(drawBaseX * MAP_SCALE + x * MAP_SCALE + i,
                                          drawBaseY * MAP_SCALE + y * MAP_SCALE + j,
                                          argb);
                            }
                    }
            }
    }

    private void drawMap(int[][] pixels, Region region, int z)
    {
        int baseX = region.getBaseX();
        int baseY = region.getBaseY();

        int len = Region.X + BLEND * 2;
        int[] hues = new int[len];
        int[] sats = new int[len];
        int[] light = new int[len];
        int[] mul = new int[len];
        int[] num = new int[len];

        boolean hasLeftRegion = regionLoader.findRegionForWorldCoordinates(baseX - 1, baseY) != null;
        boolean hasRightRegion = regionLoader.findRegionForWorldCoordinates(baseX + Region.X, baseY) != null;
        boolean hasUpRegion = regionLoader.findRegionForWorldCoordinates(baseX, baseY + Region.Y) != null;
        boolean hasDownRegion = regionLoader.findRegionForWorldCoordinates(baseX, baseY - 1) != null;

        for (int xi = (hasLeftRegion ? -BLEND * 2 : -BLEND); xi < Region.X + (hasRightRegion ? BLEND * 2 : BLEND); ++xi)
            {
                for (int yi = (hasDownRegion ? -BLEND : 0); yi < Region.Y + (hasUpRegion ? BLEND : 0); ++yi)
                    {
                        int xr = xi + BLEND;
                        if (xr >= (hasLeftRegion ? -BLEND : 0) && xr < Region.X + (hasRightRegion ? BLEND : 0))
                            {
                                Region r = regionLoader.findRegionForWorldCoordinates(baseX + xr, baseY + yi);
                                if (r != null)
                                    {
                                        int underlayId = r.getUnderlayId(z, convert(xr), convert(yi));
                                        if (underlayId > 0)
                                            {
                                                UnderlayDefinition underlay = findUnderlay(underlayId - 1);
                                                hues[yi + BLEND] += underlay.getHue();
                                                sats[yi + BLEND] += underlay.getSaturation();
                                                light[yi + BLEND] += underlay.getLightness();
                                                mul[yi + BLEND] += underlay.getHueMultiplier();
                                                num[yi + BLEND]++;
                                            }
                                    }
                            }

                        int xl = xi - BLEND;
                        if (xl >= (hasLeftRegion ? -BLEND : 0) && xl < Region.X + (hasRightRegion ? BLEND : 0))
                            {
                                Region r = regionLoader.findRegionForWorldCoordinates(baseX + xl, baseY + yi);
                                if (r != null)
                                    {
                                        int underlayId = r.getUnderlayId(z, convert(xl), convert(yi));
                                        if (underlayId > 0)
                                            {
                                                UnderlayDefinition underlay = findUnderlay(underlayId - 1);
                                                hues[yi + BLEND] -= underlay.getHue();
                                                sats[yi + BLEND] -= underlay.getSaturation();
                                                light[yi + BLEND] -= underlay.getLightness();
                                                mul[yi + BLEND] -= underlay.getHueMultiplier();
                                                num[yi + BLEND]--;
                                            }
                                    }
                            }
                    }

                if (xi >= 0 && xi < Region.X)
                    {
                        int runningHues = 0;
                        int runningSat = 0;
                        int runningLight = 0;
                        int runningMultiplier = 0;
                        int runningNumber = 0;

                        for (int yi = (hasDownRegion ? -BLEND * 2 : -BLEND); yi < Region.Y + (hasUpRegion ? BLEND * 2 : BLEND); ++yi)
                            {
                                int yu = yi + BLEND;
                                if (yu >= (hasDownRegion ? -BLEND : 0) && yu < Region.Y + (hasUpRegion ? BLEND : 0))
                                    {
                                        runningHues += hues[yu + BLEND];
                                        runningSat += sats[yu + BLEND];
                                        runningLight += light[yu + BLEND];
                                        runningMultiplier += mul[yu + BLEND];
                                        runningNumber += num[yu + BLEND];
                                    }

                                int yd = yi - BLEND;
                                if (yd >= (hasDownRegion ? -BLEND : 0) && yd < Region.Y + (hasUpRegion ? BLEND : 0))
                                    {
                                        runningHues -= hues[yd + BLEND];
                                        runningSat -= sats[yd + BLEND];
                                        runningLight -= light[yd + BLEND];
                                        runningMultiplier -= mul[yd + BLEND];
                                        runningNumber -= num[yd + BLEND];
                                    }

                                if (yi >= 0 && yi < Region.Y)
                                    {
                                        Region r = regionLoader.findRegionForWorldCoordinates(baseX + xi, baseY + yi);
                                        if (r != null)
                                            {
                                                int underlayId = r.getUnderlayId(z, convert(xi), convert(yi));
                                                int overlayId = r.getOverlayId(z, convert(xi), convert(yi));

                                                if (underlayId > 0 || overlayId > 0)
                                                    {
                                                        int underlayHsl = -1;
                                                        if (underlayId > 0)
                                                            {
                                                                int avgHue = runningHues * 256 / runningMultiplier;
                                                                int avgSat = runningSat / runningNumber;
                                                                int avgLight = runningLight / runningNumber;
                                                                // randomness is added to avgHue here

                                                                if (avgLight < 0)
                                                                    {
                                                                        avgLight = 0;
                                                                    }
                                                                else if (avgLight > 255)
                                                                    {
                                                                        avgLight = 255;
                                                                    }

                                                                underlayHsl = packHsl(avgHue, avgSat, avgLight);
                                                            }

                                                        int underlayRgb = 0;
                                                        if (underlayHsl != -1)
                                                            {
                                                                int var0 = method1792(underlayHsl, 96);
                                                                underlayRgb = colorPalette[var0] | 0xFF000000;
                                                            }

                                                        int shape, rotation;
                                                        int overlayRgb = 0;
                                                        if (overlayId == 0)
                                                            {
                                                                shape = rotation = 0;
                                                            }
                                                        else
                                                            {
                                                                shape = r.getOverlayPath(z, convert(xi), convert(yi)) + 1;
                                                                rotation = r.getOverlayRotation(z, convert(xi), convert(yi));

                                                                OverlayDefinition overlayDefinition = findOverlay(overlayId - 1);
                                                                int overlayTexture = overlayDefinition.getTexture();
                                                                int hsl;

                                                                if (overlayTexture >= 0)
                                                                    {
                                                                        hsl = rsTextureProvider.getAverageTextureRGB(overlayTexture);
                                                                    }
                                                                else if (overlayDefinition.getRgbColor() == 0xFF_00FF)
                                                                    {
                                                                        hsl = -2;
                                                                    }
                                                                else
                                                                    {
                                                                        // randomness added here
                                                                        int overlayHsl = packHsl(overlayDefinition.getHue(), overlayDefinition.getSaturation(), overlayDefinition.getLightness());
                                                                        hsl = overlayHsl;
                                                                    }

                                                                if (hsl != -2)
                                                                    {
                                                                        int var0 = adjustHSLListness0(hsl, 96);
                                                                        overlayRgb = colorPalette[var0] | 0xFF000000;
                                                                    }

                                                                if (overlayDefinition.getSecondaryRgbColor() != -1)
                                                                    {
                                                                        int hue = overlayDefinition.getOtherHue();
                                                                        int sat = overlayDefinition.getOtherSaturation();
                                                                        int olight = overlayDefinition.getOtherLightness();
                                                                        hsl = packHsl(hue, sat, olight);
                                                                        int var0 = adjustHSLListness0(hsl, 96);
                                                                        overlayRgb = colorPalette[var0] | 0xFF000000;
                                                                    }
                                                            }

                                                        if (shape == 0)
                                                            {
                                                                int drawX = xi;
                                                                int drawY = Region.Y - 1 - yi;
                                                                if (underlayRgb != 0)
                                                                    {
                                                                        drawMapSquare(pixels, drawX, drawY, underlayRgb);
                                                                    }
                                                            }
                                                        else if (shape == 1)
                                                            {
                                                                int drawX = xi;
                                                                int drawY = Region.Y - 1 - yi;
                                                                drawMapSquare(pixels, drawX, drawY, overlayRgb);
                                                            }
                                                        else
                                                            {
                                                                int drawX = xi * MAP_SCALE;
                                                                int drawY = (Region.Y - 1 - yi) * MAP_SCALE;
                                                                int[] tileShapes = TILE_SHAPE_2D[shape];
                                                                int[] tileRotations = TILE_ROTATION_2D[rotation];
                                                                if (underlayRgb != 0)
                                                                    {
                                                                        int rotIdx = 0;
                                                                        for (int i = 0; i < Region.Z; ++i)
                                                                            {
                                                                                int p1 = tileShapes[tileRotations[rotIdx++]] == 0 ? underlayRgb : overlayRgb;
                                                                                int p2 = tileShapes[tileRotations[rotIdx++]] == 0 ? underlayRgb : overlayRgb;
                                                                                int p3 = tileShapes[tileRotations[rotIdx++]] == 0 ? underlayRgb : overlayRgb;
                                                                                int p4 = tileShapes[tileRotations[rotIdx++]] == 0 ? underlayRgb : overlayRgb;
                                                                                pixels[drawX + 0][drawY + i] = p1;
                                                                                pixels[drawX + 1][drawY + i] = p2;
                                                                                pixels[drawX + 2][drawY + i] = p3;
                                                                                pixels[drawX + 3][drawY + i] = p4;
                                                                            }
                                                                    }
                                                                else
                                                                    {
                                                                        int rotIdx = 0;
                                                                        for (int i = 0; i < Region.Z; ++i)
                                                                            {
                                                                                int p1 = tileShapes[tileRotations[rotIdx++]];
                                                                                int p2 = tileShapes[tileRotations[rotIdx++]];
                                                                                int p3 = tileShapes[tileRotations[rotIdx++]];
                                                                                int p4 = tileShapes[tileRotations[rotIdx++]];

                                                                                if (p1 != 0)
                                                                                    {
                                                                                        pixels[drawX + 0][drawY + i] = overlayRgb;
                                                                                    }

                                                                                if (p2 != 0)
                                                                                    {
                                                                                        pixels[drawX + 1][drawY + i] = overlayRgb;
                                                                                    }

                                                                                if (p3 != 0)
                                                                                    {
                                                                                        pixels[drawX + 2][drawY + i] = overlayRgb;
                                                                                    }

                                                                                if (p4 != 0)
                                                                                    {
                                                                                        pixels[drawX + 3][drawY + i] = overlayRgb;
                                                                                    }
                                                                            }
                                                                    }
                                                            }
                                                    }
                                            }
                                    }
                            }
                    }
            }
    }

    private static int convert(int d)
    {
        if (d >= 0)
            {
                return d % 64;
            }
        else
            {
                return 64 - -(d % 64) - 1;
            }
    }

    private void drawObjects(BufferedImage image, int drawBaseX, int drawBaseY, Region region, int z)
    {
        if (!renderObjects)
            {
                return;
            }

        List<Location> planeLocs = new ArrayList<>();
        List<Location> pushDownLocs = new ArrayList<>();
        List<List<Location>> layers = Arrays.asList(planeLocs, pushDownLocs);
        for (int localX = 0; localX < Region.X; localX++)
            {
                int regionX = localX + region.getBaseX();
                for (int localY = 0; localY < Region.Y; localY++)
                    {
                        int regionY = localY + region.getBaseY();

                        planeLocs.clear();
                        pushDownLocs.clear();
                        boolean isBridge = (region.getTileSetting(1, localX, localY) & 2) != 0;
                        int tileZ = z + (isBridge ? 1 : 0);

                        for (Location loc : region.getLocations())
                            {
                                Position pos = loc.getPosition();
                                if (pos.getX() != regionX || pos.getY() != regionY)
                                    {
                                        continue;
                                    }

                                if (pos.getZ() == tileZ && (region.getTileSetting(z, localX, localY) & 24) == 0)
                                    {
                                        planeLocs.add(loc);
                                    }
                                else if (z < 3 && pos.getZ() == tileZ + 1 && (region.getTileSetting(z + 1, localX, localY) & 8) != 0)
                                    {
                                        pushDownLocs.add(loc);
                                    }
                            }

                        for (List<Location> locs : layers)
                            {
                                for (Location location : locs)
                                    {
                                        int type = location.getType();
                                        if (type >= 0 && type <= 3)
                                            {
                                                int rotation = location.getOrientation();

                                                ObjectDefinition object = findObject(location.getId());

                                                int drawX = (drawBaseX + localX) * MAP_SCALE;
                                                int drawY = (drawBaseY + (Region.Y - object.getSizeY() - localY)) * MAP_SCALE;

                                                int rgb = wallColor;
                                                if (object.getWallOrDoor() != 0)
                                                    {
                                                        rgb = doorColor;
                                                    }
                                                rgb |= 0xFF000000;

                                                if (object.getMapSceneID() != -1)
                                                    {
                                                        blitMapDecoration(image, drawX, drawY, object);
                                                    }
                                                else if (drawX >= 0 && drawY >= 0 && drawX < image.getWidth() && drawY < image.getHeight())
                                                    {
                                                        if (type == 0 || type == 2)
                                                            {
                                                                if (rotation == 0)
                                                                    {
                                                                        image.setRGB(drawX + 0, drawY + 0, rgb);
                                                                        image.setRGB(drawX + 0, drawY + 1, rgb);
                                                                        image.setRGB(drawX + 0, drawY + 2, rgb);
                                                                        image.setRGB(drawX + 0, drawY + 3, rgb);
                                                                    }
                                                                else if (rotation == 1)
                                                                    {
                                                                        image.setRGB(drawX + 0, drawY + 0, rgb);
                                                                        image.setRGB(drawX + 1, drawY + 0, rgb);
                                                                        image.setRGB(drawX + 2, drawY + 0, rgb);
                                                                        image.setRGB(drawX + 3, drawY + 0, rgb);
                                                                    }
                                                                else if (rotation == 2)
                                                                    {
                                                                        image.setRGB(drawX + 3, drawY + 0, rgb);
                                                                        image.setRGB(drawX + 3, drawY + 1, rgb);
                                                                        image.setRGB(drawX + 3, drawY + 2, rgb);
                                                                        image.setRGB(drawX + 3, drawY + 3, rgb);
                                                                    }
                                                                else if (rotation == 3)
                                                                    {
                                                                        image.setRGB(drawX + 0, drawY + 3, rgb);
                                                                        image.setRGB(drawX + 1, drawY + 3, rgb);
                                                                        image.setRGB(drawX + 2, drawY + 3, rgb);
                                                                        image.setRGB(drawX + 3, drawY + 3, rgb);
                                                                    }
                                                            }

                                                        if (type == 3)
                                                            {
                                                                if (rotation == 0)
                                                                    {
                                                                        image.setRGB(drawX + 0, drawY + 0, rgb);
                                                                    }
                                                                else if (rotation == 1)
                                                                    {
                                                                        image.setRGB(drawX + 3, drawY + 0, rgb);
                                                                    }
                                                                else if (rotation == 2)
                                                                    {
                                                                        image.setRGB(drawX + 3, drawY + 3, rgb);
                                                                    }
                                                                else if (rotation == 3)
                                                                    {
                                                                        image.setRGB(drawX + 0, drawY + 3, rgb);
                                                                    }
                                                            }

                                                        if (type == 2)
                                                            {
                                                                if (rotation == 3)
                                                                    {
                                                                        image.setRGB(drawX + 0, drawY + 0, rgb);
                                                                        image.setRGB(drawX + 0, drawY + 1, rgb);
                                                                        image.setRGB(drawX + 0, drawY + 2, rgb);
                                                                        image.setRGB(drawX + 0, drawY + 3, rgb);
                                                                    }
                                                                else if (rotation == 0)
                                                                    {
                                                                        image.setRGB(drawX + 0, drawY + 0, rgb);
                                                                        image.setRGB(drawX + 1, drawY + 0, rgb);
                                                                        image.setRGB(drawX + 2, drawY + 0, rgb);
                                                                        image.setRGB(drawX + 3, drawY + 0, rgb);
                                                                    }
                                                                else if (rotation == 1)
                                                                    {
                                                                        image.setRGB(drawX + 3, drawY + 0, rgb);
                                                                        image.setRGB(drawX + 3, drawY + 1, rgb);
                                                                        image.setRGB(drawX + 3, drawY + 2, rgb);
                                                                        image.setRGB(drawX + 3, drawY + 3, rgb);
                                                                    }
                                                                else if (rotation == 2)
                                                                    {
                                                                        image.setRGB(drawX + 0, drawY + 3, rgb);
                                                                        image.setRGB(drawX + 1, drawY + 3, rgb);
                                                                        image.setRGB(drawX + 2, drawY + 3, rgb);
                                                                        image.setRGB(drawX + 3, drawY + 3, rgb);
                                                                    }
                                                            }
                                                    }
                                            }
                                    }

                                for (Location location : locs)
                                    {
                                        int type = location.getType();
                                        if (type == 9)
                                            {
                                                int rotation = location.getOrientation();

                                                ObjectDefinition object = findObject(location.getId());

                                                int drawX = (drawBaseX + localX) * MAP_SCALE;
                                                int drawY = (drawBaseY + (Region.Y - object.getSizeY() - localY)) * MAP_SCALE;

                                                if (object.getMapSceneID() != -1)
                                                    {
                                                        blitMapDecoration(image, drawX, drawY, object);
                                                        continue;
                                                    }

                                                if (drawX >= 0 && drawY >= 0 && drawX < image.getWidth() && drawY < image.getHeight())
                                                    {
                                                        int rgb = 0xFFEE_EEEE;
                                                        if (object.getWallOrDoor() != 0)
                                                            {
                                                                rgb = 0xFFEE_0000;
                                                            }

                                                        if (rotation != 0 && rotation != 2)
                                                            {
                                                                image.setRGB(drawX + 0, drawY + 0, rgb);
                                                                image.setRGB(drawX + 1, drawY + 1, rgb);
                                                                image.setRGB(drawX + 2, drawY + 2, rgb);
                                                                image.setRGB(drawX + 3, drawY + 3, rgb);
                                                            }
                                                        else
                                                            {
                                                                image.setRGB(drawX + 0, drawY + 3, rgb);
                                                                image.setRGB(drawX + 1, drawY + 2, rgb);
                                                                image.setRGB(drawX + 2, drawY + 1, rgb);
                                                                image.setRGB(drawX + 3, drawY + 0, rgb);
                                                            }
                                                    }
                                            }
                                    }

                                for (Location location : locs)
                                    {
                                        int type = location.getType();
                                        if (type == 22 || (type >= 9 && type <= 11))
                                            {
                                                ObjectDefinition object = findObject(location.getId());

                                                int drawX = (drawBaseX + localX) * MAP_SCALE;
                                                int drawY = (drawBaseY + (Region.Y - object.getSizeY() - localY)) * MAP_SCALE;

                                                if (object.getMapSceneID() != -1)
                                                    {
                                                        blitMapDecoration(image, drawX, drawY, object);
                                                    }
                                            }
                                    }
                            }
                    }
            }
    }

    private void drawObjects(BufferedImage image, int z)
    {
        for (Region region : regionLoader.getRegions())
            {
                int baseX = region.getBaseX();
                int baseY = region.getBaseY();

                // to pixel X
                int drawBaseX = baseX - regionLoader.getLowestX().getBaseX();

                // to pixel Y. top most y is 0, but the top most
                // region has the greatest y, so invert
                int drawBaseY = regionLoader.getHighestY().getBaseY() - baseY;

                drawObjects(image, drawBaseX, drawBaseY, region, z);
            }
    }

    private void drawMapIcons(BufferedImage image, int drawBaseX, int drawBaseY, Region region, int z)
    {
        int baseX = region.getBaseX();
        int baseY = region.getBaseY();

        Graphics2D graphics = image.createGraphics();

        drawMapIcons(image, region, z, drawBaseX, drawBaseY);

        if (labelRegions)
            {
                graphics.setColor(Color.WHITE);
                String str = baseX + "," + baseY + " (" + region.getRegionX() + "," + region.getRegionY() + ")";
                graphics.drawString(str, drawBaseX * MAP_SCALE, drawBaseY * MAP_SCALE + graphics.getFontMetrics().getHeight());
            }

        if (outlineRegions)
            {
                graphics.setColor(Color.WHITE);
                graphics.drawRect(drawBaseX * MAP_SCALE, drawBaseY * MAP_SCALE, Region.X * MAP_SCALE, Region.Y * MAP_SCALE);
            }

        graphics.dispose();
    }

    private void drawMapIcons(BufferedImage image, int z)
    {
        // map icons
        for (Region region : regionLoader.getRegions())
            {
                int baseX = region.getBaseX();
                int baseY = region.getBaseY();

                // to pixel X
                int drawBaseX = baseX - regionLoader.getLowestX().getBaseX();

                // to pixel Y. top most y is 0, but the top most
                // region has the greatest y, so invert
                int drawBaseY = regionLoader.getHighestY().getBaseY() - baseY;

                drawMapIcons(image, drawBaseX, drawBaseY, region, z);
            }
    }

    private void drawMapLabels(BufferedImage image, int z)
    {
        if (!renderLabels)
            {
                return;
            }

        FontName[] fontSizes = new FontName[] { FontName.VERDANA_11,  FontName.VERDANA_13,  FontName.VERDANA_15 };
        List<WorldMapElementDefinition> elements = worldMapManager.getElements();
        for (WorldMapElementDefinition element : elements)
            {
                AreaDefinition area = areas.getArea(element.getAreaDefinitionId());
                Position worldPosition = element.getWorldPosition();
                if (area == null || area.getName() == null || worldPosition.getZ() != z)
                    {
                        continue;
                    }

                FontName fontSize = fontSizes[area.getTextScale()];
                FontDefinition font = fonts.findFontByName(fontSize.getName());
                String areaLabel = area.getName();
                String[] lines = areaLabel.split("<br>");
                int ascent = 0;

                for (String line : lines)
                    {
                        int advance = 0;
                        int stringWidth = font.stringWidth(line);
                        for (int i = 0; i < line.length(); ++i)
                            {
                                char c = line.charAt(i);
                                SpriteDefinition sprite = sprites.findSpriteByArchiveName(fontSize.getName(), c);
                                if (sprite.getWidth() != 0 && sprite.getHeight() != 0)
                                    {
                                        int drawX = worldPosition.getX() - regionLoader.getLowestX().getBaseX();
                                        int drawY = regionLoader.getHighestY().getBaseY() - worldPosition.getY() + Region.Y - 2;
                                        blitGlyph(image,
                                                  (drawX * MAP_SCALE) + advance - (stringWidth / 2),
                                                  (drawY * MAP_SCALE) + ascent - (font.getAscent() / 2),
                                                  area.getTextColor(),
                                                  sprite
                                                  );
                                    }

                                advance += font.getAdvances()[c];
                            }
                        ascent += font.getAscent() / 2;
                    }
            }
    }

    private void dumpMapIcons(int z) {
        Map<Integer, List<Integer>> icons = new HashMap<>();

        if (z == 0) return;

        for (Region region : regionLoader.getRegions()) {
            for (Location location : region.getLocations()) {
                ObjectDefinition od = findObject(location.getId());

                if (od.getMapAreaId() != -1) {
                    AreaDefinition area = areas.getArea(od.getMapAreaId());

                    List<Integer> x = icons.computeIfAbsent(area.spriteId, k -> new ArrayList<>());
                    x.add(location.getPosition().getX());
                    x.add(location.getPosition().getY());
                }
            }
        }

        File outDir = new File(outputDirectory + "/icons");
        outDir.mkdirs();
        for (Integer spriteId : icons.keySet()) {
            SpriteDefinition sprite = sprites.findSprite(spriteId, 0);
            BufferedImage iconImage = new BufferedImage(sprite.getWidth(), sprite.getHeight(), BufferedImage.TYPE_INT_ARGB);

            for (int x = 0; x < sprite.getWidth(); ++x) {
                for (int y = 0; y < sprite.getHeight(); ++y) {
                    int rgb = sprite.getPixels()[x + (y * sprite.getWidth())];
                    if (rgb != 0) {
                        iconImage.setRGB(x, y, rgb | 0xFF000000);
                    }
                }
            }

            try {
                File iconFile = new File(outDir, spriteId + ".png");
                ImageIO.write(iconImage, "png", iconFile);
            } catch (Exception ex) {
                log.error("Failed to write sprite file", ex);
            }
        }

        try {
            Gson gson = new Gson();
            File jsonFile = new File(outDir, "map-icons.json");
            FileWriter writer = new FileWriter(jsonFile);
            gson.toJson(icons, writer);
            writer.flush();
            writer.close();
        } catch (Exception ex) {
            log.error("Failed to write map-icons.json", ex);
        }
    }

    private ObjectDefinition findObject(int id)
    {
        return objectManager.getObject(id);
    }

    private int packHsl(int var0, int var1, int var2)
    {
        if (var2 > 179)
            {
                var1 /= 2;
            }

        if (var2 > 192)
            {
                var1 /= 2;
            }

        if (var2 > 217)
            {
                var1 /= 2;
            }

        if (var2 > 243)
            {
                var1 /= 2;
            }

        int var3 = (var1 / 32 << 7) + (var0 / 4 << 10) + var2 / 2;
        return var3;
    }

    static int method1792(int var0, int var1)
    {
        if (var0 == -1)
            {
                return 12345678;
            }
        else
            {
                var1 = (var0 & 127) * var1 / 128;
                if (var1 < 2)
                    {
                        var1 = 2;
                    }
                else if (var1 > 126)
                    {
                        var1 = 126;
                    }

                return (var0 & 65408) + var1;
            }
    }

    static final int adjustHSLListness0(int var0, int var1)
    {
        if (var0 == -2)
            {
                return 12345678;
            }
        else if (var0 == -1)
            {
                if (var1 < 2)
                    {
                        var1 = 2;
                    }
                else if (var1 > 126)
                    {
                        var1 = 126;
                    }

                return var1;
            }
        else
            {
                var1 = (var0 & 127) * var1 / 128;
                if (var1 < 2)
                    {
                        var1 = 2;
                    }
                else if (var1 > 126)
                    {
                        var1 = 126;
                    }

                return (var0 & 65408) + var1;
            }
    }

    private void drawMapSquare(int[][] pixels, int x, int y, int rgb)
    {
        x *= MAP_SCALE;
        y *= MAP_SCALE;

        for (int i = 0; i < MAP_SCALE; ++i)
            {
                for (int j = 0; j < MAP_SCALE; ++j)
                    {
                        pixels[x + i][y + j] = rgb;
                    }
            }
    }

    private void drawMapIcons(BufferedImage img, Region region, int z, int drawBaseX, int drawBaseY)
    {
        if (!renderIcons)
            {
                return;
            }

        for (Location location : region.getLocations())
            {
                int localX = location.getPosition().getX() - region.getBaseX();
                int localY = location.getPosition().getY() - region.getBaseY();

                if (z != location.getPosition().getZ())
                    {
                        continue;
                    }

                ObjectDefinition od = findObject(location.getId());

                assert od != null;

                int drawX = drawBaseX + localX;
                int drawY = drawBaseY + (Region.Y - 1 - localY);

                if (od.getMapAreaId() != -1)
                    {
                        AreaDefinition area = areas.getArea(od.getMapAreaId());
                        assert area != null;

                        SpriteDefinition sprite = sprites.findSprite(area.spriteId, 0);
                        assert sprite != null;

                        blitIcon(img,
                                 (drawX * MAP_SCALE) - (sprite.getMaxWidth() / 2),
                                 (drawY * MAP_SCALE) - (sprite.getMaxHeight() / 2),
                                 sprite);
                    }
            }

        // Draw the intermap link icons which are not stored with the map locations
        List<WorldMapElementDefinition> elements = worldMapManager.getElements();
        for (WorldMapElementDefinition element : elements)
            {
                AreaDefinition area = areas.getArea(element.getAreaDefinitionId());
                Position worldPosition = element.getWorldPosition();
                int regionX = worldPosition.getX() / Region.X;
                int regionY = worldPosition.getY() / Region.Y;

                if (area == null || area.getName() != null || worldPosition.getZ() != z || regionX != region.getRegionX() || regionY != region.getRegionY())
                    {
                        continue;
                    }

                int localX = worldPosition.getX() - region.getBaseX();
                int localY = worldPosition.getY() - region.getBaseY();
                int drawX = drawBaseX + localX;
                int drawY = drawBaseY + (Region.Y - 1 - localY);
                SpriteDefinition sprite = sprites.findSprite(area.spriteId, 0);
                blitIcon(img,
                         (drawX * MAP_SCALE) - (sprite.getMaxWidth() / 2),
                         (drawY * MAP_SCALE) - (sprite.getMaxHeight() / 2),
                         sprite);
            }
    }

    private void loadRegions() throws IOException
    {
        regionLoader.loadRegions();
        regionLoader.calculateBounds();

        log.debug("North most region: {}", regionLoader.getLowestY().getBaseY());
        log.debug("South most region: {}", regionLoader.getHighestY().getBaseY());
        log.debug("West most region:  {}", regionLoader.getLowestX().getBaseX());
        log.debug("East most region:  {}", regionLoader.getHighestX().getBaseX());
    }

    private void loadUnderlays(Store store) throws IOException
    {
        Storage storage = store.getStorage();
        Index index = store.getIndex(IndexType.CONFIGS);
        Archive archive = index.getArchive(ConfigType.UNDERLAY.getId());

        byte[] archiveData = storage.loadArchive(archive);
        ArchiveFiles files = archive.getFiles(archiveData);

        for (FSFile file : files.getFiles())
            {
                UnderlayLoader loader = new UnderlayLoader();
                UnderlayDefinition underlay = loader.load(file.getFileId(), file.getContents());

                underlays.put(underlay.getId(), underlay);
            }
    }

    private UnderlayDefinition findUnderlay(int id)
    {
        return underlays.get(id);
    }

    private void loadOverlays(Store store) throws IOException
    {
        Storage storage = store.getStorage();
        Index index = store.getIndex(IndexType.CONFIGS);
        Archive archive = index.getArchive(ConfigType.OVERLAY.getId());

        byte[] archiveData = storage.loadArchive(archive);
        ArchiveFiles files = archive.getFiles(archiveData);

        for (FSFile file : files.getFiles())
            {
                OverlayLoader loader = new OverlayLoader();
                OverlayDefinition overlay = loader.load(file.getFileId(), file.getContents());

                overlays.put(overlay.getId(), overlay);
            }
    }

    private OverlayDefinition findOverlay(int id)
    {
        return overlays.get(id);
    }

    private void loadSprites() throws IOException
    {
        Storage storage = store.getStorage();
        Index index = store.getIndex(IndexType.SPRITES);
        Archive a = index.findArchiveByName("mapscene");
        byte[] contents = a.decompress(storage.loadArchive(a));

        SpriteLoader loader = new SpriteLoader();
        mapDecorations = loader.load(a.getArchiveId(), contents);
    }

    private void blitMapDecoration(BufferedImage dst, int x, int y, ObjectDefinition object)
    {
        SpriteDefinition sprite = mapDecorations[object.getMapSceneID()];
        int ox = (object.getSizeX() * MAP_SCALE - sprite.getWidth()) / 2;
        int oy = (object.getSizeY() * MAP_SCALE - sprite.getHeight()) / 2;
        blitIcon(dst, x + ox, y + oy, sprite);
    }

    private void blitIcon(BufferedImage dst, int x, int y, SpriteDefinition sprite)
    {
        x += sprite.getOffsetX();
        y += sprite.getOffsetY();

        int ymin = Math.max(0, -y);
        int ymax = Math.min(sprite.getHeight(), dst.getHeight() - y);

        int xmin = Math.max(0, -x);
        int xmax = Math.min(sprite.getWidth(), dst.getWidth() - x);

        for (int yo = ymin; yo < ymax; yo++)
            {
                for (int xo = xmin; xo < xmax; xo++)
                    {
                        int rgb = sprite.getPixels()[xo + (yo * sprite.getWidth())];
                        if (rgb != 0)
                            {
                                dst.setRGB(x + xo, y + yo, rgb | 0xFF000000);
                            }
                    }
            }
    }

    private void blitGlyph(BufferedImage dst, int x, int y, int color, SpriteDefinition glyph)
    {
        int[] pixels = glyph.getPixels();
        int[] shadowPixels = new int[pixels.length];
        for (int i = 0; i < pixels.length; ++i)
            {
                if (pixels[i] != 0)
                    {
                        pixels[i] = color;
                        shadowPixels[i] = 0xFF000000;
                    }
            }
        SpriteDefinition shadow = new SpriteDefinition();
        shadow.setPixels(shadowPixels);
        shadow.setOffsetX(glyph.getOffsetX());
        shadow.setOffsetY(glyph.getOffsetY());
        shadow.setWidth(glyph.getWidth());
        shadow.setHeight(glyph.getHeight());

        blitIcon(dst, x + 1, y + 1, shadow);
        blitIcon(dst, x, y, glyph);
    }
}
