////////////////////////////////////////////////////////////////////////////////
// MissionGoals.cpp
// Track mission goals
// Copyright Bjoern Ganster
// Developed since 2020
////////////////////////////////////////////////////////////////////////////////

#include "MechSim.h"
#include "BGBase.h"
#include "MechShell.h"
#include "MissionGoals.h"

#define FILE_NUMBER FILE_NO_MISSION_GOALS_CPP

// Count sub objectives that have been successfully solved
size_t Objective::countSuccessfulSubObjectives() const
{
	SET_SCOPE;
	size_t result = 0;
	for (size_t i = 0; i < subObjectives.size(); ++i) {
		SET_OP;
		if (subObjectives[i].status == ObjectiveStatus::success) {
			++result;
		}
	}
	RETURN result;
}

// Constructor
MissionGoals::MissionGoals()
: mAnnouncedMissionOutcome(false),
  mTonnageSum(0),
  mUnderweightBonusFactor(0),
  mWeightLimit(300)
{
}

// Destructor
MissionGoals::~MissionGoals()
{
}

// Parse XML for sub objective
void MissionGoals::addSubObjective(Objective& objective, SubObjectiveType type, XMLTreeConstIterator& iter)
{
	SET_SCOPE;
	SubObjective subObjective;
	subObjective.type = type;
	iter.getAttributeValueByName("object", subObjective.objectName);
	subObjective.score = (int) iter.getIntAttributeValueByName("score", 1);
	subObjective.status = ObjectiveStatus::inProgress;

	if (type == SubObjectiveType::checkDistance) {
		subObjective.checkDistanceP.x = iter.getDoubleAttributeByName("x");
		subObjective.checkDistanceP.y = iter.getDoubleAttributeByName("y");
		subObjective.checkDistanceP.z = iter.getDoubleAttributeByName("z");
		subObjective.checkedDistance  = (float)iter.getDoubleAttributeByName("dist");
	}

	objective.subObjectives.push_back(subObjective);
	RETURN;
}

// Parse XML for objective
void MissionGoals::addObjective(MechShell* mechShell, XMLTreeConstIterator& iter)
{
	SET_SCOPE;
	Objective objective;
	objective.currentScore = (int) iter.getIntAttributeValueByName("startingScore", 0);
	objective.requiredScore = (int)iter.getIntAttributeValueByName("requiredScore");
	iter.getAttributeValueByName("type", objective.type);
	iter.getAttributeValueByName("text", objective.text);

	BGCPString scoreStr;
	iter.getAttributeValueByName("score", scoreStr);
	if (mechShell != nullptr) {
		mechShell->evaluate(scoreStr, true);
	}
	objective.careerScore = (int)scoreStr.toInt();

	objective.status = ObjectiveStatus::inProgress;
	iter.getAttributeValueByName("Betty", objective.bettySpeech);
	iter.getAttributeValueByName("trigger", objective.trigger);
	iter.getAttributeValueByName("setVars", objective.setVarsOnCompletion);
	objective.mTerminateMission = iter.hasAttribute("terminateMission");

	objective.mMaySucceedOnlyAtMissionEnd = false;
	BGCPString maySucceedAt;
	iter.getAttributeValueByName("maySucceedAt", maySucceedAt);
	if (maySucceedAt == "missionEnd") {
		objective.mMaySucceedOnlyAtMissionEnd = true;
	}

	if (objective.type == "Primary Objective") {
		++mPrimaryGoalsRequired;
	}

	size_t childCount = iter.getChildCount();

	for (size_t i = 0; i < childCount; ++i) {
		SET_OP;
		iter.goToChild(i);
		if (iter.nodeNameEquals("destroy")) {
			addSubObjective(objective, SubObjectiveType::attack, iter);
		} else if (iter.nodeNameEquals("visit")) {
			addSubObjective(objective, SubObjectiveType::visit, iter);
		} else if (iter.nodeNameEquals("inspect")) {
			addSubObjective(objective, SubObjectiveType::inspect, iter);
		} else if (iter.nodeNameEquals("wait")) {
			addSubObjective(objective, SubObjectiveType::sleep, iter);
		} else if (iter.nodeNameEquals("event")) {
			addSubObjective(objective, SubObjectiveType::event, iter);
		} else if (iter.nodeNameEquals("defend")) {
			addSubObjective(objective, SubObjectiveType::defend, iter);
		} else if (iter.nodeNameEquals("distanceLess")) {
			addSubObjective(objective, SubObjectiveType::checkDistance, iter);
		}
		iter.goToParent();
	}

	mObjectives.push_back(objective);
	RETURN;
}

