/* CargoHold.h
Copyright (c) 2014 by Michael Zahniser

Endless Sky is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.

Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.  See the GNU General Public License for more details.
*/

#include "CargoHold.h"

#include "DataNode.h"
#include "DataWriter.h"
#include "Depreciation.h"
#include "GameData.h"
#include "Mission.h"
#include "Outfit.h"
#include "System.h"

#include <algorithm>
#include <cmath>
#include <vector>

using namespace std;

namespace {
	// Retrieve vector of pointers to the outfits, sorted descending by size.
	vector<const Outfit *> OrderOutfitsBySize(const map<const Outfit *, int> &outfits)
	{
		vector<const Outfit *> sortedOutfits;
		for(const auto &it : outfits)
			sortedOutfits.emplace_back(it.first);
		
		sort(sortedOutfits.begin(), sortedOutfits.end(),
			[] (const Outfit *lhs, const Outfit *rhs)
			{
				return lhs->Get("mass") > rhs->Get("mass");
			}
		);
		
		return sortedOutfits;
	}
}



// Remove any items in this cargo hold.
void CargoHold::Clear()
{
	size = 0;
	bunks = 0;
	commodities.clear();
	outfits.clear();
	missionCargo.clear();
	passengers.clear();
}



// Load the cargo manifest from a DataFile. This must be done after the
// GameData is loaded, so that the sizes of any outfits are known.
void CargoHold::Load(const DataNode &node)
{
	// Cargo is stored as name / amount pairs in two lists: commodities and outfits.
	for(const DataNode &child : node)
	{
		if(child.Token(0) == "commodities")
		{
			for(const DataNode &grand : child)
				if(grand.Size() >= 2)
				{
					int tons = grand.Value(1);
					commodities[grand.Token(0)] += tons;
				}
		}
		else if(child.Token(0) == "outfits")
		{
			for(const DataNode &grand : child)
			{
				const Outfit *outfit = GameData::Outfits().Get(grand.Token(0));
				int count = (grand.Size() < 2) ? 1 : grand.Value(1);
				outfits[outfit] += count;
			}
		}
	}
}



// Save the cargo manifest to a file.
void CargoHold::Save(DataWriter &out) const
{
	bool first = true;
	for(const auto &it : commodities)
		if(it.second)
		{
			// Only write a "cargo" block if it is not going to be empty.
			if(first)
			{
				out.Write("cargo");
				out.BeginChild();
				out.Write("commodities");
				out.BeginChild();
			}
			first = false;
			
			out.Write(it.first, it.second);
		}
	// We only need to EndChild() if at least one line was written above.
	if(!first)
		out.EndChild();
	
	bool firstOutfit = true;
	for(const auto &it : outfits)
		if(it.second && !it.first->Name().empty())
		{
			// It is possible this cargo hold contained no commodities, meaning
			// we must print the opening tag now.
			if(first)
			{
				out.Write("cargo");
				out.BeginChild();
			}
			first = false;
			
			// If this is the first outfit to be written, print the opening tag.
			if(firstOutfit)
			{
				out.Write("outfits");
				out.BeginChild();
			}
			firstOutfit = false;
			
			out.Write(it.first->Name(), it.second);
		}
	// Back out any indentation blocks that are set, depending on what sorts of
	// cargo were written to the file.
	if(!firstOutfit)
		out.EndChild();
	if(!first)
		out.EndChild();
	
	// Mission cargo is not saved because it is repopulated when the missions
	// are read rather than when the cargo is read.
}



// Set the capacity of this cargo hold.
void CargoHold::SetSize(int tons)
{
	size = tons;
}



// Get the capacity of this hold.
int CargoHold::Size() const
{
	return size;
}



// Get the amount of free cargo space.
int CargoHold::Free() const
{
	return size - Used();
}



// Get the total amount of cargo space used, rounded up to the nearest ton.
// (Some outfits may have non-integral masses.)
int CargoHold::Used() const
{
	return CommoditiesSize() + OutfitsSize() + MissionCargoSize();
}



// Get the total number of tons of commodities.
int CargoHold::CommoditiesSize() const
{
	int size = 0;
	for(const auto &it : commodities)
		size += it.second;
	return size;
}



// Get the total mass of outfit cargo, rounded up to the nearest ton.
int CargoHold::OutfitsSize() const
{
	double size = 0.;
	for(const auto &it : outfits)
		size += it.second * it.first->Get("mass");
	return ceil(size);
}



// Check if any outfits are being carried. Note that some outfits may have mass
// zero, so this check cannot be done by calling OutfitsSize().
bool CargoHold::HasOutfits() const
{
	// The code for adding and removing outfits does not clear the entry in the
	// map if its value becomes zero, so we need to check all the entries:
	for(const auto &it : outfits)
		if(it.second)
			return true;
	
	return false;
}



