// SDLang-D
// Written in the D programming language.

module sdlang.token;

import std.array;
import std.base64;
import std.conv;
import std.datetime;
import std.meta;
import std.range;
import std.string;
import std.traits;
import std.typetuple;
import std.variant;

import sdlang.exception;
import sdlang.symbol;
import sdlang.util;

/// DateTime doesn't support milliseconds, but SDLang's "Date Time" type does.
/// So this is needed for any SDL "Date Time" that doesn't include a time zone.
struct DateTimeFrac
{
	DateTime dateTime;
	Duration fracSecs;
	deprecated("Use fracSecs instead.") {
		@property FracSec fracSec() const { return FracSec.from!"hnsecs"(fracSecs.total!"hnsecs"); }
		@property void fracSec(FracSec v) { fracSecs = v.hnsecs.hnsecs; }
	}
}

/++
If a "Date Time" literal in the SDL file has a time zone that's not found in
your system, you get one of these instead of a SysTime. (Because it's
impossible to indicate "unknown time zone" with `std.datetime.TimeZone`.)

The difference between this and `DateTimeFrac` is that `DateTimeFrac`
indicates that no time zone was specified in the SDL at all, whereas
`DateTimeFracUnknownZone` indicates that a time zone was specified but
data for it could not be found on your system.
+/
struct DateTimeFracUnknownZone
{
	DateTime dateTime;
	Duration fracSecs;
	deprecated("Use fracSecs instead.") {
		@property FracSec fracSec() const { return FracSec.from!"hnsecs"(fracSecs.total!"hnsecs"); }
		@property void fracSec(FracSec v) { fracSecs = v.hnsecs.hnsecs; }
	}
	string timeZone;

	bool opEquals(const DateTimeFracUnknownZone b) const
	{
		return opEquals(b);
	}
	bool opEquals(ref const DateTimeFracUnknownZone b) const
	{
		return
			this.dateTime == b.dateTime &&
			this.fracSecs  == b.fracSecs  &&
			this.timeZone == b.timeZone;
	}
}

/++
SDLang's datatypes map to D's datatypes as described below.
Most are straightforward, but take special note of the date/time-related types.

---------------------------------------------------------------
Boolean:                       bool
Null:                          typeof(null)
Unicode Character:             dchar
Double-Quote Unicode String:   string
Raw Backtick Unicode String:   string
Integer (32 bits signed):      int
Long Integer (64 bits signed): long
Float (32 bits signed):        float
Double Float (64 bits signed): double
Decimal (128+ bits signed):    real
Binary (standard Base64):      ubyte[]
Time Span:                     Duration

Date (with no time at all):           Date
Date Time (no timezone):              DateTimeFrac
Date Time (with a known timezone):    SysTime
Date Time (with an unknown timezone): DateTimeFracUnknownZone
---------------------------------------------------------------
+/
alias ValueTypes = TypeTuple!(
	bool,
	string, dchar,
	int, long,
	float, double, real,
	Date, DateTimeFrac, SysTime, DateTimeFracUnknownZone, Duration,
	ubyte[],
	typeof(null),
);

alias Value = Algebraic!( ValueTypes ); ///ditto
enum isValueType(T) = staticIndexOf!(T, ValueTypes) != -1;

enum isSink(T) =
	isOutputRange!T &&
	is(ElementType!(T)[] == string);

string toSDLString(T)(T value) if(is(T==Value) || isValueType!T)
{
	Appender!string sink;
	toSDLString(value, sink);
	return sink.data;
}

/// Throws SDLangException if value is infinity, -infinity or NaN, because
/// those are not currently supported by the SDLang spec.
void toSDLString(Sink)(Value value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	foreach(T; ValueTypes)
	{
		if(value.type == typeid(T))
		{
			toSDLString( value.get!T(), sink );
			return;
		}
	}
	
	throw new Exception("Internal SDLang-D error: Unhandled type of Value. Contains: "~value.toString());
}

