#include <string>
#include <sstream>

#include "System/StringUtil.h"

#include "IncExternAI.h"
#include "IncGlobalAI.h"

CMetalMap::CMetalMap(AIClasses* ai) {
	this->ai = ai;

	// from 0-255, the minimum percentage of metal a spot needs to have from
	// the maximum to be saved, prevents crappier spots in between taken spaces
	// (they are still perfectly valid and will generate metal mind you!)
	MinMetalForSpot = 50;
	// if more spots than that are found the map is considered a metalmap, tweak this as needed
	MaxSpots = 10000;

	// metal map has 1/2 resolution of normal map
	MetalMapHeight = ai->cb->GetMapHeight() / 2;
	MetalMapWidth = ai->cb->GetMapWidth() / 2;

	TotalCells = MetalMapHeight * MetalMapWidth;
	XtractorRadiusOrg = ai->cb->GetExtractorRadius();
	XtractorRadius = int(ai->cb->GetExtractorRadius() / 16.0f);
	DoubleRadius = XtractorRadius * 2;
	SquareRadius = XtractorRadius * XtractorRadius;
	DoubleSquareRadius = DoubleRadius * DoubleRadius;

	MexArrayA = new unsigned char [TotalCells];
	MexArrayB = new unsigned char [TotalCells];
	// used for drawing the TGA, not really needed with a couple of changes
	MexArrayC = new unsigned char [TotalCells];

	TempAverage = new int [TotalCells];
	TotalMetal = MaxMetal = NumSpotsFound = 0;
	Stopme = false;
}

CMetalMap::~CMetalMap() {
	delete[] MexArrayA;
	delete[] MexArrayB;
	delete[] MexArrayC;
	delete[] TempAverage;
}

// KLOOTNOTE: this needs to ignore spots already taken by allies
float3 CMetalMap::GetNearestMetalSpot(int builderid, const UnitDef* extractor) {
	float TempScore = 0.0f;
	float MaxDivergence = 16.0f;
	float3 spotCoords = ERRORVECTOR;
	float3 bestSpot = ERRORVECTOR;

	if (VectoredSpots.size()) {
		for (unsigned int i = 0; i != VectoredSpots.size(); i++) {
			spotCoords = ai->cb->ClosestBuildSite(extractor, VectoredSpots[i], MaxDivergence, 2);

			if (spotCoords.x >= 0.0f) {
				float distance = spotCoords.distance2D(ai->cb->GetUnitPos(builderid)) + 150;
				float myThreat = ai->thm->ThreatAtThisPoint(spotCoords);
				float spotScore = VectoredSpots[i].y / distance / (myThreat + 10);

				// along with threatmap try to search for enemy armed units around cause
				// there could be armored MEX nearby which gives the highest threat anyway...
				int numEnemies = ai->ccb->GetEnemyUnits(&ai->unitIDs[0], spotCoords, XtractorRadiusOrg * 1.5f);
				while(numEnemies > 0)
				{
					numEnemies--;
					const UnitDef* ud = ai->ccb->GetUnitDef(ai->unitIDs[numEnemies]);
					if(ud && !ud->weapons.empty())
					{
						numEnemies++;
						break;
					}
				}

				bool bOccupied = false;	// flag: metal spot is occupied by allied unit
				if(NumSpotsFound < 100)
				{
					int numAllies = ai->cb->GetFriendlyUnits(&ai->unitIDs[0], spotCoords, XtractorRadiusOrg * 1.5f);
					for(unsigned int j = 0; j < numAllies; j++)
					{
						if(ai->ut->GetCategory(ai->unitIDs[j]) == CAT_MEX)
						{
							bOccupied = true;
							break;
						}
					}
				}

				// NOTE: threat at spotCoords is determined
				// by presence of ARMED enemy units or buildings
				bool b1 = (TempScore < spotScore);
				bool b2 = (numEnemies == 0);
				bool b3 = (myThreat <= (ai->thm->GetAverageThreat() * 1.5));
				bool b4 = (ai->uh->TaskPlanExist(spotCoords, extractor));

				if (b1 && b2 && b3 && !b4 && !bOccupied) {
					TempScore = spotScore;
					bestSpot = spotCoords;
					bestSpot.y = VectoredSpots[i].y;
				}
			}
		}
	}

	// no spot found if TempScore is zero
	return bestSpot;
}

