// Copyright (C) 2003, 2005 Daisuke Arai <darai@users.sourceforge.jp>
// 
// This program is part of Protra.
//
// Protra is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see <http://www.gnu.org/licenses/>.
// 
// $Id: Context.cs,v 1.5 2008-02-28 16:19:51 panacoran Exp $

using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

namespace Protra.Lib.Lang
{
	/// <summary>
	/// プログラムの文脈を表現するクラスです。
	/// ContextTokenを切り出すのに使用します。
	/// </summary>
	public class Context : IEnumerable, IEnumerator
	{
		/// <summary>
		/// コンテキストをトークンに分割するために使用するセパレータ
		/// </summary>
		private string[] separators = {
			",", ";", "(", ")", "{", "}", "[", "]",
			"+", "-", "*", "/", "%", "&&", "||",
			"<=", ">=", "<", ">", "==", "!=", "=", "!",
			"if ", "else ", "elsif ", "while ", "def ",
			"if\t", "else\t", "elsif\t", "while\t", "def\t" };
		/// <summary>
		/// コンテキストをトークンに分割するために使用するセパレータ
		/// </summary>
		private static char[] separatorChars = {
			' ', '\t', ',', ';', '(', ')', '{', '}', '[', ']',
			'+', '-', '*', '/', '%', '&', '|', '<', '>', '=' };

		/// <summary>
		/// ライブラリの存在するディレクトリのパス
		/// </summary>
		private string libPath = "";
		/// <summary>
		/// ファイル名とファイルの中身となるstring[]を対応させるハッシュ
		/// </summary>
		private Hashtable hash = new Hashtable();
		/// <summary>
		/// コンストラクタに渡されたファイル名
		/// </summary>
		private string rootFile;
		/// <summary>
		/// 現在読み込んでいるファイル名
		/// </summary>
		private string file;
		/// <summary>
		/// 現在読み込んでいる行
		/// </summary>
		private int line;
		/// <summary>
		/// 現在読み込んでいる位置
		/// </summary>
		private int pos;
		/// <summary>
		/// 現在のトークンの文字列
		/// </summary>
		private string currentToken;
		/// <summary>
		/// #include 読み込み時に現在位置をプッシュするスタック
		/// </summary>
		private Stack stack = new Stack();
		/// <summary>
		/// 文字コード
		/// </summary>
		private Encoding encoding = Encoding.GetEncoding("Shift_JIS");

		/// <summary>
		/// コンストラクタ
		/// </summary>
		/// <exception cref="Protra.Lib.Lang.ParseException">
		/// ファイルが開けなかった場合にthrowされます。
		/// </exception>
		/// <param name="file">プログラムファイルのパス</param>
		public Context(string file)
		{
			StreamReader sr;
			try
			{
				sr = new StreamReader(file, encoding);
			}
			catch(IOException)
			{
				throw new ParseException("can't open file --- " + file, null);
			}

			ArrayList array = new ArrayList();
			string s;
			while((s = sr.ReadLine()) != null)
				array.Add(s);
			sr.Close();

			string[] tmp = (string[])array.ToArray(typeof(string));
			hash.Add(file, tmp);

			this.rootFile = this.file = file;
		}

		/// <summary>
		/// 現在位置をスタックにプッシュします。
		/// </summary>
		private void Push()
		{
			stack.Push(new ContextToken(currentToken, file, line, pos));
		}

		/// <summary>
		/// スタックから過去の位置を取り出し復帰します。
		/// </summary>
		/// <returns>スタックが空ならfalseになります。</returns>
		private bool Pop()
		{
			if(stack.Count == 0)
				return false;
			Reset((ContextToken)stack.Pop());
			return true;
		}

		/// <summary>
		/// ContextTokenを列挙するIEnumeratorを取得します。
		/// </summary>
		/// <returns>ContextTokenを列挙するIEnumeratorです。</returns>
		public IEnumerator GetEnumerator()
		{
			return this;
		}

