package projectzombie.entity;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Random;

import projectzombie.util.ClassBdf;
import projectzombie.util.TileState;
import bdf.types.BdfNamedList;
import bdf.types.BdfObject;
import gl_engine.MathHelpers;
import gl_engine.vec.Vec2d;
import gl_engine.vec.Vec2i;
import gl_engine.vec.Vec3d;
import projectzombie.Main;
import projectzombie.init.Entities;
import projectzombie.model.IModel;
import projectzombie.tiles.Tile;
import projectzombie.world.chunk.Chunk;
import projectzombie.world.layer.Layer;

public abstract class Entity implements ClassBdf
{
	public double hitbox = 1;
	public boolean isSolid = false;
	public Chunk chunk;
	private TileState tile_front;
	private TileState tile_back;
	protected static final Random rand = new Random();
	public boolean emitsLight = false;
	private boolean isDead = false;
	protected boolean hasGravity = true;
	
	private Vec3d pos;
	private Vec3d velocity;
	
	public abstract IModel getModel();
	
	public boolean isDead() {
		return isDead;
	}
	
	public Vec3d getPos() {
		return pos;
	}
	
	public void setPos(Vec3d pos) {
		this.pos = pos;
	}
	
	public Vec3d getVelocity() {
		return velocity;
	}
	
	public void setVelocity(Vec3d velocity) {
		this.velocity = velocity;
	}
	
	protected double getTilesLightDissipation() {
		if(chunk == null) return 0;
		TileState tsf = chunk.getFrontTile(pos.xz().toInt());
		TileState tsb = chunk.getBackTile(pos.xz().toInt());
		return MathHelpers.biggest(
				tsf.tile.getLightDissipation(tsf),
				tsb.tile.getLightDissipation(tsb));
	}
	
	protected double getLightWithHeight(double light) {
		double height = Math.abs(getVelocity().y) + 1;
		return light - (this.getTilesLightDissipation() * height);
	}
	
	public int getID() {
		for(int i=0;i<Entities.ENTITIES.size();i++) {
			Class<? extends Entity> ec = Entities.ENTITIES.get(i);
			if(ec == this.getClass()) {
				return i;
			}
		}
		return -1;
	}
	
	public static Entity loadEntity(BdfObject bdf)
	{
		try
		{
			// Load the entity id
			BdfNamedList nl = bdf.getNamedList();
			int id = nl.get("id").getInteger();
			
			// Send back null if the id is out of range
			if(id < 0 || id >= Entities.ENTITIES.size()) {
				System.out.println("Warning: Invalid ID detected: " + id);
				return null;
			}
			
			// Get the class and the constructor
			Class<? extends Entity> ecl = Entities.ENTITIES.get(id);
			Constructor<? extends Entity> econ = ecl.getConstructor(BdfObject.class);
			
			// Send back the new entity
			return econ.newInstance(bdf);
		}
		
		catch(
				NoSuchMethodException |
				SecurityException |
				InstantiationException |
				IllegalAccessException |
				IllegalArgumentException |
				InvocationTargetException e)
		{
			e.printStackTrace();
			
			// Send null if there was an issue
			return null;
		}
	}
	
	public Entity(Vec3d pos, Vec3d velocity)
	{
		// Store the specified values
		this.velocity = velocity.copy();
		this.pos = pos.copy();
	}
	
	protected Entity() {
	}
	
	@Override
	public void BdfClassLoad(BdfObject bdf) {
		BdfNamedList nl = bdf.getNamedList();
		pos = new Vec3d(nl.get("pos"));
		velocity = new Vec3d(nl.get("velocity"));
		hitbox = nl.get("hitbox").getDouble();
		isSolid = nl.get("isSolid").getBoolean();
		emitsLight = nl.get("emitsLight").getBoolean();
		hasGravity = nl.get("hasGravity").getBoolean();
	}
	
	@Override
	public void BdfClassSave(BdfObject bdf)
	{
		// Get the ID and return if its invalid (entity can't be saved)
		int id = getID();
		if(id == -1) return;
		
		BdfNamedList nl = bdf.getNamedList();
		nl.set("id", bdf.newObject().setInteger(getID()));
		
		BdfObject pos_bdf = bdf.newObject();
		pos.BdfClassSave(pos_bdf);
		
		BdfObject velocity_bdf = bdf.newObject();
		velocity.BdfClassSave(velocity_bdf);
		
		nl.set("pos", pos_bdf);
		nl.set("velocity", velocity_bdf);
		nl.set("hitbox", bdf.newObject().setDouble(hitbox));
		nl.set("isSolid", bdf.newObject().setBoolean(isSolid));
		nl.set("emitsLight", bdf.newObject().setBoolean(emitsLight));
		nl.set("hasGravity", bdf.newObject().setBoolean(hasGravity));
	}
	