@("toSDLString on infinity and NaN")
unittest
{
	import std.exception;
	
	auto floatInf    = float.infinity;
	auto floatNegInf = -float.infinity;
	auto floatNaN    = float.nan;

	auto doubleInf    = double.infinity;
	auto doubleNegInf = -double.infinity;
	auto doubleNaN    = double.nan;

	auto realInf    = real.infinity;
	auto realNegInf = -real.infinity;
	auto realNaN    = real.nan;

	assertNotThrown( toSDLString(0.0F) );
	assertNotThrown( toSDLString(0.0)  );
	assertNotThrown( toSDLString(0.0L) );
	
	assertThrown!ValidationException( toSDLString(floatInf) );
	assertThrown!ValidationException( toSDLString(floatNegInf) );
	assertThrown!ValidationException( toSDLString(floatNaN) );

	assertThrown!ValidationException( toSDLString(doubleInf) );
	assertThrown!ValidationException( toSDLString(doubleNegInf) );
	assertThrown!ValidationException( toSDLString(doubleNaN) );

	assertThrown!ValidationException( toSDLString(realInf) );
	assertThrown!ValidationException( toSDLString(realNegInf) );
	assertThrown!ValidationException( toSDLString(realNaN) );
	
	assertThrown!ValidationException( toSDLString(Value(floatInf)) );
	assertThrown!ValidationException( toSDLString(Value(floatNegInf)) );
	assertThrown!ValidationException( toSDLString(Value(floatNaN)) );

	assertThrown!ValidationException( toSDLString(Value(doubleInf)) );
	assertThrown!ValidationException( toSDLString(Value(doubleNegInf)) );
	assertThrown!ValidationException( toSDLString(Value(doubleNaN)) );

	assertThrown!ValidationException( toSDLString(Value(realInf)) );
	assertThrown!ValidationException( toSDLString(Value(realNegInf)) );
	assertThrown!ValidationException( toSDLString(Value(realNaN)) );
}

void toSDLString(Sink)(typeof(null) value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	sink.put("null");
}

void toSDLString(Sink)(bool value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	sink.put(value? "true" : "false");
}

//TODO: Figure out how to properly handle strings/chars containing lineSep or paraSep
void toSDLString(Sink)(string value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	sink.put('"');
	
	// This loop is UTF-safe
	foreach(char ch; value)
	{
		if     (ch == '\n') sink.put(`\n`);
		else if(ch == '\r') sink.put(`\r`);
		else if(ch == '\t') sink.put(`\t`);
		else if(ch == '\"') sink.put(`\"`);
		else if(ch == '\\') sink.put(`\\`);
		else
			sink.put(ch);
	}

	sink.put('"');
}

void toSDLString(Sink)(dchar value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	sink.put('\'');
	
	if     (value == '\n') sink.put(`\n`);
	else if(value == '\r') sink.put(`\r`);
	else if(value == '\t') sink.put(`\t`);
	else if(value == '\'') sink.put(`\'`);
	else if(value == '\\') sink.put(`\\`);
	else
		sink.put(value);

	sink.put('\'');
}

void toSDLString(Sink)(int value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	sink.put( "%s".format(value) );
}

void toSDLString(Sink)(long value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	sink.put( "%sL".format(value) );
}

private void checkUnsupportedFloatingPoint(T)(T value) if(isFloatingPoint!T)
{
	import std.exception;
	import std.math;
	
	enforce!ValidationException(
		!isInfinity(value),
		"SDLang does not currently support infinity for floating-point types"
	);

	enforce!ValidationException(
		!isNaN(value),
		"SDLang does not currently support NaN for floating-point types"
	);
}

void toSDLString(Sink)(float value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	checkUnsupportedFloatingPoint(value);
	sink.put( "%.10sF".format(value) );
}

void toSDLString(Sink)(double value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	checkUnsupportedFloatingPoint(value);
	sink.put( "%.30sD".format(value) );
}

void toSDLString(Sink)(real value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	checkUnsupportedFloatingPoint(value);
	sink.put( "%.30sBD".format(value) );
}

void toSDLString(Sink)(Date value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	sink.put(to!string(value.year));
	sink.put('/');
	sink.put(to!string(cast(int)value.month));
	sink.put('/');
	sink.put(to!string(value.day));
}

void toSDLString(Sink)(DateTimeFrac value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	toSDLString(value.dateTime.date, sink);
	sink.put(' ');
	sink.put("%.2s".format(value.dateTime.hour));
	sink.put(':');
	sink.put("%.2s".format(value.dateTime.minute));
	
	if(value.dateTime.second != 0)
	{
		sink.put(':');
		sink.put("%.2s".format(value.dateTime.second));
	}

	if(value.fracSecs != 0.msecs)
	{
		sink.put('.');
		sink.put("%.3s".format(value.fracSecs.total!"msecs"));
	}
}

