package sso;

import java.sql.*;
import java.util.*;
import sso.event.*;

/**
 *  PC is abstract because it should never be directly instantiated. Instead, a
 *  Player or NPC class should be used. The big difference between Player and
 *  NPC is that a Player contains information about past connections, usage of
 *  the system, etc., while an NPC contains a monster memory, wander path,
 *  behaviors, etc.
 */
//public abstract class PC extends Thing implements Attackable, Runnable
public class PC extends Thing implements Attackable, Runnable
{
	transient protected Thread	tStatus;		// character status update thread
	transient protected boolean	bStatus;		// keep checking status
	protected Vector	skills;
	protected int		base_mana;
	protected int		cur_mana;
	protected int		heal_rate;		// points of rating restored per minute
	protected int		mana_rate;		// points of mana restored per minute
	protected Vector	natural_weapon;	// natural weapon(s)
	protected Group		group;			// group PC is member of
	protected byte		size;

	// PC sizes
	public final static byte	SIZE_ANY 	= 0;
	public final static byte	SIZE_TINY 	= 1;
	public final static byte	SIZE_SMALL	= 2;
	public final static byte	SIZE_MAN	= 3;
	public final static byte	SIZE_LARGE	= 4;
	public final static byte	SIZE_HUGE	= 5;
	public final static byte	SIZE_GIANT	= 6;

	// Armor/Body locations
	public static int			LOC_COUNT		= 17;	// sum of locations
	public final static byte	LOC_NONE		= 0;
	public final static byte	LOC_HEAD		= 1;
	public final static byte	LOC_SHOULDERS	= 2;
	public final static byte	LOC_TORSO		= 3;
	public final static byte	LOC_ARMS		= 4;
	public final static byte	LOC_WAIST		= 5;
	public final static byte	LOC_HIPS		= 6;
	public final static byte	LOC_THIGHS		= 7;
	public final static byte	LOC_SHINS		= 8;
	public final static byte	LOC_FEET		= 9;
	public final static byte	LOC_NECK		= 10;
	public final static byte	LOC_WRISTS		= 11;
	public final static byte	LOC_FINGER		= 12;
	public final static byte	LOC_HANDS		= 13;
	public final static byte	LOC_BUCKLER		= 14;
	public final static byte	LOC_LEFT_HAND	= 15;
	public final static byte	LOC_RIGHT_HAND	= 16;
	
	/**
	 *  Constructor (protected)
	 */
	protected PC()
	{
		super();

		natural_weapon = new Vector();
		skills = new Vector();
	}

	/**
	 *  Public factory.
	 */
	public static PC createPC()
	{
		PC p = new PC();
		p.init();
		return p;
	}

	/**
	 *  Initializer for new PCs.
	 */
	protected void init() 
	{
		super.init();

		// default skills should be set here, including attribute skills
		// *** TODO ***

		tStatus = new Thread(this);
		tStatus.start();

		base_mana = 0;
		cur_mana = 0;

		heal_rate = 1;
		mana_rate = 0;

		group = null;

		size = SIZE_MAN;
	}

	/**
	 *  Get the PC size
	 */
	public byte getSize()
	{
		return size;
	}

	/**
	 *  Set the PC size
	 */
	public void setSize(byte size)
	{
		this.size = size;

		propChanged("size");
	}
	
	/**
	 *  Get the PC's skill by an existing skill's type.
	 *  If the PC doesn't know this skill, transparently substitute the parent 
	 *  skill.
	 */
	public Skill getSkill(Skill find_skill)
	{
		return getSkill(find_skill.getParentSkillName());
	} 

	/**
	 *  Get the skill by class name.
     */
	public Skill getSkill(String skill_name)
	{
		// get the current skill list
		Vector vs = (Vector)skills.clone();

		// set up a skill object to hold the found skill
		Skill sk = null;

		int i;

		// iterate through the skill list
		for (i = 0; i < vs.size(); i++)
		{
			sk = (Skill)vs.elementAt(i);

			// if the current skill matches the desired skill
			if (sk.getClass().getName().equals(skill_name))
			{
				// done
				break;
			}
		}

		// if the skill wasn't found in the skill list
		if (sk == null)
		{
			Skill skn = null;
		
			// get the parent skill
			try
			{
				skn = ((Skill)Class.forName(skill_name).newInstance()).getParentSkill();
			}
			catch (Exception e)
			{
				System.err.println("Broken skill tree near " + skill_name + "! " + e.getMessage());
			}
			
			// if a parent skill exists
			if (skn != null)
			{
				// if the parent's skill name is the same as the desired skill
				if (skn.getClass().getName().equals(skill_name))
				{
					// set the parent skill to be the desired skill
					sk = skn;
				}
				else
				{
					// (recurse) check the parent's parent
					sk = getSkill(skn.getClass().getName());
				}
			}
		}

		// in all cases, we should never return a null unless the skill wasn't
		// found at all. Not finding a skill indicates that the skill tree is
		// broken.
		
		// return found skill
		return sk;
	}

