/*
   Copyright (C) 2004 by James Gregory
   Part of the GalaxyHack project
 
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License.
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY.
 
   See the COPYING file for more details.
*/

#include "RTSUnit_Base.h"
#include "Globals.h"
#include "Group.h"
#include "Inlines.h"
#include "Projectile.h"
#include "ForceSelect.h"
#include "Random.h"

#include <sstream>
#include <iterator>

#include <boost/filesystem/operations.hpp>

using std::istringstream;
using std::istream_iterator;
using std::find;
using std::getline;

RTSUnit_Base::RTSUnit_Base(const string& iName, int iMySide, int iMyGroup):
name(iName), mySide(iMySide), myGroup(iMyGroup),
pic(0), picFlip(0),
extraMoveFrames(0), extraMovex(0), extraMovey(0), doExtraMove(1), alive(1), shieldTimer(0), explodeTimer(0) {
	bigStage = w_Ready;
	bigTarget.x = 0;
	bigTarget.y = 0;
}

RTSUnit_Base::~RTSUnit_Base() {
	FreePicture();
}

void RTSUnit_Base::LoadData() {
	istringstream input(sides[mySide].sdStruct.unitData[name]);
	istream_iterator<char> iter = input;
	istream_iterator<char> fileEnd = istream_iterator<char>();

	string tempStr;

	//type
	iter = find(iter, fileEnd, ':');
	input.ignore();
	getline(input, tempStr);
	iter = input;

	if (stringToUType.find(tempStr) == stringToUType.end()) {
		const string error = "RTSUnit_Base constructor didn't recognise unit type when loading " + name;
		throw runtime_error(error);
	}

	myType = stringToUType[tempStr];

	if (myType == UT_CaShUnit)
		myCSType = stringToCSType[tempStr];

	//this resets picture and stuff, so this must come early on
	//also, virtual, so unit loading must always take place in bottom most constructor
	DefaultTypeDepStats();

	//next the picture
	//if it doesn't exist we go back to the default
	iter = find(iter, fileEnd, ':');
	input.ignore();
	getline(input, tempStr);
	iter = input;

	string tempPath = MakePicDirString(mySide, tempStr, false);
	if (DoesFileExist(tempPath))
		picName = tempStr;
	else {
		tempPath = MakePicDirString(mySide, tempStr, true);
		if (DoesFileExist(tempPath))
			picName = tempStr;
	}	

	LoadPicture();

	//engine + shield + armour
	if (myType == UT_FrUnit || myType == UT_SmShUnit) {
		//speed
		iter = find(iter, fileEnd, ':');
		input.ignore();
		getline(input, engineName);
		iter = input;

		iter = find(iter, fileEnd, ':');
		input.ignore();
		getline(input, armourName);
		iter = input;

		iter = find(iter, fileEnd, ':');
		input.ignore();
		getline(input, shieldName);
		iter = input;
	}

	//smallType
	iter = find(iter, fileEnd, ':');
	input.ignore();
	getline(input, tempStr);
	iter = input;

	ChangeSmallType(weaponLoadLookup[tempStr], 1);

	//big type
	iter = find(iter, fileEnd, ':');
	input.ignore();
	getline(input, tempStr);
	iter = input;

	ChangeBigType(weaponLoadLookup[tempStr], 1);


	//weapon coords
	if (AutoFireUnit* tempPointer = dynamic_cast<AutoFireUnit*>(this))
		tempPointer->LoadWeapCoords(iter, fileEnd);

	ChangeEngine(engineName, true);
	ChangeArmour(armourName, true);
	ChangeShield(shieldName, true);

	//varies depending on unit type, and must come after initing armour
	InitSmall();
	bigStage = w_Ready;
}

void RTSUnit_Base::DefaultTypeDepStats() {
	shieldName = "None";
	//needs to be set before changing engine for SetSpeed()
	bigType = WT_None;
	smallType = WT_None;

	ChangeEngine(engineName, 1);
	ChangeArmour(armourName, 1);
	ChangeShield(shieldName, 1);

	ChangeSmallType(smallType);
	ChangeBigType(bigType);
}

