// Goldie: GOLD Engine for D
// Written in the D programming language.

/**
 NOTE: My terminilogy in this code is somewhat inconsistent with the file format docs.
 
       My Term  | Docs Term
       ----------------------
	   Data     | Entry  (ex, 'I':Integer)
	   Entry    | Record (ex, 'P':Parameters)
	   Record   | Record (ie, 'M':Multitype)
*/

// Read GOLD Parser Builder's .cgt (Compiled Grammar Table) files.
module goldie.file;

import tango.core.Exception;
import tango.io.Stdout;
import tango.io.device.File;
import tango.io.stream.Buffered;
import tango.io.stream.Data;
import tango.text.Util;

import semitwist.util.all;

import goldie.base;

class GoldType(T)
{
	static assert(is(T==GoldData) || is(T==GoldEntry), "T must be GoldData or GoldEntry");
	
	private char _code;
	private char[] _name;
	
	public this(char code, char[] name)
	{
		_code = code;
		_name = name;
	}
	
	public char code()
	{
		return _code;
	}

	public char[] name()
	{
		return _name.dup;
	}
}
alias GoldType!(GoldData) GoldDataType;

abstract class GoldEntryType: GoldType!(GoldEntry)
{
	// Does not include the initial GoldDataType.Byte to denote entry type
	private GoldDataType[] _entryFormat;
	private char[] entryFormatCode;
	
	public this(char code, char[] name, char[] entryFormatCode)
	{
		super(code, name);
		mixin(initMember!(entryFormatCode));
	}
	
	// WARNING: Do not call this from within a static constructor.
	//          This relies on the static constructor of GoldDataTypes
	//          having already run.
	public GoldDataType[] entryFormat()
	{
		if(_entryFormat.length == 0)
		{
			foreach(char c; entryFormatCode)
				_entryFormat ~= GoldDataTypes.fromCode(c);
		}
		
		return _entryFormat.dup;
	}
}

template GoldTypes(T)
{
	static assert(is(T==GoldDataType) || is(T==GoldEntryType), "T must be GoldDataType or GoldEntryType");

	private static T[] All;
	private static char[] nameOfT; // It's clumbsy to get exactly what I want here at compile-time
	
	public static T fromCode(char code)
	{
		foreach(T t; All)
			if(t.code() == code)
				return t;
		
		throw new IllegalArgumentException(
			"{} for code #{} '{}' doesn't exist".sformat(nameOfT, cast(ubyte)code, code)
		);
	}

	public static T fromName(char[] name)
	{
		foreach(T t; All)
			if(t.name() == name)
				return t;

		throw new IllegalArgumentException(
			"{} named '{}' doesn't exist".sformat(nameOfT, name)
		);
	}
}

enum GoldDataTypeCode : char
{
	Empty   = 'E',
	Byte    = 'b',
	Boolean = 'B',
	Integer = 'I',
	String  = 'S',
}

static class GoldDataTypes
{
	public static GoldDataType Empty;
	public static GoldDataType Byte;
	public static GoldDataType Boolean;
	public static GoldDataType Integer;
	public static GoldDataType String;

	mixin GoldTypes!(GoldDataType);
	
	public static this()
	{
		// It's clumbsy to get exactly what I want here at compile-time
		nameOfT = "GoldDataType";

		All ~= Empty   = new GoldDataType('E', "Empty");
		All ~= Byte    = new GoldDataType('b', "Byte");
		All ~= Boolean = new GoldDataType('B', "Boolean");
		All ~= Integer = new GoldDataType('I', "Integer");
		All ~= String  = new GoldDataType('S', "String");
	}
}

static class GoldEntryTypes
{
	public static GoldEntryType Parameters;
	public static GoldEntryType TableCounts;
	public static GoldEntryType CharacterSetTable;
	public static GoldEntryType Symbols;
	public static GoldEntryType Rules;
	public static GoldEntryType InitialStates;
	public static GoldEntryType DFAStates;
	public static GoldEntryType LALRStates;

	mixin GoldTypes!(GoldEntryType);

