using UnityEngine;
using System.Collections.Generic;

using Token = ScriptLexer.Token;
using Opcode = ScriptData.Opcode;
using Kind = ScriptLexer.Token.Kind;
using Function = ScriptData.Function;

public class ScriptCompiler
{
	ScriptData data;
	string path;
	Token peek, last;
	ScriptLexer lexer;
	Function func;
	Dictionary<string, ushort> symbols = new Dictionary<string, ushort>();
	Dictionary<string, ushort> strings = new Dictionary<string, ushort>();

	public class ParserException : System.Exception
	{
		public string path, error;
		public int lineno;
		public ParserException(string path, int lineno, string msg)
			: base($"{path}({lineno}): {msg}") {
			this.error = msg;
			this.path = path;
			this.lineno = lineno;
		}
	}

	void Error(Token where, string msg) {
		throw new ParserException(path, where.lineno, msg);
	}

	Token Next() {
		while (func.lineTable.Count < peek.lineno)
			func.lineTable.Add(func.bytes.Count);

		last = peek;
		peek = lexer.Next();
		return last;
	}

	Token Expect(Kind kind) {
		if (peek.kind != kind)
			Error(peek, $"Expected {kind}, got {peek}.");
		return Next();
	}

	void SetFlag(ushort i, ScriptData.StorageFlags flag) {
		ScriptData.Symbol sym = data.symbols[i];
		data.symbols[i] = new ScriptData.Symbol {
			name = sym.name,
			type = sym.type,
			storage = sym.storage | flag
		};
	}

	ushort AddSymbol(string name, string type, bool external = true) {
		if (name == "")
			return 0;
		if (!symbols.TryGetValue(name, out ushort i)) {
			if (symbols.Count >= 0xffff)
				Error(last, "Too many symbols!");
			i = (ushort)symbols.Count;

			data.symbols.Add(new ScriptData.Symbol { name=name, type=type, storage=0 });
			symbols.Add(name, i);

			if (external)
				SetFlag(i, ScriptData.StorageFlags.External);
		}
		if (data.symbols[i].type != type)
			Error(last, $"Type {type} for \"{name}\" conflicts with earlier type {data.symbols[i].type}.");
		if (!external && data.symbols[i].storage.HasFlag(ScriptData.StorageFlags.External))
			Error(last, $"Symbol \"{name}\" was used before declaration.");
		return i;
	}

	void Byte(byte b) { func.bytes.Add(b); }
	void Word(ushort w) { Byte((byte)(w&0xff)); Byte((byte)(w>>8)); }
	void Int(int i) { Byte((byte)(i&0xff)); Byte((byte)(i>>8)); Byte((byte)(i>>16)); Byte((byte)(i>>24)); }
	void Op(Opcode b) { Byte((byte)b); }
	ushort Noun(string name) { return Symbol(name, "GameObject"); }
	ushort Var(string name, bool external=true) { return Symbol(name, "Variable", external); }
	void Number(float f) { Int((int)(f * 65536.0f)); }

	void Patch(int where, int target) {
		func.bytes[where] = (byte)(target & 0xff);
		func.bytes[where+1] = (byte)(target >> 8);
	}

	ushort Symbol(string name, string type, bool external = true) {
		ushort i = AddSymbol(name, type, external);
		Word(i);
		return i;
	}

	void String(string value) {
		if (!strings.TryGetValue(value, out ushort i)) {
			if (strings.Count >= 0xffff)
				Error(last, "Too many strings!");
			i = (ushort)strings.Count;
			data.strings.Add(value);
			strings.Add(value, i);
		}
		Word(i);
	}

	ushort NewFunction(string name) {
		if (data.functions.Count > 0 && func != data.functions[0])
			Error(last, "Functions cannot be inside other functions.");
		if (data.functions.Count > 0xffff)
			Error(last, "Too many functions!");
		data.functions.Add(new Function() { name=name });
		return (ushort)(data.functions.Count-1);
	}