void toSDLString(Sink)(SysTime value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	auto dateTimeFrac = DateTimeFrac(cast(DateTime)value, value.fracSecs);
	toSDLString(dateTimeFrac, sink);
	
	sink.put("-");
	
	auto tzString = value.timezone.name;
	
	// If name didn't exist, try abbreviation.
	// Note that according to std.datetime docs, on Windows the
	// stdName/dstName may not be properly abbreviated.
	version(Windows) {} else
	if(tzString == "")
	{
		auto tz = value.timezone;
		auto stdTime = value.stdTime;
		
		if(tz.hasDST())
			tzString = tz.dstInEffect(stdTime)? tz.dstName : tz.stdName;
		else
			tzString = tz.stdName;
	}
	
	if(tzString == "")
	{
		auto offset = value.timezone.utcOffsetAt(value.stdTime);
		sink.put("GMT");

		if(offset < seconds(0))
		{
			sink.put("-");
			offset = -offset;
		}
		else
			sink.put("+");
		
		sink.put("%.2s".format(offset.split.hours));
		sink.put(":");
		sink.put("%.2s".format(offset.split.minutes));
	}
	else
		sink.put(tzString);
}

void toSDLString(Sink)(DateTimeFracUnknownZone value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	auto dateTimeFrac = DateTimeFrac(value.dateTime, value.fracSecs);
	toSDLString(dateTimeFrac, sink);
	
	sink.put("-");
	sink.put(value.timeZone);
}

void toSDLString(Sink)(Duration value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	if(value < seconds(0))
	{
		sink.put("-");
		value = -value;
	}
	
	auto days = value.total!"days"();
	if(days != 0)
	{
		sink.put("%s".format(days));
		sink.put("d:");
	}

	sink.put("%.2s".format(value.split.hours));
	sink.put(':');
	sink.put("%.2s".format(value.split.minutes));
	sink.put(':');
	sink.put("%.2s".format(value.split.seconds));

	if(value.split.msecs != 0)
	{
		sink.put('.');
		sink.put("%.3s".format(value.split.msecs));
	}
}

void toSDLString(Sink)(ubyte[] value, ref Sink sink) if(isOutputRange!(Sink,char))
{
	sink.put('[');
	sink.put( Base64.encode(value) );
	sink.put(']');
}

/// This only represents terminals. Nonterminals aren't
/// constructed since the AST is directly built during parsing.
struct Token
{
	Symbol symbol = sdlang.symbol.symbol!"Error"; /// The "type" of this token
	Location location;
	Value value; /// Only valid when `symbol` is `symbol!"Value"`, otherwise null
	string data; /// Original text from source

	@disable this();
	this(Symbol symbol, Location location, Value value=Value(null), string data=null)
	{
		this.symbol   = symbol;
		this.location = location;
		this.value    = value;
		this.data     = data;
	}
	
	/// Tokens with differing symbols are always unequal.
	/// Tokens with differing values are always unequal.
	/// Tokens with differing Value types are always unequal.
	/// Member `location` is always ignored for comparison.
	/// Member `data` is ignored for comparison *EXCEPT* when the symbol is Ident.
	bool opEquals(Token b)
	{
		return opEquals(b);
	}
	bool opEquals(ref Token b) ///ditto
	{
		if(
			this.symbol     != b.symbol     ||
			this.value.type != b.value.type ||
			this.value      != b.value
		)
			return false;
		
		if(this.symbol == .symbol!"Ident")
			return this.data == b.data;
		
		return true;
	}
	
	bool matches(string symbolName)()
	{
		return this.symbol == .symbol!symbolName;
	}
}