void CMetalMap::Init() {
	//const int frame = ai->cb->GetCurrentFrame();

	// leave this line if you want to use this class in your AI
	ai->cb->SendTextMsg("KAI Metal Class by Krogothe", 0);

	// if there's no available load file, create one and save it
	if (!LoadMetalMap()) {
		GetMetalPoints();
		SaveMetalMap();

		// std::string mapname = std::string("Metal - ") + ai->cb->GetMapName();
		// mapname.resize(mapname.size() - 4);
		// ai->debug->MakeBWTGA(MexArrayC, MetalMapWidth, MetalMapHeight, mapname);
	}

	std::stringstream msg;
		msg << "[CMetalMap::Init()] number of metal spots found: " << NumSpotsFound << "\n";
	ai->GetLogger()->Log(msg.str());
}


void CMetalMap::GetMetalPoints() {
	int* xend = new int[DoubleRadius + 1];

	for (int a = 0; a < DoubleRadius + 1; a++) {
		float z = a - XtractorRadius;
		float floatsqrradius = SquareRadius;
		xend[a] = int(sqrtf(floatsqrradius - z * z));
	}

	// load up the metal values in each pixel
	const unsigned char* metalMapArray = ai->cb->GetMetalMap();
	double TotalMetalDouble  = 0;

	for (int i = 0; i < TotalCells; i++) {
		// count the total metal so you can work out an average of the whole map
		TotalMetalDouble +=  MexArrayA[i] = metalMapArray[i];
	}

	// do the average
	AverageMetal = TotalMetalDouble / TotalCells;

	// quick test for no metal map:
	if (TotalMetalDouble < 0.9) {
		// the map doesn't have any metal, just stop
		NumSpotsFound = 0;
		delete[] xend;
		return;
	}

	// Now work out how much metal each spot can make by adding up the metal from nearby spots
	for (int y = 0; y < MetalMapHeight; y++) {
		for (int x = 0; x < MetalMapWidth; x++) {
			TotalMetal = 0;

			// first spot needs full calculation
			if (x == 0 && y == 0)
				for (int sy = y - XtractorRadius, a = 0;  sy <= y + XtractorRadius;  sy++, a++) {
					if (sy >= 0 && sy < MetalMapHeight){
						for (int sx = x - xend[a]; sx <= x + xend[a]; sx++) {
							if (sx >= 0 && sx < MetalMapWidth) {
								// get the metal from all pixels around the extractor radius
								TotalMetal += MexArrayA[sy * MetalMapWidth + sx];
							}
						}
					}
				}

			// quick calc test
			if (x > 0) {
				TotalMetal = TempAverage[y * MetalMapWidth + x - 1];
				for (int sy = y - XtractorRadius, a = 0;  sy <= y + XtractorRadius;  sy++, a++) {
					if (sy >= 0 && sy < MetalMapHeight) {
						int addX = x + xend[a];
						int remX = x - xend[a] - 1;

						if (addX < MetalMapWidth)
							TotalMetal += MexArrayA[sy * MetalMapWidth + addX];
						if (remX >= 0)
							TotalMetal -= MexArrayA[sy * MetalMapWidth + remX];
					}
				}
			}
			else if (y > 0) {
				// x == 0 here
				TotalMetal = TempAverage[(y - 1) * MetalMapWidth];
				// remove the top half
				int a = XtractorRadius;

				for (int sx = 0; sx <= XtractorRadius;  sx++, a++) {
					if (sx < MetalMapWidth) {
						int remY = y - xend[a] - 1;

						if (remY >= 0)
							TotalMetal -= MexArrayA[remY * MetalMapWidth + sx];
					}
				}

				// add the bottom half
				a = XtractorRadius;

				for (int sx = 0; sx <= XtractorRadius;  sx++, a++) {
					if (sx < MetalMapWidth) {
						int addY = y + xend[a];

						if (addY < MetalMapHeight)
							TotalMetal += MexArrayA[addY * MetalMapWidth + sx];
					}
				}

			}

			// set that spot's metal making ability (divide by cells to values are small)
			TempAverage[y * MetalMapWidth + x] = TotalMetal;

			if (MaxMetal < TotalMetal) {
				// find the spot with the highest metal to set as the map's max
				MaxMetal = TotalMetal;
			}
		}
	}

	// make a list for the distribution of values
	int* valueDist = new int[256];

	for (int i = 0; i < 256; i++) {
		// clear the array (useless?)
		valueDist[i] = 0;
	}

	// this will get the total metal a mex placed at each spot would make
	for (int i = 0; i < TotalCells; i++) {
		// scale the metal so any map will have values 0-255, no matter how much metal it has
		MexArrayB[i] = TempAverage[i] * 255 / MaxMetal;
		// clear out the array since it has never been used
		MexArrayC[i] = 0;

		int value = MexArrayB[i];
		valueDist[value]++;
	}

	// find the current best value
	int bestValue = 0;
	int numberOfValues = 0;
	int usedSpots = 0;

	for (int i = 255; i >= 0; i--) {
		if (valueDist[i] != 0) {
			bestValue = i;
			numberOfValues = valueDist[i];
			break;
		}
	}

	// make a list of the indexes of the best spots
	// (make sure that the list wont be too big)
	if (numberOfValues > 256)
		numberOfValues = 256;

	int* bestSpotList = new int[numberOfValues];

	for (int i = 0; i < TotalCells; i++) {
		if (MexArrayB[i] == bestValue) {
			// add the index of this spot to the list
			bestSpotList[usedSpots] = i;
			usedSpots++;

			if (usedSpots == numberOfValues) {
				// the list is filled, stop the loop
				usedSpots = 0;
				break;
			}
		}
	}

	for (int a = 0; a < MaxSpots; a++) {
		if (!Stopme) {
			// reset tempmetal so it can find new spots
			TempMetal = 0;
			// take the first spot
			int speedTempMetal_x = 0;
			int speedTempMetal_y = 0;
			int speedTempMetal = 0;
			bool found = false;

			while (!found) {
				if (usedSpots == numberOfValues) {
					// the list is empty now, refill it

					// make a list of all the best spots
					for (int i = 0; i < 256; i++) {
						// clear the array
						valueDist[i] = 0;
					}

					// find the metal distribution
					for (int i = 0; i < TotalCells; i++) {
						int value = MexArrayB[i];
						valueDist[value]++;
					}

					// find the current best value
					bestValue = 0;
					numberOfValues = 0;
					usedSpots = 0;

					for (int i = 255; i >= 0; i--) {
						if (valueDist[i] != 0) {
							bestValue = i;
							numberOfValues = valueDist[i];
							break;
						}
					}

					// make a list of the indexes of the best spots
					// (make sure that the list wont be too big)
					if (numberOfValues > 256)
						numberOfValues = 256;

					delete[] bestSpotList;
					bestSpotList = new int[numberOfValues];

					for (int i = 0; i < TotalCells; i++) {
						if (MexArrayB[i] == bestValue) {
							// add the index of this spot to the list
							bestSpotList[usedSpots] = i;
							usedSpots++;

							if (usedSpots == numberOfValues) {
								// the list is filled, stop the loop
								usedSpots = 0;
								break;
							}
						}
					}
				}

				// The list is not empty now.
				int spotIndex = bestSpotList[usedSpots];

				if (MexArrayB[spotIndex] == bestValue) {
					// the spot is still valid, so use it
					speedTempMetal_x = spotIndex % MetalMapWidth;
					speedTempMetal_y = spotIndex / MetalMapWidth;
					speedTempMetal = bestValue;
					found = true;
				}

				// update the bestSpotList index
				usedSpots++;
			}

			coordx = speedTempMetal_x;
			coordy = speedTempMetal_y;
			TempMetal = speedTempMetal;
		}

		if (TempMetal < MinMetalForSpot) {
			// if the spots get too crappy it will stop running the loops to speed it all up
			Stopme = 1;
		}

		if (!Stopme) {
			// format metal coords to game-coords
			BufferSpot.x = coordx * 16 + 8;
			BufferSpot.z = coordy * 16 + 8;
			// gets the actual amount of metal an extractor can make
			BufferSpot.y = TempMetal * (ai->cb->GetMaxMetal()) * MaxMetal / 255;
			VectoredSpots.push_back(BufferSpot);

			// plot TGA array (not necessary) for debug
			MexArrayC[coordy * MetalMapWidth + coordx] = TempMetal;
			NumSpotsFound += 1;

			// small speedup of "wipes the metal around the spot so its not counted twice"
			for (int sy = coordy - XtractorRadius, a = 0;  sy <= coordy + XtractorRadius;  sy++, a++) {
				if (sy >= 0 && sy < MetalMapHeight) {
					int clearXStart = coordx - xend[a];
					int clearXEnd = coordx + xend[a];

					if (clearXStart < 0)
						clearXStart = 0;
					if (clearXEnd >= MetalMapWidth)
						clearXEnd = MetalMapWidth - 1;

					for (int xClear = clearXStart; xClear <= clearXEnd; xClear++) {
						// wipes the metal around the spot so it's not counted twice
						MexArrayA[sy * MetalMapWidth + xClear] = 0;
						MexArrayB[sy * MetalMapWidth + xClear] = 0;
						TempAverage[sy * MetalMapWidth + xClear] = 0;
					}
				}
			}

			// redo the whole averaging process around the picked spot so other spots can be found around it
			for (int y = coordy - DoubleRadius; y <= coordy + DoubleRadius; y++) {
				if (y >=0 && y < MetalMapHeight) {
					for (int x = coordx - DoubleRadius; x <= coordx + DoubleRadius; x++) {
						if (x >=0 && x < MetalMapWidth) {
							TotalMetal = 0;

							// comment out for debug
							if (x == 0 && y == 0)
								for (int sy = y - XtractorRadius, a = 0;  sy <= y + XtractorRadius;  sy++, a++) {
									if (sy >= 0 && sy < MetalMapHeight) {
										for (int sx = x - xend[a]; sx <= x + xend[a]; sx++) {
											if (sx >= 0 && sx < MetalMapWidth) {
												// get the metal from all pixels around the extractor radius
												TotalMetal += MexArrayA[sy * MetalMapWidth + sx];
											}
										}
									}
								}

							// quick calc test
							if (x > 0) {
								TotalMetal = TempAverage[y * MetalMapWidth + x - 1];

								for (int sy = y - XtractorRadius, a = 0;  sy <= y + XtractorRadius;  sy++, a++) {
									if (sy >= 0 && sy < MetalMapHeight) {
										int addX = x + xend[a];
										int remX = x - xend[a] - 1;

										if (addX < MetalMapWidth)
											TotalMetal += MexArrayA[sy * MetalMapWidth + addX];
										if (remX >= 0)
											TotalMetal -= MexArrayA[sy * MetalMapWidth + remX];
									}
								}
							}
							else if (y > 0) {
								// x == 0 here
								TotalMetal = TempAverage[(y - 1) * MetalMapWidth];
								// remove the top half
								int a = XtractorRadius;

								for (int sx = 0; sx <= XtractorRadius;  sx++, a++) {
									if (sx < MetalMapWidth) {
										int remY = y - xend[a] - 1;

										if (remY >= 0)
											TotalMetal -= MexArrayA[remY * MetalMapWidth + sx];
									}
								}

								// add the bottom half
								a = XtractorRadius;

								for (int sx = 0; sx <= XtractorRadius;  sx++, a++) {
									if (sx < MetalMapWidth) {
										int addY = y + xend[a];

										if (addY < MetalMapHeight)
											TotalMetal += MexArrayA[addY * MetalMapWidth + sx];
									}
								}
							}

							TempAverage[y * MetalMapWidth + x] = TotalMetal;
							// set that spot's metal amount
							MexArrayB[y * MetalMapWidth + x] = TotalMetal * 255 / MaxMetal;
						}
					}
				}
			}
		}
	}

	// kill the lists
	delete[] bestSpotList;
	delete[] valueDist;
	delete[] xend;

	// 0.95 used for for reliability
	// bool isMetalMap = (NumSpotsFound > MaxSpots * 0.95);
}