		/// <summary>
		/// 内部カーソルを1だけ進めます。
		/// </summary>
		/// <returns>終端まで達していたらfalseになります。</returns>
		public bool MoveNext()
		{
			while(true)
			{
				if(line >= ((string[])hash[file]).Length)
				if(! Pop())
					break;
				
				string[] lines =(string[])hash[file];
				string s;

				if(pos == 0)
				{
					// 空行読み飛ばし
					Regex regex = new Regex(@"^[\t ]*$|^[\t ]*//.*$");
					if(regex.IsMatch(lines[line]))
					{
						line++;
						while(line < lines.Length && regex.IsMatch(lines[line]))
							line++;
						if(line >= lines.Length)
							continue;
					}

					// #include 行であるかチェックする
					regex = new Regex(@"^[\t ]*#include[\t ]+<(.*)>");
					Match m = regex.Match(lines[line]);
					if(m.Success)
					{
						string includeFile = m.Groups[1].Value.Replace('/', Path.DirectorySeparatorChar);
						includeFile = libPath + includeFile + ".pt";
						if(! hash.Contains(includeFile))
						{
							try
							{
								StreamReader sr = new StreamReader(includeFile, encoding);
								ArrayList array = new ArrayList();
								while((s = sr.ReadLine()) != null)
									array.Add(s);
								sr.Close();

								string[] tmp = (string[])array.ToArray(typeof(string));
								hash.Add(includeFile, tmp);
							}
							catch(IOException)
							{
								throw new ParseException
									("can't open include file --- " + includeFile, (ContextToken)this.Current);
							}
						}

						line++;
						if(line < lines.Length)
							Push();
						file = includeFile;
						line = pos = 0;
						continue;
					}
				}

				// トークンを切り出す
				s = lines[line];
				if(! s.EndsWith(";"))
					s += ";";
				while(pos < s.Length && (s[pos] == '\t' || s[pos] == ' '))
					pos++;
				string sub = s.Substring(pos, s.Length - pos);
				if(sub.StartsWith("//")) // コメント
				{
					line++;
					pos = 0;
					currentToken = ";";
					return true;
				}
				// セパレータと一致すれば切り出す
				foreach(string sep in separators)
					if(sub.StartsWith(sep))
					{
						currentToken = sep.Replace('\t', ' ');
						pos += sep.Length;
						if(pos >= s.Length)
						{
							line++; pos = 0;
						}
						return true;
					}
				if(sub[0] == '"') // 文字列
				{
					for(int i = pos + 1; i < s.Length; i++)
						if(s[i] == '"')
						{
							i++;
							currentToken = s.Substring(pos, i - pos);
							pos = i;
							return true;
						}
					throw new ParseException("string is not closed", (ContextToken)this.Current);
				}
				else if(sub[0] == '_') // 次の行への継続
				{
					line++;
					pos = 0;
				}
				else // 通常のトークン
				{
					for(int i = pos + 1; i < s.Length; i++)
						for(int j = 0; j < separatorChars.Length; j++)
							if(s[i] == separatorChars[j])
							{
								currentToken = s.Substring(pos, i - pos);
								pos = i;
								return true;
							}
				}
			}

			currentToken = null;
			return false;
		}

		/// <summary>
		/// カーソルを先頭にリセットします。
		/// </summary>
		public void Reset()
		{
			currentToken = null;
			file = rootFile;
			line = pos = 0;
		}

		/// <summary>
		/// カーソルを指定位置まで戻します。
		/// </summary>
		/// <param name="ct">カーソル位置を記憶したContextToken</param>
		public void Reset(ContextToken ct)
		{
			currentToken = ct.ToString();
			file = ct.FileName;
			line = ct.Line;
			pos = ct.Position;
		}

		/// <summary>
		/// 現在のContextTokenを取得します。
		/// </summary>
		public object Current
		{
			get { return new ContextToken(currentToken, file, line, pos); }
		}

		/// <summary>
		/// ライブラリの存在するディレクトリのパスを取得または設定します。
		/// </summary>
		public string LibraryPath
		{
			get { return libPath; }
			set
			{
				if(value[value.Length - 1] != Path.DirectorySeparatorChar)
					value += Path.DirectorySeparatorChar;
				libPath = value;
			}
		}
	}
}
