package com.pesets.game;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.MathUtils;
import com.pesets.game.items.EquipItem;
import com.pesets.game.items.Item;
import com.pesets.game.map.PassageMap;
import com.pesets.game.skills.Skill;
import com.pesets.game.skills.SkillFactory;

public class Player extends GameObject {
	public static final float VisionRange = 200;
	public static final float RespawnTime = 10;
	public static final float GlobalCastCd = 0.5f;
	
	private ShapeRenderer renderer = new ShapeRenderer();
	private Enemy target = null;
	
	private float timeToRespawn = 0;
	
	private Set<GameObject> ignoredTargets = new HashSet<GameObject>();
	private int experience = 0;
	private EquipItem[] eqiup = new EquipItem[5];
	private ArrayList<Item> inventory = new ArrayList<Item>(50);
	private int totalWeight = 0;
	private ArrayList<Skill> skills;
	private ArrayList<Skill> castSkills;
	private float castColdown = 0;
	private float dodgeChance = 0;
	private float berserkTime = 0;
	private float armorMultiplier = 1;
	private FourWaySprite sprite;
	private int firstStatePiority = 0, secondStatePriority = 1;
	private int gold = 0;

	public Player()
	{
		super(null);
		
		sprite = new FourWaySprite("player.png", 48, 64);
		speed = 100;
		strength = 10;
		agility = 10;
		stamina = 10;
		intelligence = 10;
		level = 1;
		
		skills = SkillFactory.generateSkills();
		castSkills = new ArrayList<Skill>();
		for (Skill s : skills)
			castSkills.add(s);
		
		for (int i = 0; i < 50; i++)
		{
			inventory.add(null);
		}
		width = sprite.width;
		height = sprite.height;
	}
	
	public void save(DataOutputStream out) throws IOException
	{
		out.writeInt(level);
		out.writeInt(experience);
		out.writeInt(stamina);
		out.writeInt(strength);
		out.writeInt(agility);
		out.writeInt(intelligence);
		out.writeInt(gold);
		out.writeInt(firstStatePiority);
		out.writeInt(secondStatePriority);
		for (Item it : eqiup)
		{
			out.writeBoolean(it == null);
			if (it != null)
				it.toStream(out);
		}
		for (Item it : inventory)
		{
			out.writeBoolean(it == null);
			if (it != null)
				it.toStream(out);
		}
		for (Skill s : skills)
		{
			out.writeInt(s.getLevel());
		}
		out.writeFloat(PlayerStatistics.getInstance().totalTimePlayed);
	}
	
	public void load(DataInputStream in) throws IOException
	{
		level = in.readInt();
		experience = in.readInt();
		stamina = in.readInt();
		strength = in.readInt();
		agility = in.readInt();
		intelligence = in.readInt();
		gold = in.readInt();
		firstStatePiority = in.readInt();
		secondStatePriority = in.readInt();
		for (int i = 0; i < eqiup.length; i++)
		{
			if (in.readBoolean() == false)
			{
				eqiup[i] = new EquipItem(in);
				totalWeight += eqiup[i].getWeight();
			}
		}
		for (int i = 0; i < inventory.size(); i++)
		{
			if (in.readBoolean() == false)
			{
				inventory.set(i, new EquipItem(in));
				totalWeight += inventory.get(i).getWeight();
			}
		}
		for (Skill s : skills)
		{
			s.setLevel(in.readInt());
		}
		PlayerStatistics.getInstance().totalTimePlayed = in.readFloat();
	}
	
	private void addItem(Item item) {	
		if (item == null) return;
		if (totalWeight + item.getWeight() > getMaxWeight())
			return;

		for (int i = 0; i < inventory.size(); i++)
		{
			if (inventory.get(i) == null)
			{
				inventory.set(i, item);
				totalWeight += item.getWeight();
				break;
			}
		}
	}

	public void spawn()
	{
		hp = getMaxHP();
		attackCD = 0;
		state = ObjectState.Moving;
		path.clear();
		target = null;
		for (Skill s : skills)
		{
			s.resetCd();
		}
		castColdown = 0;
	}

	@Override
	public void render(SpriteBatch batch) {
		batch.end();
		renderer.setProjectionMatrix(batch.getProjectionMatrix());
		if (isDead())
		{
			renderer.begin(ShapeType.FilledRectangle);

			renderer.setColor(0.5f, 0.5f, 0.5f, 1);
			renderer.filledRect(x - 4, y, 8, 32);
			renderer.filledRect(x - 12, y + 16, 24, 8);
			
			renderer.end();
			batch.begin();
			return;
		}
		Gdx.gl.glEnable(GL10.GL_BLEND);
		Gdx.gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
		renderer.begin(ShapeType.FilledCircle);
			
		renderer.setColor(1, 1, 1, 0.1f);
		renderer.filledCircle(x, y, VisionRange);
		
		renderer.end();
		batch.begin();
		sprite.setPos(x, y);
		sprite.draw(batch, 1);
	}
	