	void EndFunction() {
		Op(Opcode.Return);
		if (func.bytes.Count > 0xffff)
			Error(last, "Function is too long!");
	}

	void Dialogue() {
		Token dlg = Expect(Kind.Dialogue);

		ScriptData.DlgFlags flags = ScriptData.DlgFlags.None;
		if (dlg.flags != "") {
			char[] seps = { ',' };
			foreach (string flag in dlg.flags.Split(seps)) {
				switch (flag) {
					case "caption": flags |= ScriptData.DlgFlags.Caption; break;
					case "1st": flags |= ScriptData.DlgFlags.First; break;
					case "2nd": flags |= ScriptData.DlgFlags.Second; break;
					case "3rd": flags |= ScriptData.DlgFlags.Third; break;
					case "nth": flags |= ScriptData.DlgFlags.Nth; break;
					default: Error(last, $"Unknown dialogue flag '{flag}'."); break;
				}
			}
		}

		Op(Opcode.Says);
		Noun(dlg.noun);
		String(dlg.dialogue);
		Byte((byte)flags);
	}

	void ActorCommand() {
		string subject = Expect(Kind.Noun).noun;
		while (true) {
			Opcode verb = Expect(Kind.Verb).verb;
			Op(verb);
			ushort id = Noun(subject);
			switch (verb) {
				case Opcode.WalksTo: Noun(Expect(Kind.Noun).noun); break;
				case Opcode.HeadsFor: Noun(Expect(Kind.Noun).noun); break;
				case Opcode.Waits: Number(Expect(Kind.Number).number); break;
				case Opcode.Opens: break;
				case Opcode.Closes: break;
				case Opcode.Exits: break;
				case Opcode.StaysFocused: break;
				case Opcode.LosesFocus: break;
				case Opcode.Uses: Noun(Expect(Kind.Noun).noun); break;
				case Opcode.Sees: Noun(Expect(Kind.Noun).noun); break;
				case Opcode.Toots: Noun(Expect(Kind.Noun).noun); break;
				case Opcode.Drops:
				case Opcode.Receives:
					Number(Expect(Kind.Number).number);
					Symbol(Expect(Kind.Symbol).symbol, "Item");
					break;
				case Opcode.Enters:
					SetFlag(id, ScriptData.StorageFlags.Dynamic);
					break;
				case Opcode.EntersVia:
					Noun(Expect(Kind.Noun).noun);
					SetFlag(id, ScriptData.StorageFlags.Dynamic);
					break;
				default: Error(last, $"An actor never {verb}."); break;
			}
			if (peek.kind != Kind.Comma)
				break;
			Next();
		}
	}

	void Block() {
		Expect(Kind.Indent);
		while (peek.kind != Kind.Dedent)
			Statement();
		Expect(Kind.Dedent);
	}

	void DefineThread(string name) {
		ushort fnid = NewFunction(name);

		Op(Opcode.DefFn);
		ushort sym = Symbol(name, "Function", false);
		Word(fnid);

		Expect(Kind.Is);
		Expect(Kind.Colon);
		func = data.functions[fnid];
		Block();
		EndFunction();
		func = data.functions[0];
	}

	void SymbolCommand() {
		string sym = Expect(Kind.Symbol).symbol;
		if (peek.kind == Kind.Is) {
			DefineThread(sym);
			return;
		}
		while (true) {
			Token what = Expect(Kind.Verb);
			Op(what.verb);
			switch (what.verb) {
				case Opcode.Starts: Symbol(sym, "Function"); break;
				case Opcode.Stops: Symbol(sym, "Function"); break;
				case Opcode.SetVar: Var(sym); break;
				case Opcode.ClearVar: Var(sym); break;
				case Opcode.DefSaved: Var(sym, false); break;
				case Opcode.DefLocal: Var(sym, false); break;
				default: Error(what, $"{sym} can't {what.verb}."); break;
			}
			if (peek.kind != Kind.Comma)
				break;
			Next();
		}
	}

