package me.hammerle.supersnuvi.entity;

import me.hammerle.supersnuvi.entity.components.animator.EntityAnimator;
import me.hammerle.supersnuvi.entity.components.ai.EntityController;
import java.util.LinkedList;
import me.hammerle.supersnuvi.entity.components.Energy;
import me.hammerle.supersnuvi.entity.components.Health;
import me.hammerle.supersnuvi.entity.components.ItemCollector;
import me.hammerle.supersnuvi.entity.components.Movement;
import me.hammerle.supersnuvi.entity.components.MovementPenalty;
import me.hammerle.supersnuvi.gamelogic.Level;
import me.hammerle.supersnuvi.rendering.Game;
import me.hammerle.supersnuvi.tiles.Location;
import me.hammerle.supersnuvi.util.CollisionBox;
import me.hammerle.supersnuvi.util.Face;
import me.hammerle.supersnuvi.util.Utils;

public final class Entity
{
    // 1 Tile = 1 m
    // Pixel    m       s      Pixel
    // ----- * --- * ------ = -------
    //   m      s     Tick     Tick
    // added correction factor, real life gravity seems to strong
    public static double GRAVITY = Game.TILE_SIZE * 9.81 / 60 * 0.1;
    
    private double posX;
    private double posY;
    private final double width;
    private final double height;
    
    private double prevPosX;
    private double prevPrevY;
    
    private double prevMotionX;
    private double prevMotionY;
    private double motionX;
    private double motionY;
    private boolean onGround;
    
    private final CollisionBox collisionBox;
    
    private final Level level;
    
    // components
    protected EntityAnimator animator;   
    protected EntityController controller;
    protected Health health;
    protected Energy energy;
    protected Movement move;
    protected ItemCollector itemCollector;
    protected MovementPenalty movePenalty;
    
    protected Entity(Level level, double x, double y, double width, double height)
    {
        this.level = level;
        this.posX = x;
        this.posY = y;
        this.prevPosX = x;
        this.prevPrevY = y;
        this.motionX = 0;
        this.motionY = 0;
        this.onGround = false;
        this.collisionBox = new CollisionBox(0, 0, width, height);
        this.width = width;
        this.height = height;
        
        // components
        this.animator = EntityAnimator.NULL;
        this.controller = EntityController.NULL;
        this.health = Health.NULL;
        this.energy = Energy.NULL;
        this.move =  Movement.NULL;
        this.itemCollector =  ItemCollector.NULL;
        this.movePenalty = MovementPenalty.NULL;
    }

    public Level getLevel()
    {
        return level;
    }
    
    //--------------------------------------------------------------------------
    // components
    //--------------------------------------------------------------------------
    
    public EntityAnimator getAnimator()
    {
        return animator;
    }
    
    public Health getHealth()
    {
        return health;
    }
    
    public Energy getEnergy()
    {
        return energy;
    }
    
    public Movement getMovement()
    {
        return move;
    }
    
    public ItemCollector getItemCollector()
    {
        return itemCollector;
    }
    
    public MovementPenalty getMovementPenalty()
    {
        return movePenalty;
    }
    
    //--------------------------------------------------------------------------
    // basic stuff
    //--------------------------------------------------------------------------
    
    public double squaredDistance(Entity other)
    {
        return (posX - other.posX) * (posX - other.posX) + (posY - other.posY) * (posY - other.posY);
    }
    
    public double signedDistanceX(Entity other)
    {
        if(posX < other.posX)
        {
            return Math.min(0.0, posX + width - other.posX);
        }
        return Math.max(0.0, posX - (other.posX + other.width));
    }
    
    public double getWidth()
    {
        return width;
    }
    
    public double getHeight()
    {
        return height;
    }
    
    public double getX()
    {
        return posX;
    }
    
    public double getY()
    {
        return posY;
    }
    
    //--------------------------------------------------------------------------
    // ticking
    //--------------------------------------------------------------------------
    
