package webidl;
import webidl.Data;

private enum Token {
	TEof;
	TId( s : String );
	TPOpen;
	TPClose;
	TBrOpen;
	TBrClose;
	TBkOpen;
	TBkClose;
	TSemicolon;
	TComma;
	TOp( op : String );
	TString( str : String );
}


class Parser {

	public var line : Int;
	var input : haxe.io.Input;
	var char : Int;
	var ops : Array<Bool>;
	var idents : Array<Bool>;
	var tokens : Array<Token>;
	var pos = 0;
	var fileName : String;

	public function new() {
		var opChars = "+*/-=!><&|^%~";
		var identChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";
		idents = new Array();
		ops = new Array();
		for( i in 0...identChars.length )
			idents[identChars.charCodeAt(i)] = true;
		for( i in 0...opChars.length )
			ops[opChars.charCodeAt(i)] = true;
	}

	public function parseFile( fileName : String, input : haxe.io.Input ) {
		this.fileName = fileName;
		pos = 0;
		line = 1;
		char = -1;
		tokens = [];
		this.input = input;
		var out = [];
		while( true ) {
			var tk = token();
			if( tk == TEof ) break;
			push(tk);
			out.push(parseDecl());
		}
		return out;
	}

	function parseDecl() {
		var attr = attributes();
		var pmin = this.pos;
		switch( token() ) {
		case TId("interface"):
			var name = ident();
			ensure(TBrOpen);
			var fields = [];
			while( true ) {
				var tk = token();
				if( tk == TBrClose ) break;
				push(tk);
				fields.push(parseField());
			}
			ensure(TSemicolon);
			return { pos : makePos(pmin), kind : DInterface(name, attr, fields) };
		case TId("enum"):
			var name = ident();
			ensure(TBrOpen);
			var values = [];
			if( !maybe(TBrClose) )
				while( true ) {
					switch( token() ) {
					case TString(str): values.push(str);
					case var tk: unexpected(tk);
					}
					switch( token() ) {
					case TBrClose: break;
					case TComma: continue;
					case var tk: unexpected(tk);
					}
				}
			ensure(TSemicolon);
			return { pos : makePos(pmin), kind : DEnum(name, values) };
		case TId(name):
			if( attr.length > 0 )
				throw "assert";
			ensure(TId("implements"));
			var intf = ident();
			ensure(TSemicolon);
			return { pos : makePos(pmin), kind : DImplements(name, intf) };

		case var tk:
			return unexpected(tk);
		}
	}

	function attributes() {
		if( !maybe(TBkOpen) )
			return [];
		var attrs = [];
		while( true ) {
			var attr = switch( ident() ) {
			case "Value": AValue;
			case "Ref": ARef;
			case "Const": AConst;
			case "NoDelete": ANoDelete;
			case "Prefix":
				ensure(TOp("="));
				APrefix(switch( token() ) { case TString(s): s; case var tk: unexpected(tk); });
			case "JSImplementation":
				ensure(TOp("="));
				AJSImplementation(switch( token() ) { case TString(s): s; case var tk: unexpected(tk); });
			case "Operator":
				ensure(TOp("="));
				AOperator(switch( token() ) { case TString(s): s; case var tk: unexpected(tk); });
			case var attr:
				error("Unsupported attribute " + attr);
				null;
			}
			attrs.push(attr);
			if( !maybe(TComma) ) break;
		}
		ensure(TBkClose);
		return attrs;
	}

	function type() : Type {
		var id = ident();
		var t = switch( id ) {
		case "void": TVoid;
		case "float": TFloat;
		case "double": TDouble;
		case "long", "int": TInt; // long ensures 32 bits
		case "short": TShort;
		case "boolean", "bool": TBool;
		case "any": TAny;
		case "VoidPtr": TVoidPtr;
		default: TCustom(id);
		};
		if( maybe(TBkOpen) ) {
			ensure(TBkClose);
			t = TArray(t);
		}
		return t;
	}

	function makePos( pmin : Int ) {
		return { file : fileName, line : line, pos : pmin };
	}