@("sdlang token")
unittest
{
	auto loc  = Location("", 0, 0, 0);
	auto loc2 = Location("a", 1, 1, 1);

	assert(Token(symbol!"EOL",loc) == Token(symbol!"EOL",loc ));
	assert(Token(symbol!"EOL",loc) == Token(symbol!"EOL",loc2));
	assert(Token(symbol!":",  loc) == Token(symbol!":",  loc ));
	assert(Token(symbol!"EOL",loc) != Token(symbol!":",  loc ));
	assert(Token(symbol!"EOL",loc,Value(null),"\n") == Token(symbol!"EOL",loc,Value(null),"\n"));

	assert(Token(symbol!"EOL",loc,Value(null),"\n") == Token(symbol!"EOL",loc,Value(null),";" ));
	assert(Token(symbol!"EOL",loc,Value(null),"A" ) == Token(symbol!"EOL",loc,Value(null),"B" ));
	assert(Token(symbol!":",  loc,Value(null),"A" ) == Token(symbol!":",  loc,Value(null),"BB"));
	assert(Token(symbol!"EOL",loc,Value(null),"A" ) != Token(symbol!":",  loc,Value(null),"A" ));

	assert(Token(symbol!"Ident",loc,Value(null),"foo") == Token(symbol!"Ident",loc,Value(null),"foo"));
	assert(Token(symbol!"Ident",loc,Value(null),"foo") != Token(symbol!"Ident",loc,Value(null),"BAR"));

	assert(Token(symbol!"Value",loc,Value(null),"foo") == Token(symbol!"Value",loc, Value(null),"foo"));
	assert(Token(symbol!"Value",loc,Value(null),"foo") == Token(symbol!"Value",loc2,Value(null),"foo"));
	assert(Token(symbol!"Value",loc,Value(null),"foo") == Token(symbol!"Value",loc, Value(null),"BAR"));
	assert(Token(symbol!"Value",loc,Value(   7),"foo") == Token(symbol!"Value",loc, Value(   7),"BAR"));
	assert(Token(symbol!"Value",loc,Value(   7),"foo") != Token(symbol!"Value",loc, Value( "A"),"foo"));
	assert(Token(symbol!"Value",loc,Value(   7),"foo") != Token(symbol!"Value",loc, Value(   2),"foo"));
	assert(Token(symbol!"Value",loc,Value(cast(int)7)) != Token(symbol!"Value",loc, Value(cast(long)7)));
	assert(Token(symbol!"Value",loc,Value(cast(float)1.2)) != Token(symbol!"Value",loc, Value(cast(double)1.2)));
}