    public void tick() 
    {
        controller.tick();
        health.tick();
        movePenalty.reset();
        
        if(isAffectedByGravity())
        {
            motionY -= GRAVITY;
        }
        
        prevMotionX = motionX;
        prevMotionY = motionY;
        
        prevPosX = posX;
        prevPrevY = posY;
        setAllowedMotion();
        posX += motionX;
        posY += motionY;   
        
        motionX = Utils.round(motionX);
        motionY = Utils.round(motionY);
        
        if(motionY != 0)
        {
            onGround = false;
        }
        
        posX = Utils.round(posX);
        posY = Utils.round(posY);
        
        LinkedList<CollisionBox> list = level.getMovementBoxesAt(this, getBox());
        if(!list.isEmpty())
        {
            //System.out.println(getBox());
            //list.forEach(s -> System.out.println(s));
            //System.out.println("WELL");
            posX = prevPosX;
            posY = prevPrevY;
        }
        
        movePenalty.setFriction(0.7);
        doCollision();
        movePenalty.applyFriction();
        energy.tick();
        animator.tick();
    }
    
    //--------------------------------------------------------------------------
    // gravity, friction
    //--------------------------------------------------------------------------

    public boolean isAffectedByGravity()
    {
        return true;
    }
    
    public boolean isOnGround()
    {
        return onGround;
    }   
    
    public void setNoGround()
    {
        onGround = false;
    }   
    
    //--------------------------------------------------------------------------
    // collision stuff
    //--------------------------------------------------------------------------
    
    public CollisionBox getBox() 
    {
        return collisionBox.reset().offset(posX, posY);
    }
    
