package pathgame.rendering;

import me.hammerle.snuviengine.api.FontRenderer;
import me.hammerle.snuviengine.api.Renderer;
import me.hammerle.snuviengine.api.Texture;
import me.hammerle.snuviengine.api.TextureRenderer;
import pathgame.gameplay.Keys;
import pathgame.gameplay.Player;
import pathgame.tilemap.Tile;
import pathgame.tilemap.TileMap;
import pathgame.tilemap.TileRenderType;

/**
 * A renderer for tile maps.
 *
 * @author kajetan
 */
public class TileMapRenderer
{

    // prevents rendering artifacts especially on different zoom levels
    private final static float ERROR = 1.0f / 512.0F;
    private final static float T_ERROR = 1.0f / 4096.0F;

    private final Texture tileTexture = new Texture("resources/tiles.png");
    private final TextureRenderer textureRenderer = new TextureRenderer(20 * 20 * 2); // default to 20x20 map
    private final TextureRenderer swampWaterOverlayRenderer = new TextureRenderer(20 * 20 * 2); // default to 20x20 map
    private final TextureRenderer waveRenderer = new TextureRenderer(20 * 20 * 2); // default to 20x20 map
    private final TextureRenderer grassOverlayRenderer = new TextureRenderer(20 * 20 * 2); // default to 20x20 map
    private float scale = 1.0f;
    
    private final static String[] OVERLAY = new String[]
    {
        "&a1", "&a2", "&a3", "&e4", "&e5", "&66", "&67", "&68", "&c9"
    };
    
    private static String[] getWavePath()
    {
        String[] path = new String[20];
        for(int i = 0; i < path.length; i++)
        {
            path[i] = "resources/waves/wave" + (i + 1) + ".png";
        }
        return path;
    }
    
    private final Texture.Animation waves = tileTexture.addAnimation((int) (8 * TileRenderer.TILE_SIZE), (int) (2 * TileRenderer.TILE_SIZE), getWavePath());
    private int counter = 0;
    
    /**
     * Creates a new tile map renderer.
     *
     */
    public TileMapRenderer()
    {
    }

    /**
     * Sets the scale of the map.
     *
     * @param scale the scale of the map
     */
    public void setScale(float scale)
    {
        this.scale = scale;
    }

    /**
     * Returns the scale of the map.
     *
     * @return the scale of the map
     */
    public float getScale()
    {
        return scale;
    }

    /**
     * Returns the scaled render width of a map.
     *
     * @param map a map
     * @return the scaled render width of a map
     */
    public float getWidth(TileMap map)
    {
        return map.getWidth() * TileRenderer.TILE_SIZE * scale;
    }

    /**
     * Returns the scaled render height of a map.
     *
     * @param map a map
     * @return the scaled render height of a map
     */
    public float getHeight(TileMap map)
    {
        return map.getHeight() * TileRenderer.TILE_SIZE * scale;
    }

    private float toTexture(int x)
    {
        return x * TileRenderer.TILE_SIZE / TileTexture.TEXTURE_SIZE;
    }

    private void addTile(TextureRenderer tr, int x, int y, float tMinX, float tMinY, float tMaxX, float tMaxY)
    {
        tr.addRectangle(
                x * TileRenderer.TILE_SIZE - ERROR, y * TileRenderer.TILE_SIZE - ERROR,
                (x + 1) * TileRenderer.TILE_SIZE + ERROR, (y + 1) * TileRenderer.TILE_SIZE + ERROR,
                tMinX + T_ERROR, tMinY + T_ERROR,
                tMaxX - T_ERROR, tMaxY - T_ERROR);
    }

    private void addTileOverlay(TextureRenderer tr, int x, int y, int ox, int oy)
    {
        addTile(tr, x, y, toTexture(ox), toTexture(oy), toTexture(ox + 1), toTexture(oy + 1));
    }

    private boolean isInRange(TileMap map, int x, int y)
    {
        return x >= 0 && y >= 0 && x < map.getWidth() && y < map.getHeight();
    }

    private boolean isSwamp(TileMap map, int x, int y)
    {
        return isInRange(map, x, y) && map.getTile(x, y).getRenderType() == TileRenderType.SWAMP;
    }

    private boolean isSwampOrWaterOrBorder(TileMap map, int x, int y)
    {
        if(!isInRange(map, x, y))
        {
            return true;
        }
        TileRenderType type = map.getTile(x, y).getRenderType();
        return type == TileRenderType.SWAMP || type == TileRenderType.WATER;
    }

    private void addWaterSwampOverlay(TileMap map, int x, int y)
    {
        boolean n = isSwamp(map, x, y - 1);
        boolean e = isSwamp(map, x + 1, y);
        boolean s = isSwamp(map, x, y + 1);
        boolean w = isSwamp(map, x - 1, y);

        if(!n && !w && isSwamp(map, x - 1, y - 1)) // upper, left corner
        {
            addTileOverlay(swampWaterOverlayRenderer, x, y, 7, 13);
        }
        if(!n && !e && isSwamp(map, x + 1, y - 1)) // upper, right corner
        {
            addTileOverlay(swampWaterOverlayRenderer, x, y, 6, 13);
        }
        if(!s && !w && isSwamp(map, x - 1, y + 1)) // lower, left corner
        {
            addTileOverlay(swampWaterOverlayRenderer, x, y, 5, 13);
        }
        if(!s && !e && isSwamp(map, x + 1, y + 1)) // lower, right corner
        {
            addTileOverlay(swampWaterOverlayRenderer, x, y, 4, 13);
        }

        int index = 0;
        index += n ? 0 : 8;
        index += e ? 0 : 4;
        index += s ? 0 : 2;
        index += w ? 0 : 1;
        if(index == 15)
        {
            return;
        }
        addTileOverlay(swampWaterOverlayRenderer, x, y, index, 15);
    }

