// Analyzes brick geometry to detect common symmetry planes.
// -------------------------------------------------------------------

//Begin generating the symmetry table
function ndCreateSymmetryTable()
{
	if($ND::SymmetryTableCreating)
		return;

	//Tell everyone what is happening
	messageAll('', "\c6(\c3New Duplicator\c6) \c6Creating brick symmetry table...");

	//Make sure we have the uiname table for manual symmetry
	if(!$UINameTableCreated)
		createUINameTable();

	//Delete previous data
	deleteVariables("$ND::Symmetry*");

	$ND::SymmetryTableCreated = false;
	$ND::SymmetryTableCreating = true;
	$ND::SymmetryTableStarted = getRealTime();

	$NDT::SimpleCount = 0;
	$NDT::MeshCount = 0;

	$NDT::AsymXCountTotal = 0;
	$NDT::AsymZCountTotal = 0;

	echo("ND: Start building brick symmetry table...");
	echo("==========================================================================");

	ndTickCreateSymmetryTable(0, getDatablockGroupSize());
}

//Process symmetry for 6 datablocks each tick
function ndTickCreateSymmetryTable(%lastIndex, %max)
{
	%processed = 0;
	%limit = $Server::Dedicated ? 400 : 200;

	for(%i = %lastIndex; %i < %max; %i++)
	{
		%db = getDatablock(%i);

		if(%db.getClassName() $= "FxDtsBrickData")
		{
			%processed += ndTestBrickSymmetry(%db);

			if(%processed > %limit)
			{
				schedule(30, 0, ndTickCreateSymmetryTable, %i + 1, %max);
				return;
			}
		}
	}

	%simple = $NDT::SimpleCount;
	%mesh = $NDT::MeshCount;

	%asymx = $NDT::AsymXCountTotal;
	%asymz = $NDT::AsymZCountTotal;

	echo("==========================================================================");
	echo("ND: Finished basic symmetry tests: " @ %simple @ " simple, " @ %mesh @ " with mesh, " @ %asymx @ " asymmetric, " @ %asymz @ " z-asymmetric");

	ndFindSymmetricPairs();
}

//Attempt to find symmetric pairs between asymmetric bricks
function ndFindSymmetricPairs()
{
	echo("ND: Starting X symmetric pair search...");
	echo("==========================================================================");

	for(%i = 0; %i < $NDT::AsymXCountTotal; %i++)
	{
		%index = $NDT::AsymXBrick[%i];

		if(!$NDT::SkipAsymX[%index])
			ndFindSymmetricPairX(%index);
	}

	echo("==========================================================================");
	echo("ND: Finished finding symmetric pairs");
	echo("ND: Starting Z symmetric pair search...");
	echo("==========================================================================");

	for(%i = 0; %i < $NDT::AsymZCountTotal; %i++)
	{
		%index = $NDT::AsymZBrick[%i];

		if(!$NDT::SkipAsymZ[%index])
			ndFindSymmetricPairZ(%index);
	}

	//Delete temporary arrays
	deleteVariables("$NDT::*");

	echo("==========================================================================");
	echo("ND: Finished finding Z symmetric pairs");
	echo("ND: Symmetry table complete in " @ (getRealTime() - $ND::SymmetryTableStarted) / 1000 @ " seconds");

	$ND::SymmetryTableCreated = true;
	$ND::SymmetryTableCreating = false;

	//We're done!
	%seconds = mFloatLength((getRealTime() - $ND::SymmetryTableStarted) / 1000, 0);
	messageAll('', "\c6(\c3New Duplicator\c6) \c6Created brick symmetry table in " @ %seconds @ " seconds.");
}