	public void levelUp()
	{
		strength += 1;
		agility += 1;
		intelligence += 1;
		stamina += 1;
		switch (firstStatePiority)
		{
		case EquipItem.StateSta:
			stamina += 2;
			break;
		case EquipItem.StateStr:
			strength += 2;
			break;
		case EquipItem.StateAgi:
			agility += 2;
			break;
		case EquipItem.StateInt:
			intelligence += 2;
			break;
		}
		switch (secondStatePriority)
		{
		case EquipItem.StateSta:
			stamina += 1;
			break;
		case EquipItem.StateStr:
			strength += 1;
			break;
		case EquipItem.StateAgi:
			agility += 1;
			break;
		case EquipItem.StateInt:
			intelligence += 1;
			break;
		}
		experience -= getMaxExperience();
		level++;
		hp = getMaxHP();
	}
	
	public void processKill()
	{
		PlayerStatistics.getInstance().enemiesKilled++;
		int expForKill = target.getExpForKill();
		int levelDifference = getLevel() - target.getLevel();
		if (levelDifference > 5)
		{
			if (levelDifference > 10)
				expForKill = 0;
			else
				expForKill /= (1 << levelDifference);
		}
		if (level >= 40) expForKill = 0;
		experience += expForKill;
		PlayerStatistics.getInstance().expEarned += expForKill;
		addItem(EquipItem.drop(target.getLevel()));
		target = null;
		if (experience >= getMaxExperience())
			levelUp();
	}
	
	public void processEnemy(float dt, Enemy enemy)
	{
		if (enemy != null && (target == null || target.dist2(this) > enemy.dist2(this)))
		{
			target = enemy;
		}
		if (target == null) return;
		
		if (target.dist2(this) <= Map.CellSize * Map.CellSize)
		{

			PlayerStatistics.getInstance().timeInBattle += dt;
			sprite.setDirectionToPos(target.x, target.y);
			if (castColdown <= 0)
			{
				for (Skill skill : castSkills)
				{
					if (skill.getLevel() <= 0) continue;
					if (skill.getEvent().equals(Skill.ActEvent.Cast) && skill.canBeCasted(this, target))
					{
						skill.cast(this, target);
						skill.setColdown();
						castColdown = GlobalCastCd;
						PlayerStatistics.getInstance().skillCasted++;
						break;
					}
				}
			}
			for (Skill skill : skills)
			{
				if (skill.getLevel() <= 0) continue;
				if (skill.getEvent().equals(Skill.ActEvent.Timer) && skill.canBeCasted(this, target))
				{
					skill.cast(this, target);
					skill.setColdown();
					break;
				}
			}
			
			state = ObjectState.Attack;
			if (attackCD <= 0)
			{
				for (Skill skill : skills)
				{
					if (skill.getLevel() <= 0) continue;
					if (skill.getEvent().equals(Skill.ActEvent.Attack) && skill.canBeCasted(this, target))
					{
						skill.cast(this, target);
						skill.setColdown();
					}
				}
				target.hit(this);
				attackCD += getAttackColdown();
			}
			else
			{
				attackCD -= dt;
				if (berserkTime > 0)
				{
					attackCD -= 0.5f *dt;
				}
			}
			
			if (target.isDead())
			{
				processKill();
			}
			return;
		}
			

		state = ObjectState.Moving;
		
		if (path.isEmpty())
		{
			int x1 = (int)(Math.floor(x / Map.CellSize));
			int y1 = (int)(Math.floor(y / Map.CellSize));
			int x2 = (int)(Math.floor(target.x / Map.CellSize));
			int y2 = (int)(Math.floor(target.y / Map.CellSize));
			
			path = callback.getPassage().findPath(x1, y1, x2, y2);
			if (path.isEmpty())
			{
				ignoredTargets.add(target);
				target = null;
			}
			return;
		}
		
	}

	@Override
	public void update(float dt) {
		super.update(dt);

		PlayerStatistics.getInstance().timePlayed += dt;
		
		if (isDead())
		{
			state = ObjectState.Death;
			timeToRespawn -= dt;
			if (timeToRespawn <= 0)
			{
				spawn();
			}
			PlayerStatistics.getInstance().timeSpendDead += dt;
			return;
		}
		
		if (castColdown > 0)
		{
			castColdown -= dt;
		}
		if (berserkTime > 0)
		{
			berserkTime -= dt;
		}
		for (Skill s : skills)
		{
			s.update(dt + dt * getColdownReduction());
		}
		
		if (target != null)
		{
			processEnemy(dt, null);
		}
		else
		{
			ArrayList<GameObject> enemies = callback.getObjects(this, VisionRange, Enemy.class);
			Enemy enemy = null;
			
			if (enemies.isEmpty() == false)
			{
				float dist = 0;
				for (GameObject o : enemies)
				{
					if (o.isDead()) continue;
					if (ignoredTargets.contains(o)) continue;
					float currentDist = o.dist2(this);
					if (enemy == null || currentDist < dist)
					{
						enemy = (Enemy)o;
						dist = currentDist;
					}
				}
				
				if (enemy != null)
				{
					path.clear();
					processEnemy(dt, enemy);
				}
			}

			if (enemies.isEmpty() || enemy == null)
			{
				target = null;
				state = ObjectState.Moving;
			}
		}
		
		//Wandering around when nothing to do
		if (state.equals(ObjectState.Moving) && path.isEmpty())
		{
			PassageMap map = callback.getPassage();
			int x1 = (int)(Math.floor(x / Map.CellSize));
			int y1 = (int)(Math.floor(y / Map.CellSize));
			int x2 = 0;
			int y2 = 0;
			do
			{
				x2 = MathUtils.random(x1 - 10, x1 + 10);
				y2 = MathUtils.random(y1 - 5, y1 + 10);
			}
			while (map.isCanPass(x2, y2) == false);
			path = map.findPath(x1, y1, x2, y2);
		}
		
		if (state.equals(ObjectState.Moving))
		{
			sprite.update(dt);
			if (path.isEmpty() == false)
			{
				sprite.setPos(x, y);
				sprite.setDirectionToPos(path.getFirst());
			}
		}
	}