    private void setAllowedMotion()
    {
        if(motionX == 0 && motionY == 0)
        {
            return;
        }
        double dirX = motionX;
        double dirY = motionY;
        double startX = posX;
        double startY = posY;
        double endX = startX + dirX;
        double endY = startY + dirY;
        
        CollisionBox eBox = getBox();
        double halfWidth = eBox.getWidth() / 2;
        double halfHeight = eBox.getHeight() / 2;
        eBox = eBox.copy();
        
        startX += halfWidth;
        startY += halfHeight;
        endX += halfWidth;
        endY += halfHeight;
        
        // expanding area by the size of the entity
        CollisionBox expandedBox = eBox.copy().expand(motionX, motionY);
        
        LinkedList<CollisionBox> allBoxes = level.getMovementBoxesAt(this, expandedBox);
        if(allBoxes.isEmpty())
        {
            return;
        }        
        
        double x;
        double y;
        char lastWall = 0;
                
        if(dirX >= 0)
        {
            if(dirY <= 0) // Right - Down
            {
                for(CollisionBox box : allBoxes)
                {
                    box.grow(halfWidth, halfHeight);
                    if(endX <= box.getMinX() || endY >= box.getMaxY() ||
                        startX >= box.getMaxX() || startY <= box.getMinY())
                    {
                        continue;
                    }
                    x = Utils.interpolateX(startX, startY, endX, endY, box.getMaxY());
                    y = Utils.interpolateY(startX, startY, endX, endY, box.getMinX());
                    if(y > box.getMinY() && y < box.getMaxY())
                    {
                        endX = box.getMinX();
                        endY = y;
                        lastWall = 'x';
                    }
                    else if(x > box.getMinX() && x < box.getMaxX())
                    {
                        endY = box.getMaxY();
                        endX = x;
                        lastWall = 'y';
                    }
                    else if(x >= box.getMinX() && x <= box.getMaxX())
                    {
                        lastWall = 'y';
                    }
                }
                if(lastWall == 'x')
                {
                    double lineEndY = startY + dirY;
                    for(CollisionBox box : allBoxes)
                    {
                        if(endX <= box.getMinX() || lineEndY >= box.getMaxY() ||
                            endX >= box.getMaxX() || endY <= box.getMinY())
                        {
                            continue;
                        }
                        lineEndY = box.getMaxY();
                    }
                    endY = lineEndY;
                }
                else if(lastWall == 'y')
                {
                    double lineEndX = startX + dirX;
                    for(CollisionBox box : allBoxes)
                    {
                        if(lineEndX <= box.getMinX() || endY >= box.getMaxY() ||
                            endX >= box.getMaxX() || endY <= box.getMinY())
                        {
                            continue;
                        }
                        lineEndX = box.getMinX();
                    }
                    endX = lineEndX;
                }
            }
            else // Right - Up
            {
                for(CollisionBox box : allBoxes)
                {
                    box.grow(halfWidth, halfHeight);
                    if(endX <= box.getMinX() || endY <= box.getMinY() ||
                        startX >= box.getMaxX() || startY >= box.getMaxY())
                    {
                        continue;
                    }
                    x = Utils.interpolateX(startX, startY, endX, endY, box.getMinY());
                    y = Utils.interpolateY(startX, startY, endX, endY, box.getMinX());
                    if(y > box.getMinY() && y < box.getMaxY())
                    {
                        endX = box.getMinX();
                        endY = y;
                        lastWall = 'x';
                    }
                    else if(x > box.getMinX() && x < box.getMaxX())
                    {
                        endY = box.getMinY();
                        endX = x;
                        lastWall = 'y';
                    }
                    else if(x >= box.getMinX() && x <= box.getMaxX())
                    {
                        lastWall = 'y';
                    }
                }
                if(lastWall == 'x')
                {
                    double lineEndY = startY + dirY;
                    for(CollisionBox box : allBoxes)
                    {
                        if(endX <= box.getMinX() || lineEndY <= box.getMinY() ||
                            endX >= box.getMaxX() || endY >= box.getMaxY())
                        {
                            continue;
                        }
                        lineEndY = box.getMinY();
                    }
                    endY = lineEndY;
                }
                else if(lastWall == 'y')
                {
                    double lineEndX = startX + dirX;
                    for(CollisionBox box : allBoxes)
                    {
                        if(lineEndX <= box.getMinX() || endY >= box.getMaxY() ||
                            endX >= box.getMaxX() || endY <= box.getMinY())
                        {
                            continue;
                        }
                        lineEndX = box.getMinX();
                    }
                    endX = lineEndX;
                }
            }
        }
        else
        {
            if(dirY <= 0) // Left - Down
            {
                for(CollisionBox box : allBoxes)
                {
                    box.grow(halfWidth, halfHeight);
                    if(endX >= box.getMaxX() || endY >= box.getMaxY() ||
                        startX <= box.getMinX() || startY <= box.getMinY())
                    {
                        continue;
                    }
                    x = Utils.interpolateX(startX, startY, endX, endY, box.getMaxY());
                    y = Utils.interpolateY(startX, startY, endX, endY, box.getMaxX());
                    if(y > box.getMinY() && y < box.getMaxY())
                    {
                        endX = box.getMaxX();
                        endY = y;
                        lastWall = 'x';
                    }
                    else if(x > box.getMinX() && x < box.getMaxX())
                    {
                        endY = box.getMaxY();
                        endX = x;
                        lastWall = 'y';
                    }
                    else if(x >= box.getMinX() && x <= box.getMaxX())
                    {
                        lastWall = 'y';
                    }
                }
                if(lastWall == 'x')
                {
                    double lineEndY = startY + dirY;
                    for(CollisionBox box : allBoxes)
                    {
                        if(endX <= box.getMinX() || lineEndY >= box.getMaxY() ||
                            endX >= box.getMaxX() || endY <= box.getMinY())
                        {
                            continue;
                        }
                        lineEndY = box.getMaxY();
                    }
                    endY = lineEndY;
                }
                else if(lastWall == 'y')
                {
                    double lineEndX = startX + dirX;
                    for(CollisionBox box : allBoxes)
                    {
                        if(lineEndX >= box.getMaxX() || endY >= box.getMaxY() ||
                            endX <= box.getMinX() || endY <= box.getMinY())
                        {
                            continue;
                        }
                        lineEndX = box.getMaxX();
                    }
                    endX = lineEndX;
                }
            }
            else  // Left - Up
            {
                for(CollisionBox box : allBoxes)
                {
                    box.grow(halfWidth, halfHeight);
                    if(endX >= box.getMaxX() || endY <= box.getMinY() ||
                        startX <= box.getMinX() || startY >= box.getMaxY())
                    {
                        continue;
                    }
                    x = Utils.interpolateX(startX, startY, endX, endY, box.getMinY());
                    y = Utils.interpolateY(startX, startY, endX, endY, box.getMaxX());
                    if(y > box.getMinY() && y < box.getMaxY())
                    {
                        endX = box.getMaxX();
                        endY = y;
                        lastWall = 'x';
                    }
                    else if(x > box.getMinX() && x < box.getMaxX())
                    {
                        endY = box.getMinY();
                        endX = x;
                        lastWall = 'y';
                    }
                    else if(x >= box.getMinX() && x <= box.getMaxX())
                    {
                        lastWall = 'y';
                    }
                }
                if(lastWall == 'x')
                {
                    double lineEndY = startY + dirY;
                    for(CollisionBox box : allBoxes)
                    {
                        if(endX <= box.getMinX() || lineEndY <= box.getMinY() ||
                            endX >= box.getMaxX() || endY >= box.getMaxY())
                        {
                            continue;
                        }
                        lineEndY = box.getMinY();
                    }
                    endY = lineEndY;
                }
                else if(lastWall == 'y')
                {
                    double lineEndX = startX + dirX;
                    for(CollisionBox box : allBoxes)
                    {
                        if(lineEndX >= box.getMaxX() || endY >= box.getMaxY() ||
                            endX <= box.getMinX() || endY <= box.getMinY())
                        {
                            continue;
                        }
                        lineEndX = box.getMaxX();
                    }
                    endX = lineEndX;
                }
            }
        }
        
        motionX = endX - startX;
        motionY = endY - startY;
        
        if(dirY < 0 && motionY == 0)
        {
            onGround = true;
        }
        
        if(Math.abs(motionY) >= 1000 || Math.abs(motionX) >= 1000)
        {
            System.err.println("illegal movement " + motionX + " " + motionY);
            System.exit(0);
        }
        
        /*if(!onGround && Math.abs(dirY) - Math.abs(motionY) > 1)
        {
            System.out.println("\nVORHER " + dirX + " " + dirY);
            System.out.println("NACHHER " + motionX + " " + motionY);
            System.out.println("\nPOS " + posX + " " + posY);
            System.exit(0);
        }*/
        //System.exit(0);
    }
    
