// Copyright (C) 2008, 2013 panacoran <panacoran@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: Buffer.cs 474 2013-06-28 04:03:17Z panacoran $

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

namespace Protra.Lib.Lang
{
    /// <summary>
    /// ファイルの読み込み位置を保持する構造体。
    /// </summary>
    internal struct Position
    {
        /// <summary>
        /// StreamReaderを取得あるいは設定する。
        /// </summary>
        public StreamReader Reader { get; set; }

        /// <summary>
        /// ファイル名を取得あるいは設定する。
        /// </summary>
        public string Filename { get; set; }

        /// <summary>
        /// 行番号を取得あるいは設定する。
        /// </summary>
        public int LineNo { get; set; }
    }

    /// <summary>
    /// 空白のスキップと#include/require文の処理を行う行バッファ
    /// </summary>
    public class Buffer : IDisposable
    {
        private static readonly Regex SkipRegex = new Regex(@"\A\s*(?://.*|#.*)?", RegexOptions.Compiled);

        private static readonly Regex IncludeRegex = new Regex(@"\A\s*(?:#include\s*<(.*)>|require\s*""(.*)"")",
                                                               RegexOptions.Compiled);

        private static readonly Regex MagicCommentRegex =
            new Regex(@"\A(?://|#)\s+([-\w]+:\s+[^;]+(;?|(;\s+[-\w]+:\s+[^;]+)+);?)\Z", RegexOptions.Compiled);

        private static readonly Encoding Encoding = Encoding.GetEncoding("Shift_JIS");
        private Position _pos;
        private readonly Stack<Position> _posStack;
        private readonly Dictionary<string, string> _magicComment;
        private bool _disposed;
        private bool _head;

        /// <summary>
        /// ファイル名を取得する。
        /// </summary>
        public string Filename
        {
            get { return _pos.Filename; }
        }

        /// <summary>
        /// 行番号を取得する。
        /// </summary>
        public int LineNo
        {
            get { return _pos.LineNo; }
        }

        /// <summary>
        /// 行バッファを取得する。
        /// </summary>
        public string Line { get; private set; }

        /// <summary>
        /// 現在の読み取り位置を取得する。
        /// </summary>
        public int Column { get; set; }

        /// <summary>
        /// コンストラクタ。
        /// </summary>
        /// <param name="file">読み込むファイルのパス名を指定する。</param>
        /// <param name="magicComment">マジックコメントを返すDictionary</param>
        public Buffer(string file, Dictionary<string, string> magicComment)
        {
            try
            {
                _pos.Reader = new StreamReader(file, Encoding);
            }
            catch (IOException)
            {
                throw new ParseException("Can't open file --- " + file, null);
            }
            _pos.Filename = file;
            _posStack = new Stack<Position>();
            _magicComment = magicComment;
            _disposed = false;
            _head = true;
        }

        /// <summary>
        /// 次の空白ではない文字まで読み込み位置を進める。
        /// </summary>
        public void Next()
        {
            do
            {
                Match m;
                if (Line == null)
                {
                    ReadLine();
                    if (Line == null)
                        break;
                    if (_head && (m = MagicCommentRegex.Match(Line)).Success)
                    {
                        ParseMagicComment(m.Groups[1].Value);
                        Line = null;
                        continue;
                    }
                    _head = false;
                    if ((m = IncludeRegex.Match(Line)).Success)
                    {
                        OpenIncludeFile((m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Value));
                        Line = null;
                        continue;
                    }
                }
                m = SkipRegex.Match(Line, Column, Line.Length - Column); // 必ず成功する。
                Column += m.Length;
                if (m.Length == Line.Length) // スペースとコメントだけなら読み飛ばす。
                    Line = null;
                else if (Column == Line.Length - 1 && (Line[Column] == '\\' || Line[Column] == '_')) // \と_による継続行の処理。
                    Line = null;
                else if (Column == Line.Length)
                {
                    if (Line[Column - 1] == ';')
                        Line = null;
                    else
                    {
                        // ';'で終わっていない改行には;を挿入する。
                        Line = ";";
                        Column = 0;
                    }
                }
            } while (Line == null);
        }

        private void ReadLine()
        {
            while ((Line = _pos.Reader.ReadLine()) == null)
            {
                _pos.Reader.Close();
                if (_posStack.Count == 0)
                    return;
                _pos = _posStack.Pop();
            }
            _pos.LineNo++;
            Column = 0;
        }

        private static readonly Regex MagicCommentElementRegex = new Regex(@"(?<key>[-\w]+):\s+(?<value>[^;]+)");

        private void ParseMagicComment(string line)
        {
            var m = MagicCommentElementRegex.Match(line);
            while (m.Success)
            {
                _magicComment[m.Groups["key"].Value] = m.Groups["value"].Value;
                m = m.NextMatch();
            }
        }

        private void OpenIncludeFile(string name)
        {
            _posStack.Push(_pos);
            name = name.Replace('/', Path.DirectorySeparatorChar);
            var lib = Global.DirLib + Path.DirectorySeparatorChar + name + ".pt";
            try
            {
                _pos.Reader = new StreamReader(lib, Encoding);
            }
            catch (IOException)
            {
                var token = new Token {LineNo = _pos.LineNo, Filename = _pos.Filename};
                throw new ParseException("Can't open file --- " + lib, token);
            }
            _pos.LineNo = 0;
            _pos.Filename = lib;
        }

        /// <summary>
        /// 資源を開放する。
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// 開いているファイルをすべて閉じる。
        /// </summary>
        /// <param name="disposing">マネージとアンマネージの両方のリソースを開放するならtrue。アンマネージだけならfalse。</param>
        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
                return;
            if (disposing)
            {
                _pos.Reader.Dispose();
                foreach (var p in _posStack)
                    p.Reader.Dispose();
            }
            _disposed = true;
        }
    }
}