	public static this()
	{
		// It's clumbsy to get exactly what I want here at compile-time
		nameOfT = "GoldEntryType";
		
		All ~= Parameters        = new GoldEntryParameters();
		All ~= TableCounts       = new GoldEntryTableCounts();
		All ~= CharacterSetTable = new GoldEntryCharacterSetTable();
		All ~= Symbols           = new GoldEntrySymbols();
		All ~= Rules             = new GoldEntryRules();
		All ~= InitialStates     = new GoldEntryInitialStates();
		All ~= DFAStates         = new GoldEntryDFAStates();
		All ~= LALRStates        = new GoldEntryLALRStates();
	}
}

class GoldData
{
	private GoldDataType _type;
	private union GoldDataValue
	{
		ubyte   Byte;
		bool    Boolean;
		short   Integer;
		wchar[] String;
	}
	private GoldDataValue value;
	
	public this(GoldDataType _type)
	{
		mixin(initMember!(_type));
	}
	
	public this()
	{
		_type = GoldDataTypes.Empty;
	}
	
	public this(ubyte value)
	{
		_type = GoldDataTypes.Byte;
		this.value.Byte = value;
	}
	
	public this(bool value)
	{
		_type = GoldDataTypes.Boolean;
		this.value.Boolean = value;
	}
	
	public this(short value)
	{
		_type = GoldDataTypes.Integer;
		this.value.Integer = value;
	}
	
	public this(wchar[] value)
	{
		_type = GoldDataTypes.String;
		this.value.String = value;
	}
	
	public void read(DataInput reader)
	{
		char typeCode = cast(char)reader.getByte();
		_type = GoldDataTypes.fromCode(typeCode);
		
		if(_type == GoldDataTypes.Empty)
		{
			// Do nothing
		}
		else if(_type == GoldDataTypes.Byte)
		{
			value.Byte = cast(ubyte)reader.getByte();
		}
		else if(_type == GoldDataTypes.Boolean)
		{
			auto inBool = reader.getByte();
			if(inBool == 0 || inBool == 1)
				value.Boolean = (inBool == 1);
			else
				throw new IOException(
					"Expected a GoldDataTypes.Boolean (0 or 1) but got {}".sformat(inBool)
				);
		}
		else if(_type == GoldDataTypes.Integer)
		{
			value.Integer = reader.getShort();
		}
		else if(_type == GoldDataTypes.String)
		{
			value.String = readNullTerminatedWString(reader);
		}
		else
		{
			throw new Exception(
				"Unknown GoldDataType #{} '{}'".sformat(cast(ubyte)typeCode, typeCode)
			);
		}
	}

	public void read(DataInput reader, GoldDataType expectedType)
	{
		read(reader);

		if(_type != expectedType)
		{
			throw new Exception(
				"Expected #{} '{}' ('[]') type, but got #{} '{}'".sformat(
					cast(ubyte)expectedType.code(), expectedType.code(),
					expectedType.name(),
					cast(ubyte)_type.code(), type.code())
				);
		}
	}

	public GoldDataType type()
	{
		return _type;
	}
	
	public ubyte valueByte()
	{
		if(_type != GoldDataTypes.Byte)
			throw new Exception("Type is {}, not Byte".sformat(_type.name()));
			
		return value.Byte;
	}
	
	public bool valueBoolean()
	{
		if(_type != GoldDataTypes.Boolean)
			throw new Exception("Type is {}, not Boolean".sformat(_type.name()));
			
		return value.Boolean;
	}
	
	public short valueInteger()
	{
		if(_type != GoldDataTypes.Integer)
			throw new Exception("Type is {}, not Integer".sformat(_type.name()));
			
		return value.Integer;
	}
	
	public wchar[] valueString()
	{
		if(_type != GoldDataTypes.String)
			throw new Exception("Type is {}, not String".sformat(_type.name()));
			
		return value.String.dup;
	}
}

GoldData readGoldData(DataInput reader)
{
	auto data = new GoldData();
	data.read(reader);
	
	return data;
}

