// Copyright (c) 2008, NTT DATA Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.  

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using TERASOLUNA.Fw.Common.Logging;

namespace TERASOLUNA.Fw.Web.Controller
{
    /// <summary>
    /// 
    /// </summary>
    public abstract class MultipartItem : IMultipartItem
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(MultipartItem));

        /// <summary>
        /// マルチパートデータのヘッダ情報から Content-Disposition の値を取得する際、キーとして利用する文字列です。
        /// </summary>
        /// <remarks>
        /// <para>定数の値は "CONTENT-DISPOSITION" です。</para>
        /// </remarks>
        protected static readonly string CONTENT_DISPOSITION = "CONTENT-DISPOSITION";

        /// <summary>
        /// マルチパート要素名を取得する際、キーとして利用する文字列です。
        /// </summary>
        /// <remarks>
        /// <para>定数の値は "NAME" です。</para>
        /// </remarks>
        protected static readonly string NAME_CODE = "NAME";

        /// <summary>
        /// 区切り文字です。
        /// </summary>
        /// <remarks>
        /// <para>定数の値は "--" です。</para>
        /// </remarks>
        protected static readonly string SEPARATE_CODE = "--";

        /// <summary>
        /// 改行コードです。
        /// </summary>
        /// <remarks>
        /// <para>定数の値は "\r\n" です。</para>
        /// </remarks>
        protected static readonly string CRLF_CODE = "\r\n";

        /// <summary>
        /// アップロードデータのヘッダ情報です。
        /// </summary>
        private IDictionary<string, string> _headerList = new Dictionary<string ,string>();

        /// <summary>
        /// アップロードデータのマルチパートの要素名です。
        /// </summary>
        private string _name = null;

        /// <summary>
        /// テキストをエンコードするための <seealso cref="Encoding"/> です。
        /// </summary>
        private Encoding _encoding = null;

        /// <summary>
        /// ボディ部の開始位置を表す位置情報です。
        /// </summary>
        private long _positionStart = 0;

        /// <summary>
        /// ボディ部の終了位置を表す位置情報です。
        /// </summary>
        private long _positionEnd = 0;

        /// <summary>
        /// アップロードデータのヘッダ情報を取得または設定します。
        /// </summary>
        /// <value>
        /// アップロードデータのヘッダ情報です。
        /// </value>
        public IDictionary<string, string> HeaderList
        {
            get
            {
                return _headerList;
            }
            set
            {
                _headerList = value;
            }
        }

        /// <summary>
        /// アップロードデータのマルチパートの要素名を取得または設定します。
        /// </summary>
        /// <value>
        /// アップロードデータのマルチパートの要素名です。
        /// </value>
        public string Name
        {
            get
            {
                return _name;
            }
            set
            {
                _name = value;
            }
        }

        /// <summary>
        /// テキストをエンコードするための <seealso cref="Encoding"/> を取得または設定します。
        /// </summary>
        /// <value>
        /// テキストをエンコードするための <seealso cref="Encoding"/> です。
        /// </value>
        public Encoding Encoding
        {
            get
            {
                return _encoding;
            }
            set
            {
                _encoding = value;
            }
        }


        /// <summary>
        /// 格納されているデータがテキストかどうかを取得します。
        /// </summary>
        /// <value>
        /// 格納されているデータがテキストかどうかです。
        /// </value>
        public abstract bool IsText { get; }

        /// <summary>
        /// ファイルパスを含むファイル名を取得します。
        /// </summary>
        /// <value>
        /// ファイルパスを含むファイル名。
        /// </value>
        /// <remarks>
        /// 必ず null を返却します。
        /// </remarks>
        public virtual string Filename
        {
            get
            {
                return null;
            }
        }

        /// <summary>
        /// アップロードデータを <seealso cref="Stream"/> 型にて取得します。
        /// </summary>
        /// <value>
        /// <see cref="Stream"/> 型のアップロードデータ。
        /// </value>
        /// <remarks>
        /// 必ず null を返却します。
        /// </remarks>
        public virtual Stream OutputStream
        {
            get
            {
                return null;
            }
        }

        /// <summary>
        /// アップロードデータを文字列型にて取得します。
        /// </summary>
        /// <value>
        /// 文字列型のアップロードデータ。
        /// </value>
        /// <remarks>
        /// 必ず null を返却します。
        /// </remarks>
        public virtual string Text
        {
            get
            {
                return null;
            }
        }

        /// <summary>
        /// ボディ部の開始位置を表す位置情報を取得または設定します。
        /// </summary>
        /// <value>
        /// ボディ部の開始位置を表す位置情報。
        /// </value>
        public long PositionStart
        {
            get
            {
                return _positionStart;
            }
            protected set
            {
                _positionStart = value;
            }
        }

        /// <summary>
        /// ボディ部の終了位置を表す位置情報を取得または設定します。
        /// </summary>
        /// <value>
        /// ボディ部の終了位置を表す位置情報。
        /// </value>
        public long PositionEnd
        {
            get
            {
                return _positionEnd;
            }
            protected set
            {
                _positionEnd = value;
            }
        }

        /// <summary>
        /// <see cref="MultipartItem"/> クラスの新しいインスタンスを初期化します
        /// </summary>
        /// <param name="boundary">アップロードデータを区切るための文字列。</param>
        /// <param name="partStream">アップロードされたデータ。</param>
        /// <param name="headerList">アップロードデータのヘッダ情報。</param>
        /// <param name="encoding">テキストをエンコードするための <seealso cref="Encoding"/> 。</param>
        /// <exception cref="ArgumentNullException">
        /// <list type="bullet">
        /// <item>
        /// <paramref name="boundary"/>が null 参照です。
        /// </item>
        /// <item>
        /// <paramref name="partStream"/>が null 参照です。
        /// </item>
        /// <item>
        /// <paramref name="headerList"/>が null 参照です。
        /// </item>
        /// <item>
        /// <paramref name="encoding"/>が null 参照です。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="boundary"/> が空文字列です。
        /// </exception>
        /// <exception cref="InvalidRequestException">
        /// ヘッダ情報にマルチパート要素名が存在しません。
        /// </exception>
        protected MultipartItem(string boundary, 
                                 Stream partStream, 
                                 IDictionary<string, string> headerList, 
                                 Encoding encoding)
        {
            if (boundary == null)
            {
                ArgumentNullException exception = new ArgumentNullException("boundary");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "boundary"), exception);
                }
                throw exception;
            }
            if (boundary.Length == 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_STRING, "boundary");
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }
            if (partStream == null)
            {
                ArgumentNullException exception = new ArgumentNullException("partStream");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "partStream"), exception);
                }
                throw exception;
            }
            if (headerList == null)
            {
                ArgumentNullException exception = new ArgumentNullException("headerList");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "headerList"), exception);
                }
                throw exception;
            }
            if (encoding == null)
            {
                ArgumentNullException exception = new ArgumentNullException("encoding");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "encoding"), exception);
                }
                throw exception;
            }

            _headerList = headerList;
            _name = GetName();
            _encoding = encoding;
        }

        /// <summary>
        /// ボディ部が存在し、形式が正しいかどうかを判断します。
        /// </summary>
        /// <remarks>
        /// ボディ部の終了を意味する改行コード＋区切り文字列(-- boundary )を探します。
        /// </remarks>
        /// <param name="boundary">アップロードデータを区切るための文字列。</param>
        /// <param name="partStream">アップロードされたデータ。</param>
        /// <exception cref="InvalidRequestException">
        /// ボディ部に終端文字列が存在しません。
        /// </exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")]
        protected void FindBody(string boundary, Stream partStream)
        {
            PositionStart = partStream.Position;
            PositionEnd = 0;
            byte[] readByteBackup = new byte[1];
            byte[] bytesBoundary = Encoding.GetBytes(SEPARATE_CODE + boundary);
            int i = 0;

            // ストリームの終端までループする
            while (partStream.Read(readByteBackup, 0, 1) > 0)
            {
                if (bytesBoundary[i] == readByteBackup[0])
                {
                    // バウンダリバイト配列を１つ進める
                    i++;
                    if (i == bytesBoundary.Length)
                    {
                        // 終端子発見
                        PositionEnd = partStream.Position - CRLF_CODE.Length - SEPARATE_CODE.Length - boundary.Length;
                        break;
                    }
                }
                else
                {
                    // バウンダリバイト配列のインデックスを最初に戻す
                    i = 0;
                }
            }
            
            // 区切り文字が見つからずStreamの最後まで行った場合
            if (PositionEnd == 0)
            {
                InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_NOT_FOUND_ENDCODE);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
        }

        /// <summary>
        /// ヘッダ情報より name 属性の値を探します。
        /// </summary>
        /// <returns>マルチパート要素名です。</returns>
        /// <exception cref="InvalidRequestException">
        /// ヘッダ情報にマルチパート要素名が存在しません。
        /// </exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        protected string GetName()
        {
            IDictionary<string, string> separatedList = SeparateHeader();

            if (separatedList.ContainsKey(NAME_CODE))
            {
                return separatedList[NAME_CODE];
            }
            else
            {
                // ヘッダ情報にマルチパート要素名が見つからなかった場合
                InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_NOT_FOUND_NAME);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
        }

        /// <summary>
        /// ヘッダ情報をより詳細な key ,  value に分割します。
        /// </summary>
        /// <returns>ヘッダ情報を詳細化して配列に格納したものです。</returns>
        /// <exception cref="InvalidRequestException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// ヘッダ情報に <seealso cref="CONTENT_DISPOSITION"/> が存在しません。
        /// </item>
        /// <item>
        /// ヘッダ情報の key が重複しています。
        /// </item>
        /// </list>
        /// </exception>
        protected IDictionary<string, string> SeparateHeader()
        {
            if (!HeaderList.ContainsKey(CONTENT_DISPOSITION))
            {
                // ヘッダ情報にcontent-dispositionが見つからなかった場合
                InvalidRequestException exception =  new InvalidRequestException(Properties.Resources.E_NOT_FOUND_CONTENTDISPOSITION);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            string key = null;
            string value = null;

            string headerValue = HeaderList[CONTENT_DISPOSITION];

            string[] separatedValue = headerValue.Split(';');

            IDictionary<string, string> separatedList = new Dictionary<string, string>();
            
            foreach (string s in separatedValue)
            {
                int separatedPosition = s.IndexOf('=');
                if (separatedPosition != -1)
                {
                    key = s.Substring(0, separatedPosition).Trim().ToUpper();
                    value = s.Substring(separatedPosition + 1, s.Length - 1 - separatedPosition).Trim();
                    if (separatedList.ContainsKey(key))
                    {
                        // キー値が重複したとき
                        InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_NOT_ACCEPTED_HEADER);
                        if (_log.IsErrorEnabled)
                        {
                            _log.Error(exception.Message, exception);
                        }
                        throw exception;
                    }
                    separatedList.Add(key, value);
                }
            }

            return separatedList;
        }
        
        /// <summary>
        /// リソースを解放します。
        /// </summary>
        /// <remarks>
        /// <see cref="MultipartItem"/> 派生クラスでリソースを操作する場合は、適切に実装してください。
        /// </remarks>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly")]
        public abstract void Dispose();

    }
}