	/**
	 *  Add a skill.
	 */
	public boolean addSkill(Skill skill)
	{
		// don't add the skill if it's already there..
		if (!getSkill(skill).equals(skill.getClass().getName()))
		{
			skills.addElement(skill);
			propChanged("skills");
			return true;
		}

		return false;
	}

	/**
	 *  Remove the specified skill
	 */
	public void removeSkill(Skill skill)
	{
		removeSkill(skill.getClass().getName());
	}

	/**
	 *  Remove the specified skill
	 */
	public void removeSkill(String skill_name)
	{
		Skill skill = getSkill(skill_name);

		// only remove the skill if the skill name matches
		if (skill.getClass().getName().equals(skill_name))
		{
			skills.removeElement(skill);
			propChanged("skills");
		}
	}

	/**
	 *  Get the base mana
	 */
	public int getBaseMana()
	{
		return base_mana;
	}

	/**
	 *  Set the base mana
	 */
	public void setBaseMana(int mana)
	{
		base_mana = mana;
		propChanged("base_mana");
	}

	/**
	 *  Get the current mana
	 */
	public int getMana()
	{
		return cur_mana;
	}

	/**
	 *  Set the current mana
	 */
	public void setMana(int mana)
	{
		cur_mana = mana;
		propChanged("cur_mana");
	}

	/**
	 *  Get the heal rate (hits healed per minute)
	 */
	public int getHealRate()
	{
		return heal_rate;
	}

	/**
	 *  Set the heal rate
	 */
	public void setHealRate(int rate)
	{
		heal_rate = rate;
		propChanged("heal_rate");
	}

	/**
	 *  Get the mana recharge rate (mana per minute)
	 */
	public int getManaRate()
	{
		return mana_rate;
	}

	/**
	 *  Set the mana recharge rate
	 */
	public void setManaRate(int rate)
	{
		mana_rate = rate;
		propChanged("mana_rate");
	}

	/**
	 *  Get natural weapons
	 */
	public Vector getNaturalWeapons()
	{
		return natural_weapon;
	}

	/**
	 *  Add a natural weapon
	 */
	public void addNaturalWeapon(Weapon weapon)
	{
		natural_weapon.addElement(weapon);
		propChanged("natural_weapon");
	}

	/**
	 *  Remove a natural weapon
	 */
	public void removeNaturalWeapon(Weapon weapon)
	{
		natural_weapon.removeElement(weapon);
		propChanged("natural_weapon");
	}

	/**
	 *  Get the current group.
	 */
	public Group getGroup()
	{
		return group;
	}
	
	/**
	 *  Remove the PC from the current group.
	 */
	public void leaveGroup()
	{
		if (group != null)
		{
			group.removeMember(this);
			propChanged("group");
		}
	}

	/**
	 *  Join the specified group.
	 */
	public void joinGroup(Group spec)
	{
		if (group != null)
		{
			leaveGroup();
		}

		spec.addMember(this);
		group = spec;

		propChanged("group");
	}

	/**
	 *  Die
	 */
	public void die()
	{
		setFlag(Effect.DEAD);
		// this should start a thread to rot the corpse
		// *** TODO ***
	}

	/**
	 *  Hurt the PC
	 */
	public void hurt(int damage)
	{
		// if not invulnerable
		if (!((getFlags() & Effect.INVULNERABLE) == Effect.INVULNERABLE))
		{
			if (cur_rating - damage < 0)
			{
				setRating(0);

				// die
				die();
			}
			else
			{
				setRating(cur_rating - damage);
			}
		}
	}

	/**
	 *  Heal the PC
	 */
	public void heal(int points)
	{
		// if not diseased
		if (!((getFlags() & Effect.DISEASED) == Effect.DISEASED))
		{
			if ((getFlags() & Effect.RESTING) == Effect.RESTING)
			{
				// if resting, get twice as many points back
				points *= 2;
			}

			// check hits to make sure healing can happen
			if ((cur_rating + points > base_rating) && !((getFlags() & DEAD) == DEAD))
			{
				setRating(base_rating);
			}
			else
			{
				setRating(cur_rating + points);
			}
		}
	}

