/**
 * Authors: k.inaba
 * License: NYSL 0.9982 http://www.kmonos.net/nysl/
 *
 * Runtime data structures for Polemy programming language.
 */
module polemy.value;
import polemy._common;
import polemy.failure;
import polemy.ast;
import polemy.layer;

/// Runtime values of Polemy

abstract class Value
{
	override bool opEquals(Object rhs) { return 0==opCmp(rhs); }
}

///
class IntValue : Value
{
	BigInt data;

	this(bool n)   { this.data = n?1:0; }
	this(int n)    { this.data = n; }
	this(long n)   { this.data = n; }
	this(BigInt n) { this.data = n; }
	this(string n) { this.data = BigInt(n); }
	override string toString() const { return toDecimalString(cast(BigInt)data); }
	override int opCmp(Object rhs) {
		if(auto r = cast(IntValue)rhs) return data.opCmp(r.data);
		if(auto r = cast(Value)rhs)    return typeid(this).opCmp(typeid(r));
		throw genex!RuntimeException("comparison with value and somithing other");
	}
	mixin SimpleToHash;
}

///
class StrValue : Value
{
	string data;

	mixin SimpleConstructor;
	override string toString() const { return data; }
	override int opCmp(Object rhs) {
		if(auto r = cast(StrValue)rhs) return typeid(string).compare(&data, &r.data);
		if(auto r = cast(Value)rhs)    return typeid(this).opCmp(typeid(r));
		throw genex!RuntimeException("comparison with value and somithing other");
	}
	mixin SimpleToHash;
}

///
class UndefinedValue : Value
{
	mixin SimpleConstructor;
	override string toString() const { return "<undefined>"; }
	override int opCmp(Object rhs) {
		if(auto r = cast(StrValue)rhs) return 0;
		if(auto r = cast(Value)rhs)    return typeid(this).opCmp(typeid(r));
		throw genex!RuntimeException("comparison with value and somithing other");
	}
	mixin SimpleToHash;
}

///
abstract class FunValue : Value
{
	const(Parameter[]) params();
	Table definitionContext();
	Value invoke(Layer lay, Table ctx, LexPosition pos);
}

/// Context (variable environment)
/// Simlar to prototype chain of ECMAScript etc.
/// But extended with the notion of "Layer"

class Table : Value
{
	enum Kind {PropagateSet, NotPropagateSet};

	this( Table proto=null, Kind k = Kind.PropagateSet )
		{ this.prototype = proto; this.kind = k; }

	/// Set the value v to the index i of layer lay
	void set(string i, Layer lay, Value v)
	{
		if( setIfExist(i, lay, v) )
			return;
		data[i][lay] = v;
	}

	/// True if index i has value in layer lay
	bool has(string i, Layer lay) const
	{
		if( i in data )
			return !!(lay in data[i]);
		if( prototype is null )
			return false;
		return prototype.has(i, lay);
	}
	
	/// Return the value of index i at layer lay. Throws if it is not set
	Value get(string i, Layer lay, LexPosition pos=null)
	{
		if( i in data ) {
			if( lay !in data[i] )
				throw genex!RuntimeException(pos, sprintf!"'%s' is not set in %s layer"(i,lay));
			return data[i][lay];
		}
		if( prototype is null )
			throw genex!RuntimeException(pos, sprintf!"'%s' not found in %s layer"(i,lay));
		return prototype.get(i, lay, pos);
	}

	/// t.access!T(lay,a,b,...) returns t.get(a,lay).get(b,lay).... if exists
	/// and has type T. Returns null otherwise
	T access(T,S...)( Layer lay, string path, S rest )
	{
		static if( rest.length == 0 )
		{
			if( this.has(path, lay) )
				return cast(T) this.get(path, lay);
		}
		else
		{
			if(auto next = this.access!Table(lay,path))
				return next.access!T(lay,rest);
		}
		return null;
	}

	/// Is this an empty table?
	bool empty()
	{
		return data.length==0 && (prototype is null || prototype.empty);
	}
	
	/// Can be seen as a cons-list?
	bool isList()
	{
		Table t = this;
		while(t.has("car", ValueLayer) && t.has("cdr", ValueLayer))
			if(auto tt = cast(Table)t.get("cdr", ValueLayer))
				t = tt;
			else
				return false;
		return t.empty;
	}

	/// Regard table as a cons-list and convert to an array
	Value[] toList()
	{
		Value[] result;
		Table t = this;
		while(t.has("car", ValueLayer) && t.has("cdr", ValueLayer))
		{
			result ~= t.get("car", ValueLayer);
			if(auto tt = cast(Table)t.get("cdr", ValueLayer))
				t = tt;
			else
				throw genex!RuntimeException("this table is not a cons-list");
		}
		if( t.empty )
			return result;
		throw genex!RuntimeException("this table is not a cons-list");
	}	

	/// Get the list of direct entries ignoring prototypes in sorted order
	Tuple!(string,Layer,Value)[] direct_entries()
	{
		Tuple!(string,Layer,Value)[] arr;
		foreach(k, l2d; data)
			foreach(l,d; l2d)
				arr ~= tuple(k,l,d);
		arr.sort();
		return arr;
	}
	