	@Override
	public void hit(GameObject obj) {
		if (MathUtils.random() < dodgeChance)
		{
			return;
		}
		super.hit(obj);
		
		if (isDead())
		{
			timeToRespawn = RespawnTime;
		}
	}

	public int getExperience() {
		return experience;
	}
	
	public int getMaxExperience() {
		return level * level * 25 + 50;
	}
	
	public Enemy getTarget()
	{
		return target;
	}
	
	public ArrayList<Item> getInventory()
	{
		return inventory;
	}
	
	public EquipItem[] getEquip()
	{
		return eqiup;
	}
	
	public void useItem(Item item)
	{
		if (item instanceof EquipItem)
		{
			EquipItem it = (EquipItem) item;
			if (it.getLevel() > level) return;
			EquipItem slot = eqiup[it.getSlot()];
			eqiup[it.getSlot()] = it;
			for (int i = 0; i < inventory.size(); i++)
			{
				if (inventory.get(i) == item)
				{
					inventory.set(i, slot);
					break;
				}
			}
		} //if equip item
	}
	
	public void dropItem(Item item)
	{
		for (int i = 0; i < inventory.size(); i++)
		{
			if (inventory.get(i) == item)
			{
				inventory.set(i, null);
				totalWeight  -= item.getWeight();
				gold += item.getCost();
				break;
			}
		}
	}
	
	public int getBonusState(int stateId)
	{
		int sum = 0;
		for (EquipItem it : eqiup)
		{
			if (it != null)
				sum += it.getStateById(stateId);
		}
		
		return sum;
	}

	@Override
	public int getStrength() {
		return super.getStrength() + getBonusState(EquipItem.StateStr);
	}

	@Override
	public int getAgility() {
		return super.getAgility() + getBonusState(EquipItem.StateAgi);
	}

	@Override
	public int getStamina() {
		return super.getStamina() + getBonusState(EquipItem.StateSta);
	}

	@Override
	public int getIntelligence() {
		return super.getIntelligence() + getBonusState(EquipItem.StateInt);
	}
	
	public int getWeight() {
		return totalWeight;
	}
	
	public void sortInventory() {
		Collections.sort(inventory, new Comparator<Item>() {

			@Override
			public int compare(Item o1, Item o2) {
				if (o2 == null)
					return -1;
				if (o1 == null)
					return 1;
				if (o1 instanceof EquipItem && o2 instanceof EquipItem)
				{
					int slot1 = ((EquipItem)o1).getSlot();
					int slot2 = ((EquipItem)o2).getSlot();
					if (slot1 != slot2)
					{
						return slot1 - slot2;
					}
				}
				
				return o2.getCost() - o1.getCost();
			}
		});
	}

	public ArrayList<Skill> getSkills() {
		return skills;
	}
	
	public ArrayList<Skill> getCastSkills() {
		return castSkills;
	}
	
	@Override
	public float getDamageReduction() {
		float reduction = 1 - super.getDamageReduction();
		reduction *= armorMultiplier;
		return Math.max(0.15f, 1 - reduction);
	}

	public float getSkillCd() {
		return castColdown;
	}

	public float getDodgeChance() {
		return dodgeChance;
	}

	public void setDodgeChance(float dodgeChance) {
		this.dodgeChance = dodgeChance;
	}

	public float getBerserkTime() {
		return berserkTime;
	}

	public void setBerserkTime(float time) {
		this.berserkTime = time;
	}

	public float getArmorMultiplier() {
		return armorMultiplier;
	}

	public void setArmorMultiplier(float armorMultiplier) {
		this.armorMultiplier = armorMultiplier;
	}

	public int getSecondStatePriority() {
		return secondStatePriority;
	}

	public void setSecondStatePriority(int secondSkillPriority) {
		this.secondStatePriority = secondSkillPriority;
	}

	public int getFirstStatePiority() {
		return firstStatePiority;
	}

	public void setFirstStatePiority(int firstSkillPiority) {
		this.firstStatePiority = firstSkillPiority;
	}

	public int getGold() {
		return gold;
	}

	public void setGold(int gold) {
		this.gold = gold;
	}
}
