/*
 * CSV I/O Library.NET
 * Copyright (C) 2005, uguu All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 *  - Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 
 *  - Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace Uguu.IO.Csv
{
    /// <summary>
    ///     CSV データに出現するトークンを表します。
    /// </summary>
    internal sealed class CsvToken
    {
        /// <summary>
        ///     トークン「列区切り」を表す定数です。
        /// </summary>
        internal const int  TOKEN_DELIMITER = 0;
        /// <summary>
        ///     トークン「行区切り」を表す定数です。
        /// </summary>
        internal const int  TOKEN_NEW_LINE = 1;
        /// <summary>
        ///     トークン「クォーテーション」を表す定数です。
        /// </summary>
        internal const int  TOKEN_QUOTATION = 2;
        /// <summary>
        ///     ただの文字列を表す定数です。
        /// </summary>
        internal const int  TOKEN_CHARACTER = 3;
        /// <summary>
        ///     トークンの種類。
        /// </summary>
        internal int Type;
        /// <summary>
        ///     トークンの値。
        /// </summary>
        internal string Value;
        /// <summary>
        ///     <see cref="CsvToken"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <param name="type">トークンの種類。</param>
        /// <param name="value">トークンの値。</param>
        internal CsvToken(int type, string value)
        {
            this.Type = type;
            this.Value = value;
        }
    }
    /// <summary>
    ///     <see cref="TextReader"/> インスタンスから文字列を読み込み、トークンごとに解析します。
    /// </summary>
    /// <remarks>
    ///     <see cref="CsvReader"/> クラスがデータを解析するために使用します。
    /// </remarks>
    internal sealed class CsvTokenizer : IDisposable
    {
        /// <summary>
        ///     トークンの読み込み元 <see cref="TextReader"/> インスタンス。
        /// </summary>
        private TextReader      reader;
        /// <summary>
        ///     解析対象の CSV トークンの配列。
        /// </summary>
        private CsvToken[]      csvTokenArray;
        /// <summary>
        ///     解析対象の CSV トークンの最大長。
        /// </summary>
        private int             tokenMaxLength;
        /// <summary>
        ///     読み込んだ文字列のバッファ。
        /// </summary>
        private StringBuilder   buffer = new StringBuilder();
        /// <summary>
        ///     指定した <see cref="TextReader"/> インスタンスから文字列を読み込みトークンごとに解析する、
        ///     <see cref="CsvTokenizer"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <param name="reader">トークンの読み込み元 <see cref="TextReader"/> インスタンス。</param>
        /// <param name="newLine">行区切り文字。</param>
        /// <param name="delimiter">列区切り文字。</param>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="reader"/> 引数が null 参照の場合。
        ///     <paramref name="newLine"/> 引数、 <paramref name="delimiter"/> 引数が null 参照か空文字列の場合。
        /// </exception>
        internal CsvTokenizer(TextReader reader, string newLine, string delimiter)
        {
            // 引数をチェックします。
            if (reader == null)
            {
                throw new ArgumentNullException("reader");
            }
            if (newLine == null || newLine == string.Empty)
            {
                throw new ArgumentNullException("newLine");
            }
            if (delimiter == null || delimiter == string.Empty)
            {
                throw new ArgumentNullException("delimiter");
            }
            // インスタンスを初期化します。
            this.reader = reader;
            
            this.csvTokenArray = new CsvToken[3];
            this.csvTokenArray[0] = new CsvToken(CsvToken.TOKEN_NEW_LINE, newLine);
            this.csvTokenArray[1] = new CsvToken(CsvToken.TOKEN_DELIMITER, delimiter);
            this.csvTokenArray[2] = new CsvToken(CsvToken.TOKEN_QUOTATION, "\"");
            
            foreach (CsvToken csvToken in this.csvTokenArray)
            {
                if (tokenMaxLength < csvToken.Value.Length)
                {
                    tokenMaxLength = csvToken.Value.Length;
                }
            }
        }
        /// <summary>
        ///     次のトークンを読み込みます。
        /// </summary>
        /// <exception cref="ObjectDisposedException">トークナイザーが既に閉じている場合。</exception>
        /// <returns>
        ///     読み込んだトークン。
        ///     CSV データの終端に達した場合は null 参照。
        /// </returns>
        public CsvToken NextToken()
        {
            CsvToken token = null;
            StringBuilder value = new StringBuilder();
            
            // インスタンスの状態のチェック。
            if (this.reader == null)
            {
                throw new ObjectDisposedException("reader");
            }
            
            // リーダーから文字列を読み込みます。
            while (true)
            {
                // バッファを満たします。
                if (this.buffer.Length < this.tokenMaxLength)
                {
                    char[] charBuffer = new char[this.tokenMaxLength - this.buffer.Length];
                    int len = this.reader.ReadBlock(charBuffer, 0, charBuffer.Length);
                    this.buffer.Append(charBuffer, 0, len);
                }
                
                // バッファが空の場合、終端に達したと判断します。
                if (this.buffer.Length == 0)
                {
                    if (value.Length > 0)
                    {
                        token = new CsvToken(CsvToken.TOKEN_CHARACTER, value.ToString());
                    }
                    else
                    {
                        token = null;
                    }
                    break;
                }
                
                // 読み込んだ文字とトークンを比較します。
                foreach (CsvToken csvToken in this.csvTokenArray)
                {
                    if (this.buffer.ToString().StartsWith(csvToken.Value))
                    {
                        if (value.Length > 0)
                        {
                            token = new CsvToken(CsvToken.TOKEN_CHARACTER, value.ToString());
                        }
                        else
                        {
                            token = csvToken;
                            this.buffer.Remove(0, csvToken.Value.Length);
                        }
                        break;
                    }
                }
                if (token != null)
                {
                    break;
                }
                
                // トークンと一致しない場合、バッファから値に1文字移動します。
                value.Append(this.buffer[0]);
                this.buffer.Remove(0, 1);
            }
            
            return token;
        }
        /// <summary>
        ///     トークナイザーを閉じます。
        /// </summary>
        /// <remarks>
        ///     具体的には、トークンの読み込み元リーダーを閉じます。
        /// </remarks>
        public void Dispose()
        {
            if (this.reader != null)
            {
                this.reader.Close();
                this.reader = null;
            }
        }
    }
    /// <summary>
    ///     <see cref="TextReader"/> インスタンスから文字列を読み込み、 CSV データとして解析します。
    /// </summary>
    /// <remarks>
    ///     コンストラクタの指定により、以下のように挙動を変更することができます。
    ///     <list type="bullet">
    ///         <item><description>
    ///             newLine 引数を指定することにより、改行文字を自由に指定することができます
    ///             (例えば、 \r を改行文字としたり (UNIX) 、 \r\n を改行文字としたり (Windows)) 。
    ///         </description></item>
    ///         <item><description>
    ///             delimiter 引数を指定することにより、区切り文字を自由に指定することができます
    ///             (例えば、 , を区切り文字としたり (CSV) 、 \t を区切り文字としたり (TSV)) 。
    ///         </description></item>
    ///         <item><description>
    ///             ignoreEofLine 引数を指定することにより、 EOF だけの行を無視して読み込みを終了するかどうかを指定することができます。
    ///             (true を指定すると、 EOF だけの行は読み込まず、 Read() メソッドは false を返します。
    ///             false を指定すると、 EOF だけの行も空行として読み込みます。)
    ///         </description></item>
    ///     </list>
    /// </remarks>
    public sealed class CsvReader : IDisposable
    {
        /// <summary>
        ///     読み込み状態「新しい行の開始」を表す定数です。
        /// </summary>
        private const int   READ_STATE_NEW_RECORD        = 0;
        /// <summary>
        ///     読み込み状態「列区切り」を表す定数です。
        /// </summary>
        private const int   READ_STATE_DELIMITER         = 1;
        /// <summary>
        ///     読み込み状態「値」を表す定数です。
        /// </summary>
        private const int   READ_STATE_VALUE             = 2;
        /// <summary>
        ///     読み込み状態「エンクォート値」を表す定数です。
        /// </summary>
        private const int   READ_STATE_ENQUOTE_VALUE     = 3;
        /// <summary>
        ///     読み込み状態「エンクォート値の終端」を表す定数です。
        /// </summary>
        private const int   READ_STATE_ENQUOTE_VALUE_END = 4;
        /// <summary>
        ///     読み込み状態「データの終端」を表す定数です。
        /// </summary>
        private const int   READ_STATE_END_OF_DATA       = 5;
        /// <summary>
        ///     トークナイザー。
        /// </summary>
        private CsvTokenizer    tokenizer;
        /// <summary>
        ///     現在の行の値の配列。
        /// </summary>
        private string[]        values;
        /// <summary>
        ///     読み込み状態。
        /// </summary>
        private int             state;
        /// <summary>
        ///     行番号。
        /// </summary>
        /// <remarks>
        ///     0 から始まります。
        /// </remarks>
        private int             rowIndex = -1;
        /// <summary>
        ///     EOF だけの行を無視するかどうか。
        /// </summary>
        private bool            ignoreEofLine = true;
        /// <summary>
        ///     指定した <see cref="TextReader"/> インスタンスから文字列を読み込み、 CSV データとして解析する、
        ///     <see cref="CsvReader"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <remarks>
        ///     このコンストラクタでインスタンスを初期化した場合、改行文字は <see cref="Environment.NewLine"/> 、区切り文字は "," となります。
        ///     また、 EOF だけの行を無視します。
        /// </remarks>
        /// <param name="reader">CSV データの読み込み元リーダー。</param>
        /// <exception cref="ArgumentNullException"><paramref name="reader"/> 引数が null 参照の場合。</exception>
        public CsvReader(TextReader reader) : this(reader, Environment.NewLine, ",", true)
        {
        }
        /// <summary>
        ///     指定した <see cref="TextReader"/> インスタンスから文字列を読み込み、指定した改行文字と区切り文字で CSV データを解析する、
        ///     <see cref="CsvReader"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <param name="reader">CSV データの読み込み元リーダー。</param>
        /// <param name="newLine">リーダーが認識する改行文字。</param>
        /// <param name="delimiter">リーダーが認識する区切り文字。</param>
        /// <param name="ignoreEofLine">EOF だけの行を無視するかどうか。</param>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="reader"/> 引数が null 参照の場合。
        ///     <paramref name="newLine"/> 引数、 <paramref name="delimiter"/> 引数が null 参照か空文字列の場合。
        /// </exception>
        public CsvReader(TextReader reader, string newLine, string delimiter, bool ignoreEofLine)
        {
            // 引数をチェックします。
            if (reader == null)
            {
                throw new ArgumentNullException("reader");
            }
            if (newLine == null || newLine == string.Empty)
            {
                throw new ArgumentNullException("newLine");
            }
            if (delimiter == null || delimiter == string.Empty)
            {
                throw new ArgumentNullException("delimiter");
            }
            // インスタンスを初期化します。
            this.tokenizer = new CsvTokenizer(reader, newLine, delimiter);
            this.ignoreEofLine = ignoreEofLine;
        }
        /// <summary>
        ///     次の行に移動します。
        /// </summary>
        /// <returns>
        ///     次の行が存在する場合は true 、終端に達していて次の行が存在しない場合は false 。
        /// </returns>
        /// <exception cref="ObjectDisposedException">リーダーが既に閉じている場合。</exception>
        public bool Read()
        {
            // インスタンスの状態をチェックします。
            if (this.tokenizer == null)
            {
                throw new ObjectDisposedException("tokenizer");
            }
            
            // 既に終端に達している場合、 false を返します。
            if (this.state == READ_STATE_END_OF_DATA)
            {
                this.values = null;
                return false;
            }
            
            // トークナイザーからトークンを読み込みます。
            ArrayList valueList = new ArrayList();
            StringBuilder buffer = new StringBuilder();
            
            while (true)
            {
                CsvToken token = this.tokenizer.NextToken();
                
                // リーダーの読み込み状態、及びトークンの種類によって処理を行います。
				if (this.state == READ_STATE_NEW_RECORD)
				{
					if (token == null)
					{
						this.state = READ_STATE_END_OF_DATA;
						break;
					}
					else if (token.Type == CsvToken.TOKEN_DELIMITER)
					{
						valueList.Add(buffer.ToString());
						buffer = new StringBuilder();

						this.state = READ_STATE_DELIMITER;
					}
					else if (token.Type == CsvToken.TOKEN_NEW_LINE)
					{
						this.state = READ_STATE_NEW_RECORD;
						break;
					}
					else if (token.Type == CsvToken.TOKEN_QUOTATION)
					{
						this.state = READ_STATE_ENQUOTE_VALUE;
					}
					else if (token.Type == CsvToken.TOKEN_CHARACTER)
					{
						buffer.Append(token.Value);

						this.state = READ_STATE_VALUE;
					}
					else
					{
					    Debug.Fail(string.Format("Token Type: {0}", token.Type));
					}
				}
				else if (this.state == READ_STATE_DELIMITER)
				{
					if (token == null)
					{
						valueList.Add(buffer.ToString());

						this.state = READ_STATE_END_OF_DATA;
						break;
					}
					else if (token.Type == CsvToken.TOKEN_DELIMITER)
					{
						valueList.Add(buffer.ToString());
						buffer = new StringBuilder();

						this.state = READ_STATE_DELIMITER;
					}
					else if (token.Type == CsvToken.TOKEN_NEW_LINE)
					{
						valueList.Add(buffer.ToString());

						this.state = READ_STATE_NEW_RECORD;
						break;
					}
					else if (token.Type == CsvToken.TOKEN_QUOTATION)
					{
						this.state = READ_STATE_ENQUOTE_VALUE;
					}
					else if (token.Type == CsvToken.TOKEN_CHARACTER)
					{
						buffer.Append(token.Value);

						this.state = READ_STATE_VALUE;
					}
					else
					{
					    Debug.Fail(string.Format("Token Type: {0}", token.Type));
					}
				}
				else if (this.state == READ_STATE_VALUE)
				{
					if (token == null)
					{
						valueList.Add(buffer.ToString());

						this.state = READ_STATE_END_OF_DATA;
						break;
					}
					else if (token.Type == CsvToken.TOKEN_DELIMITER)
					{
						valueList.Add(buffer.ToString());
						buffer = new StringBuilder();

						this.state = READ_STATE_DELIMITER;
					}
					else if (token.Type == CsvToken.TOKEN_NEW_LINE)
					{
						valueList.Add(buffer.ToString());

						this.state = READ_STATE_NEW_RECORD;
						break;
					}
					else if (token.Type == CsvToken.TOKEN_QUOTATION)
					{
						buffer.Append(token.Value);

						this.state = READ_STATE_VALUE;
					}
					else if (token.Type == CsvToken.TOKEN_CHARACTER)
					{
						buffer.Append(token.Value);

						this.state = READ_STATE_VALUE;
					}
					else
					{
						Debug.Fail(string.Format("Token Type: {0}", token.Type));
					}
				}
				else if (this.state == READ_STATE_ENQUOTE_VALUE)
				{
					if (token == null)
					{
						valueList.Add(buffer.ToString());

						this.state = READ_STATE_END_OF_DATA;
						break;
					}
					else if (token.Type == CsvToken.TOKEN_DELIMITER)
					{
						buffer.Append(token.Value);

						this.state = READ_STATE_ENQUOTE_VALUE;
					}
					else if (token.Type == CsvToken.TOKEN_NEW_LINE)
					{
						buffer.Append(token.Value);

						this.state = READ_STATE_ENQUOTE_VALUE;
					}
					else if (token.Type == CsvToken.TOKEN_QUOTATION)
					{
						this.state = READ_STATE_ENQUOTE_VALUE_END;
					}
					else if (token.Type == CsvToken.TOKEN_CHARACTER)
					{
						buffer.Append(token.Value);

						this.state = READ_STATE_ENQUOTE_VALUE;
					}
					else
					{
						Debug.Fail(string.Format("Token Type: {0}", token.Type));
					}
				}
				else if (this.state == READ_STATE_ENQUOTE_VALUE_END)
				{
					if (token == null)
					{
						valueList.Add(buffer.ToString());

						this.state = READ_STATE_END_OF_DATA;
						break;
					}
					else if (token.Type == CsvToken.TOKEN_DELIMITER)
					{
						valueList.Add(buffer.ToString());
						buffer = new StringBuilder();

						this.state = READ_STATE_DELIMITER;
					}
					else if (token.Type == CsvToken.TOKEN_NEW_LINE)
					{
						valueList.Add(buffer.ToString());

						this.state = READ_STATE_NEW_RECORD;
						break;
					}
					else if (token.Type == CsvToken.TOKEN_QUOTATION)
					{
						buffer.Append(token.Value);

						this.state = READ_STATE_ENQUOTE_VALUE;
					}
					else if (token.Type == CsvToken.TOKEN_CHARACTER)
					{
						buffer.Append(token.Value);

						this.state = READ_STATE_ENQUOTE_VALUE;
					}
					else
					{
						Debug.Fail(string.Format("Token Type: {0}", token.Type));
					}
				}
				else
				{
				    Debug.Fail(string.Format("Reader State: {0}", this.state));
				}
            }
            
            this.values = (string[]) valueList.ToArray(typeof(string));
            this.rowIndex++;
            
            // EOF だけの行の場合、読み込みに失敗したことにします。
            if (this.ignoreEofLine && this.values.Length == 0 && this.state == READ_STATE_END_OF_DATA)
            {
                this.values = null;
                return false;
            }
            else
            {
                return true;
            }
        }
        /// <summary>
        ///     現在の行の項目の数を取得します。
        /// </summary>
        /// <exception cref="InvalidOperationException">
        ///     読み込み位置が最初の行の前や終端行のあとにある時にこのプロパティを参照した場合。
        /// </exception>
        /// <exception cref="ObjectDisposedException">リーダーが既に閉じている場合。</exception>
        public int FieldCount
        {
            get
            {
                if (this.tokenizer == null)
                {
                    throw new ObjectDisposedException("tokenizer");
                }
                if (this.values == null)
                {
                    throw new InvalidOperationException();
                }
                return values.Length;
            }
        }
        /// <summary>
        ///     現在の行の位置を表すインデックス番号を取得します。
        /// </summary>
        /// <remarks>
        ///     現在の行が最初の行の前にある場合は -1 になります。
        /// </remarks>
        public int RowIndex
        {
            get
            {
                return this.rowIndex;
            }
        }
        /// <summary>
        ///     現在の行の指定した項目の値を取得します。
        /// </summary>
        /// <exception cref="InvalidOperationException">
        ///     読み込み位置が最初の行の前や終端行のあとにある時にこのプロパティを参照した場合。
        /// </exception>
        /// <exception cref="ObjectDisposedException">リーダーが既に閉じている場合。</exception>
        public string this[int index]
        {
            get
            {
                if (this.tokenizer == null)
                {
                    throw new ObjectDisposedException("tokenizer");
                }
                if (values == null)
                {
                    throw new InvalidOperationException();
                }
                return this.values[index];
            }
        }
        /// <summary>
        ///     リーダーを閉じます。
        /// </summary>
        /// <remarks>
        ///     トークンの読み込み元リーダーも閉じます。
        /// </remarks>
        public void Close()
        {
            if (this.tokenizer != null)
            {
                this.tokenizer.Dispose();
                this.tokenizer = null;
            }
        }
        /// <summary>
        ///     リーダーを閉じます。
        /// </summary>
        /// <remarks>
        ///     トークンの読み込み元リーダーも閉じます。
        /// </remarks>
        public void Dispose()
        {
            this.Close();
        }
        /// <summary>
        ///     リーダーが閉じているかどうかを取得します。
        /// </summary>
        public bool IsClosed
        {
            get
            {
                return (this.tokenizer == null);
            }
        }
    }
}