// Get the total mass of mission cargo.
int CargoHold::MissionCargoSize() const
{
	int size = 0;
	for(const auto &it : missionCargo)
		size += it.second;
	return size;
}



// Check if any mission cargo is being carried. Some mission cargo has no mass,
// so this cannot be done by calling MissionCargoSize().
bool CargoHold::HasMissionCargo() const
{
	return !missionCargo.empty();
}



// Check if there is anythign in this cargo hold (including passengers).
bool CargoHold::IsEmpty() const
{
	// The outfits map's entries are not erased if they are equal to zero, so
	// it's not enough to just test outfits.empty().
	return commodities.empty() && !HasOutfits() && missionCargo.empty() && passengers.empty();
}



// Set the number of free bunks for passengers.
void CargoHold::SetBunks(int count)
{
	bunks = count;
}



// Check how many bunks are free.
int CargoHold::Bunks() const
{
	return bunks - Passengers();
}



// Check how many bunks are occupied by passengers.
int CargoHold::Passengers() const
{
	int count = 0;
	for(const auto &it : passengers)
		count += it.second;
	return count;
}



// Normal cargo:
int CargoHold::Get(const string &commodity) const
{
	map<string, int>::const_iterator it = commodities.find(commodity);
	return (it == commodities.end() ? 0 : it->second);
}



// Spare outfits (including plunder and mined materials):
int CargoHold::Get(const Outfit *outfit) const
{
	map<const Outfit *, int>::const_iterator it = outfits.find(outfit);
	return (it == outfits.end() ? 0 : it->second);
}



// Mission cargo:
int CargoHold::Get(const Mission *mission) const
{
	map<const Mission *, int>::const_iterator it = missionCargo.find(mission);
	return (it == missionCargo.end() ? 0 : it->second);
}



// Check how many passengers for the given mission are being carried.
int CargoHold::GetPassengers(const Mission *mission) const
{
	map<const Mission *, int>::const_iterator it = passengers.find(mission);
	return (it == passengers.end() ? 0 : it->second);
}



// Access the commodities map directly.
const map<string, int> &CargoHold::Commodities() const
{
	return commodities;
}



// Access the outfits map directly.
const map<const Outfit *, int> &CargoHold::Outfits() const
{
	return outfits;
}



// Access the mission cargo map directly.
const map<const Mission *, int> &CargoHold::MissionCargo() const
{
	return missionCargo;
}



// Access the mission passenger map directly.
const map<const Mission *, int> &CargoHold::PassengerList() const
{
	return passengers;
}



// Transfer ordinary commodities from one cargo hold to another.
int CargoHold::Transfer(const string &commodity, int amount, CargoHold *to)
{
	// Whichever ship is removing the cargo is limited by how much it has
	// available. The receiving ship is limited by its free space, but only if
	// its space is limited (i.e. size is not set to -1).
	amount = min(amount, Get(commodity));
	if(size >= 0)
		amount = max(amount, -max(Free(), 0));
	if(to)
	{
		amount = max(amount, -to->Get(commodity));
		if(to->size >= 0)
			amount = min(amount, max(to->Free(), 0));
	}
	if(!amount)
		return 0;
	
	// The "to" hold need not be defined.
	commodities[commodity] -= amount;
	if(to)
		to->commodities[commodity] += amount;
	
	return amount;
}



// Transfer outfits from one cargo hold to another.
int CargoHold::Transfer(const Outfit *outfit, int amount, CargoHold *to)
{
	double mass = outfit->Get("mass");
	
	// Whichever ship is removing the cargo is limited by how much it has
	// available. The receiving ship is limited by its free space, but only if
	// its space is limited (i.e. size is not set to -1).
	amount = min(amount, Get(outfit));
	if(size >= 0 && mass)
		amount = max<int>(amount, -max(Free(), 0) / mass);
	if(to)
	{
		amount = max(amount, -to->Get(outfit));
		if(to->size >= 0 && mass)
			amount = min<int>(amount, max(to->Free(), 0) / mass);
	}
	if(!amount)
		return 0;
	
	// The "to" hold need not be defined.
	outfits[outfit] -= amount;
	if(to)
		to->outfits[outfit] += amount;
	
	return amount;
}



// Transfer mission cargo from one cargo hold to another.
int CargoHold::Transfer(const Mission *mission, int amount, CargoHold *to)
{
	// Special case: if the mission cargo has zero size, always transfer it. But
	// if it has nonzero size and zero can fit, do _not_ transfer it.
	if(amount)
	{
		// Take your free capacity into account here too.
		amount = min(amount, Get(mission));
		if(size >= 0)
			amount = max(amount, -max(Free(), 0));
		if(to)
		{
			amount = max(amount, -to->Get(mission));
			if(to->size >= 0)
				amount = min(amount, max(to->Free(), 0));
		}
		if(!amount)
			return 0;
	}
	
	// The "to" hold need not be defined.
	missionCargo[mission] -= amount;
	if(to)
		to->missionCargo[mission] += amount;
	
	return amount;
}



