// code to manage file formats and parsing
#include "stdafx.h"
#include "Form1.h"
#include <vector>
#include "Life.h"
using namespace std;

using namespace LifeSim;
using namespace System::Drawing;

namespace {

// todo -write all to return false on fail, move status text to parameter

/*
 * .RLE files are ASCII text files with a one-line header,
 * and followed by a variable-length run-length encoded image.
 *
 * The header may be preceded by any number of blank lines and/or
 * comment lines beginning with a pound sign '#'.
 *
 * The header consists of several fields separated by commas,
 * and ending in a newline.
 * The first two fields are "x = nnn" and "y = nnn", (where nnn
 * is an integer) respectively specifying the pattern width and height.
 * These fields are required.
 *
 * An optional rule field, of the form "rule = Bnnn/Snnn" may follow,
 * encoding totalistic rules.  By convention, this field is required
 * a rule different from Life (B3/S23) is used.
 *
 * The data portion consists of one or more runs, terminated by
 * an exclamation mark '!'.
 * The runs are defined by a single character, preceded by an optional
 * integer repeat factor (which defaults to 1 if omitted).
 * The character may be one of the following:
 *	$	Newline
 *	b	Dead cell
 *	o	Living cell, or first living state in multi-state Life
 *	x*	Second living state in multi-state Life
 *	y*	Third living state in multi-state Life
 *	z*	Fourth living state in multi-state Life
 *	other	Unrecognized codes should be interpreted as living cells
 * (NOTE: characters marked * above are extensions used by this program,
 *  but not part of the generally accepted de-facto RLE standard;
 *  however, x, y, and z behave correctly when treated as 'other')
 * Embedded white space is ignored.
 *
 * The de-facto RLE standard adds the following additional stipulations:
 * Lines should not exceed 70 characters to avoid confusing brain-damaged
 * e-mail programs, so newlines should be added as required.
 * Newlines and other white-space may not be inserted in the middle of a
 * repeat factor, nor between a character and its repeat factor.
 * This program can, however, read files which violate these stipulations.
 *
 *
 *
 * *.L files are the same compression, first line is of form 
 *       x = 286, y = 163, rule = B3/S135
 * then RLE image data, then comments with no prefixing symbols.
 *
 * 
 */

// load RLE file, return status string and comment, true iff success
bool LoadRLE(String ^ filename, String ^% status, String ^% comment, 
			 vector<unsigned char> & buffer, int & width, int & height, // image info
			 bool useLMethod = false // *.L file or RLE file
			 )
	{
	using namespace System::Text;
	using namespace System::IO;
	
	StringBuilder ^ sb = gcnew StringBuilder();

	int xpos = 0, ypos = 0; // current draw cursor

	width = height = -1; // default 

	StreamReader ^ re = File::OpenText(filename);
	String ^ input = nullptr;
	bool firstLine = true, done = false;
	enum NextItem {Width, Height, Rule, None} next = None; // type of item we expect
	while (((input = re->ReadLine()) != nullptr) && (false == done))
		{
		if ('#' == input[0])
			{ // comment - todo - add to comments section
			comment = String::Concat(comment,input->Substring(2),"\n");
			}
		else if	(true == firstLine)
			{ // parse out info
			firstLine = false;
			array<Char>^chars = {',',' ','='};
			array<String^>^split = input->Split( chars );
			IEnumerator^ myEnum = split->GetEnumerator();
			while ( myEnum->MoveNext() )
				{
				String^ tok = safe_cast<String^>(myEnum->Current);
				if ("" == tok)
					continue;
				if (Width == next)
					width = Convert::ToInt32(tok)+1;  // sizes are inclusive? seem so
				else if (Height == next)
					height = Convert::ToInt32(tok)+1; // sizes are inclusive? seem so
				else if ((Rule == next) && ("B3/S23" != tok))
					{
					status = "Only game of life rule B3/S23 currently supported";
					return false;
					}
				next = None;
				if ("x" == tok)
					next = Width;
				else if ("y" == tok)
					next = Height;
				else if ("rule" == tok)
					next = Rule;
//				sb->Append(tok);
				}
			if ((-1 == width) || (-1 == height))
				{
				status = "Image size could not be parsed";
				return false;
				}
			buffer.resize(width*height);
			memset(&buffer[0],0,width*height);
			}
		else
			{ // data line, process
 /*
 * The data portion consists of one or more runs, terminated by
 * an exclamation mark '!'.
 * The runs are defined by a single character, preceded by an optional
 * integer repeat factor (which defaults to 1 if omitted).
 * The character may be one of the following:
 *	$	Newline
 *	b	Dead cell
 *	o	Living cell, or first living state in multi-state Life
 *	x*	Second living state in multi-state Life
 *	y*	Third living state in multi-state Life
 *	z*	Fourth living state in multi-state Life
 *	other	Unrecognized codes should be interpreted as living cells
 * (NOTE: characters marked * above are extensions used by this program,
 *  but not part of the generally accepted de-facto RLE standard;
 *  however, x, y, and z behave correctly when treated as 'other')
 * Embedded white space is ignored. */

			int val = 0;
			for (int pos = 0; (pos < input->Length) && (false == done); ++pos)
				{
				char ch = input[pos];
				if ((' ' == ch) || ('\t' == ch) || ('\n' == ch) || ('\r' == ch))
					continue; // skip whitespace
				if ('$' == ch)
					{ // newline
					if (0 == val)
						val = 1; // default
					ypos += val;
					val = 0;
					xpos = 0;
					}
				else if (('0' <= ch) && (ch <= '9'))
					{ // prefix
					val *= 10;
					val += ch - '0';
					}
				else if ('b' == ch)
					{ // dead cell
					if (0 == val)
						val = 1; // default
					while (val--)
						{
						if ((0 <= xpos) && (xpos < width) && (0<= ypos) && (ypos < height))
							buffer[xpos + width*ypos] = 0;
						else
							{ // out of bounds
							status = "Coord out of bounds";
							return false;
							}
						++xpos;
						}
					val = 0;
					}
				else if ('!' == ch)
					done = true;
				else 
					{ // live cell - either 'o','x','y','z' or unrecognized non whitespace
					if (0 == val)
						val = 1; // default
					while (val--)
						{
						if ((0 <= xpos) && (xpos < width) && (0<= ypos) && (ypos < height))
							buffer[xpos + width*ypos] = 1;
						else
							{ // out of bounds
							status = "Coord out of bounds";
							return false;
							}
						++xpos;
						}
					val = 0;
					}

				}
			}
		} // while lines left and not data break
//	if (true == useLMethod)
		{ // read rest as comments
		while ((input = re->ReadLine()) != nullptr)
			comment = String::Concat(comment,input,"\n");
		}

		
	status = sb->ToString();
	return true;
	} // LoadRLE

// load LIF file, return status string, comment string, and true if success
bool LoadLIF(String ^ filename, String ^% status, String ^% comment, vector<unsigned char> & buffer, int & width, int & height)
	{
	using namespace System::Text;
	using namespace System::IO;
	
	int xpos = 0, ypos = 0; // current draw cursor

	width = height = -1; // default - make large as we go
	int minx = INT_MAX, miny = INT_MAX, maxx = INT_MIN, maxy = INT_MIN;
	vector<int> coords; // (x,y) coords of set cells

	StreamReader ^ re = File::OpenText(filename);
	String ^ input = nullptr;
	int version = 0; // should become 105 or 106
	
	enum NextItem {Version,Width, Height, Rule, None} next = Version; // type of item we expect

	while ((input = re->ReadLine()) != nullptr)
		{
		if (0 == input->Length)
			continue;
		if ('#' == input[0])
			{ 
			if (Version == next)
				{ // check version number
				if ((input->Substring(1) != "Life 1.05") && (input->Substring(1) != "Life 1.06"))
					{
					status = "Life 1.05 and 1.06 only LIF file supported";
					return false;
					}
				if (input->Substring(1) == "Life 1.05")
					version = 105;
				else if (input->Substring(1) == "Life 1.06")
					version = 106;
				next = None;
				}
			else if (('C' == input[1]) || ('D' == input[1])) // comment line
				comment = String::Concat(comment,input->Substring(2),"\n");
			else if ('N' == input[1]) 
				{
				// normal conway rule
				}
			else if (('R' == input[1]) && ("23/3" != input->Substring(3,4)))
				{ // unknown rule unless Conway "R 23/3"
				status = String::Concat("Unknown life rule ",input->Substring(2));
				return false;
				}
			else if ('P' == input[1])
				{ // location block, read in until
				array<Char>^chars = {',',' ','='};
				array<String^>^split = input->Split( chars );
				IEnumerator^ myEnum = split->GetEnumerator();
				next = Width;
				while ( myEnum->MoveNext() )
					{
					String^ tok = safe_cast<String^>(myEnum->Current);
					if (("" == tok) || ('#' == tok[0]))
						continue;
					if (Width == next)
						{
						xpos = Convert::ToInt32(tok)+1;  // sizes are inclusive? seem so
						next = Height;
						}
					else if (Height == next)
						ypos = Convert::ToInt32(tok)+1; // sizes are inclusive? seem so
					}
				next = None;
				}
			}
		else
			{ // data line, process 
			if (106 == version)
				{ // location block, read in until
				array<Char>^chars = {',',' ','='};
				array<String^>^split = input->Split( chars );
				IEnumerator^ myEnum = split->GetEnumerator();
				next = Width;
				while ( myEnum->MoveNext() )
					{
					String^ tok = safe_cast<String^>(myEnum->Current);
					if (("" == tok) || ('#' == tok[0]))
						continue;
					if (Width == next)
						{
						xpos = Convert::ToInt32(tok);
						next = Height;
						}
					else if (Height == next)
						ypos = Convert::ToInt32(tok);
					}
				next = None;
				// set pixel
				coords.push_back(xpos);
				coords.push_back(ypos);
				maxx = max(xpos,maxx);
				minx = min(xpos,minx);
				maxy = max(ypos,maxy);
				miny = min(ypos,miny);
				}
			else if (version == 105)
				{
				//String ^ str = String::Concat(xpos,", ",ypos); // todo - remove
				//System::Diagnostics::Debug::WriteLine(str);

				int xstart = xpos;
				for (int pos = 0; pos < input->Length; ++pos)
					{
					char ch = input[pos];
					if ((' ' == ch) || ('\t' == ch) || ('\n' == ch) || ('\r' == ch))
						continue; // skip whitespace
					if ('.' == ch)
						{ // dead cell
						++xpos;
						}
					else if ('*' == ch)
						{ // live cell
						// set pixel
						coords.push_back(xpos);
						coords.push_back(ypos);
						maxx = max(xpos,maxx);
						minx = min(xpos,minx);
						maxy = max(ypos,maxy);
						miny = min(ypos,miny);
						++xpos;
						}
					} // for each character
				// newline
				++ypos;   
				xpos = xstart;
				}
			else
				{ // should never reach here
				status = "Unknown version";
				return false;
				}
			} // is version 1.05
		}

	// if gotten this far, fill a buffer
	width  = maxx - minx + 3; // a little padding
	height = maxy - miny + 3;
	if ((width > 16384) || (height > 16384))
		{ // we clamp size arbitrarily to avoid huge load times
		status = "Image too large";
		return false;
		}

	buffer.resize(width*height);
	for (unsigned int pos = 0 ; pos < coords.size(); pos += 2)
		{
		xpos = coords[pos]   - minx;
		ypos = coords[pos+1] - miny;
		buffer[xpos + ypos * width] = 1;
		}

	status = String::Concat("Size ",width,", ",ypos);
	return true;
	} // LoadLIF


// load image file, return status string and comment, true if success
bool LoadImage(String ^ filename, String ^% status, vector<unsigned char> & buffer, int & width, int & height)
	{
	using namespace System::Text;
	Bitmap ^ img = gcnew Bitmap(Bitmap::FromFile(filename));
	StringBuilder ^ sb = gcnew StringBuilder();
	width = img->Width, height = img->Height;
	sb->Append("Size: ");
	sb->Append(width);
	sb->Append(", ");
	sb->Append(height);
	buffer.resize(width*height);

	for (int y = 0; y < height; ++y)
		for (int x = 0; x < width; ++x)
			{
			Color col = img->GetPixel(x,y);
			if (0 == col.R)
				{
				buffer[x + width*y] = 1;
				}
			}
	status = sb->ToString();
	return true;
	} // LoadImage


// get the uppercase file extension from the file
String ^ FileExtension(String ^ filename)
	{
	String ^ ext = filename->Substring(filename->LastIndexOf(".")+1);
	return ext->ToUpper();
	} // FileExtension

// return last part of path component
String ^ LastPathComponent(String ^ str)
	{
	return str->Substring(str->LastIndexOf("\\")+1);
	} // LastPathComponent

// add files from current directory matching pattern
// return count of them
int AddMatchingFiles(TreeNode ^ node, String ^ dir, String ^ pattern)
	{
	using namespace System::IO;
	// Process the list of files found in the directory.
	array<String^>^fileEntries = Directory::GetFiles( dir, pattern );
	IEnumerator^ files = fileEntries->GetEnumerator();
	while ( files->MoveNext() )
		{
		String^ filename = safe_cast<String^>(files->Current);
		TreeNode ^ newNode = gcnew TreeNode(LastPathComponent(filename));
		newNode->Tag = filename; // save for opening later
		node->Nodes->Add(newNode);
		}
	return fileEntries->Length;
	} // AddMatchingFiles

// recursively fill nodes with file structure
// return count of files (not directories)
int RecurseFillDirectories(TreeNode ^ node, String ^ dir)
	{
	int fileCount = 0;
	using namespace System::IO;
	// Recurse into subdirectories of this directory.
	array<String^>^subdirectoryEntries = Directory::GetDirectories( dir );
	IEnumerator^ dirs = subdirectoryEntries->GetEnumerator();
	while ( dirs->MoveNext() )
		{
		String^ subdir = safe_cast<String^>(dirs->Current);
		TreeNode ^ newNode = gcnew TreeNode(LastPathComponent(subdir));
		newNode->Tag = ""; // marks a directory
		fileCount += RecurseFillDirectories(newNode, subdir);
		node->Nodes->Add(newNode);
		}
#ifdef _DEBUG
	fileCount += AddMatchingFiles(node,dir,"*.*"); // all files 
#else
	// todo - formats for now? GIF, PNG, BMP, RLE, LIF, *.L ?
	fileCount += AddMatchingFiles(node,dir,"*.BMP");
	fileCount += AddMatchingFiles(node,dir,"*.PNG");
	fileCount += AddMatchingFiles(node,dir,"*.GIF");
	fileCount += AddMatchingFiles(node,dir,"*.RLE");
	fileCount += AddMatchingFiles(node,dir,"*.LIF");
	fileCount += AddMatchingFiles(node,dir,"*.L");
#endif
	return fileCount;
	} // RecurseFillDirectories

	}; // anon