void RTSUnit_Base::LoadPicture() {
	//can't have differently scaled pictures sharing the same name
	string fileName = picName;

	if (myType == UT_CaShUnit) {
		switch (myCSType) {
		case CST_Heavy:
			picName += "HCS";
			break;

		case CST_Medium:
			picName += "MCS";
			break;

		case CST_Light:
			picName += "LCS";
			break;
		}
	}

	if (sides[mySide].unitPictures.find(picName) == sides[mySide].unitPictures.end()) {
		string dirName = MakePicDirString(mySide, fileName, false);
		if (!DoesFileExist(dirName))
			dirName = MakePicDirString(mySide, fileName, true);

		JSDL.BitmapHDtoFile(dirName.c_str());
		sides[mySide].unitPictures[picName] = JSDL.BitmapFiletoSurface(0, 0, width, height);

		JSDL.FlipHorizontal();
		sides[mySide].unitPictures[picName + "Flip"] = JSDL.BitmapFiletoSurface(0, 0, width, height);
		JSDL.BitmapCloseFile();
	}
	else {
		++(sides[mySide].unitPictures[picName]->refcount);
		++(sides[mySide].unitPictures[picName + "Flip"]->refcount);
	}

	pic = sides[mySide].unitPictures[picName];
	picFlip = sides[mySide].unitPictures[picName + "Flip"];
}


void RTSUnit_Base::SetPos(float ix, float iy) {
	myx = ix;
	myy = iy;
	
	CoordsInt center = GetCenter();

	if (center.x < worldWidth / 2)
		bFlip = 0;
	else
		bFlip = 1;
}

void RTSUnit_Base::Move(float distx, float disty) {
	myx += distx;
	myy += disty;

	if (distx < 0)
		bFlip = true;
	else if (distx > 0)
		bFlip = false;
}

void RTSUnit_Base::AddExtraMoveFrames() {
	//we may be outside because of previous in-group movement,
	//or the bounding rect may have got smaller.

	SDL_Rect groupRect;
	sides[mySide].groups[myGroup].GetRect(groupRect);

	bool alreadyExtra = 0;

	if (myx < groupRect.x) {
		extraMoveFrames = 1;
		extraMovex = 1;
		extraMovey = 0;
	}
	if (myy < groupRect.y) {
		extraMoveFrames = 1;
		if (!alreadyExtra)
			extraMovex = 0;
		extraMovey = 1;
	}
	if (myx + width > groupRect.x + groupRect.w) {
		extraMoveFrames = 1;
		extraMovex = -1;
		if (!alreadyExtra)
			extraMovey = 0;
	}
	if (myy + height > groupRect.y + groupRect.h) {
		extraMoveFrames = 1;
		if (!alreadyExtra)
			extraMovex = 0;
		extraMovey = -1;
	}

	//if we're inside the rect, add on a bit of random in group movement
	//for next time
	if (!extraMoveFrames) {
		extraMovex = Random() % 3 - 1;

		extraMovey = Random() % 3 - 1;
		extraMoveFrames = Random() % 30;

		if (extraMoveFrames == 0)
			doExtraMove = 1;
	}
}

void RTSUnit_Base::Fire(AICommands& theCommands) {
	FireSmall(theCommands);
	FireBig(theCommands);
}

void RTSUnit_Base::FireBig(AICommands& theCommands) {
	if (theCommands.bFire && bigStage == w_Ready && theCommands.fireTargetDist <= weaponLookup[bigType].range && bigAmmo > 0) {
		bigStage = w_Aiming;
		bigTarget = theCommands.fireTarget;
		bigTimer = frameCounter;
		bigAiming = Random() % maxAiming;
	}
	
	//if they are now out of range or we have decided not to fire any more, stop
	if (bigStage == w_Aiming && (!theCommands.bFire || theCommands.fireTargetDist > weaponLookup[bigType].range))
		bigStage = w_Ready;

	//else if aiming time is up...
	//this code assumes we are a small ship, which isn't so clear given we are in RTSUnit_Base
	else if (bigStage == w_Firing && weaponLookup[bigType].category != WCAT_Large) {
		//only fire if we are moving in the right plane (we may well not be if dogfighting)
		if (sides[mySide].groups[myGroup].GetSpeedPlaneMatchesTarget()) {
			bigStage = w_Reloading;
			bigTimer = frameCounter;
            CoordsInt center = GetCenter();
			projectiles.push_back(Projectile(center.x, center.y, bigTarget, bigType, sides[mySide].laserColor));

			//note that if there are more infinite ammo big weapons made, this needs changing
			--bigAmmo;
		}
		//else we wait, and with no reloading are allowed to fire as soon as we are properly aligned, assuming still within range	
	}
}


