package me.hammerle.supersnuvi.gamelogic;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import me.hammerle.supersnuvi.entity.Entity;
import me.hammerle.supersnuvi.entity.Hero;
import me.hammerle.supersnuvi.input.IKeyHandler;
import me.hammerle.supersnuvi.rendering.IGameRenderer;
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;

public final class Level 
{
    private final StateRenderer state;
    private final IGameRenderer renderer;
    private final Color TRANSPARENT_WHITE = new Color(1, 1, 1, 0.5);
    
    private final String name;
    
    private boolean worldLoaded;
    
    private int width;
    private int height;
    private int[][] background;
    private int[][] background2;
    private int[][] foreground;
    
    private final HashMap<Integer, Entity> entities;
    private Hero hero;
    private int entityCounter;
    
    private boolean shouldReset;
    private boolean done;
    
    private int souls;
    private final int maxSouls;
    
    public Level(StateRenderer state, File f)
    {
        this.state = state;
        this.renderer = state.getRenderer();
        this.name = getNiceName(f.getName());
        this.worldLoaded = loadMap(f);
        this.entities = new HashMap<>();
        this.entityCounter = 0;
        this.shouldReset = false;
        this.done = false;
        
        resetLevel();
        
        int sum = 0;
        for(int x = 0; x < width; x++)
        {
            for(int y = 0; y < height; y++)
            {
                if(background2[x][y] == BottledSoulTile.ID)
                {
                    sum++;
                }
            }
        }
        maxSouls = sum;
    }
    
    private String getNiceName(String badName)
    {
        int i = badName.indexOf('.');
        if(i != -1)
        {
            badName = badName.substring(0, i);
        }
        return badName.replace("_", " ");
    }
    