	/// Get the whole list of observable entries in unspecified order
	Tuple!(string,Layer,Value)[] entries()
	{
		bool[string] hidden;
		Tuple!(string,Layer,Value)[] arr;
		enumerateEntries(hidden, arr);
		return arr;
	}

	private void enumerateEntries( ref bool[string] hidden, ref Tuple!(string,Layer,Value)[] arr )
	{
		foreach(k, l2d; data)
			if( k !in hidden )
			{
				foreach(l,d; l2d)
					arr ~= tuple(k,l,d);
				hidden[k] = true;
			}
		if(prototype !is null)
			prototype.enumerateEntries(hidden, arr);
	}

	override string toString()
	{
		if( isList() )
			return text(toList());
		return "{" ~ toStringWithoutParen() ~ "}";
	}

	override int opCmp(Object rhs)
	{
		if(auto r = cast(Table)rhs) {
			Tuple!(string,Layer,Value)[] ls = this.entries();
			Tuple!(string,Layer,Value)[] rs = r.entries();
			if( ls.length != rs.length )
				return (ls.length < rs.length ? -1 : +1);
			ls.sort();
			rs.sort();
			foreach(i,_; ls)
				if(auto c = ls[i].opCmp(rs[i]))
					return c;
			return 0;
		}
		if(auto r = cast(Value)rhs) return typeid(this).opCmp(typeid(r));
		throw genex!RuntimeException("comparison with value and somithing other");
	}

	override hash_t toHash()
	{
		Tuple!(string,Layer,Value)[] ls = this.entries();
		ls.sort();
		hash_t h;
		foreach(e; ls)
			h += structuralHash(e[0])+structuralHash(e[1])+structuralHash(e[2]);
		return h;
	}

private:
	Table                prototype;
	Kind                 kind;
	Value[Layer][string] data;

	string toStringWithoutParen() const
	{
		string result;
		bool first = true;
		foreach(k, l2d; data)
			foreach(l,d; l2d)
			{
				if(first) first=false; else result~=", ";
				result ~= k;
				if( l.empty )
					result ~= "(emptylayer)";
				else if( l != ValueLayer )
					result ~= l;
				result ~= ":";
				result ~= text(cast(Value)d);
			}
		if( prototype !is null )
		{
			result ~= " / ";
			result ~= prototype.toStringWithoutParen();
		}
		return result;
	}

	bool setIfExist(string i, Layer lay, Value v)
	{
		if( i in data )
		{
			data[i][lay] = v;
			return true;
		}
		if( kind==Kind.PropagateSet && prototype !is null )
			return prototype.setIfExist(i, lay, v);
		return false;
	}
}

unittest
{
	Table c0 = new Table;
	Table c01 = new Table(c0, Table.Kind.NotPropagateSet);
	Table c012 = new Table(c01, Table.Kind.PropagateSet);
	Table c013 = new Table(c01, Table.Kind.PropagateSet);

	assert_nothrow( c012.set("x", ValueLayer, new IntValue(12)) );
	assert_throw!RuntimeException( c013.get("x", ValueLayer) );
	assert_nothrow( c013.set("x", ValueLayer, new IntValue(13)) );
	assert_eq( c013.get("x", ValueLayer), new IntValue(13) );
	assert_eq( c012.get("x", ValueLayer), new IntValue(12) );
	assert_throw!RuntimeException( c01.get("x", ValueLayer) );

	assert_nothrow( c01.set("y", ValueLayer, new IntValue(1)) );
	assert_eq( c013.get("y", ValueLayer), new IntValue(1) );
	assert_eq( c012.get("y", ValueLayer), new IntValue(1) );
	assert_eq( c01.get("y", ValueLayer), new IntValue(1) );

	assert_nothrow( c0.set("z", ValueLayer, new IntValue(0)) );
	assert_eq( c013.get("z", ValueLayer), new IntValue(0) );
	assert_eq( c012.get("z", ValueLayer), new IntValue(0) );
	assert_eq( c01.get("z", ValueLayer), new IntValue(0) );
	assert_eq( c0.get("z", ValueLayer), new IntValue(0) );

	assert_nothrow( c012.set("y", ValueLayer, new IntValue(444)) );
	assert_eq( c013.get("y", ValueLayer), new IntValue(444) );
	assert_eq( c012.get("y", ValueLayer), new IntValue(444) );
	assert_eq( c01.get("y", ValueLayer), new IntValue(444) );

	assert_nothrow( c012.set("z", ValueLayer, new IntValue(555)) );
	assert_eq( c013.get("z", ValueLayer), new IntValue(0) );
	assert_eq( c012.get("z", ValueLayer), new IntValue(555) );
	assert_eq( c01.get("z", ValueLayer), new IntValue(0) );
	assert_eq( c0.get("z", ValueLayer), new IntValue(0) );

	// [TODO] define the semantics and test @layers
}