// Parse data from XML
void MissionGoals::parseFromXml(XMLTreeConstIterator& iter, MechShell* mechShell)
{
	SET_SCOPE;
	mPrimaryGoalsRequired = 0;
	size_t childCount = iter.getChildCount();

	for (size_t i = 0; i < childCount; ++i) {
		SET_OP;
		iter.goToChild(i);
		if (iter.nodeNameEquals("goal")) {
			addObjective(mechShell, iter);
		} else if (iter.nodeNameEquals("underweightBonus")) {
			mUnderweightBonusFactor = (int)iter.getIntAttributeValueByName("factor");
			mWeightLimit = (int)iter.getIntAttributeValueByName("limit");
		}
		iter.goToParent();
	}

	if (iter.hasAttribute("primaryGoalsRequired")) {
		mPrimaryGoalsRequired = (size_t) iter.getIntAttributeValueByName("primaryGoalsRequired");
	}

	RETURN;
}

void checkSuccess(Objective& objective, SubObjectiveType newStatus, bool missionEnd, bool& endSim)
{
	SET_SCOPE;
	auto* mechSim = getMechSim();
	if(objective.currentScore >= objective.requiredScore) {
		SET_OP;
		if (!objective.bettySpeech.empty()) {
			mechSim->enqueueVoice(objective.bettySpeech.getPtr<char>());
			objective.bettySpeech.clear();
		}

		if (!objective.trigger.empty()) {
			SET_OP;
			mechSim->updateObjectStatus(objective.trigger, newStatus, missionEnd);
			objective.trigger.clear(); // make sure it doesn't trigger again
		}

		if (!objective.setVarsOnCompletion.empty()) {
			SET_OP;
			MechShell* shell = mechSim->getShell();
			if (shell != nullptr) {
				shell->processAssignment(objective.setVarsOnCompletion, false);
				objective.setVarsOnCompletion.clear();
			}
		}

		if (objective.mTerminateMission) {
			// Store that the mission should be ended, but allow the rest of the function to continue
			// (e.g., to determine whether the mission was a success)
			endSim = true;
		}
	}

	RETURN;
}

// Notify class of a updated object status (sender can be a nav point, vehicle or GameObject)
void MissionGoals::updateObjectStatus(const BGCPString& id, SubObjectiveType newStatus, bool missionEnd)
{
	// As soon as the mission result has been announced, the mission result should not change anymore,
	// but the player should still be allowed to fullfil secondary and tertiary objectives, unless the player dies.
	// However, if the mission fails, e.g. because an object was destroyed without being inspected,
	// the mission fails, and that verdict should be final.
	// Allow definition of an escape point - if reached, the mission should terminate automatically.
	auto* mechSim = getMechSim();
	bool endSim = false;

	// Check if object identified by id is part of a sub objective
	SET_SCOPE;
	int objectivesFulfilledJustNow = 0;
	for (size_t i = 0; i < mObjectives.size(); ++i) {
		auto& objective = mObjectives[i];

		for (size_t j = 0; j < objective.subObjectives.size(); ++j) {
			auto& subObjective = objective.subObjectives[j];
			if (subObjective.objectName == id 
			&& subObjective.type == newStatus) {
				if (subObjective.status != ObjectiveStatus::success) {
					SET_OP;
					objective.currentScore += subObjective.score;

					if (!objective.mMaySucceedOnlyAtMissionEnd || missionEnd) {
						checkSuccess(objective, newStatus, missionEnd, endSim);
						subObjective.status = ObjectiveStatus::success;
						++objectivesFulfilledJustNow;
					}
				}
			}
		}
	}

	// Check the status of the main goals
	SET_OP;
	if (!mAnnouncedMissionOutcome && objectivesFulfilledJustNow > 0) {
		size_t primaryObjectivesComplete = 0;
		bool missionFailed = false;
		for (size_t i = 0; i < mObjectives.size(); ++i) {
			SET_OP;
			auto& objective = mObjectives[i];
			if (objective.currentScore >= objective.requiredScore && objective.type == "Primary Objective") {

				++primaryObjectivesComplete;
			}
			if (objective.currentScore >= objective.requiredScore && objective.type.lowerStringASCII() == "failif") {
				missionFailed = true;
			}
		}

		if (primaryObjectivesComplete >= mPrimaryGoalsRequired && !mAnnouncedMissionOutcome) {
			SET_OP;
			mechSim->enqueueVoice("GENE001S.SFL");
			mAnnouncedMissionOutcome = true;
			mechSim->mMissionSuccessful = true;
		}

		if (missionFailed && !mAnnouncedMissionOutcome) {
			SET_OP;
			mechSim->enqueueVoice("GENE001F.SFL");
			mAnnouncedMissionOutcome = true;
			mechSim->mMissionSuccessful = false;
		}
	}

	SET_OP;
	if (endSim) {
		mechSim->endSim();
	}

	RETURN;
}

