package me.hammerle.supersnuvi.gamelogic;

import java.io.File;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeSet;
import java.util.stream.Collectors;
import me.hammerle.supersnuvi.entity.Entity;
import me.hammerle.supersnuvi.entity.Hero;
import me.hammerle.supersnuvi.tiles.BottledSoulTile;
import me.hammerle.supersnuvi.tiles.Location;
import me.hammerle.supersnuvi.tiles.StartTile;
import me.hammerle.supersnuvi.tiles.Tile;
import me.hammerle.supersnuvi.util.CollisionBox;
import me.hammerle.supersnuvi.rendering.IRenderer;

public final class Level 
{
    public final static double MAX_ENERGY = 100.0;
    
    private final StateRenderer state;
    private final IRenderer renderer;
    
    private final boolean worldLoaded;
    private final LevelData data;
    
    private final String name;
    
    private final HashMap<Integer, Entity> entities;
    private Hero hero;
    private int entityCounter;
    
    private boolean shouldReset;
    private boolean done;
    
    private int souls;
    private int maxSouls;
    
    private double time;
    
    private TreeSet<Point> spawns;
    
    private double energy;
    
    public Level(StateRenderer state, File f)
    {
        this.state = state;
        this.renderer = state.getRenderer();
        this.data = new LevelData(f);
        this.worldLoaded = data.load();
        this.entities = new HashMap<>();
        this.entityCounter = 0;
        this.shouldReset = false;
        this.done = false;
        this.time = 0.0;
        
        this.spawns = new TreeSet<>();
        
        if(worldLoaded)
        {
            this.name = data.getString("name", "error");
            maxSouls = 0;
            data.forEachInteractTile((x, y, tile) -> 
            {
                if(tile == BottledSoulTile.ID)
                {
                    maxSouls++;
                }
                else if(tile == StartTile.ID)
                {
                    spawns.add(new Point(x, y));
                }
            });
        }
        else
        {
            this.name = "error";
            maxSouls = 0;
        }
        
        // there must be at least one spawn
        if(spawns.isEmpty())
        {
            spawns.add(new Point(5, 5));
        }
        
        resetLevel();
    }
    
    // -------------------------------------------------------------------------
    // basic stuff
    // -------------------------------------------------------------------------

    public String getName() 
    {
        return name;
    }
    
    public void finishLevel()
    {
        shouldReset = true;
        done = true;
    }
    
    public boolean shouldFinish()
    {
        return done;
    }
    
    public boolean shouldReset()
    {
        return shouldReset;
    }
    
    public void scheduleReset()
    {
        shouldReset = true;
    }
    
    public void resetLevel()
    {
        state.resetTiles();
        energy = MAX_ENERGY;
        souls = 0;
        shouldReset = false;
        done = false;
        Hero h = spawnHero();
        hero = h;
        entities.clear();
        entities.put(entityCounter++, h);
    }
    
    public Hero spawnHero()
    {
        Hero h = (Hero) entities.values().stream().filter(ent -> ent instanceof Hero).findFirst().orElse(null);
        if(h == null || hero.getX() < 0)
        {
            // first spawn or out of map, use first spawn
            Point p = spawns.first();
            return new Hero(this, renderer.toCoord(p.getX()), renderer.toCoord(p.getY()));
        }
        else
        {
            // hero is somewhere in the map, getting last spawn
            Point p = spawns.floor(new Point(renderer.toBlock(hero.getX()), renderer.toBlock(hero.getY())));
            if(p == null)
            {
                p = spawns.first();
            }
            return new Hero(this, renderer.toCoord(p.getX()), renderer.toCoord(p.getY()));
        }
    }
    
    public void increaseSouls()
    {
        souls++;
    }
    
    public void addEnergy(double d)
    {
        energy += d;
        if(energy > MAX_ENERGY)
        {
            energy = MAX_ENERGY;
        }
        else if(energy < 0.0)
        {
            energy = 0.0;
        }
    }
    
    public double getEnergy()
    {
        return energy;
    }
    
    // -------------------------------------------------------------------------
    // tick
    // -------------------------------------------------------------------------
    
    public void update()
    {
        if(worldLoaded)
        {
            if(hero.getY() < 0)
            {
                resetLevel();
                return;
            }
            
            time += 0.0125;
            
            state.tickTiles();
            
            // doing entity logic first
            entities.values().forEach(entity -> 
            {
                entity.tick();
            });

            if(!hero.isMoving())
            {
                addEnergy(0.1);
            }
        }
    }   
    
