using System;
using System.Diagnostics;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;

using miew.Enumerable;
using miew.String;

namespace agree
{
	public abstract class ConfigFileReader
	{
		protected readonly Config gc;
		protected ConfigFileReader(Config gc)
		{
			this.gc = gc;
		}
		public abstract void ReadConfigFile(String file);
	};

	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	/// <summary>
	/// 
	/// </summary>
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	[Serializable]
	public class LkbConfig : ConfigFileReader
	{
		[NonSerialized]
		public static readonly Char[] one_space = { ' ' };
		[NonSerialized]
		public static readonly Char[] sq_dq = { '\'', '\"' };

		public LkbConfig(Config gc)
			: base(gc)
		{
			gc.types.top_type = "*top*";
			gc.types.string_type = "string";

			gc.types.typs_list = "*list*";
			gc.types.typs_empty_list = "*null*";
			gc.types.typs_diff_list = "diff-list";

			gc.types.f_list_head = "first";
			gc.types.f_list_tail = "rest";
			gc.types.f_dlist_list = "list";
			gc.types.f_dlist_last = "last";

			gc.grammar.orth_path = new FsPath("stem");
			gc.grammar.rule_args_path = new FsPath("args");
		}

		[NonSerialized]
		String cur_file;

		[NonSerialized]
		readonly static Dictionary<String, Char> lisp_char_codes = new Dictionary<String, char>
		{
			{"space",' '},
			{"fullwidth_question_mark",'\uFF1F'},
			{"horizontal_ellipsis",'\u2026'},
			{"fullwidth_full_stop",'\uFF0E'},
			{"fullwidth_exclamation_mark",'\uFF01'},
			{"black_circle",'\u25CF'},
			{"fullwidth_comma",'\uFF0C'},
			{"ideographic_full_stop",'\u3002'},
			{"white_circle",'\u25CB'},
			{"katakana_middle_dot",'\uFF65'},
			{"ideographic_space",'\u3000'},
		};