void RTSUnit_Base::BeenHit(int power) {
	shieldCurrent-= power;
	shieldTimer = frameCounter;

	if (shieldCurrent < 0) {
		armourCurrent-= abs(shieldCurrent);
		shieldCurrent = 0;
	}

	if (armourCurrent < 0)
		armourCurrent = 0;
}

void RTSUnit_Base::Upkeep() {
	//shields
	if (shieldCurrent < shieldMax && frameCounter - shieldTimer > shieldRecharge) {
		++shieldCurrent;
		shieldTimer = frameCounter;
	}

	//check for big reloading
	if (bigStage == w_Reloading && frameCounter - bigTimer > weaponLookup[bigType].reload)
		bigStage = w_Ready;

	//check for big aiming (time decided when we choose
	//a target)
	if (bigStage == w_Aiming && frameCounter - bigTimer > bigAiming) {
		if (weaponLookup[bigType].category == WCAT_Large) {
			TargetDesc targetInfo;

			sides[bigTarget.x].groups[bigTarget.y].FiredAt(targetInfo);

			bigTargetUnit = targetInfo.whichUnit;
			targetWeakSpot = targetInfo.weakSpot;

			bigTimer = frameCounter;

			weHitWithBig = Projectile_Base::CheckToHit(weaponLookup[bigType].accuracy, targetInfo.difficulty);

			if (!weHitWithBig) {
				targetWeakSpot.x += Random() % 21 - 10;
				targetWeakSpot.y += Random() % 21 - 10;
			} else if (sides[bigTarget.x].groups[bigTarget.y].GetType() != UT_SmShUnit && sides[bigTarget.x].groups[bigTarget.y].GetAlive())
				projectiles.push_back(Projectile(targetInfo.currentx + targetWeakSpot.x, targetInfo.currenty + targetWeakSpot.y));
		}

		bigStage = w_Firing;
	}

	//check for large laser finishing firing
	if (weaponLookup[bigType].category == WCAT_Large && bigStage == w_Firing) {
		if (frameCounter - bigTimer > weaponLookup[bigType].length) {
			bigStage = w_Reloading;
			bigTimer = frameCounter;
			if (weHitWithBig)
				sides[bigTarget.x].groups[bigTarget.y].BeenHit(bigTargetUnit, weaponLookup[bigType].power);
		}
	}
}

void RTSUnit_Base::DeadUpkeep() {
	if (explodeTimer)
		--explodeTimer;
}

//only called if group is on the screen
void RTSUnit_Base::DrawSelf() {
	USRect.x = static_cast<int>(myx) - viewx;
	USRect.y = static_cast<int>(myy) - viewy;
	USRect.w = width;
	USRect.h = height;

	if (alive) {
		if (bFlip)
			JSDL.Blt(picFlip, USRect);

		else
			JSDL.Blt(pic, USRect);
	}
	else if (explodeTimer)
		Explode();
}

void RTSUnit_Base::DrawUnitPic(int x, int y) {
	USRect.x = x;
	USRect.y = y;
	USRect.w = width;
	USRect.h = height;

	JSDL.Blt(pic, USRect);
}

int RTSUnit_Base::GetPointsValue() const {
	int total = 0;

	switch (myType) {
	case UT_CaShUnit:
		switch (myCSType) {
		case CST_Heavy:
			total += globalSettings.HCSCost;
			break;

		case CST_Medium:
			total += globalSettings.MCSCost;
			break;

		case CST_Light:
			total += globalSettings.LCSCost;
			break;
		}
		break;

	case UT_FrUnit:
		total += globalSettings.FrCost;
		break;

	case UT_SmShUnit:
		total += globalSettings.SSCost;
		break;
	}

	if (myType != UT_CaShUnit) {
		total += equipLookup[engineName].points;
		total += equipLookup[shieldName].points;
		total += equipLookup[armourName].points;
	}

	int temp = weaponLookup[smallType].points;
	total += temp * smallNumber;

	total += weaponLookup[bigType].points;

	return total;
}