// Build a string about the mission goals, returns current score
int MissionGoals::showMissionGoals(BGCPString& str, bool showScore, const BGCPString& scoreUnit, bool missionEnd) const
{
	SET_SCOPE;
	if (showScore) {
		str = "\n\n";
	} else {
		str.clear();
	}
	BGCPString lastType;
	int score = 0;

	for (size_t i = 0; i < mObjectives.size(); ++i) {
		SET_OP;
		auto& objective = mObjectives[i];
		BGCPString line;
		int objectiveScore = 0;
		if (objective.type.equalsNoCase("killBounty")) {
			SET_OP;
			size_t kills = objective.countSuccessfulSubObjectives();
			size_t maxKills = objective.subObjectives.size();
			line = objective.text + BGCPString(": ") + BGCPString::fromInt(kills) + BGCPString("/") + BGCPString::fromInt(maxKills);
			objectiveScore = kills * objective.careerScore;
		} else {
			SET_OP;
			if (objective.type != lastType && !objective.type.equalsNoCase("hidden")) {
				str += objective.type + "(s):\n";
				lastType = objective.type;
			}
			if (objective.currentScore < objective.requiredScore || (objective.mMaySucceedOnlyAtMissionEnd && !missionEnd)) {
				line = objective.text + ": " + BGCPString::fromInt(objective.currentScore) + "/" + BGCPString::fromInt(objective.requiredScore);
			} else {
				if (objective.type.equalsNoCase("failif")) {
					line = objective.text + ": failed";
				} else {
					line = objective.text + ": successful";
					objectiveScore = objective.careerScore;
				}
			}
		}
		if (showScore) {
			line += BGCPString("\t ") + BGCPString::fromInt(objectiveScore) + scoreUnit;
		}


		if (!objective.type.equalsNoCase("hidden")) {
			str += line + "\n";
		}
		score += objectiveScore;
	}

	if (showScore) {
		str += BGCPString("Total: ") + BGCPString::fromInt(score) + scoreUnit;
	}
	RETURN score;
}

// Set star weight for calculating the underweight bonus
void MissionGoals::setTonnage(int newVal)
{
	mTonnageSum = newVal;
}

// Check if the player's position triggers distance-based sub objectives (gra3, mcc1, cyan)
void MissionGoals::checkDistanceSubObjectives(const Point3D& playerPos)
{
	SET_SCOPE;
	bool endSim = false;
	auto* mechSim = getMechSim();

	for (size_t i = 0; i < mObjectives.size(); ++i) {
		Objective& objective = mObjectives[i];
		for (size_t j = 0; j < objective.subObjectives.size(); ++j) {
			SubObjective& subObjective = objective.subObjectives[j];
			if (subObjective.type == SubObjectiveType::checkDistance) {
				if ((subObjective.checkDistanceP - playerPos).length() < subObjective.checkedDistance) {
					if (subObjective.status != ObjectiveStatus::success) {
						SET_OP;
						subObjective.status = ObjectiveStatus::success;
						objective.currentScore += subObjective.score;
						if (!objective.mMaySucceedOnlyAtMissionEnd) {
							checkSuccess(objective, SubObjectiveType::checkDistance, false, endSim);
							subObjective.status = ObjectiveStatus::success;
						}
					}
				}
			}
		}
	}

	SET_OP;
	if (endSim) {
		mechSim->endSim();
	}

	RETURN;
}