	function parseField() : Field {
		var attr = attributes();
		var pmin = this.pos;

		if( maybe(TId("attribute")) ) {
			var t = type();
			var name = ident();
			ensure(TSemicolon);
			return { name : name, kind : FAttribute({ t : t, attr : attr }), pos : makePos(pmin) };
		}

		if( maybe(TId("const")) ) {
			var type = type();
			var name = ident();
			ensure(TOp("="));
			var value = tokenString(token());
			ensure(TSemicolon);
			return { name: name, kind : DConst(name, type, value), pos : makePos(pmin) };
		}

		var tret = type();
		var name = ident();
		ensure(TPOpen);
		var args = [];
		if( !maybe(TPClose) ) {
			while( true ) {
				var attr = attributes();
				var opt = maybe(TId("optional"));
				var t = type();
				var name = ident();
				args.push({ name : name, t : { t : t, attr : attr }, opt : opt });
				switch( token() ) {
				case TPClose:
					break;
				case TComma:
					continue;
				case var tk:
					unexpected(tk);
				}
			}
		}
		ensure(TSemicolon);
		return { name : name, kind : FMethod(args, { attr : attr, t : tret }), pos : makePos(pmin) };
	}

	// --- Lexing

	function invalidChar(c:Int) {
		error("Invalid char "+c+"("+String.fromCharCode(c)+")");
	}

	function error( msg : String ) {
		throw msg+" line "+line;
	}

	function unexpected( tk ) : Dynamic {
		error("Unexpected " + tokenString(tk));
		return null;
	}

	function tokenString( tk ) {
		return switch( tk ) {
		case TEof: "<eof>";
		case TId(id): id;
		case TPOpen: "(";
		case TPClose: ")";
		case TBkOpen: "[";
		case TBkClose: "]";
		case TBrOpen: "{";
		case TBrClose: "}";
		case TComma: ",";
		case TSemicolon: ";";
		case TOp(op): op;
		case TString(str): '"' + str + '"';
		}
	}

	inline function push(tk) {
		tokens.push(tk);
	}

	function ensure(tk) {
		var t = token();
		if( t != tk && !std.Type.enumEq(t,tk) ) unexpected(t);
	}

	function maybe(tk) {
		var t = token();
		if( t == tk || std.Type.enumEq(t,tk) )
			return true;
		push(t);
		return false;
	}

	function ident() {
		var tk = token();
		switch( tk ) {
		case TId(id): return id;
		default:
			unexpected(tk);
			return null;
		}
	}

	function readChar() {
		pos++;
		return try input.readByte() catch( e : Dynamic ) 0;
	}