@("sdlang Value.toSDLString()")
unittest
{
	// Bool and null
	assert(Value(null ).toSDLString() == "null");
	assert(Value(true ).toSDLString() == "true");
	assert(Value(false).toSDLString() == "false");
	
	// Base64 Binary
	assert(Value(cast(ubyte[])"hello world".dup).toSDLString() == "[aGVsbG8gd29ybGQ=]");

	// Integer
	assert(Value(cast( int) 7).toSDLString() ==  "7");
	assert(Value(cast( int)-7).toSDLString() == "-7");
	assert(Value(cast( int) 0).toSDLString() ==  "0");

	assert(Value(cast(long) 7).toSDLString() ==  "7L");
	assert(Value(cast(long)-7).toSDLString() == "-7L");
	assert(Value(cast(long) 0).toSDLString() ==  "0L");

	// Floating point
	assert(Value(cast(float) 1.5).toSDLString() ==  "1.5F");
	assert(Value(cast(float)-1.5).toSDLString() == "-1.5F");
	assert(Value(cast(float)   0).toSDLString() ==    "0F");

	assert(Value(cast(double) 1.5).toSDLString() ==  "1.5D");
	assert(Value(cast(double)-1.5).toSDLString() == "-1.5D");
	assert(Value(cast(double)   0).toSDLString() ==    "0D");

	assert(Value(cast(real) 1.5).toSDLString() ==  "1.5BD");
	assert(Value(cast(real)-1.5).toSDLString() == "-1.5BD");
	assert(Value(cast(real)   0).toSDLString() ==    "0BD");

	// String
	assert(Value("hello"  ).toSDLString() == `"hello"`);
	assert(Value(" hello ").toSDLString() == `" hello "`);
	assert(Value(""       ).toSDLString() == `""`);
	assert(Value("hello \r\n\t\"\\ world").toSDLString() == `"hello \r\n\t\"\\ world"`);
	assert(Value("日本語").toSDLString() == `"日本語"`);

	// Chars
	assert(Value(cast(dchar) 'A').toSDLString() ==  `'A'`);
	assert(Value(cast(dchar)'\r').toSDLString() == `'\r'`);
	assert(Value(cast(dchar)'\n').toSDLString() == `'\n'`);
	assert(Value(cast(dchar)'\t').toSDLString() == `'\t'`);
	assert(Value(cast(dchar)'\'').toSDLString() == `'\''`);
	assert(Value(cast(dchar)'\\').toSDLString() == `'\\'`);
	assert(Value(cast(dchar) '月').toSDLString() ==  `'月'`);

	// Date
	assert(Value(Date( 2004,10,31)).toSDLString() == "2004/10/31");
	assert(Value(Date(-2004,10,31)).toSDLString() == "-2004/10/31");

	// DateTimeFrac w/o Frac
	assert(Value(DateTimeFrac(DateTime(2004,10,31,  14,30,15))).toSDLString() == "2004/10/31 14:30:15");
	assert(Value(DateTimeFrac(DateTime(2004,10,31,   1, 2, 3))).toSDLString() == "2004/10/31 01:02:03");
	assert(Value(DateTimeFrac(DateTime(-2004,10,31, 14,30,15))).toSDLString() == "-2004/10/31 14:30:15");

	// DateTimeFrac w/ Frac
	assert(Value(DateTimeFrac(DateTime(2004,10,31,  14,30,15), 123.msecs)).toSDLString() == "2004/10/31 14:30:15.123");
	assert(Value(DateTimeFrac(DateTime(2004,10,31,  14,30,15), 120.msecs)).toSDLString() == "2004/10/31 14:30:15.120");
	assert(Value(DateTimeFrac(DateTime(2004,10,31,  14,30,15), 100.msecs)).toSDLString() == "2004/10/31 14:30:15.100");
	assert(Value(DateTimeFrac(DateTime(2004,10,31,  14,30,15),  12.msecs)).toSDLString() == "2004/10/31 14:30:15.012");
	assert(Value(DateTimeFrac(DateTime(2004,10,31,  14,30,15),   1.msecs)).toSDLString() == "2004/10/31 14:30:15.001");
	assert(Value(DateTimeFrac(DateTime(-2004,10,31, 14,30,15), 123.msecs)).toSDLString() == "-2004/10/31 14:30:15.123");

	// DateTimeFracUnknownZone
	assert(Value(DateTimeFracUnknownZone(DateTime(2004,10,31, 14,30,15), 123.msecs, "Foo/Bar")).toSDLString() == "2004/10/31 14:30:15.123-Foo/Bar");

	// SysTime
	assert(Value(SysTime(DateTime(2004,10,31, 14,30,15), new immutable SimpleTimeZone( hours(0)             ))).toSDLString() == "2004/10/31 14:30:15-GMT+00:00");
	assert(Value(SysTime(DateTime(2004,10,31,  1, 2, 3), new immutable SimpleTimeZone( hours(0)             ))).toSDLString() == "2004/10/31 01:02:03-GMT+00:00");
	assert(Value(SysTime(DateTime(2004,10,31, 14,30,15), new immutable SimpleTimeZone( hours(2)+minutes(10) ))).toSDLString() == "2004/10/31 14:30:15-GMT+02:10");
	assert(Value(SysTime(DateTime(2004,10,31, 14,30,15), new immutable SimpleTimeZone(-hours(5)-minutes(30) ))).toSDLString() == "2004/10/31 14:30:15-GMT-05:30");
	assert(Value(SysTime(DateTime(2004,10,31, 14,30,15), new immutable SimpleTimeZone( hours(2)+minutes( 3) ))).toSDLString() == "2004/10/31 14:30:15-GMT+02:03");
	assert(Value(SysTime(DateTime(2004,10,31, 14,30,15), 123.msecs, new immutable SimpleTimeZone( hours(0) ))).toSDLString() == "2004/10/31 14:30:15.123-GMT+00:00");

	// Duration
	assert( "12:14:42"         == Value( days( 0)+hours(12)+minutes(14)+seconds(42)+msecs(  0)).toSDLString());
	assert("-12:14:42"         == Value(-days( 0)-hours(12)-minutes(14)-seconds(42)-msecs(  0)).toSDLString());
	assert( "00:09:12"         == Value( days( 0)+hours( 0)+minutes( 9)+seconds(12)+msecs(  0)).toSDLString());
	assert( "00:00:01.023"     == Value( days( 0)+hours( 0)+minutes( 0)+seconds( 1)+msecs( 23)).toSDLString());
	assert( "23d:05:21:23.532" == Value( days(23)+hours( 5)+minutes(21)+seconds(23)+msecs(532)).toSDLString());
	assert( "23d:05:21:23.530" == Value( days(23)+hours( 5)+minutes(21)+seconds(23)+msecs(530)).toSDLString());
	assert( "23d:05:21:23.500" == Value( days(23)+hours( 5)+minutes(21)+seconds(23)+msecs(500)).toSDLString());
	assert("-23d:05:21:23.532" == Value(-days(23)-hours( 5)-minutes(21)-seconds(23)-msecs(532)).toSDLString());
	assert("-23d:05:21:23.500" == Value(-days(23)-hours( 5)-minutes(21)-seconds(23)-msecs(500)).toSDLString());
	assert( "23d:05:21:23"     == Value( days(23)+hours( 5)+minutes(21)+seconds(23)+msecs(  0)).toSDLString());
}