	public void tick(Chunk chunk, Layer layer)
	{
		//speed = 1;
		//angle = MathHelpers.mod(angle, 360);
		if(chunk == null) chunk = layer.getChunk(pos.xz());
		this.chunk = chunk;
		
		Vec2i tpos = pos.xz().toInt();
		boolean pos_is_legal = moveIsLegal(pos.xz());
		
		tile_back = chunk.getBackTile(tpos);
		tile_front = chunk.getFrontTile(tpos);
		
		if(velocity.x != 0 || velocity.y != 0 || velocity.z != 0)
		{
			Vec3d new_pos = pos.add(velocity);
			
			double slipperiness = Math.max(
					tile_back.tile.getSlipperiness(tile_back),
					tile_front.tile.getSlipperiness(tile_front));
			
			if(isSolid) {
				
				if(moveIsLegal(new Vec2d(new_pos.x, pos.z)) || !pos_is_legal) {
					pos.x = new_pos.x;
				} else {
					velocity.x *= -0.25;
				}
				
				if(moveIsLegal(new Vec2d(pos.x, new_pos.z)) || !pos_is_legal) {
					pos.z = new_pos.z;
				} else {
					velocity.z *= -0.25;
				}
				
			} else {
				pos.x = new_pos.x;
				pos.z = new_pos.z;
			}
			
			if(new_pos.y > 0) {
				pos.y = new_pos.y;
			} else if(pos.y >= 0) {
				velocity.y *= -0.25;
				velocity.x *= slipperiness;
				velocity.z *= slipperiness;
				pos.y = 0;
			}
		}
		
		if(hasGravity) {
			velocity.y -= MathHelpers.FallSpeed;
		}
		
		if(isSolid)
		{
			moveAwayFromSolidEntities(layer);
			
			if(pos.y <= 0) {
				tile_back.tile.onWalkedOn(chunk, layer, tpos, this, tile_back);
				tile_front.tile.onWalkedOn(chunk, layer, tpos, this, tile_front);
			}
			
			// Move the player out of a solid area
			if(!pos_is_legal)
			{
				Vec2i collision_pos = pos.xz().toInt();
				Vec2d direction = pos.xz().subtract(collision_pos.toDouble()).subtract(0.5);
				
				// Do this to prevent division by zero, just move in an arbitrary direction
				if(direction.x == 0 && direction.y == 0) {
					push(new Vec3d(0.1, 0, 0));
				}
				
				else {
					push(direction.multiply(0.1).xny());
				}
			}
		}
	}
	
	protected void moveAwayFromSolidEntities(Layer layer)
	{
		if(isSolid)
		{
			for(Entity e : layer.getNearbyEntities(pos.xz(), hitbox))
			{
				if(e.isSolid && e != this)
				{
					double distance = pos.distance(e.pos);
					double angle = MathHelpers.atan2(e.pos.z - pos.z, e.pos.x - pos.x);
					
					Vec3d vec_push = MathHelpers.moveTowards2(distance == 0 ? 1 : (0.5 / distance), angle).xny();
					
					push(vec_push.multiply(-0.125));
				}
			}
		}
	}
	
	public void push(Vec3d vec)
	{
		if(pos.y <= 0)
		{
			Layer layer = Main.world.getLayer();
			
			Vec2i tpos = pos.xz().toInt();
			tile_back = layer.getBackTile(tpos);
			tile_front = layer.getFrontTile(tpos);
			
			double slipperiness = Math.max(
					tile_back.tile.getSlipperiness(tile_back),
					tile_front.tile.getSlipperiness(tile_front));
			
			double resistance = Math.max(
					tile_back.tile.getResistance(tile_back),
					tile_front.tile.getResistance(tile_front));
			
			velocity = velocity.add(vec.multiply((1 - slipperiness) * (1 - resistance)));
		}
		
		else
		{
			velocity = velocity.add(vec);
		}
	}
	
	public void kill() {
		chunk.killEntity(this);
		isDead = true;
	}
	
	public boolean activateTile(Vec2i tpos)
	{
		// Get the tile position and the layer
		Layer layer = Main.world.getLayer();
		TileState tile_front = layer.getFrontTile(tpos);
		TileState tile_back = layer.getBackTile(tpos);
		
		// Activate both tiles
		return (
			tile_front.tile.onActivated(layer, tpos, this, tile_front) ||
			tile_back.tile.onActivated(layer, tpos, this, tile_back));
	}
	
	public void activateSteppedOnTile()
	{
		// Get the tile position and the layer
		Layer layer = Main.world.getLayer();
		Vec2i tpos = new Vec2i(MathHelpers.floor(pos.x)+0, MathHelpers.floor(pos.z)+0);
		
		// Activate both tiles
		tile_front.tile.onWalkedOn(chunk, layer, tpos, this, tile_front);
		tile_back.tile.onWalkedOn(chunk, layer, tpos, this, tile_back);
	}
	
	private boolean moveIsLegal(Vec2d pos)
	{
		Vec2i t_pos = new Vec2i(MathHelpers.floor(pos.x)+0, MathHelpers.floor(pos.y)+0);
		Chunk chunk = Main.world.getLayer().getChunk(pos);
		tile_back = chunk.getBackTile(t_pos);
		tile_front = chunk.getFrontTile(t_pos);
		
		// Is this entity solid
		if(isSolid)
		{
			// Check the tile the player is standing on
			Vec2i tpos = new Vec2i(MathHelpers.floor(pos.x)+0, MathHelpers.floor(pos.y)+0);
			
			{
				// Get the tile
				Tile t = tile_back.tile;
				
				// Check the tiles hitbox if the tile is solid
				if(t.tileSolid)
				{
					// Is the entity in the tiles hitbox
					Vec2d check = pos.subtract(new Vec2d(tpos.x + 0.5, tpos.y + 0.5));
					if(Math.max(Math.abs(check.x), Math.abs(check.y)) <= t.tileHitbox)
					{
						// Send back false
						return false;
					}
				}
			}
			
			{
				// Get the front tile
				Tile t = tile_front.tile;
				
				// Check the tiles hitbox if the tile is solid
				if(t.tileSolid)
				{
					// Is the entity in the tiles hitbox
					Vec2d check = pos.subtract(new Vec2d(tpos.x + 0.5, tpos.y + 0.5));
					if(Math.max(Math.abs(check.x), Math.abs(check.y)) <= t.tileHitbox)
					{
						// Send back false
						return false;
					}
				}
			}
		}
		
		// Send back true if everything passes
		return true;
	}
	
	public double getLightLevel() {
		return 0;
	}
}