void Form1::LoadPatternView(String ^ basedir)
	{
	using namespace System::IO;
	patternView->Nodes->Clear();

	// save current
	String ^ curDir = Directory::GetCurrentDirectory();

	// read them all in
	TreeNode ^ node = gcnew TreeNode("Patterns");
	int fileCount = RecurseFillDirectories(node,basedir);
	for (int pos =0 ; pos < node->Nodes->Count; ++pos)
		patternView->Nodes->Add(node->Nodes[pos]);
	
	// restore current
	Directory::SetCurrentDirectory(curDir);

	// set text
	patternText->Text = String::Concat("Patterns (",fileCount,")");

	} // LoadPatternView

bool Form1::LoadPattern(String ^ filename, String ^% comment, int & imgw, int & imgh, LifeSimulator * sim)
	{
	String ^ ext = FileExtension(filename);
	String ^ stat = "";

	comment = "";

//	int imgw, imgh;
	vector<unsigned char> buffer;

	bool result;
	if (("PNG" == ext) || ("GIF" == ext) || ("BMP" == ext))
		{
		result = LoadImage(filename,stat,buffer,imgw,imgh);
		status->Text = stat;
		}
	else if (("RLE" == ext) || ("L" == ext))
		{ // todo - *.L files have all sorts of crap in them - find other method?
		result = LoadRLE(filename,stat,comment,
			buffer,imgw,imgh,
			"L" == ext
			);
		status->Text = stat;
		}
	else if ("LIF" == ext)
		{
		result = LoadLIF(filename,stat,comment,buffer,imgw,imgh);
		status->Text = stat;
		}
	else
		{
		MessageBox::Show(String::Concat("File format ", ext, " currently unsupported"));
		return false;
		}
	if (false == result)
		{
		return false; // todo - message
		}
	sim->Reset();
	if (true == sim->UsesLoadBuffer())
		{
		sim->LoadBuffer(&buffer[0],imgw,imgh);
		}
	else
		{ // copy back

		sim->ConvertToExternal();

		// center in image
		int w,h;
		sim->GetSize(w,h);

		if ((imgw >= w) || (imgh >= h))
			{
			System::Text::StringBuilder ^ sb = gcnew System::Text::StringBuilder();
			sb->Append(String::Concat("Image ",imgw,", ",imgh," too big"));
			status->Text = sb->ToString();
			}
		else
			{
			int dx,dy; // for centering RLE image in sim
			dx = (w-imgw)/2, dy = (h-imgh)/2;

			for (int j = 0; j < imgh; ++j)
				for (int i = 0; i < imgw; ++i)
					sim->SetValue(i+dx,j+dy,buffer[i+j*imgw]);
			}
		}
	sim->ConvertToInternal();
	sim->CountPopulation();
	return true;
	} // LoadPattern


// end - FileFormat.cpp