int RTSUnit_Base::GetMaxPoints() const {
	switch (myType) {
	case UT_SmShUnit:
		return globalSettings.maxSSPoints;
		break;

	case UT_FrUnit:
		return globalSettings.maxFrPoints;
		break;

	case UT_CaShUnit:
		return infinite_constant;
		break;
	}
}

int RTSUnit_Base::GetCapacity() const {
	throw runtime_error("Attempt to get capacity of a non-capital ship");
	return 0;
}

//relative to centre of unit
CoordsInt RTSUnit_Base::GetWeakSpot() const {
	CoordsInt ret = {0, 0};
    //relative to centre of unit
	return ret;
}

void RTSUnit_Base::ChangeUnitPic(const string& newPic) {
	FreePicture();

	picName = newPic;

	LoadPicture();
}

void RTSUnit_Base::FreePicture() {
	bool delSurface = false;

	if (pic->refcount == 1)
		delSurface = true;

	SDL_FreeSurface(pic);
	SDL_FreeSurface(picFlip);

	if (delSurface) {
		sides[mySide].unitPictures.erase(picName);
		sides[mySide].unitPictures.erase(picName + "Flip");
	}
}

void RTSUnit_Base::ChangeSmallType(WeaponType newType, bool ignorePoints) {
	WeaponType oldType = smallType;

	smallType = newType;
	SetSmallNumber();

	if (!ignorePoints && GetPointsValue() > GetMaxPoints()) {
		smallType = oldType;
		SetSmallNumber();
		throw MinorError("Not enough points left to upgrade to that small weapon");
	}
}

void RTSUnit_Base::ChangeBigType(WeaponType newType, bool ignorePoints) {
	WeaponType oldType = bigType;

	bigType = newType;
	bigAmmo = weaponLookup[newType].startAmmo;
	SetSpeed();

	if (!ignorePoints && GetPointsValue() > GetMaxPoints()) {
		bigType = oldType;
		bigAmmo = weaponLookup[oldType].startAmmo;
		SetSpeed();
		throw MinorError("Not enough points left to upgrade to that big weapon");
	}
}

void RTSUnit_Base::ChangeEngine(const string& newType, bool ignorePoints) {
	string oldName = engineName;

	engineName = newType;
	SetSpeed();

	if (!ignorePoints && GetPointsValue() > GetMaxPoints()) {
		engineName = oldName;
		SetSpeed();
		throw MinorError("Not enough points left to upgrade to that engine");
	}
}

void RTSUnit_Base::ChangeArmour(const string& newStat, bool ignorePoints) {
	string oldName = armourName;

	armourName = newStat;
	armourCurrent = armourMax = equipLookup[newStat].max;

	if (!ignorePoints && GetPointsValue() > GetMaxPoints()) {
		armourName = oldName;
		armourCurrent = armourMax = equipLookup[oldName].max;
		throw MinorError("Not enough points left to upgrade to that armour");
	}
}

void RTSUnit_Base::ChangeShield(const string& newStat, bool ignorePoints) {
	string oldName = shieldName;

	shieldName = newStat;
	shieldCurrent = shieldMax = equipLookup[newStat].max;
	shieldRecharge = equipLookup[newStat].recharge;

	if (!ignorePoints && GetPointsValue() > GetMaxPoints()) {
		shieldName = oldName;
		shieldCurrent = shieldMax = equipLookup[oldName].max;
		shieldRecharge = equipLookup[oldName].recharge;
		throw MinorError("Not enough points left to upgrade to that shield");
	}
}

void RTSUnit_Base::ChangeCSType(CapShipType newType) {
	throw runtime_error("Changing CSType of a non-capital ship");
}

string RTSUnit_Base::UTypeToPicDir(UnitType uType) {
	switch (uType) {
	case UT_CaShUnit:
		return "capship/";
		break;

	case UT_FrUnit:
		return "frigate/";
		break;

	case UT_SmShUnit:
		return "smallship/";
		break;
	}

	throw runtime_error("UTypeToPicDir went wrong");
}

void RTSUnit_Base::SetSpeed() {
	speed = equipLookup[engineName].max;
	if (weaponLookup[bigType].category == WCAT_Torpedo)
		speed = speed * 0.75;
}