//Test symmetry of a single blb file
function ndTestBrickSymmetry(%datablock)
{
	//Open blb file
	%file = new FileObject();
	%file.openForRead(%datablock.brickFile);

	//Skip brick size - irrelevant
	%file.readLine();

	//Simple bricks are always fully symmetric
	if(%file.readLine() $= "BRICK")
	{
		$NDT::SimpleCount++;

		$ND::Symmetry[%datablock] = 1;
		$ND::SymmetryZ[%datablock] = true;

		%file.close();
		%file.delete();
		return 2;
	}

	//Not simple, get mesh data index in temp arrays
	%dbi = $NDT::MeshCount;
	$NDT::MeshCount++;

	//Load mesh from blb file
	%faces = 0;
	%points = 0;

	while(!%file.isEOF())
	{
		//Find start of face
		%line = %file.readLine();

		if(getSubStr(%line, 0, 4) $= "TEX:")
		{
			%tex = trim(getSubStr(%line, 4, strLen(%line)));

			//Top and bottom faces have different topology, skip
			if(%tex $= "TOP" || %tex $= "BOTTOMLOOP" || %tex $= "BOTTOMEDGE")
				continue;

			//Add face
			$NDT::FaceTexId[%dbi, %faces] = (%tex $= "SIDE" ? 0 : (%tex $= "RAMP" ? 1 : 2));

			//Skip useless lines
			while(trim(%file.readLine()) !$= "POSITION:") {}

			//Add the 4 points
			for(%i = 0; %i < 4; %i++)
			{
				//Read next line
				%line = %file.readLine();

				//Skip useless blank lines
				while(!strLen(%line))
					%line = %file.readLine();

				//Remove formatting from point
				%pos = vectorAdd(%line, "0 0 0");

				//Round down two digits to fix float errors
				%pos = mFloatLength(getWord(%pos, 0), 3) * 1.0
				   SPC mFloatLength(getWord(%pos, 1), 3) * 1.0
				   SPC mFloatLength(getWord(%pos, 2), 3) * 1.0;

				//Get index of this point
				if(!%ptIndex = $NDT::PtAtPosition[%dbi, %pos])
				{
					//Points array is 1-indexed so we can quickly test !PtAtPosition[...]
					%points++;
					%ptIndex = %points;

					//Add new point to array
					$NDT::PtPosition[%dbi, %points] = %pos;
					$NDT::PtAtPosition[%dbi, %pos] = %points;
				}

				//Add face to point
				if(!$NDT::PtInFace[%dbi, %faces, %ptIndex])
				{
					//Increase first then subtract 1 to get 0 the first time
					%fIndex = $NDT::FacesAtPt[%dbi, %ptIndex]++ - 1;
					$NDT::FaceAtPt[%dbi, %ptIndex, %fIndex] = %faces;
				}

				//Add point to face
				$NDT::FacePt[%dbi, %faces, %i] = %ptIndex;
				$NDT::PtInFace[%dbi, %faces, %ptIndex] = true;
			}

			//Added face
			%faces++;
		}
	}

	$NDT::FaceCount[%dbi] = %faces;
	$NDT::Datablock[%dbi] = %datablock;

	%file.close();
	%file.delete();

	//Possible symmetries:
	// 0: asymmetric
	// 1: x & y
	// 2: x
	// 3: y
	// 4: x+y
	// 5: x-y

	//We will test in the following order:
	// X
	// Y
	// X+Y
	// X-Y
	// Z

	//Check manual symmetry first
	%sym = $ND::ManualSymmetry[%datablock.uiname];

	if(%sym !$= "")
	{
		if(!%sym)
		{
			//Try to find the other brick
			%otherdb = $UINameTable[$ND::ManualSymmetryDB[%datablock.uiname]];
			%offset = $ND::ManualSymmetryOffset[%datablock.uiname];

			//...
			if(!isObject(%otherdb))
			{
				%otherdb = "";
				%offset = 0;
				echo("ND: " @ %datablock.uiname @ " has manual symmetry but the paired brick does not exist");
			}

			$ND::SymmetryXDatablock[%datablock] = %otherdb;
			$ND::SymmetryXOffset[%datablock] = %offset;
		}

		%manualSym = true;
	}
	else
	{
		%failX = ndTestSelfSymmetry(%dbi, 0);
		%failY = ndTestSelfSymmetry(%dbi, 1);

		//Diagonals are only needed if the brick isn't symmetric to the axis
		if(%failX && %failY)
			%failXY = ndTestSelfSymmetry(%dbi, 3);

		//One diagonal is enough, only test second if first one fails
		if(%failXY)
			%failYX = ndTestSelfSymmetry(%dbi, 4);

		//X, Y symmetry
		if(!%failX && !%failY)
			%sym = 1;
		else if(!%failX)
			%sym = 2;
		else if(!%failY)
			%sym = 3;
		else if(!%failXY)
			%sym = 4;
		else if(!%failYX)
			%sym = 5;
		else
			%sym = 0;
	}

	//Check manual symmetry first
	%symZ = $ND::ManualSymmetryZ[%datablock.uiname];

	//Z symmetry
	if(%symZ !$= "")
	{
		if(!%symZ)
		{
			//Try to find the other brick
			%otherdb = $UINameTable[$ND::ManualSymmetryZDB[%datablock.uiname]];
			%offset = $ND::ManualSymmetryZOffset[%datablock.uiname];

			//...
			if(!isObject(%otherdb))
			{
				%otherdb = "";
				%offset = 0;
				echo("ND: " @ %datablock.uiname @ " has manual Z symmetry but the paired brick does not exist");
			}

			$ND::SymmetryZDatablock[%datablock] = %otherdb;
			$ND::SymmetryZOffset[%datablock] = %offset;
		}

		%manualZSym = true;
	}
	else
		%symZ = !ndTestSelfSymmetry(%dbi, 2);

	if(!%manualSym && !%sym)
	{
		//Add to lookup table of X-asymmetric bricks of this type
		%bIndex = $NDT::AsymXCount[%faces, %symZ]++;
		$NDT::AsymXBrick[%faces, %symZ, %bIndex] = %dbi;

		//Add to list of asymmetric bricks
		$NDT::AsymXBrick[$NDT::AsymXCountTotal] = %dbi;
		$NDT::AsymXCountTotal++;
	}

	if(!%manualZSym && !%symZ)
	{
		//Add to lookup table of Z-asymmetric bricks of this type
		%bIndex = $NDT::AsymZCount[%faces, %sym]++;
		$NDT::AsymZBrick[%faces, %sym, %bIndex] = %dbi;

		//Add to list of Z-asymmetric bricks
		$NDT::AsymZBrick[$NDT::AsymZCountTotal] = %dbi;
		$NDT::AsymZCountTotal++;
	}

	//Save symmetries
	$ND::Symmetry[%datablock] = %sym;
	$ND::SymmetryZ[%datablock] = %symZ;

	//Return processed faces
	return %faces;
}