GoldData readGoldData(DataInput reader, GoldDataType expectedType)
{
	auto data = new GoldData();
	data.read(reader, expectedType);
	
	return data;
}

GoldEntry readGoldEntry(DataInput reader)
{
	auto entry = new GoldEntry();
	entry.read(reader);
	
	return entry;
}

class GoldEntry
{
	// content does not include 'M' (for Multitype), number of data, or entry type code
	private GoldData[] content;
	private GoldEntryType _type;
	
	public void read(DataInput reader)
	{
		char recordTypeCode = cast(char)reader.getByte();
		if(recordTypeCode != 'M')
			throw new Exception(
				"Expected record type 'M' (Multitype) but found #{} '{}'".sformat(
					cast(ubyte)recordTypeCode, recordTypeCode
				)
			);
		
		auto numData = reader.getShort();
		numData--; // Don't count the entry type code

		auto entryType = readGoldData(reader, GoldDataTypes.Byte);

		_type = GoldEntryTypes.fromCode(entryType.valueByte());
		foreach(GoldDataType dataType; _type.entryFormat())
			content ~= readGoldData(reader, dataType);
	
		// Load the rest
		// TODO: type checking on loaded data
		while(content.length < numData)
			content ~= readGoldData(reader);
			
		// TODO?: Create appropriate data view struct thing?
	}
	
	public GoldEntryType type()
	{
		return _type;
	}
	
	public int length()
	{
		return content.length;
	}
	
	public GoldData opIndex(int index)
	{
		return content[index];
	}
}

class GoldEntryParameters : GoldEntryType
{
	this()
	{
		super('P', "Parameters",
		[
			GoldDataTypeCode.String,  // Name
			GoldDataTypeCode.String,  // Version
			GoldDataTypeCode.String,  // Author
			GoldDataTypeCode.String,  // About
			GoldDataTypeCode.Boolean, // Case Sensitive?
			GoldDataTypeCode.Integer  // Start Symbol
		]);
	}
}

class GoldEntryTableCounts : GoldEntryType
{
	this()
	{
		super('T', "TableCounts",
		[
			GoldDataTypeCode.Integer, // Number of Symbol Tables
			GoldDataTypeCode.Integer, // Number of Character Set Tables
			GoldDataTypeCode.Integer, // Number of Rule Tables
			GoldDataTypeCode.Integer, // Number of DFA Tables
			GoldDataTypeCode.Integer  // Number of LALR Tables
		]);
	}
}

class GoldEntryCharacterSetTable : GoldEntryType
{
	this()
	{
		super('C', "CharacterSetTable",
		[
			GoldDataTypeCode.Integer, // Index
			GoldDataTypeCode.String   // Characters
		]);
	}
}

class GoldEntrySymbols : GoldEntryType
{
	this()
	{
		super('S', "Symbols",
		[
			GoldDataTypeCode.Integer, // Index
			GoldDataTypeCode.String,  // Name
			GoldDataTypeCode.Integer  // Kind
		]);
	}
}

class GoldEntryRules : GoldEntryType
{
	this()
	{
		super('R', "Rules",
		[
			GoldDataTypeCode.Integer, // Index
			GoldDataTypeCode.Integer, // Non-Terminal
			GoldDataTypeCode.Empty    // (Reserved)
			// TODO: ...more...
		]);
	}
}

class GoldEntryInitialStates : GoldEntryType
{
	this()
	{
		super('I', "InitialStates",
		[
			GoldDataTypeCode.Integer, // DFA State
			GoldDataTypeCode.Integer  // LALR State
		]);
	}
}

class GoldEntryDFAStates : GoldEntryType
{
	this()
	{
		super('D', "DFAStates",
		[
			GoldDataTypeCode.Integer, // Index
			GoldDataTypeCode.Boolean, // Accept State?
			GoldDataTypeCode.Integer, // Accept Index
			GoldDataTypeCode.Empty    // (Reserved)
			// TODO: ...more...
		]);
	}
}

class GoldEntryLALRStates : GoldEntryType
{
	this()
	{
		super('L', "LALRStates",
		[
			GoldDataTypeCode.Integer, // Index
			GoldDataTypeCode.Empty    // (Reserved)
			// TODO: ...more...
		]);
	}
}