	/**
	 *  Drain mana (due to spell, effect, etc)
	 */
	public void drain(int mana)
	{
		// if not archmage
		if (!((getFlags() & Effect.ARCHMAGE) == Effect.ARCHMAGE))
		{
			if (cur_mana - mana < 0)
			{
				setMana(0);
			}
			else
			{
				setMana(cur_mana - mana);
			}
		}
	}

	/**
	 *  Restore mana
	 */
	public void restoreMana(int mana)
	{
		if (cur_mana + mana <= base_mana)
		{
			setMana(cur_mana + mana);
		}
		else
		{
			setMana(base_mana);
		}
	}

	/**
	 *  Status thread.
	 */
	public void run()
	{
		bStatus = true;
	
		long time;
		long last_heal = System.currentTimeMillis();
	
		while (bStatus)
		{
			time = System.currentTimeMillis();

			// check for heal
			if (time >= last_heal + heal_rate)
			{
				heal(heal_rate);
				restoreMana(mana_rate);
			}

			try
			{
				tStatus.sleep(60000);	// sleep for one minutes
			}
			catch (InterruptedException e)
			{
				//
			}
		}
	}

	/**
	 *  Get max capacity. (str * 10)
	 *  OVERRIDE METHOD.
	 */
	public int getCapacity()
	{
		// get the strength skill
		Skill str = getSkill("sso.skill.Strength");

		int cap = super.getCapacity();

		if (str != null)
		{
			cap += (str.getRating() * 10);
		}
		
		return cap; 
	}

	/**
	 *  Get value. Most PCs can't have value (no slavery).
	 */
	public int getValue()
	{
		return 0;
	}

	/**
 	 *  Perform a skill roll. Account for skill defaulting.
	 */
	public int roll(String skill_name, int mod)
	{
		Skill sk = getSkill(skill_name);

		if (!(sk.getClass().getName().equals(skill_name)))
		{
			try
			{
				mod -= ((Skill)(Class.forName(skill_name).newInstance())).getDefaultPenalty();
			}
			catch (Exception e)
			{
				System.err.println("Skill heirarchy broken: couldn't instantiate a " + skill_name +": " + e.getMessage());
			}
		}

		return sk.roll() + mod;
	}

	/**
	 *  Get the wielded weapon.
	 */
	public Weapon getWielded()
	{
		GameObject invobj;
		Weapon weapon;

		Vector vi = (Vector)contents.clone();

		for (int i = 0; i < vi.size(); i++)
		{
			invobj = (GameObject)vi.elementAt(i);

			if (invobj instanceof Weapon)
			{
				weapon = (Weapon)invobj;

				if (weapon.isEquipped())
				{
					// found an equipped weapon...return it
					return weapon;
				}
			}
		}

		// no weapon equipped...return (random) natural weapon
		return (Weapon)getNaturalWeapons().elementAt(Random.roll(getNaturalWeapons().size()) - 1);
	}
				
	/**
	 *  Get the armor at the given location
	 */
	public Armor getArmor(byte loc)
	{
		Object invobj;
		Armor armor;

		Vector vi = (Vector)contents.clone();

		for (int i = 0; i < vi.size(); i++)
		{
			invobj = (GameObject)vi.elementAt(i);

			if (invobj instanceof Armor)
			{
				armor = (Armor)invobj;

				if (armor.isEquipped())
				{
					if (armor.getBodyLocation() == loc)
					{
						return armor;
					}
				}
			}
		}

		// not found
		return null;
	}

	/**
	 *  Attack another attackable
	 */
	public Attack doAttack(Attackable target)
	{
		Attack attack = new Attack();
		attack.setAttacker(this);
		attack.setDefender(target);
		attack.setWeapon(getWielded());
		attack.attack();
		return attack;
	}

	/**
	 *  Determine what happens when this PC gets attacked (is the defender)
	 *  Implements Attackable.attack(Attack)
	 *  Should be overridden by children (such as NPCs for hate lists).
     */
	public void attack(Attack attack)
	{
		int result = attack.getResult();
		int dmg = attack.getDamageRoll();
		byte loc = (byte)(attack.getDamageLocation() / 14); 

		// resistances have already been handled by the Attack object
		// check for armor in the hit location
		Armor armor = getArmor(loc);

		if (armor != null)
		{
			dmg = armor.absorb(dmg);
		}

		// check for critical hit
		if ((result & Attack.CRITICAL_HIT) == Attack.CRITICAL_HIT)
		{
			// multiply damage by 10
			dmg = dmg * 10;
		}

		// apply damage
		hurt(dmg);
	}