//Find symmetric pair between two bricks on X axis
function ndFindSymmetricPairX(%dbi)
{
	if($NDT::SkipAsymX[%dbi])
		return;

	%datablock = $NDT::Datablock[%dbi];

	%zsym = $ND::SymmetryZ[%datablock];
	%faces = $NDT::FaceCount[%dbi];
	%count = $NDT::AsymXCount[%faces, %zsym];

	//Only potential match is the brick itself - fail
	if(%count == 1)
	{
		echo("ND: No X match for " @ %datablock.getName() @ " (" @ %datablock.category @ "/" @ %datablock.subCategory @ "/" @ %datablock.uiname @ ")");
		return;
	}

	%off = -1;
	$NDT::SkipAsymX[%dbi] = true;

	for(%i = 1; %i <= %count; %i++)
	{
		%other = $NDT::AsymXBrick[%faces, %zsym, %i];

		//Don't compare with bricks that already have a pair
		if($NDT::SkipAsymX[%other])
			continue;

		//Test all 4 possible rotations
		//Not using loop due to lack of goto command
		if(!ndTestPairSymmetry(%dbi, %other, true, 0))
		{
			%off = 0;
			break;
		}

		if(!ndTestPairSymmetry(%dbi, %other, true, 1))
		{
			%off = 1;
			break;
		}

		if(!ndTestPairSymmetry(%dbi, %other, true, 2))
		{
			%off = 2;
			break;
		}

		if(!ndTestPairSymmetry(%dbi, %other, true, 3))
		{
			%off = 3;
			break;
		}
	}

	if(%off != -1)
	{
		%otherdb = $NDT::Datablock[%other];

		//Save symmetry
		$ND::SymmetryXDatablock[%datablock] = %otherdb;
		$ND::SymmetryXOffset[%datablock] = %off;

		$ND::SymmetryXDatablock[%otherdb] = %datablock;
		$ND::SymmetryXOffset[%otherdb] = -%off;

		//No need to process the other brick again
		$NDT::SkipAsymX[%other] = true;
	}
	else
		echo("ND: No X match for " @ %datablock.getName() @ " (" @ %datablock.category @ "/" @ %datablock.subCategory @ "/" @ %datablock.uiname @ ")");
}