	void Loop() {
		Expect(Kind.Repeat);
		Expect(Kind.Colon);
		ushort label = (ushort)func.bytes.Count;
		Block();
		Op(Opcode.Jump);
		Word(label);
	}

	void Prefix() {
		Token token = Next() ;
		switch (token.kind) {
			case Kind.Not:
				Expr(Kind.Not);
				Op(Opcode.Not);
				break;
			case Kind.Symbol:
				Op(Opcode.PushVar);
				Var(token.symbol);
				break;
			case Kind.Noun:
				Op(Opcode.PushVar);
				Noun(token.noun);
				break;
			default:
				Error(token, "Invalid expression.");
				break;
		}
	}

	void Expr(Kind prec = Kind.Expr) {
		Prefix();
		while (peek.kind >= prec) {
			Token op = Next();
			switch (op.kind) {
				case Kind.Has:
					Op(Opcode.Has);
					Number(Expect(Kind.Number).number);
					Symbol(Expect(Kind.Symbol).symbol, "Item");
					break;
				case Kind.DoesntHave:
					Op(Opcode.Has);
					Number(Expect(Kind.Number).number);
					Symbol(Expect(Kind.Symbol).symbol, "Item");
					Op(Opcode.Not);
					break;
				default: Error(op, $"Unknown operator '{op}'."); break;
			}
		}
	}

	void IfThen() {
		Expect(Kind.If);
		Expr();
		Op(Opcode.JFalse);
		int patch1 = func.bytes.Count;
		Word(0);
		Expect(Kind.Colon);
		Block();
		Op(Opcode.Jump);
		int patch2 = func.bytes.Count;
		Word(0);
		Patch(patch1, func.bytes.Count);
		if (peek.kind == Kind.Else) {
			Next();
			Expect(Kind.Colon);
			Block();
		}
		Patch(patch2, func.bytes.Count);
	}

	void Condition() {
		ushort fnid = NewFunction($"<Condition #{data.functions.Count}>");
		Expect(Kind.When);
		string subj = Expect(Kind.Noun).noun;
		Opcode verb = Expect(Kind.Verb).verb;
		string obj = "";
		switch (verb) {
			case Opcode.Uses: obj = Expect(Kind.Noun).noun; break;
			case Opcode.Sees: obj = Expect(Kind.Noun).noun; break;
			case Opcode.Toots: obj = Expect(Kind.Noun).noun; break;
		}

		func = data.functions[fnid];
		Expect(Kind.Colon);
		Block();
		EndFunction();
		func = data.functions[0];

		ScriptData.Condition cond = new ScriptData.Condition {
			subject = AddSymbol(subj, "GameObject"),
			verb = verb,
			obj = AddSymbol(obj, "GameObject"),
			function = fnid
		};
		data.conditions.Add(cond);
	}

	void Statement() {
		switch (peek.kind) {
			case Kind.Dialogue:		Dialogue(); break;
			case Kind.Noun:			ActorCommand(); break;
			case Kind.Symbol:		SymbolCommand(); break;
			case Kind.Repeat:		Loop(); break;
			case Kind.If:			IfThen(); break;
			case Kind.When:			Condition(); break;
			default:				Error(peek, $"Expected a statement, got {peek}."); break;
		}
	}

	void Program() {
		while (peek.kind != Kind.End) {
			Statement();
		}
	}

	public ScriptCompiler(ScriptData data, string text, string path) {
		this.data = data;
		this.lexer = new ScriptLexer(text, path);
		this.path = path;

		// Reserve zero as a null symbol.
		AddSymbol("<null>", "<null>", false);

		NewFunction("<global>");
		func = data.functions[0];
		Next();
		Program();
		EndFunction();
	}
}