// Transfer mission passengers from one cargo hold to another.
int CargoHold::TransferPassengers(const Mission *mission, int amount, CargoHold *to)
{
	// Limit the transfer by the amount of passengers available to transfer and
	// the bunks free on the ship receiving them.
	amount = min(amount, GetPassengers(mission));
	if(bunks >= 0)
		amount = max(amount, -max(Bunks(), 0));
	if(to)
	{
		amount = max(amount, -to->GetPassengers(mission));
		if(to->bunks >= 0)
			amount = min(amount, max(to->Bunks(), 0));
	}
	if(!amount)
		return 0;
	
	// The "to" hold need not be defined.
	passengers[mission] -= amount;
	if(to)
		to->passengers[mission] += amount;
	
	return amount;
}



// Transfer as much as the given cargo hold has capacity for. The priority is
// first mission cargo, then spare outfits, then ordinary commodities.
void CargoHold::TransferAll(CargoHold *to)
{
	// If there is no destination specified, just unload everything.
	if(!to)
	{
		commodities.clear();
		outfits.clear();
		missionCargo.clear();
		passengers.clear();
		return;
	}
	
	for(const auto &it : passengers)
		TransferPassengers(it.first, it.second, to);
	// Handle zero-size mission cargo correctly. For mission cargo, having an
	// entry in the map, but with a size of zero, is different than not having
	// an entry at all.
	auto mit = missionCargo.begin();
	while(mit != missionCargo.end())
	{
		Transfer(mit->first, mit->second, to);
		if(!mit->second)
			mit = missionCargo.erase(mit);
		else
			++mit;
	}
	const std::vector<const Outfit *> outfitOrder = OrderOutfitsBySize(outfits);
	for(const auto &outfit : outfitOrder)
		Transfer(outfit, outfits[outfit], to);
	for(const auto &it : commodities)
		Transfer(it.first, it.second, to);
}



// Add the given amount of the given commodity.
int CargoHold::Add(const string &commodity, int amount)
{
	return -Transfer(commodity, -amount);
}



// Add the given number of copies of the given outfit.
int CargoHold::Add(const Outfit *outfit, int amount)
{
	return -Transfer(outfit, -amount);
}



// Remove the given amount of the given commodity.
int CargoHold::Remove(const string &commodity, int amount)
{
	return Transfer(commodity, amount);
}



// Remove the given number of copies of the given outfit.
int CargoHold::Remove(const Outfit *outfit, int amount)
{
	return Transfer(outfit, amount);
}



// Add all the cargo and passengers associated with the given mission.
void CargoHold::AddMissionCargo(const Mission *mission)
{
	// If the mission defines a cargo string, create an entry for it even if the
	// cargo size is zero. This is so that, for example, your cargo listing can
	// show "important documents" even if the documents take up no cargo space.
	if(mission && !mission->Cargo().empty())
		missionCargo[mission] += mission->CargoSize();
	if(mission && mission->Passengers())
		passengers[mission] += mission->Passengers();
}



// Remove all the cargo and passengers (if any) associated with the given mission.
void CargoHold::RemoveMissionCargo(const Mission *mission)
{
	missionCargo.erase(mission);
	passengers.erase(mission);
}


	
// Get the total value of all this cargo, in the given system.
int64_t CargoHold::Value(const System *system) const
{
	int64_t value = 0;
	for(const auto &it : commodities)
		value += system->Trade(it.first) * it.second;
	// For outfits, assume they're fully depreciated, since that will always be
	// the case unless the player bought into cargo for some reason.
	for(const auto &it : outfits)
		value += it.first->Cost() * it.second * Depreciation::Full();
	return value;
}


	
// If anything you are carrying is illegal, return the maximum fine you can
// be charged. If the returned value is negative, you are carrying something
// so bad that it warrants a death sentence.
int CargoHold::IllegalCargoFine() const
{
	int worst = 0;
	// Carrying an illegal outfit is only half as bad as having it equipped.
	for(const auto &it : outfits)
	{
		int fine = it.first->Get("illegal");
		if(fine < 0)
			return fine;
		worst = max(worst, fine / 2);
	}
	
	for(const auto &it : missionCargo)
	{
		int fine = it.first->IllegalCargoFine();
		if(fine < 0)
			return fine;
		worst = max(worst, fine);
	}
	return worst;
}