	/**
	 *  Create rows to save PC
	 */
	protected void createRows()
	{
		super.createRows();

		Statement stmt = null;

		try
		{
			stmt = Persistant.getStatement();

			stmt.executeUpdate("INSERT INTO PC (pc_id, base_mana, cur_mana, heal_rate, mana_rate, size) VALUES (" + id + ", " + base_mana + ", " + cur_mana + ", " + heal_rate + ", " + mana_rate + ", " + size + ")");

			// natural weapon should be taken care of by weapon constructor
			Vector vw = (Vector)natural_weapon.clone();

			for (int i = 0; i < vw.size(); i++)
			{
				stmt.executeUpdate("INSERT INTO PC_NaturalWeapons (pc_id, weapon_id) VALUES (" + id + ", " + ((GameObject)vw.elementAt(i)).getID() + ")");
			}

			stmt.close();
			stmt = null;
		}
		catch (SQLException e)
		{
			System.err.println("Trying to create rows to store PC (" + id + "): " + e.getMessage());
		}

		if (stmt != null)
		{
			try
			{
				stmt.close();
			}
			catch (SQLException e)
			{
				//
			}

			stmt = null;
		}
	}

	/**
	 *  Store this object in the database
	 */
	public void store()
	{
		super.store();

		Statement stmt = null;

		try
		{
			stmt = Persistant.getStatement();

			// don't tell contents or group to store
			// prevent circular references
			// only save tables we know about

			stmt.executeUpdate("UPDATE PC SET base_mana=" + base_mana + ", cur_mana=" + cur_mana + ", heal_rate=" + heal_rate + ", mana_rate=" + mana_rate + ", size=" + size + " WHERE pc_id=" + id);

			stmt.executeUpdate("DELETE FROM PC_Skills WHERE pc_id=" + id);
			
			Vector v = (Vector)skills.clone();
			int i;

			for (i = 0; i < v.size(); i++)
			{
				stmt.executeUpdate("INSERT INTO PC_Skills (pc_id, skill_id) VALUES (" + id + ", " + ((Skill)v.elementAt(i)).getID() + ")");
			}

			stmt.executeUpdate("DELETE FROM PC_NaturalWeapons WHERE pc_id=" +
			id);

			v = (Vector)natural_weapon.clone();

			for (i = 0; i < v.size(); i++)
			{
				stmt.executeUpdate("INSERT INTO PC_NaturalWeapons (pc_id, weapon_id) VALUES (" + id + ", " + ((GameObject)v.elementAt(i)).getID() + ")");
			}

			// let the group fend for itself
			
			stmt.close();
			stmt = null;
		}
		catch (SQLException e)
		{
			System.err.println("Trying to store PC (" + id +"): " +
			e.getMessage());
		}

		if (stmt != null)
		{
			try
			{
				stmt.close();
			}
			catch (SQLException e)
			{
				//
			}
			
			stmt = null;
		}
	}

	/**
	 *  Load PC object
	 */
	public static PC loadPC(int id)
	{
		PC pc = null;

		if (Registry.isLoaded(id))
		{
			return (PC)Registry.get(id);
		}
		else
		{
			pc = new PC();
			pc.id = id;
			pc.load();
		}

		return pc;
	}

	/**
	 *  Load this instance.
	 */
	protected void load()
	{
		super.load();
	
		Statement stmt = null;
		ResultSet rs = null;

		try
		{
			stmt = Persistant.getStatement();

			rs = stmt.executeQuery("SELECT * FROM PC WHERE pc_id=" + id);

			while (rs.next())
			{
				base_mana = rs.getInt("base_mana");
				cur_mana = rs.getInt("cur_mana");
				heal_rate = rs.getInt("heal_rate");
				mana_rate = rs.getInt("mana_rate");
				size = rs.getByte("size");
			}

			rs.close();
			rs = null;

			rs = stmt.executeQuery("SELECT * FROM PC_Skills WHERE pc_id=" + id);

			while (rs.next())
			{
				skills.addElement(Skill.loadSkill(rs.getInt("skill_id")));
			}

			rs.close();
			rs = null;

			rs = stmt.executeQuery("SELECT * FROM PC_NaturalWeapons WHERE pc_id=" + id);

			natural_weapon = new Vector();

			while (rs.next())
			{
				natural_weapon.addElement((Weapon)GameObject.loadGameObject(rs.getInt("weapon_id")));
			}

			rs.close();
			rs = null;

			rs = stmt.executeQuery("SELECT * FROM Group_PCs WHERE pc_id=" + id);

			while (rs.next())
			{
				group = Group.loadGroup(rs.getInt("group_id"));
			}

			rs.close();
			rs = null;

			stmt.close();
			stmt = null;
		}
		catch (SQLException e)
		{
			System.err.println("Trying to load PC (" + id + "): " +
			e.getMessage());
		}

		if (rs != null)
		{
			try
			{
				rs.close();
			}
			catch (SQLException e)
			{
				//
			}

			rs = null;
		}

		if (stmt != null)
		{
			try
			{
				stmt.close();
			}
			catch (SQLException e)
			{
				//
			}

			stmt = null;
		}
	}

