using UnityEngine;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading;

public class ScriptLexer
{
	public struct Token {
		public enum Kind {
			End,
			Comma,
			Colon,
			Indent,
			Dedent,
			Noun,
			Verb,
			Symbol,
			Number,
			Dialogue,
			When,
			If,
			Else,
			Is,
			Repeat,

			// Operators in precedence order:
			Expr = 128,
			Not,
			Has,
			DoesntHave,
		}

		public Kind kind;
		public int lineno;
		public string dialogue, flags;
		public float number;
		public string noun;
		public string symbol;
		public ScriptData.Opcode verb;

		public override string ToString() {
			switch (kind) {
				case Kind.End: return "end of file";
				case Kind.Comma: return "comma";
				case Kind.Colon: return "colon";
				case Kind.Indent: return "indentation";
				case Kind.Dedent: return "un-indentation";
				case Kind.Noun: return $"noun '{noun}'";
				case Kind.Verb: return $"verb '{verb}'";
				case Kind.Symbol: return $"symbol '{symbol}'";
				case Kind.Dialogue: return "dialogue line";
				case Kind.When: return "'when' clause";
				case Kind.Is: return "'is' keyword";
				case Kind.If: return "'if' clause";
				case Kind.Else: return "'else' clause";
				case Kind.Not: return "'not' keyword";
				case Kind.Has: return "'has' inquiry";
				case Kind.DoesntHave: return "'has-not' inquiry";
				case Kind.Repeat: return "'repeat' clause";
				case Kind.Number: return $"number {number}";
				default: return "<error>";
			}
		}
	}

	Regex rNoun = new Regex(@"^([A-Z][A-Za-z0-9\-_]+)");
	Regex rNumber = new Regex(@"^([+-]?[0-9]+(\.[0-9]+)?)");
	Regex rDialogue = new Regex(@"^([A-Z][A-Za-z0-9\-_]+)(?:\s*\(([^)]+?)\))?:([^\n]+)");
	Regex rSymbol = new Regex(@"^""([^""]*)""");
	Regex rEmptyLine = new Regex(@"^([\t\r ]*)(#[^\n]*)?(?=\n)");

	Dictionary<string, ScriptData.Opcode> verbs = new Dictionary<string, ScriptData.Opcode> {
		["walks to"] = ScriptData.Opcode.WalksTo,
		["heads for"] = ScriptData.Opcode.HeadsFor,
		["exits"] = ScriptData.Opcode.Exits,
		["waits"] = ScriptData.Opcode.Waits,
		["opens"] = ScriptData.Opcode.Opens,
		["closes"] = ScriptData.Opcode.Closes,
		["starts"] = ScriptData.Opcode.Starts,
		["stops"] = ScriptData.Opcode.Stops,
		["uses"] = ScriptData.Opcode.Uses,
		["sees"] = ScriptData.Opcode.Sees,
		["toots"] = ScriptData.Opcode.Toots,
		["drops"] = ScriptData.Opcode.Drops,
		["receives"] = ScriptData.Opcode.Receives,
		["is saved"] = ScriptData.Opcode.DefSaved,
		["is unsaved"] = ScriptData.Opcode.DefLocal,
		["becomes set"] = ScriptData.Opcode.SetVar,
		["becomes clear"] = ScriptData.Opcode.ClearVar,
		["enters via"] = ScriptData.Opcode.EntersVia,
		["enters"] = ScriptData.Opcode.Enters,
		["stays focused"] = ScriptData.Opcode.StaysFocused,
		["loses focus"] = ScriptData.Opcode.LosesFocus,
	};

	Dictionary<string, Token.Kind> keywords = new Dictionary<string, Token.Kind> {
		["when"] = Token.Kind.When,
		["repeat"] = Token.Kind.Repeat,
		["if"] = Token.Kind.If,
		["else"] = Token.Kind.Else,
		["is"] = Token.Kind.Is,
		["not"] = Token.Kind.Not,
		["has"] = Token.Kind.Has,
		["doesn't have"] = Token.Kind.DoesntHave,
	};