//Find symmetric pair between two bricks on Z axis
function ndFindSymmetricPairZ(%dbi)
{
	if($NDT::SkipAsymZ[%dbi])
		return;

	%datablock = $NDT::Datablock[%dbi];

	%sym = $ND::Symmetry[%datablock];
	%faces = $NDT::FaceCount[%dbi];
	%count = $NDT::AsymZCount[%faces, %sym];

	//Only potential match is the brick itself - fail
	if(%count == 1)
	{
		echo("ND: No Z match for " @ %datablock.getName() @ " (" @ %datablock.category @ "/" @ %datablock.subCategory @ "/" @ %datablock.uiname @ ")");
		return;
	}

	%off = -1;

	for(%i = 1; %i <= %count; %i++)
	{
		%other = $NDT::AsymZBrick[%faces, %sym, %i];

		//Don't compare with bricks that already have a pair
		if($NDT::SkipAsymZ[%other])
			continue;

		//Test all 4 possible rotations
		//Not using loop due to lack of goto command
		if(!ndTestPairSymmetry(%dbi, %other, false, 0))
		{
			%off = 0;
			break;
		}

		if(!ndTestPairSymmetry(%dbi, %other, false, 1))
		{
			%off = 1;
			break;
		}

		if(!ndTestPairSymmetry(%dbi, %other, false, 2))
		{
			%off = 2;
			break;
		}

		if(!ndTestPairSymmetry(%dbi, %other, false, 3))
		{
			%off = 3;
			break;
		}
	}

	//It's possible for a brick to match itself rotated
	//here, so only mark it after the search
	$NDT::SkipAsymZ[%dbi] = true;

	if(%off != -1)
	{
		%otherdb = $NDT::Datablock[%other];

		//Save symmetry
		$ND::SymmetryZDatablock[%datablock] = %otherdb;
		$ND::SymmetryZOffset[%datablock] = %off;

		$ND::SymmetryZDatablock[%otherdb] = %datablock;
		$ND::SymmetryZOffset[%otherdb] = -%off;

		//No need to process the other brick again
		$NDT::SkipAsymZ[%other] = true;
	}
	else
		echo("ND: No Z match for " @ %datablock.getName() @ " (" @ %datablock.category @ "/" @ %datablock.subCategory @ "/" @ %datablock.uiname @ ")");
}

//Test a mesh for a single symmetry plane in itself
function ndTestSelfSymmetry(%dbi, %plane)
{
	%fail = false;
	%faces = $NDT::FaceCount[%dbi];

	for(%i = 0; %i < %faces; %i++)
	{
		//If this face was already used by another mirror, skip
		if(%skipFace[%i])
			continue;

		//Attempt to find the mirrored points
		for(%j = 0; %j < 4; %j++)
		{
			%pt = $NDT::FacePt[%dbi, %i, %j];

			//Do we already know the mirrored one?
			if(%mirrPt[%pt])
			{
				%mirr[%j] = %mirrPt[%pt];
				continue;
			}

			//Get position of point
			%v = $NDT::PtPosition[%dbi, %pt];

			//Get point at mirrored position based on plane
			switch$(%plane)
			{
				//Flip X
				case 0:  %mirr = $NDT::PtAtPosition[%dbi, -firstWord(%v) SPC restWords(%v)];
				//Flip Y
				case 1:  %mirr = $NDT::PtAtPosition[%dbi, getWord(%v, 0) SPC -getWord(%v, 1) SPC getWord(%v, 2)];
				//Flip Z
				case 2:  %mirr = $NDT::PtAtPosition[%dbi, getWords(%v, 0, 1) SPC -getWord(%v, 2)];
				//Mirror along X+Y
				case 3:  %mirr = $NDT::PtAtPosition[%dbi, -getWord(%v, 1) SPC -getWord(%v, 0) SPC getWord(%v, 2)];
				//Mirror along X-Y
				default: %mirr = $NDT::PtAtPosition[%dbi, getWord(%v, 1) SPC getWord(%v, 0) SPC getWord(%v, 2)];
			}

			if(%mirr)
			{
				%mirrPt[%pt] = %mirr;
				%mirrPt[%mirr] = %pt;

				%mirr[%j] = %mirr;
			}
			else
			{
				%fail = true;
				break;
			}
		}

		if(%fail)
			break;

		//Test whether the points have a common face
		%fail = true;
		%count = $NDT::FacesAtPt[%dbi, %mirr0];

		for(%j = 0; %j < %count; %j++)
		{
			%potentialFace = $NDT::FaceAtPt[%dbi, %mirr0, %j];

			//Mirrored face must have the same texture id
			if($NDT::FaceTexId[%dbi, %i] != $NDT::FaceTexId[%dbi, %potentialFace])
				continue;

			//Check whether remaining points are in the face
			if(!$NDT::PtInFace[%dbi, %potentialFace, %mirr1])
				continue;

			if(!$NDT::PtInFace[%dbi, %potentialFace, %mirr2])
				continue;

			if(!$NDT::PtInFace[%dbi, %potentialFace, %mirr3])
				continue;

			//We found a matching face!
			%skipFace[%potentialFace] = true;
			%fail = false;
			break;
		}

		if(%fail)
			break;
	}

	return %fail;
}