	/**
	 *  Remove the PC from the database.
	 */
	public void unregister()
	{
		// stop the update thread
		bStatus = false;

		// erase skills
		Vector vs = (Vector)skills.clone();
		int i;

		for (i = 0; i < vs.size(); i++)
		{
			((Skill)vs.elementAt(i)).unregister();
		}

		// erase natural weapons
		vs = (Vector)natural_weapon.clone();

		for (i = 0; i < vs.size(); i++)
		{
			((GameObject)vs.elementAt(i)).unregister();
		}

		// drop out of the group
		leaveGroup();

		// delete rows
		Statement stmt = null;

		try
		{
			stmt = Persistant.getStatement();

			stmt.executeUpdate("DELETE FROM Group_PCs WHERE pc_id=" + id);
			stmt.executeUpdate("DELETE FROM PC_Skills WHERE pc_id=" + id);
			stmt.executeUpdate("DELETE FROM PC_NaturalWeapons WHERE pc_id=" +
			id);
			stmt.executeUpdate("DELETE FROM PC WHERE pc_id=" + id);

			stmt.close();
			stmt = null;
		}
		catch (SQLException e)
		{
			System.err.println("Trying to delete PC (" + id + "): " +
			e.getMessage());
		}

		if (stmt != null)
		{
			try
			{
				stmt.close();
			}
			catch (SQLException e)
			{
				//
			}

			stmt = null;
		}
		
		super.unregister();
	}

	/**
	 *  Tester
	 */
	public static void main(String [] args)
	{
		PC pc = PC.createPC();

		pc.store();
		int id = pc.getID();

		pc = null;

		try
		{
			Thread.sleep(15000);
		}
		catch (InterruptedException e)
		{
			//
		}
		
		pc = PC.loadPC(id);

		// now, the big test
		// name the PC
		pc.setName("Rogar");
		pc.setDescription("A lad, about nine, who likes to mend nets and catch scuttlefish on the shoreline.");
		System.out.println("PC is " + pc.getName() + " and: " + pc.getDescription());

		// create two game locations
		Tile t1 = Tile.createTile();
		t1.setName("The Seashore");
		Tile t2 = Tile.createTile();
		t2.setName("The Water");
		t2.setFlag(Tile.WET);
		t2.setFlag(Tile.IMPASSABLE);
		System.out.println("t2 is " + t2.getName() + " and flags: " + t2.getFlags());

		// move the PC to t1
		pc.move(t1);
		System.out.println("PC moved? " + (pc.getLocation() == t1));

		// create an equipment item
		Equipment eq = Equipment.createEquipment();
		eq.setName("A dagger");
		eq.setDescription("Is this a dagger I see before me? Dont worry. Its not real and it cant hurt little Rogar");
		eq.setWeight(10);
		eq.setRating(100);
		eq.move(pc);

		if (eq.getLocation() != pc)
		{
			System.out.println("Little " + pc.getName() + " cries because " + eq.getName() + " is too heavy!");
			pc.setCapacity(1000);
			eq.move(pc);
			if (eq.getLocation() != pc)
			{
				System.out.println("PC cap " + pc.getCapacity());
				System.out.println("EQ wgt " + eq.getWeight());
			}
		}
		else
		{
			System.out.println(pc.getName() + " is carrying " + eq.getName());
		}
		System.out.println(eq.getName() + " has a rating of " + eq.getRating());
		
		// move into the water
		pc.move(t2);

		if (pc.getLocation() != t2)
		{
			System.out.println(pc.getName() + " can't walk on water.");
		}
		else
		{
			System.out.println("It's a miracle! " + pc.getName() + " can walk on water!");
		}

		t1.store();
		t2.store();
		pc.store();

		try
		{
			Thread.sleep(45000);
		}
		catch (InterruptedException e)
		{
			//
		}

		pc.unregister();
		eq.unregister();
		t2.unregister();
		t1.unregister();

		System.out.println("Done!");
	}
}