	ScriptData data;
	string path;
	string file;
	int filepos;
	int lineno;
	int indent;
	bool newline;
	List<Token> tokens = new List<Token>();
	int next = 0;

	bool Match(Regex r, out Match m) {
		m = r.Match(file, filepos, file.Length-filepos);
		if (m.Success)
			filepos += m.Length;
		return m.Success;
	}

	bool Match(string s) {
		if (filepos+s.Length < file.Length && file.Substring(filepos, s.Length) == s) {
			filepos += s.Length;
			return true;
		}
		return false;
	}

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

	public Token Next() {
		return tokens[next++];
	}

	void Read() {
		Match m;
		bool firstOnLine = false;

		// Apply Python-style indenting.
		if (newline) {
			newline = false;
			firstOnLine = true;

			// Check indent.
			int newindent = 0;
			while (filepos < file.Length && file[filepos] == '\t') {
				newindent++;
				filepos++;
			}

			// Skip blank lines.
			if (Match(rEmptyLine, out m))
				return;

			if (filepos < file.Length && file[filepos] == ' ')
				Error(lineno, "Non-tab indentation.");

			int diff = newindent - indent;
			indent = newindent;
			if (diff != 0) {
				for (int i = 0; i<diff; i++)
					tokens.Add(new Token { kind = Token.Kind.Indent, lineno=lineno });
				for (int i = 0; i>diff; i--)
					tokens.Add(new Token { kind = Token.Kind.Dedent, lineno=lineno });
			}
		}

		if (filepos == file.Length)
			return;

		char c = file[filepos];
		switch (c) {
			case '\r': filepos++; return;
			case '\n': filepos++; lineno++; newline = true; return;
			case ' ': filepos++; return;
			case '#':
				while (filepos < file.Length && file[filepos] != '\n')
					filepos++;
				return;
			case ',':
				filepos++;
				tokens.Add(new Token { kind = Token.Kind.Comma, lineno=lineno });
				return;
			case ':':
				filepos++;
				tokens.Add(new Token { kind = Token.Kind.Colon, lineno=lineno });
				return;
			default:
				if (firstOnLine && Match(rDialogue, out m)) {
					tokens.Add(new Token { kind = Token.Kind.Dialogue, lineno=lineno,
						dialogue = m.Groups[3].Value.Trim(),
						noun = m.Groups[1].Value,
						flags = m.Groups[2].Value
					});
					return;
				}
				if (Match(rNoun, out m)) {
					tokens.Add(new Token { kind = Token.Kind.Noun, noun = m.Groups[1].Value, lineno=lineno });
					return;
				}
				if (Match(rNumber, out m)) {
					float f = float.Parse(m.Groups[1].Value);
					tokens.Add(new Token { kind = Token.Kind.Number, number = f, lineno=lineno });
					return;
				}
				if (Match(rSymbol, out m)) {
					tokens.Add(new Token { kind = Token.Kind.Symbol, lineno=lineno, symbol = m.Groups[1].Value });
					return;
				}
				if (char.IsLower(file[filepos])) {
					foreach (var pair in verbs) {
						if (Match(pair.Key)) {
							tokens.Add(new Token { kind = Token.Kind.Verb, verb = pair.Value, lineno=lineno });
							return;
						}
					}
					foreach (var pair in keywords) {
						if (Match(pair.Key)) {
							tokens.Add(new Token { kind = pair.Value, lineno=lineno });
							return;
						}
					}
				}
				string text = file.Substring(filepos, System.Math.Min(10, file.Length-filepos));
				Error(lineno, $"Syntax error at '{Regex.Escape(text)}'.");
				break;
		}
	}

	public ScriptLexer(string text, string path) {
		this.path = path;
		file = text;
		filepos = 0;
		lineno = 1;
		indent = 0;
		newline = true;

		while (filepos < file.Length)
			Read();
		for (int i=0;i<indent;i++)
			tokens.Add(new Token { kind = Token.Kind.Dedent, lineno=lineno });

		tokens.Add(new Token { kind = Token.Kind.End, lineno = lineno });
	}
}