		void SetGlobalVar(String param, String value)
		{
			if (value.Length > 0 && value[0] == '\'')
				value = value.Substring(1);
			if (value.Length > 0 && value[0] == '(' && value[value.Length - 1] == ')')
				value = value.Substring(1, value.Length - 2);

			if (param.Length == 0 || param[0] != '*' || param[param.Length - 1] != '*')
			{
				Debug.Print("Skipping unrecognized parameter: {0}", param);
				return;
			}
			param = param.Substring(1, param.Length - 2);

			switch (param)
			{
				case "start-symbol":
					if (gc.grammar.start_symbols == null)
						gc.grammar.start_symbols = new List<String>();
					gc.grammar.start_symbols.Add(value);
					break;

				case "toptype":
					gc.types.top_type = value;
					break;
				case "string-type":
					gc.types.string_type = value;
					break;
				case "orth-path":
					gc.grammar.orth_path = new FsPath(value);
					break;

				case "list-type":
					gc.types.typs_list = value;
					break;
				case "list-tail":
					gc.types.f_list_tail = value;
					break;
				case "list-head":
					gc.types.f_list_head = value;
					break;
				case "empty-list-type":
					gc.types.typs_empty_list = value;
					break;

				case "diff-list-type":
					gc.types.typs_diff_list = value;
					break;
				case "diff-list-list":
					gc.types.f_dlist_list = value;
					break;
				case "diff-list-last":
					gc.types.f_dlist_last = value;
					break;

				case "deleted-daughter-features":
					{
						if (gc.parser.deleted_daughters == null)
							gc.parser.deleted_daughters = new List<String>();
						gc.parser.deleted_daughters.AddRange(value.Split(one_space, StringSplitOptions.RemoveEmptyEntries));
					}
					break;

				case "active-parsing-p":
					/* not relevant; ignored */
					break;

				case "irregular-forms-only-p":
					gc.parser.IrregularFormsOnly = value == "t";
					break;

				case "chart-packing-p":
					if (value == "t")
						gc.parser.packing = Config.Parser.PackingOpts.Full;
					else if (value == "nil")
						gc.parser.packing = Config.Parser.PackingOpts.None;
					break;

				case "packing-restrictor":
					{
						if (gc.parser.packing_restrictors == null)
							gc.parser.packing_restrictors = new List<String>();
						gc.parser.packing_restrictors.AddRange(value.Split(one_space, StringSplitOptions.RemoveEmptyEntries));
					}
					break;

				case "check-paths":
					{
						int ix = 0;
						while ((ix = value.IndexOf("  ", ix)) != -1)
							value = value.Remove(ix, 1);
						if (value.StartsWith("quote (") && value.EndsWith(")"))
							value = value.Substring(7, value.Length - 8);

						String[] all_paths = value.Split(new String[] { ") (" }, StringSplitOptions.None);
						if (all_paths.Length == 0)
							break;

						String s_tmp = all_paths[0];
						if (s_tmp.Length > 0 && s_tmp[0] == '(')
							all_paths[0] = s_tmp.Substring(1);

						s_tmp = all_paths[all_paths.Length - 1];
						if (s_tmp.Length > 0 && s_tmp.EndsWith(")"))
							all_paths[all_paths.Length - 1] = s_tmp.Remove(s_tmp.Length - 1);

						if (gc.parser.s_quick_check_paths==null)
							gc.parser.s_quick_check_paths = new List<string>();
						gc.parser.s_quick_check_paths.AddRange(all_paths.Select(qcp => qcp.ExtractParenthesized()));
					}
					break;

				case "punctuation-characters":
					{
						foreach (String _s in value.Split(one_space, StringSplitOptions.RemoveEmptyEntries))
						{
							if (_s == "append")
								continue;
							String s = _s;
							if (s.StartsWith("'("))
								s = s.Substring(2);
							if (s.StartsWith(@"#\"))
							{
								if (s.Length > 3)
									s = s.TrimEnd(')');
								s = s.Substring(2);
								if (s.Length == 1)
									gc.parser.punctuation_chars.Add(s[0]);
								else
									gc.parser.punctuation_chars.Add(lisp_char_codes[s]);
							}
						}
					}
					break;

				case "chart-limit":
					if (!int.TryParse(value, out gc.parser.chart_limit))
						throw new Exception("Error: parsing 'chart-limit', expected an integer");
					break;
				case "maximum-number-of-edges":
					if (!int.TryParse(value, out gc.parser.max_edges))
						throw new Exception("Error: parsing 'maximum-number-of-edges', expected an integer");
					break;

				case "key-daughter-path":
					gc.grammar.key_daughter_path = new FsPath(value);
					break;

				case "key-daughter-type":
					gc.grammar.typs_key_daughter = value;
					break;

				/// Node label configuration
				case "simple-tree-display":
					gc.nodeLabels.SimpleTreeDisplay = value == "t";
					break;
				case "label-path":
					gc.nodeLabels.LabelPath = new FsPath(value);
					break;
				case "prefix-path":
					gc.nodeLabels.PrefixPath = new FsPath(value);
					break;
				case "suffix-path":
					gc.nodeLabels.SuffixPath = new FsPath(value);
					break;
				case "local-path":
					gc.nodeLabels.LocalPath = new FsPath(value);
					break;
				case "recursive-path":
					gc.nodeLabels.RecursivePath = new FsPath(value);
					break;
				case "label-fs-path":
					gc.nodeLabels.LabelFsPath = new FsPath(value);
					break;
				case "label-template-type":
					gc.nodeLabels.LabelTemplateType = value;
					break;


				default:
					Debug.Print("Ignoring option '*{0}* = {1}' in '{2}'", param, value, cur_file);
					break;
			}
		}

		public override void ReadConfigFile(String file)
		{
			if (!File.Exists(file))
				return;
			using (StreamReader sr = new StreamReader(file))
			{
				this.cur_file = file;
				String r_line, line = String.Empty;
				int line_no = 0;
			next_line:
				while ((r_line = sr.ReadLine()) != null)
				{
					line_no++;
					r_line = r_line.Trim();
					if (r_line.Length == 0)
						continue;

					if (r_line == "#|")
					{
						while ((r_line = sr.ReadLine()) != null)
						{
							line_no++;
							if ((r_line = r_line.Trim()) == "|#")
								goto next_line;
						}
						throw new Exception("Error: end of file encountered looking for end of block comment '|#'");
					}
					if (r_line[0] == ';')
					{
						if (line.Length > 0)
							throw new Exception(String.Format("Error: ';' in {0}, line {1}", file, line_no));
						continue;
					}
					line += " " + r_line;

					int nest = 0;
					Char prev = default(Char);
					for (int i = 0; i < line.Length; i++)
					{
						if (line[i] == '(' && prev != '\\')
							nest++;
						if (line[i] == ')' && prev != '\\')
						{
							if (nest == 0)
								throw new Exception(String.Format("Error: ')' in {0}, line {1}", file, line_no));
							nest--;
						}
						prev = line[i];
					}
					// incomplete line
					if (nest != 0)
						continue;

					line = line.Trim();
					if (line == String.Empty)
						continue;

					if (line[0] != '(' || line[line.Length - 1] != ')')
						throw new Exception(String.Format("Error: '{0}' in {1}, line {2}", line[0], file, line_no));
					line = line.Substring(1, line.Length - 2);

					String[] rgs = line.ToLower().LispInsulatedSplit(' ').ToArray();

					if (rgs[0] == "defparameter" || rgs[0] == "def-lkb-parameter")
					{
						if (rgs.Length > 4)
							throw new Exception(String.Format("Error: in {0}, line {1}; too many arguments for {2}", file, line_no, rgs[0]));
						if (rgs.Length < 3)
							throw new Exception(String.Format("Error: in {0}, line {1}; not enough arguments for {2}", file, line_no, rgs[0]));
						SetGlobalVar(rgs[1], rgs[2]);
					}
					else if (rgs[0] == "setf" || rgs[0] == "in-package")
					{
					}
					else
						throw new Exception(String.Format("Error: '{0}' in {1}, line {2}; expected 'defparameter', 'def-lkb-parameter', or 'setf'.", rgs[0], file, line_no));

					line = String.Empty;
				}

				if (line.Length > 0)
					throw new Exception(String.Format("Error: unexpected end of file {0}; expected ')'.", file));
			}
		}
	};

	static class LispHelperExtension
	{
		public static IEnumerable<String> LispInsulatedSplit(this String s, Char ch_split)
		{
			int cb, i, i_last = 0;
			Char prev = default(Char);
			int nest = 0;
			bool f_q = false;
			for (i = 0; i < s.Length; i++)
			{
				Char ch = s[i];
				if (ch == '"' && prev != '\\')
					f_q = !f_q;
				else if (ch == '(' && prev != '\\')
					nest++;
				else if (ch == ')' && prev != '\\' && nest > 0)
					nest--;
				else if (nest == 0 && !f_q && ch == ch_split)
				{
					if ((cb = i - i_last) > 0)
						yield return s.Substring(i_last, cb);
					i_last = i + 1;
				}
				prev = ch;
			}
			if ((cb = i - i_last) > 0)
				yield return s.Substring(i_last, cb);
		}
	}

	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	/// <summary>
	/// 
	/// </summary>
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	[Serializable]
	public class PetGlobals : ConfigFileReader
	{
		[NonSerialized]
		public static readonly Char[] one_space = { ' ' };
		[NonSerialized]
		public static readonly Char[] sq_dq = { '\'', '\"' };

		public PetGlobals(Config gc)
			: base(gc)
		{
			gc.types.top_type = "top";
			gc.types.string_type = "string";

			gc.types.typs_list = "list";
			gc.types.typs_empty_list = "nil";
			gc.types.typs_diff_list = "difflist";

			gc.types.f_list_head = "first";
			gc.types.f_list_tail = "rest";
			gc.types.f_dlist_list = "list";
			gc.types.f_dlist_last = "last";

			gc.grammar.orth_path = new FsPath("orth");
			gc.grammar.rule_args_path = new FsPath("args");

			String v = ConfigurationManager.AppSettings["opt_key"];
			Config.Parser.KeyStrategy ks;
			if (v != null && Enum.TryParse<Config.Parser.KeyStrategy>(v, out ks))
				gc.parser.ParsingStrategy = ks;
		}

		void SetGlobalVar(String file, int line_no, String[] rgs)
		{
			String kw = rgs[0].ToLower();
			String arg = null;
			String[] args = null;
			if (rgs.Length > 1)
			{
				if (rgs[1] != ":=")
					throw new Exception(String.Format("Error: expected ':=' in {0}, line {1}", file, line_no));
				if (rgs.Length == 2)
					throw new Exception(String.Format("Error: nothing specified on the right of ':=' in {0}, line {1}", file, line_no));
				args = rgs.Skip(2).ToArray();
				arg = args[0];
			}
			else
				arg = rgs[0];

			arg = String.Intern(arg.Trim(sq_dq).ToLower());

			switch (kw)
			{
				case "encoding":
					break;

				case "irregular-forms-only":
					if (args != null)
						throw new Exception(String.Format("Error: 'irregular-forms-only' is a boolean option; in {0}, line {1}", file, line_no));
					gc.parser.IrregularFormsOnly = true;
					break;

				case "case-sensitive":
					if (args != null)
						throw new Exception(String.Format("Error: 'case-sensitive' is a boolean option; in {0}, line {1}", file, line_no));
					gc.parser.f_case_sensitive = true;
					break;

				case "lex-entries-can-fail":
					if (args != null)
						throw new Exception(String.Format("Error: 'lex-entries-can-fail' is a boolean option; in {0}, line {1}", file, line_no));
					gc.parser.f_lex_entries_can_fail = true;
					break;

				case "unidirectional-chart-dependencies":
					if (args != null)
						throw new Exception(String.Format("Error: 'unidirectional-chart-dependencies' is a boolean option; in {0}, line {1}", file, line_no));
					gc.parser.ChartDependencyScope = Config.Parser.DependencyScope.Unidirectional;
					break;

				case "chart-dependencies":
					gc.parser.ChartDependencyPaths = rgs.Skip(2).PairOff();
					break;

				case "special-name-top":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than one instance of the special 'top' type; in {0}, line {1}", file, line_no));
					gc.types.top_type = arg;
					break;

				case "special-name-cons":
					/* don't know why this is needed */
					break;

				case "special-name-symbol":
					/* don't know what this is for */
					break;

				case "special-name-string":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than one instance of the special 'string' type; in {0}, line {1}", file, line_no));
					gc.types.string_type = arg;
					break;

				case "special-name-list":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than one instance of the special 'list' type; in {0}, line {1}", file, line_no));
					gc.types.typs_list = arg;
					break;

				case "special-name-nil":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than one instance of the special 'empty list' type; in {0}, line {1}", file, line_no));
					gc.types.typs_empty_list = arg;
					break;

				case "special-name-difflist":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than one instance of the special 'difference list' type; in {0}, line {1}", file, line_no));
					gc.types.typs_diff_list = arg;
					break;

				case "special-name-attr-first":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than symbol for the special 'list head' feature; in {0}, line {1}", file, line_no));
					gc.types.f_list_head = arg;
					break;

				case "special-name-attr-rest":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than symbol for the special 'list tail' feature; in {0}, line {1}", file, line_no));
					gc.types.f_list_tail = arg;
					break;

				case "special-name-attr-list":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than symbol for the special 'difference list head' feature; in {0}, line {1}", file, line_no));
					gc.types.f_dlist_list = arg;
					break;

				case "special-name-attr-last":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than symbol for the special 'difference list tail' feature; in {0}, line {1}", file, line_no));
					gc.types.f_dlist_last = arg;
					break;

				case "special-name-attr-args":
					/* don't know if/why this is different from rule-args-path */
					break;

				case "rule-args-path":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than symbol for the rule arguments path; in {0}, line {1}", file, line_no));
					gc.grammar.rule_args_path = new FsPath(arg);
					break;

				case "keyarg-marker-path":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than symbol for the key daughter path; in {0}, line {1}", file, line_no));
					gc.grammar.key_daughter_path = new FsPath(arg);
					break;

				case "true-type":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than symbol for the key daughter asserted type; in {0}, line {1}", file, line_no));
					gc.grammar.typs_key_daughter = arg;
					break;

				case "orth-path":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than one orthography path; in {0}, line {1}", file, line_no));
					gc.grammar.orth_path = new FsPath(arg);
					break;

				case "head-dtr-path":
					if (args.Length > 1)
						throw new Exception(String.Format("Error: cannot define more than one head daughter path; in {0}, line {1}", file, line_no));
					gc.grammar.head_dtr_path = arg;
					break;

				case "deleted-daughters":
					if (gc.parser.deleted_daughters == null)
						gc.parser.deleted_daughters = new List<String>();
					gc.parser.deleted_daughters.AddRange(args.Select(s => s.ToLower()));
					break;

				case "packing-restrictor":
					if (gc.parser.packing_restrictors == null)
						gc.parser.packing_restrictors = new List<String>();
					gc.parser.packing_restrictors.AddRange(args.Select(s => s.ToLower()));
					break;

				case "start-symbols":
					if (gc.grammar.start_symbols == null)
						gc.grammar.start_symbols = new List<String>();
					gc.grammar.start_symbols.AddRange(args.Select(s => s.ToLower().TrimStart('$')));
					break;

				case "spanning-only-rules":
					if (gc.parser.span_only_rules == null)
						gc.parser.span_only_rules = new List<String>();
					gc.parser.span_only_rules.AddRange(args.Select(s => s.ToLower().TrimStart('$')));
					break;

				case "punctuation-characters":
					gc.parser.punctuation_chars.UnionWith(arg);
					break;

				case "postload-lisp-files":
				case "preload-lisp-files":
					/* we will do our own MRS extraction */
					break;

				case "pn-label-path":
					gc.nodeLabels.LabelPath = new FsPath(arg);
					break;
				case "pn-prefix-path":
					gc.nodeLabels.PrefixPath = new FsPath(arg);
					break;
				case "pn-suffix-path":
					gc.nodeLabels.SuffixPath = new FsPath(arg);
					break;
				case "pn-recursive-path":
					gc.nodeLabels.RecursivePath = new FsPath(arg);
					break;
				case "pn-local-path":
					gc.nodeLabels.LocalPath = new FsPath(arg);
					break;
				case "pn-label-fs-path":
					gc.nodeLabels.LabelFsPath = new FsPath(arg);
					break;
				case "pn-label-type":
					gc.nodeLabels.LabelTemplateType = arg;
					break;
				case "pn-meta-type":
					gc.nodeLabels.MetaTemplateType = arg;
					break;

				default:
					//Console.WriteLine("{0} {1}", kw, args.StringJoin("\t"));
					break;
			}
		}

		public override void ReadConfigFile(String file)
		{
			StringBuilder sb = new StringBuilder();
			int line_no = 0;
			using (StreamReader sr = new StreamReader(file))
			{
				String r_line;
				while ((r_line = sr.ReadLine()) != null)
				{
					line_no++;
					int ix = r_line.QuoteInsulatedIndexOf(';');
					if (ix != -1)
						r_line = r_line.Remove(ix);
					r_line = r_line.Trim();
					if (r_line != String.Empty)
						sb.Append(" ;" + line_no.ToString() + " " + r_line.Select(ch => ch < ' ' ? ' ' : ch).NewString());
				}
			}
			foreach (String stmt in sb.ToString().QuoteInsulatedSplit('.'))
			{
				String cur_file_dir = Path.GetDirectoryName(file);
				String[] parts = stmt.Split(one_space, StringSplitOptions.RemoveEmptyEntries);
				while (parts.Length > 0 && parts[0][0] == ';')
				{
					line_no = int.Parse(parts[0].Substring(1));
					parts = parts.Skip(1).ToArray();
				}
				String kw = parts[0].ToLower();
				if (kw == "include")
				{
					if (parts.Length != 2)
						throw new Exception(String.Format("Error: '{0}.' in {1}, line {2}", parts.StringJoin(" "), file, line_no));

					String inc_file = parts[1].Trim(sq_dq);
					if (!Path.HasExtension(inc_file))
						inc_file = Path.ChangeExtension(inc_file, ".set");
					String try_file = Path.Combine(cur_file_dir, inc_file);
					if (File.Exists(try_file))
					{
						ReadConfigFile(try_file);
						continue;
					}
					if (cur_file_dir != Environment.CurrentDirectory)
					{
						try_file = Path.Combine(Environment.CurrentDirectory, inc_file);
						if (File.Exists(try_file))
						{
							ReadConfigFile(try_file);
							continue;
						}
					}
					throw new Exception(String.Format("Error: include file '{0}' not found in {1}, line {2}", inc_file, line_no));
				}
				SetGlobalVar(file, line_no, parts.Where(s => s[0] != ';').ToArray());
			}
		}
	};
}