    public void render()
    {
        if(worldLoaded)
        {
            // setting the right view center
            double rWidth = renderer.getWidth();
            double rHeight = renderer.getHeight();
            
            double centerX = Math.min(Math.max(0, hero.getX() - rWidth / 2), data.getWidth() * 32 - rWidth);
            double centerY = Math.min(Math.max(0, hero.getY() - rHeight / 2), data.getHeight() * 32 - rHeight);
            
            renderer.setViewCenter(centerX, centerY);
            
            int startX = renderer.getFirstVisibleBlockX();
            int startY = renderer.getFirstVisibleBlockY();
            int endX = Math.min(renderer.getLastVisibleBlockX() + 1, data.getWidth());
            int endY = Math.min(renderer.getLastVisibleBlockY() + 1, data.getHeight());
            
            data.forEachBackground((x, y, tile) ->
            {
                if(tile != -1)
                {
                    Tile t = state.getTile(tile);
                    renderer.drawBlockImage(t.getImage(x, y), x, y, t.getRenderOffsetX(), t.getRenderOffsetY());
                }
            }, startX, endX, startY, endY);
            
            entities.values().forEach(en -> 
            {
                if(en.drawImageFlipped())
                {
                    renderer.drawFlippedImage(en.getImage(), en.getRenderX() + en.getRenderOffsetX(), en.getRenderY() + en.getRenderOffsetY());
                }
                else
                {
                    renderer.drawImage(en.getImage(), en.getRenderX() + en.getRenderOffsetX(), en.getRenderY() + en.getRenderOffsetY());
                }
            });
            
            data.forEachForeground((x, y, tile) ->
            {
                if(tile != -1)
                {
                    Tile t = state.getTile(tile);
                    renderer.drawBlockImage(t.getImage(x, y), x, y, t.getRenderOffsetX(), t.getRenderOffsetY());
                }
            }, startX, endX, startY, endY);
            
            // menu rendering
            // |-------------------------------------| 
            // | B00/00 | ENERGYENERGY | HPHP | TIME |
            // |-------------------------------------| 
            renderer.prepareTextDrawing(255, 255, 255, 1.0, 40);
            {
                double baseLine = renderer.getTextHeight(1);
                double line = baseLine * 1.5;
                double y = baseLine * 0.25;
                
                renderer.save();
                {
                    renderer.setFillColor(0, 0, 0, 0.5);
                    renderer.fillRectangle(0, 0, renderer.getWidth(), line);  
                }
                renderer.restore();

                // soul rendering
                char[] c = new char[5];
                if(souls <= 9)
                {
                    c[0] = '0';
                    c[1] = (char) (souls + '0');
                }
                else if(souls > 99)
                {
                    c[0] = 'X';
                    c[1] = 'X';
                }
                else
                {
                    c[0] = (char) ((souls / 10) + '0');
                    c[1] = (char) ((souls % 10) + '0');
                }
                c[2] = '/';
                if(maxSouls <= 9)
                {
                    c[3] = '0';
                    c[4] = (char) (maxSouls + '0');
                }
                else if(maxSouls > 99)
                {
                    c[3] = 'X';
                    c[4] = 'X';
                }
                else
                {
                    c[3] = (char) ((maxSouls / 10) + '0');
                    c[4] = (char) ((maxSouls % 10) + '0');
                }

                double x = 5;
                renderer.drawFixedImage(BottledSoulTile.IMAGE, x, 0, line, line);
                x += line;
                renderer.drawText(x, y, c);
                x += renderer.getTextWidth(5) + 10;

                // energy rendering
                double leftX = (renderer.getWidth() - 40.0 - renderer.getTextWidth(10) - line) * 0.5;
                //double energyWidth = renderer.getWidth() * 0.2;
                //double ex = (renderer.getWidth() / 2) - energyWidth;
                //energyWidth *= 2;
                renderer.save();
                {
                    renderer.setFillColor(0, 0, 0, 1.0);
                    renderer.fillRectangle(x, y, leftX, baseLine);
                    renderer.setFillColor(0, 0, 150, 1.0);
                    renderer.fillRectangle(x + 3, y + 3, (leftX * (energy / MAX_ENERGY)) - 6, baseLine - 6);
                }
                renderer.restore();
                x += leftX + 10;
                
                // hp rendering
                renderer.save();
                {
                    renderer.setFillColor(0, 0, 0, 1.0);
                    renderer.fillRectangle(x, y, leftX, baseLine);
                    renderer.setFillColor(150, 0, 0, 1.0);
                    renderer.fillRectangle(x + 3, y + 3, (leftX * (hero.getHealth() / hero.getMaxHealth())) - 6, baseLine - 6);
                }
                renderer.restore();
                x += leftX + 10;

                // time rendering
                if(time >= 999.9)
                {
                    c[0] = '9';
                    c[1] = '9';
                    c[2] = '9';
                    c[3] = '.';
                    c[4] = '9';
                    renderer.drawText(x, y, c);
                }
                else
                {
                    renderer.drawText(x, y, String.format("%05.1f", time).toCharArray());
                }
            }
            renderer.stopTextDrawing();
        }
    }
    