class GoldFile
{
	// May not really need this
	private GoldEntry[] entries;

	private const wchar[] expectedFileType = "GOLD Parser Tables/v1.0"w;
	
	public wchar[] grammarName;
	public wchar[] grammarVersion;
	public wchar[] grammarAuthor;
	public wchar[] about;
	public bool    caseSensitive;
	public int     startSymbolIndex;

	public GoldSymbol[]     symbolTable;
	public GoldCharSet[]    characterSetTable;
	public GoldRule[]       ruleTable;
	public GoldDFAState[]   dfaTable;
	public GoldLALRState[]  lalrTable;
	
	public int initialDFAState;
	public int initialLALRState;

	public this(char[] infilename)
	{
		load(infilename);
	}
	
	private void ensureTableCountsLoaded(bool tableCountsLoaded, char[] entryTypeName)
	{
		if(!tableCountsLoaded)
			throw new IOException(
				"TableCounts entry not yet found when trying to read {} entry".sformat(entryTypeName)
			);
	}
	
	private void load(char[] infilename)
	{
		try
		{
			auto file = new File(infilename);
			scope(exit) file.close();
			
			auto readBuffer = new BufferedInput(file.input);
			auto reader = new DataInput(readBuffer);
			reader.endian(DataInput.Little);
			
			// Load data
			// Read null-term string "GOLD Parser Tables/1.0" (using read func in semitwist.util)
			wchar[] fileType = readNullTerminatedWString(reader);
			if(fileType != expectedFileType)
				throw new IOException(
					"Unexpected filetype field\nExpected: \"{}\"\nFound:    \"{}\"".sformat(
						expectedFileType, fileType
					)
				);
			
			// Read some records
			bool paramatersLoaded    = false;
			bool tableCountsLoaded   = false;
			bool initialStatesLoaded = false;

			while(file.position() - readBuffer.readable() < file.length())
			{
				auto entry = readGoldEntry(reader);
				auto entryType = entry.type();
			
				if(entryType == GoldEntryTypes.Parameters)
				{
					//Stdout.formatln("Found: Parameters");
					if(paramatersLoaded)
						throw new IOException("Multiple Parameters entries");
						
					grammarName      = entry[0].valueString();
					grammarVersion   = entry[1].valueString();
					grammarAuthor    = entry[2].valueString();
					about            = entry[3].valueString();
					caseSensitive    = entry[4].valueBoolean();
					startSymbolIndex = entry[5].valueInteger();
					
					paramatersLoaded = true;
				}
				else if(entryType == GoldEntryTypes.TableCounts)
				{
					//Stdout.formatln("Found: TableCounts");
					if(tableCountsLoaded)
						throw new IOException("Multiple TableCounts entries");
						
					symbolTable.length       = entry[0].valueInteger();
					characterSetTable.length = entry[1].valueInteger();
					ruleTable.length         = entry[2].valueInteger();
					dfaTable.length          = entry[3].valueInteger();
					lalrTable.length         = entry[4].valueInteger();
					
					tableCountsLoaded = true;
				}
				else if(entryType == GoldEntryTypes.InitialStates)
				{
					//Stdout.formatln("Found: InitialStates");
					if(initialStatesLoaded)
						throw new IOException("Multiple InitialStates entries");
						
					initialDFAState  = entry[0].valueInteger();
					initialLALRState = entry[1].valueInteger();
					
					initialStatesLoaded = true;
				}
				else if(entryType == GoldEntryTypes.CharacterSetTable)
				{
					//Stdout.formatln("Found: CharacterSetTable");
					ensureTableCountsLoaded(tableCountsLoaded, "CharacterSetTable");

					int index = entry[0].valueInteger();
					if(index >= characterSetTable.length)
						throw new IOException(
							"Invalid CharacterSetTable index. Found {}, expected < {}"
								.sformat(index, characterSetTable.length)
						);
									 
					characterSetTable[index].chars = entry[1].valueString();
				}
				else if(entryType == GoldEntryTypes.Symbols)
				{
					//Stdout.formatln("Found: Symbols");
					ensureTableCountsLoaded(tableCountsLoaded, "Symbols");

					int index = entry[0].valueInteger();
					if(index >= symbolTable.length)
						throw new IOException(
							"Invalid Symbols index. Found {}, expected < {}"
								.sformat(index, symbolTable.length)
						);
									 
					symbolTable[index].name = entry[1].valueString();
					symbolTable[index].type = cast(GoldSymbolType)entry[2].valueInteger();
					symbolTable[index].id = index;
				}
				else if(entryType == GoldEntryTypes.Rules)
				{
					//Stdout.formatln("Found: Rules");
					ensureTableCountsLoaded(tableCountsLoaded, "Rules");

					int index = entry[0].valueInteger();
					if(index >= ruleTable.length)
						throw new IOException(
							"Invalid Rules index. Found {}, expected < {}"
								.sformat(index, ruleTable.length)
						);
									 
					ruleTable[index].nonTerminalIndex = entry[1].valueInteger();
					int numTerminals = entry.length - 3;
					ruleTable[index].terminalIndicies.length = numTerminals;
					for(int i=0; i<numTerminals; i++)
					{
						ruleTable[index].terminalIndicies[i] = entry[i+3].valueInteger();
					}
				}
				else if(entryType == GoldEntryTypes.DFAStates)
				{
					//Stdout.formatln("Found: DFAStates");
					ensureTableCountsLoaded(tableCountsLoaded, "DFAStates");

					int index = entry[0].valueInteger();
					if(index >= dfaTable.length)
						throw new IOException(
							"Invalid DFAStates index. Found {}, expected < {}"
								.sformat(index, dfaTable.length)
						);
									 
					dfaTable[index].accept = entry[1].valueBoolean();
					dfaTable[index].acceptSymbolIndex = entry[2].valueInteger();
					int numEdges = (entry.length - 4) / 3;
					dfaTable[index].edges.length = numEdges;
					for(int i=0; i<numEdges; i++)
					{
						dfaTable[index].edges[i].charSetIndex         = entry[(i*3) + 4].valueInteger();
						dfaTable[index].edges[i].targetDFAStateIndex  = entry[(i*3) + 5].valueInteger();
					}
				}
				else if(entryType == GoldEntryTypes.LALRStates)
				{
					//Stdout.formatln("Found: LALRStates");
					ensureTableCountsLoaded(tableCountsLoaded, "LALRStates");

					int index = entry[0].valueInteger();
					if(index >= lalrTable.length)
						throw new IOException(
							"Invalid LALRStates index. Found {}, expected < {}"
								.sformat(index, lalrTable.length)
						);
									 
					int numActions = (entry.length - 2) / 4;
					lalrTable[index].actions.length = numActions;
					for(int i=0; i<numActions; i++)
					{
						lalrTable[index].actions[i].symbolId = entry[(i*4) + 2].valueInteger();
						lalrTable[index].actions[i].type     = cast(GoldLALRAction.Type)entry[(i*4) + 3].valueInteger();
						lalrTable[index].actions[i].target   = entry[(i*4) + 4].valueInteger();
					}
				}
				else
				{
					//Stdout.formatln("Found: Unknown");
					throw new Exception(
						"Unknown entry type #{} ({})"
							.sformat(cast(ubyte)entryType.code(), entryType.code())
					);
				}
				//Stdout.formatln("Done");

				entries ~= entry;
			}
			
			if(!paramatersLoaded)
				throw new IOException("Paramaters entry missing");
			if(!tableCountsLoaded)
				throw new IOException("TableCounts entry missing");
			if(!initialStatesLoaded)
				throw new IOException("InitialStates entry missing");
			
			// TODO: Create "lenient load" flag
			// TODO: Make sure there are no unloaded table indicies
		}
		catch(Exception e)
		{
			// Should be IOException, but IOException doesn't support inner exceptions
			throw new Exception("Problem loading GOLD .cgt file", e);
		}
	}
}