    // -------------------------------------------------------------------------
    // 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();
        souls = 0;
        shouldReset = 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)
        {
            int x = 0;
            int y;
            for(; x < width; x++)
            {
                for(y = 0; y < height; y++)
                {
                    if(background2[x][y] == StartTile.ID)
                    {
                        return new Hero(this, renderer.toCoord(x), renderer.toCoord(y), 32, 64);
                    }
                }
            }
        }
        else
        {
            int x = Math.min(width - 1, renderer.toBlock(h.getX()));
            int y = 0;
            for(; x >= 0; x--)
            {
                for(y = 0; y < height; y++)
                {
                    if(background2[x][y] == StartTile.ID)
                    {
                        return new Hero(this, renderer.toCoord(x), renderer.toCoord(y), 32, 64);
                    }
                }
            }
            
            // hero is left from the first spawn, searching from the start
            for(x = 0; x < width; x++)
            {
                for(y = 0; y < height; y++)
                {
                    if(background2[x][y] == StartTile.ID)
                    {
                        return new Hero(this, renderer.toCoord(x), renderer.toCoord(y), 32, 64);
                    }
                }
            }
        }
        // should never happen
        return new Hero(this, 0, 0, 32, 64);
    }
    
    public void increaseSouls()
    {
        souls++;
    }
    
    // -------------------------------------------------------------------------
    // map loading
    // -------------------------------------------------------------------------
    
    private void printError(File f, String message)
    {
        System.err.println("level error '" + f + "' - " + message);
        width = 0;
        height = 0;
        background = null;
        background2 = null;
        foreground = null;
        worldLoaded = false;
    }
    
    private boolean loadMap(File f)
    {
        if(!f.exists())
        {
            printError(f, "level does not exist");
            return false;
        }
        List<String> strings;
        try
        {
            strings = Files.readAllLines(f.toPath());
        } 
        catch(IOException ex) 
        {
            printError(f, "IOException on load");
            return false;
        }
        if(strings.isEmpty())
        {
            printError(f, "level is empty");
            return false;
        }
        // assuming we have the following format
        // X, X, X
        // X, X, X
        // #
        // X, X, X
        // X, X, X
        // #
        // X, X, X
        // X, X, X
        int size = strings.size();
        
        int first = 0;
        while(true)
        {
            if(first >= size)
            {
                printError(f, "missing first #");
                return false;
            }
            if(strings.get(first).equals("#"))
            {
                break;
            }
            first++;
        }
        int second = first + 1;
        while(true)
        {
            if(second >= size)
            {
                printError(f, "missing second #");
                return false;
            }
            if(strings.get(second).equals("#"))
            {
                break;
            }
            second++;
        }
        
        if(first == 0 || first != second - first - 1 || second - first - 1 != size - second - 1)
        {
            printError(f, "invalid number of lines");
            return false;
        }
        
        height = first;
        int i = 0;
        int mod = first - 1;
        try
        {
            // background 1
            String[] parts = strings.get(i).split("[ ]*,[ ]*");
            width = parts.length;
            background = new int[width][height];
            for(int j = 0; j < parts.length; j++)
            {
                background[j][mod - i] = Integer.parseInt(parts[j].trim());
            }
            i++;
            while(i < first)
            {
                parts = strings.get(i).split("[ ]*,[ ]*");
                if(parts.length != width)
                {
                    printError(f, "invalid number of line entries");
                    return false;
                }
                for(int j = 0; j < parts.length; j++)
                {
                    background[j][mod - i] = Integer.parseInt(parts[j].trim());
                }
                i++;
            }

            // background 2
            i++;
            mod = second - 1;
            background2 = new int[width][height];
            while(i < second)
            {
                parts = strings.get(i).split("[ ]*,[ ]*");
                if(parts.length != width)
                {
                    printError(f, "invalid number of line entries");
                    return false;
                }
                for(int j = 0; j < parts.length; j++)
                {
                    background2[j][mod - i] = Integer.parseInt(parts[j].trim());
                }
                i++;
            }

            // foreground
            i++;
            mod = size - 1;
            foreground = new int[width][height];
            while(i < size)
            {
                parts = strings.get(i).split("[ ]*,[ ]*");
                if(parts.length != width)
                {
                    printError(f, "invalid number of line entries");
                    return false;
                }
                for(int j = 0; j < parts.length; j++)
                {
                    foreground[j][mod - i] = Integer.parseInt(parts[j].trim());
                }
                i++;
            }
            return true;
        }
        catch(NumberFormatException ex)
        {
            printError(f, "invalid number format on line " + (i + 1));
            return false;
        }
    }
    
    // -------------------------------------------------------------------------
    // tick
    // -------------------------------------------------------------------------
    
    public void tick(IKeyHandler keys)
    {
        if(worldLoaded)
        {
            // setting the right view center
            double centerX = Math.max(0, hero.getX() - (renderer.getWidth() / 2));
            double centerY = Math.max(0, hero.getY() - (renderer.getHeight() / 2));
            renderer.setViewCenter(centerX, centerY);
            
            // end
            
            if(hero.getY() < 0)
            {
                resetLevel();
                return;
            }
            
            state.tick();
            
            // doing entity logic first
            entities.values().forEach(entity -> 
            {
                entity.controlTick(keys);
                entity.resetMovementData();
                entity.tick();
            });

            int startX = renderer.getFirstVisibleBlockX();
            int startY = renderer.getFirstVisibleBlockY();
            int endX = Math.min(renderer.getLastVisibleBlockX() + 1, width);
            int endY = Math.min(renderer.getLastVisibleBlockY() + 1, height);
            
            Image sky = state.getTile(128, 0, 0);
            int id;
            for(int x = startX; x < endX; x++)
            {
                for(int y = startY; y < endY; y++)
                {
                    // drawing background 0
                    // prevents black spots
                    renderer.drawBlockImage(sky, x, y);
                    // end
                    id = background[x][y];
                    if(id != -1)
                    {
                        renderer.drawBlockImage(state.getTile(id, x, y), x, y);
                    }
                    id = background2[x][y];
                    if(id != -1)
                    {
                        renderer.drawBlockImage(state.getTile(id, x, y), x, y);
                    }
                }
            }
            
            // render entities here
            entities.values().forEach(en -> 
            {
                if(en.drawImageFlipped())
                {
                    renderer.drawFlippedImage(en.getImage(), en.getRenderX(), en.getRenderY());
                }
                else
                {
                    renderer.drawImage(en.getImage(), en.getRenderX(), en.getRenderY());
                }
            });
            // end entity rendering
            
            for(int x = startX; x < endX; x++)
            {
                for(int y = startY; y < endY; y++)
                {
                    id = foreground[x][y];
                    if(id != -1)
                    {
                        renderer.drawBlockImage(state.getTile(id, x, y), x, y);
                    }
                }
            }
            
            // rendering souls
            String s = souls + "/" + maxSouls;
            double w = renderer.getStringWidth(s) + BottledSoulTile.IMAGE.getWidth();
            renderer.fillRec(0, 0, w, BottledSoulTile.IMAGE.getHeight(), TRANSPARENT_WHITE);
            
            renderer.drawFixedImage(BottledSoulTile.IMAGE, 0, 0);
            renderer.prepareStringDrawing(Color.BLACK);
            double d = (BottledSoulTile.IMAGE.getHeight() - renderer.getLineHeight()) / 2;
            renderer.drawString(s, BottledSoulTile.IMAGE.getWidth(), d);
            renderer.stopStringDrawing();
        }
    }   
    
    // -------------------------------------------------------------------------
    // collision box, interaction layer
    // -------------------------------------------------------------------------
    
    public Tile getInteractionTile(int x, int y)
    {
        if(x < 0 || y < 0 || x >= width || y >= height)
        {
            return StateRenderer.FALLBACK_TILE;
        }
        return state.getInteractionTile(background2[x][y]);
    }
    
    public CollisionBox getTileMovementBox(int x, int y)
    {
        if(x < 0 || y < 0 || x >= width || y >= height)
        {
            return CollisionBox.NULL_BOX;
        }
        Tile tile = state.getInteractionTile(background2[x][y]);
        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)
    {
        if(x < 0 || y < 0 || x >= width || y >= height)
        {
            return CollisionBox.NULL_BOX;
        }
        Tile tile = state.getInteractionTile(background2[x][y]);
        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;
    }
}