    private void addGrassOverlay(TileMap map, int x, int y)
    {
        boolean n = isSwampOrWaterOrBorder(map, x, y - 1);
        boolean e = isSwampOrWaterOrBorder(map, x + 1, y);
        boolean s = isSwampOrWaterOrBorder(map, x, y + 1);
        boolean w = isSwampOrWaterOrBorder(map, x - 1, y);

        if(n && w && !isSwampOrWaterOrBorder(map, x - 1, y - 1)) // upper, left corner
        {
            addTileOverlay(grassOverlayRenderer, x, y, 3, 13);
        }
        if(n && e && !isSwampOrWaterOrBorder(map, x + 1, y - 1)) // upper, right corner
        {
            addTileOverlay(grassOverlayRenderer, x, y, 2, 13);
        }
        if(s && w && !isSwampOrWaterOrBorder(map, x - 1, y + 1)) // lower, left corner
        {
            addTileOverlay(grassOverlayRenderer, x, y, 1, 13);
        }
        if(s && e && !isSwampOrWaterOrBorder(map, x + 1, y + 1)) // lower, right corner
        {
            addTileOverlay(grassOverlayRenderer, x, y, 0, 13);
        }

        int index = 0;
        index += !n ? 0 : 8;
        index += !e ? 0 : 4;
        index += !s ? 0 : 2;
        index += !w ? 0 : 1;
        if(index == 15)
        {
            return;
        }

        addTileOverlay(grassOverlayRenderer, x, y, index, 14);
    }

    private void updateData(TileMap map)
    {
        textureRenderer.clear();
        swampWaterOverlayRenderer.clear();
        waveRenderer.clear();
        grassOverlayRenderer.clear();
        for(int x = 0; x < map.getWidth(); x++)
        {
            for(int y = 0; y < map.getHeight(); y++)
            {
                Tile t = map.getTile(x, y);
                TileTexture tt = TileRenderer.getTileTexture(map, t, x, y);
                if(tt == null)
                {
                    continue;
                }
                if(t.getRenderType() == TileRenderType.WATER)
                {
                    addWaterSwampOverlay(map, x, y);
                    addGrassOverlay(map, x, y);
                    addTileOverlay(waveRenderer, x, y, 8, 2);
                }
                else if(t.getRenderType() == TileRenderType.SWAMP)
                {
                    addGrassOverlay(map, x, y);
                }
                addTile(textureRenderer, x, y, tt.getMinX(), tt.getMinY(), tt.getMaxX(), tt.getMaxY());
            }
        }
        textureRenderer.build();
        swampWaterOverlayRenderer.build();
        waveRenderer.build();
        grassOverlayRenderer.build();
    }

    /**
     * Ticks the renderer. Used for animated tiles.
     *
     */
    public void tick()
    {
        // tick tile animations here
        counter++;
        if(counter >= 1)
        {
            tileTexture.bind();
            waves.nextFrame();
            counter = 0;
        }
    }

    /**
     * Draws the given map at the given offset.
     *
     * @param map a map
     * @param r the renderer given by the engine
     * @param forceUpdate whether an update of the render data should be forced
     * @param offX the x coordinate of the offset
     * @param offY the y coordinate of the offset
     */
    public void renderTick(TileMap map, Renderer r, Player p, boolean forceUpdate, float offX, float offY)
    {
        r.setTextureEnabled(true);
        r.setColorEnabled(false);
        r.setMixColorEnabled(false);

        tileTexture.bind();

        if(forceUpdate || map.isDirty())
        {
            updateData(map);
            map.clean();
        }

        float viewScale = r.getViewScale();
        offX = (int) (offX * viewScale) / viewScale;
        offY = (int) (offY * viewScale) / viewScale;

        r.translateTo(offX, offY);
        r.scale(scale, scale);
        r.updateMatrix();
        textureRenderer.draw();
        waveRenderer.draw();
        swampWaterOverlayRenderer.draw();
        grassOverlayRenderer.draw();

        if(Keys.OVERLAY_KEY.isDown())
        {
            r.translateTo(0.0f, 0.0f);
            r.updateMatrix();
            r.setTextureEnabled(false);
            r.getColorRenderer().drawRectangle(0, 0, r.getViewWidth(), r.getViewHeight(), 0x44000000);
            r.setTextureEnabled(true);
            r.setColorEnabled(true);

            r.translateTo(offX, offY);
            r.scale(scale * 2.0f, scale * 2.0f);
            r.updateMatrix();

            FontRenderer fr = r.getFontRenderer();

            float midX = (TileRenderer.TILE_SIZE - fr.getWidth() * 2) * 0.5f * 0.5f;
            float midY = (TileRenderer.TILE_SIZE - (fr.getHeight() - 1) * 2) * 0.5f * 0.5f;
            for(int x = 0; x < map.getWidth(); x++)
            {
                for(int y = 0; y < map.getHeight(); y++)
                {
                    Tile t = map.getTile(x, y);
                    float tx = midX + TileRenderer.TILE_SIZE * x * 0.5f;
                    float ty =  midY + TileRenderer.TILE_SIZE * y * 0.5f;
                    if(t.isBlockingMovement(p))
                    {
                        fr.drawString(tx, ty, true, "&4-");
                    }
                    else
                    {
                        fr.drawString(tx, ty, true, OVERLAY[map.getTile(x, y).getEnergyCost(p) - 1]);
                    }
                }
            }
        }
    }
}