    // -------------------------------------------------------------------------
    // collision box, interaction layer
    // -------------------------------------------------------------------------
    
    public Tile getInteractionTile(int x, int y)
    {
        int i = data.getInteractionTile(x, y);
        if(i == -1)
        {
            return StateRenderer.FALLBACK_TILE;
        }
        return state.getInteractionTile(i);
    }
    
    public CollisionBox getTileMovementBox(int x, int y)
    {
        int i = data.getInteractionTile(x, y);
        if(i == -1)
        {
            return CollisionBox.NULL_BOX;
        }
        Tile tile = state.getInteractionTile(i);
        return tile.getMovementBox(x, y).reset().offset(renderer.toCoord(x), renderer.toCoord(y));
    }
    
    public LinkedList<Location> getTilesInMovementOf(CollisionBox cb)
    {
        LinkedList<Location> boxes = new LinkedList<>();
        int startX = renderer.toBlock(cb.getMinX());
        int endX = renderer.toBlock(cb.getMaxX());
        int startY = renderer.toBlock(cb.getMinY());
        int endY = renderer.toBlock(cb.getMaxY());
        
        for(int x = startX; x <= endX; x++)
        {
            for(int y = startY; y <= endY; y++)
            {
                if(getTileMovementBox(x, y).intersects(cb))
                {
                    boxes.add(new Location(getInteractionTile(x, y), x, y));
                }
            }
        }
        return boxes;
    }
    
    public CollisionBox getTileCollisionBox(int x, int y)
    {
        int i = data.getInteractionTile(x, y);
        if(i == -1)
        {
            return CollisionBox.NULL_BOX;
        }
        Tile tile = state.getInteractionTile(i);
        return tile.getCollisionBox(x, y).reset().offset(renderer.toCoord(x), renderer.toCoord(y));
    }
    
    public LinkedList<Location> getTilesCollidingWith(CollisionBox cb)
    {
        LinkedList<Location> boxes = new LinkedList<>();
        int startX = renderer.toBlock(cb.getMinX());
        int endX = renderer.toBlock(cb.getMaxX());
        int startY = renderer.toBlock(cb.getMinY());
        int endY = renderer.toBlock(cb.getMaxY());
        
        for(int x = startX; x <= endX; x++)
        {
            for(int y = startY; y <= endY; y++)
            {
                if(getTileCollisionBox(x, y).intersects(cb))
                {
                    boxes.add(new Location(getInteractionTile(x, y), x, y));
                }
            }
        }
        return boxes;
    }
    
    public List<Entity> getEntitiesCollidingWith(Entity not, CollisionBox cb)
    {
        return entities.values().stream().filter(ent -> ent != not && ent.getBox().intersects(cb)).collect(Collectors.toList());
    }
    
    public LinkedList<CollisionBox> getMovementBoxesAt(Entity not, CollisionBox cb)
    {
        LinkedList<CollisionBox> boxes = new LinkedList<>(entities.values().stream()
                        .filter(ent -> ent != not && ent.getBox().intersects(cb))
                        .map(ent -> ent.getBox())
                        .collect(Collectors.toList()));
        int startX = renderer.toBlock(cb.getMinX());
        int endX = renderer.toBlock(cb.getMaxX());
        int startY = renderer.toBlock(cb.getMinY());
        int endY = renderer.toBlock(cb.getMaxY());
        
        CollisionBox box;
        for(int x = startX; x <= endX; x++)
        {
            for(int y = startY; y <= endY; y++)
            {
                box = getTileMovementBox(x, y);
                if(box.intersects(cb))
                {
                    boxes.add(box.copy());
                }
            }
        }
        return boxes;
    }
}