void CMetalMap::SaveMetalMap() {
	std::string map = GetCacheName();
	FILE* saveFile = fopen(map.c_str(), "wb");

	assert(saveFile != NULL);

	fwrite(&NumSpotsFound, sizeof(int), 1, saveFile);
	fwrite(&AverageMetal, sizeof(float), 1, saveFile);

	for (int i = 0; i < NumSpotsFound; i++) {
		fwrite(&VectoredSpots[i], sizeof(float3), 1, saveFile);
	}

	fclose(saveFile);
}

bool CMetalMap::LoadMetalMap() {
	std::string map = GetCacheName();
	FILE* loadFile = fopen(map.c_str(), "rb");

	if (loadFile != NULL) {
		fread(&NumSpotsFound, sizeof(int), 1, loadFile);
		VectoredSpots.resize(NumSpotsFound);
		fread(&AverageMetal, sizeof(float), 1, loadFile);

		for (int i = 0; i < NumSpotsFound; i++) {
			fread(&VectoredSpots[i], sizeof(float3), 1, loadFile);
		}

		fclose(loadFile);
		return true;
	}

	return false;
}



std::string CMetalMap::GetCacheName() const {
	// name is used for human readability,
	// while hash is used for uniqueness
	// (in case the map maker forgets changing the name inbetween versions)
	std::string relFile =
		std::string(METALFOLDER) +
		AIUtil::MakeFileSystemCompatible(ai->cb->GetMapName()) +
		"-" + IntToString(ai->cb->GetMapHash(), "%x") +
		".Metal";
	std::string absFile = AIUtil::GetAbsFileName(ai->cb, relFile);

	return absFile;
}