//Test X or Z symmetry between two meshes with rotation offset
function ndTestPairSymmetry(%dbi, %other, %plane, %rotation)
{
	%fail = false;
	%faces = $NDT::FaceCount[%dbi];

	for(%i = 0; %i < %faces; %i++)
	{
		//Attempt to find the mirrored points
		for(%j = 0; %j < 4; %j++)
		{
			%pt = $NDT::FacePt[%dbi, %i, %j];

			//Do we already know the mirrored one?
			if(%mirrPt[%pt])
			{
				%mirr[%j] = %mirrPt[%pt];
				continue;
			}

			//Get position of point
			%v = $NDT::PtPosition[%dbi, %pt];

			//true = X, false = Z
			if(%plane)
			{
				//Get point at mirrored position based on rotation
				switch(%rotation)
				{
					//Flip X
					case 0:  %mirr = $NDT::PtAtPosition[%other, -firstWord(%v) SPC restWords(%v)];
					//Flip X, rotate 90
					case 1:  %mirr = $NDT::PtAtPosition[%other, getWord(%v, 1) SPC getWord(%v, 0) SPC getWord(%v, 2)];
					//Flip X, rotate 180
					case 2:  %mirr = $NDT::PtAtPosition[%other, getWord(%v, 0) SPC -getWord(%v, 1) SPC getWord(%v, 2)];
					//Flip X, rotate 270
					default: %mirr = $NDT::PtAtPosition[%other, -getWord(%v, 1) SPC -getWord(%v, 0) SPC getWord(%v, 2)];
				}
			}
			else
			{
				//Get point at mirrored position based on rotation
				switch(%rotation)
				{
					//Flip Z
					case 0:  %mirr = $NDT::PtAtPosition[%other, getWord(%v, 0) SPC getWord(%v, 1) SPC -getWord(%v, 2)];
					//Flip Z, rotate 90
					case 1:  %mirr = $NDT::PtAtPosition[%other, getWord(%v, 1) SPC -getWord(%v, 0) SPC -getWord(%v, 2)];
					//Flip Z, rotate 180
					case 2:  %mirr = $NDT::PtAtPosition[%other, -getWord(%v, 0) SPC -getWord(%v, 1) SPC -getWord(%v, 2)];
					//Flip Z, rotate 270
					default: %mirr = $NDT::PtAtPosition[%other, -getWord(%v, 1) SPC getWord(%v, 0) SPC -getWord(%v, 2)];
				}
			}

			if(%mirr)
			{
				%mirrPt[%pt] = %mirr;
				%mirr[%j] = %mirr;
			}
			else
			{
				%fail = true;
				break;
			}
		}

		if(%fail)
			break;

		//Test whether the points have a common face
		%fail = true;
		%count = $NDT::FacesAtPt[%other, %mirr0];

		for(%j = 0; %j < %count; %j++)
		{
			%potentialFace = $NDT::FaceAtPt[%other, %mirr0, %j];

			//Mirrored face must have the same texture id
			if($NDT::FaceTexId[%dbi, %i] != $NDT::FaceTexId[%other, %potentialFace])
				continue;

			//Check whether remaining points are in the face
			if(!$NDT::PtInFace[%other, %potentialFace, %mirr1])
				continue;

			if(!$NDT::PtInFace[%other, %potentialFace, %mirr2])
				continue;

			if(!$NDT::PtInFace[%other, %potentialFace, %mirr3])
				continue;

			//We found a matching face!
			%fail = false;
			break;
		}

		if(%fail)
			break;
	}

	return %fail;
}