	function token() : Token {
		if( tokens.length > 0 )
			return tokens.shift();
		var char;
		if( this.char < 0 )
			char = readChar();
		else {
			char = this.char;
			this.char = -1;
		}
		while( true ) {
			switch( char ) {
			case 0: return TEof;
			case 32,9,13: // space, tab, CR
			case 10: line++; // LF
/*			case 48,49,50,51,52,53,54,55,56,57: // 0...9
				var n = (char - 48) * 1.0;
				var exp = 0.;
				while( true ) {
					char = readChar();
					exp *= 10;
					switch( char ) {
					case 48,49,50,51,52,53,54,55,56,57:
						n = n * 10 + (char - 48);
					case 46:
						if( exp > 0 ) {
							// in case of '...'
							if( exp == 10 && readChar() == 46 ) {
								push(TOp("..."));
								var i = Std.int(n);
								return TConst( (i == n) ? CInt(i) : CFloat(n) );
							}
							invalidChar(char);
						}
						exp = 1.;
					case 120: // x
						if( n > 0 || exp > 0 )
							invalidChar(char);
						// read hexa
						#if haxe3
						var n = 0;
						while( true ) {
							char = readChar();
							switch( char ) {
							case 48,49,50,51,52,53,54,55,56,57: // 0-9
								n = (n << 4) + char - 48;
							case 65,66,67,68,69,70: // A-F
								n = (n << 4) + (char - 55);
							case 97,98,99,100,101,102: // a-f
								n = (n << 4) + (char - 87);
							default:
								this.char = char;
								return TConst(CInt(n));
							}
						}
						#else
						var n = haxe.Int32.ofInt(0);
						while( true ) {
							char = readChar();
							switch( char ) {
							case 48,49,50,51,52,53,54,55,56,57: // 0-9
								n = haxe.Int32.add(haxe.Int32.shl(n,4), cast (char - 48));
							case 65,66,67,68,69,70: // A-F
								n = haxe.Int32.add(haxe.Int32.shl(n,4), cast (char - 55));
							case 97,98,99,100,101,102: // a-f
								n = haxe.Int32.add(haxe.Int32.shl(n,4), cast (char - 87));
							default:
								this.char = char;
								// we allow to parse hexadecimal Int32 in Neko, but when the value will be
								// evaluated by Interpreter, a failure will occur if no Int32 operation is
								// performed
								var v = try CInt(haxe.Int32.toInt(n)) catch( e : Dynamic ) CInt32(n);
								return TConst(v);
							}
						}
						#end
					default:
						this.char = char;
						var i = Std.int(n);
						return TConst( (exp > 0) ? CFloat(n * 10 / exp) : ((i == n) ? CInt(i) : CFloat(n)) );
					}
				}*/
			case 59: return TSemicolon;
			case 40: return TPOpen;
			case 41: return TPClose;
			case 44: return TComma;
/*			case 46:
				char = readChar();
				switch( char ) {
				case 48,49,50,51,52,53,54,55,56,57:
					var n = char - 48;
					var exp = 1;
					while( true ) {
						char = readChar();
						exp *= 10;
						switch( char ) {
						case 48,49,50,51,52,53,54,55,56,57:
							n = n * 10 + (char - 48);
						default:
							this.char = char;
							return TConst( CFloat(n/exp) );
						}
					}
				case 46:
					char = readChar();
					if( char != 46 )
						invalidChar(char);
					return TOp("...");
				default:
					this.char = char;
					return TDot;
				}*/
			case 123: return TBrOpen;
			case 125: return TBrClose;
			case 91: return TBkOpen;
			case 93: return TBkClose;
			case 39: return TString(readString(39));
			case 34: return TString(readString(34));
//			case 63: return TQuestion;
//			case 58: return TDoubleDot;
			case '='.code:
				char = readChar();
				if( char == '='.code )
					return TOp("==");
				else if ( char == '>'.code )
					return TOp("=>");
				this.char = char;
				return TOp("=");
			default:
				if( ops[char] ) {
					var op = String.fromCharCode(char);
					var prev = -1;
					while( true ) {
						char = readChar();
						if( !ops[char] || prev == '='.code ) {
							if( op.charCodeAt(0) == '/'.code )
								return tokenComment(op,char);
							this.char = char;
							return TOp(op);
						}
						prev = char;
						op += String.fromCharCode(char);
					}
				}
				if( idents[char] ) {
					var id = String.fromCharCode(char);
					while( true ) {
						char = readChar();
						if( !idents[char] ) {
							this.char = char;
							return TId(id);
						}
						id += String.fromCharCode(char);
					}
				}
				invalidChar(char);
			}
			char = readChar();
		}
		return null;
	}

	function tokenComment( op : String, char : Int ) {
		var c = op.charCodeAt(1);
		var s = input;
		if( c == '/'.code ) { // comment
			try {
				while( char != '\r'.code && char != '\n'.code ) {
					pos++;
					char = s.readByte();
				}
				this.char = char;
			} catch( e : Dynamic ) {
			}
			return token();
		}
		if( c == '*'.code ) { /* comment */
			var old = line;
			if( op == "/**/" ) {
				this.char = char;
				return token();
			}
			try {
				while( true ) {
					while( char != '*'.code ) {
						if( char == '\n'.code ) line++;
						pos++;
						char = s.readByte();
					}
					pos++;
					char = s.readByte();
					if( char == '/'.code )
						break;
				}
			} catch( e : Dynamic ) {
				line = old;
				error("Unterminated comment");
			}
			return token();
		}
		this.char = char;
		return TOp(op);
	}

	function readString( until ) {
		var c = 0;
		var b = new haxe.io.BytesOutput();
		var esc = false;
		var old = line;
		var s = input;
		while( true ) {
			try {
				pos++;
				c = s.readByte();
			} catch( e : Dynamic ) {
				line = old;
				error("Unterminated string");
			}
			if( esc ) {
				esc = false;
				switch( c ) {
				case 'n'.code: b.writeByte(10);
				case 'r'.code: b.writeByte(13);
				case 't'.code: b.writeByte(9);
				case "'".code, '"'.code, '\\'.code: b.writeByte(c);
				case '/'.code: b.writeByte(c);
				default: invalidChar(c);
				}
			} else if( c == 92 )
				esc = true;
			else if( c == until )
				break;
			else {
				if( c == 10 ) line++;
				b.writeByte(c);
			}
		}
		return b.getBytes().toString();
	}

}