// 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: Scanner.cs 474 2013-06-28 04:03:17Z panacoran $

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

namespace Protra.Lib.Lang
{
    /// <summary>
    /// 字句解析を行うクラス。
    /// </summary>
    public class Scanner : IDisposable
    {
        private static readonly Regex LocalVarRegex = new Regex(@"\A[a-z][a-zA-Z_0-9]*", RegexOptions.Compiled);
        private static readonly Regex GlobalVarRegex = new Regex(@"\A\$[a-zA-Z_0-9]+", RegexOptions.Compiled);
        private static readonly Regex FunctionNameRegex = new Regex(@"\A[A-Z][a-zA-Z_0-9]*", RegexOptions.Compiled);
        private static readonly Regex FloatRegex = new Regex(@"\A(?:[1-9][0-9]*|0)\.[0-9]+", RegexOptions.Compiled);
        private static readonly Regex IntRegex = new Regex(@"\A(?:[1-9][0-9]*|0)", RegexOptions.Compiled);
        private readonly Buffer _buffer;
        private readonly Queue<Token> _tokens = new Queue<Token>();
        private bool _disposed;

        /// <summary>
        /// 読んだ字句を取得する。
        /// </summary>
        public Token Token { get; private set; }

        /// <summary>
        /// マジックコメントのDictionaryを取得する。
        /// </summary>
        public Dictionary<string, string> MagicComment { get; private set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="file">プログラムのファイル名</param>
        public Scanner(string file)
        {
            MagicComment = new Dictionary<string, string>();
            _buffer = new Buffer(file, MagicComment);
            _disposed = false;
        }

        /// <summary>
        /// 字句を先読みする。
        /// </summary>
        /// <returns>読んだ字句を返す。</returns>
        public Token Peek()
        {
            var token = NextToken();
            _tokens.Enqueue(token);
            return token;
        }

        /// <summary>
        /// 字句を読む。
        /// </summary>
        /// <returns>EOFに達していたらfalseを返す。</returns>
        public bool Scan()
        {
            if (_tokens.Count > 0)
            {
                Token = _tokens.Dequeue();
                return true;
            }
            Token = NextToken();
            return Token.Type != TokenType.EOF;
        }

        private Token NextToken()
        {
            _buffer.Next();
            var token = new Token {Filename = _buffer.Filename, LineNo = _buffer.LineNo};
            if (_buffer.Line == null)
            {
                token.Value = null;
                token.Type = TokenType.EOF;
                return token;
            }
            var m = LocalVarRegex.Match(_buffer.Line, _buffer.Column, _buffer.Line.Length - _buffer.Column);
            if (m.Success)
            {
                token.Value = m.Value;
                switch (token.Value)
                {
                    case "if":
                    case "else":
                    case "elsif":
                    case "while":
                    case "def":
                    case "null":
                        token.Type = TokenType.Reserved;
                        break;
                    case "int":
                    case "float":
                    case "string":
                        token.Type = TokenType.Type;
                        break;
                    default:
                        token.Type = TokenType.LocalVariable;
                        break;
                }
                _buffer.Column += m.Length;
                return token;
            }
            m = GlobalVarRegex.Match(_buffer.Line, _buffer.Column, _buffer.Line.Length - _buffer.Column);
            if (m.Success)
            {
                token.Type = TokenType.GlobalVariable;
                token.Value = m.Value;
                _buffer.Column += m.Length;
                return token;
            }
            m = FunctionNameRegex.Match(_buffer.Line, _buffer.Column, _buffer.Line.Length - _buffer.Column);
            if (m.Success)
            {
                token.Type = TokenType.FunctionName;
                token.Value = m.Value;
                _buffer.Column += m.Length;
                return token;
            }
            m = FloatRegex.Match(_buffer.Line, _buffer.Column, _buffer.Line.Length - _buffer.Column);
            if (m.Success)
            {
                token.Type = TokenType.Float;
                token.FloatValue = double.Parse(m.Value);
                _buffer.Column += m.Length;
                return token;
            }
            m = IntRegex.Match(_buffer.Line, _buffer.Column, _buffer.Line.Length - _buffer.Column);
            if (m.Success)
            {
                token.Type = TokenType.Int;
                token.IntValue = int.Parse(m.Value);
                _buffer.Column += m.Length;
                return token;
            }
            switch (_buffer.Line[_buffer.Column])
            {
                case ',':
                case ';':
                case '(':
                case ')':
                case '{':
                case '}':
                case '[':
                case ']':
                case '+':
                case '-':
                case '*':
                case '/':
                case '%':
                    token.Type = TokenType.Operator;
                    token.Value = _buffer.Line.Substring(_buffer.Column, 1);
                    _buffer.Column++;
                    return token;
                case '&':
                    token.Type = TokenType.Operator;
                    token.Value = "&";
                    _buffer.Column++;
                    if (_buffer.Column < _buffer.Line.Length &&
                        _buffer.Line[_buffer.Column] == '&')
                    {
                        token.Value = "&&";
                        _buffer.Column++;
                    }
                    return token;
                case '|':
                    token.Type = TokenType.Operator;
                    token.Value = "|";
                    _buffer.Column++;
                    if (_buffer.Column < _buffer.Line.Length &&
                        _buffer.Line[_buffer.Column] == '|')
                    {
                        token.Value = "||";
                        _buffer.Column++;
                    }
                    return token;
                case '<':
                case '>':
                case '!':
                case '=':
                    token.Type = TokenType.Operator;
                    token.Value = _buffer.Line.Substring(_buffer.Column, 1);
                    _buffer.Column++;
                    if (_buffer.Line[_buffer.Column] == '=')
                    {
                        token.Value += "=";
                        _buffer.Column++;
                    }
                    return token;
                case '"':
                    token.Type = TokenType.String;
                    var start = ++_buffer.Column;
                    while (_buffer.Column < _buffer.Line.Length &&
                           _buffer.Line[_buffer.Column] != '"')
                        _buffer.Column++;
                    if (_buffer.Column >= _buffer.Line.Length)
                        throw new ParseException("string is not closed.", token);
                    token.Value = _buffer.Line.Substring(start, _buffer.Column++ - start);
                    return token;
            }
            token.Type = TokenType.Unknown;
            token.Value = null;
            return token;
        }

        /// <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)
                _buffer.Dispose();
            _disposed = true;
        }
    }
}