    private void doCollision()
    {
        CollisionBox box = getBox().copy();
        for(Face f : Face.values())
        {
            box.reset();
            box.expand(f.getCollisionOffsetX(), f.getCollisionOffsetY());
            level.getEntitiesCollidingWith(this, box).forEach(ent -> this.onCollideWithEntity(ent, f));
            level.getTilesCollidingWith(box).forEach(loc -> 
            {
                this.onCollideWithTile(loc, f);
                loc.getTile().onEntityCollide(this, loc.getX(), loc.getY(), f);
            });
        }
    }
    
    public void onCollideWithTile(Location loc, Face face)
    {
        controller.onCollideWithTile(loc, face);
    }
    
    public void onCollideWithEntity(Entity ent, Face face)
    {
        controller.onCollideWithEntity(ent, face);
    }
    
    //--------------------------------------------------------------------------
    // basic coord and motion stuff
    //--------------------------------------------------------------------------
    
    public double getPreviousX()
    {
        return prevPosX;
    }
    
    public double getPreviousY()
    {
        return prevPrevY;
    }
    
    public boolean isMoving()
    {
        return motionX != 0.0 || motionY != 0.0;
    }
    
    public final double getMotionX()
    {
        return motionX;
    }
    
    public final void setMotionX(double motionX)
    {
        this.motionX = motionX;
    }
    
    public final double getMotionY()
    {
        return motionY;
    }
    
    public final void setMotionY(double motionY)
    {
        this.motionY = motionY;
    }
    
    public final double getPreviousMotionX()
    {
        return prevMotionX;
    }
    
    public final double getPreviousMotionY()
    {
        return prevMotionY;
    }
}
