// 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>
    /// <see cref="IMultipartItem"/> を作成するためのクラスです。
    /// </summary>
    public class MultipartItemFactory : IMultipartItemFactory
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(MultipartItemFactory));

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

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

        /// <summary>
        /// アップロードデータがテキストであるかを判断する際、キーとして利用する文字列です。
        /// </summary>
        /// <remarks>
        /// <para>定数の値は "APPLICATION/X-WWW-FORM-URLENCODED" です。</para>
        /// </remarks>
        protected static readonly string UPLOADTYPE_TEXT = "APPLICATION/X-WWW-FORM-URLENCODED";

        /// <summary>
        /// アップロードデータがファイルであるかを判断する際、キーとして利用する文字列です。
        /// </summary>
        /// <remarks>
        /// <para>定数の値は "APPLICATION/OCTET-STREAM" です。</para>
        /// </remarks>
        protected static readonly string UPLOADTYPE_FILE = "APPLICATION/OCTET-STREAM";

        /// <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 string _boundary = null;

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

        /// <summary>
        /// アップロードデータを区切るための文字列を取得または設定します。
        /// </summary>
        /// <value>
        /// アップロードデータを区切るための文字列です。
        /// </value>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="value"/> が null 参照です。
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="value"/> が空文字列です。
        /// </exception>
        public string Boundary
        {
            get 
            { 
                return _boundary; 
            }
            set
            {
                if (value == null)
                {
                    ArgumentNullException exception = new ArgumentNullException("Boundary");
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(string.Format(
                            Properties.Resources.E_NULL_PROPERTY_VALUE, "Boundary"), exception);
                    }
                    throw exception;
                }
                if (value.Length == 0)
                {
                    string message = string.Format(Properties.Resources.E_EMPTY_PROPERTY_VALUE, "Boundary");
                    ArgumentException exception = new ArgumentException(message);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(message, exception);
                    }
                    throw exception;
                }
                _boundary = value;
            }
        }

        /// <summary>
        /// テキストをエンコードするための <seealso cref="Encoding"/> を取得または設定します。
        /// </summary>
        /// <value>
        /// テキストをエンコードするための <seealso cref="Encoding"/> です。
        /// </value>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="value"/> が null 参照です。
        /// </exception>
        public virtual Encoding Encoding
        {
            get
            {
                return _encoding;
            }
            set
            {
                if (value == null)
                {
                    ArgumentNullException exception = new ArgumentNullException("Encoding");
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(string.Format(
                            Properties.Resources.E_NULL_PROPERTY_VALUE, "Encoding"), exception);
                    }
                    throw exception;
                }
                _encoding = value;
            }
        }

        /// <summary>
        /// <see cref="MultipartItemFactory"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <remarks>
        /// デフォルトコンストラクタです。
        /// </remarks>
        public MultipartItemFactory()
        {
        }

        /// <summary>
        /// <paramref name="partStream"/> を元にアップロードデータを <see cref="IMultipartItem"/> に作成します。
        /// </summary>
        /// <param name="partStream">アップロードされたデータ。</param>
        /// <returns>アップロードデータを保持している <see cref="IMultipartItem"/> クラスのインスタンス。</returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="partStream"/> が null 参照です。
        /// </exception>
        /// <exception cref="InvalidRequestException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="partStream"/> が読み込めません。
        /// </item>
        /// <item>
        /// ヘッダ情報に <seealso cref="CONTENT_DISPOSITION"/> が存在しません。
        /// </item>
        /// <item>
        /// ヘッダ情報にマルチパート要素名が存在しません。
        /// </item>
        /// <item>
        /// ヘッダ情報に <seealso cref="CONTENT_TYPE"/> が存在しません。
        /// </item>
        /// <item>
        /// ヘッダ情報の <seealso cref="CONTENT_TYPE"/> の値が不正です。
        /// </item>
        /// <item>
        /// ボディ部に終端文字列が存在しません。
        /// </item>
        /// <item>
        /// ヘッダ情報に <seealso cref="MultipartFileItem.FILENAME_CODE"/> が存在しません。
        /// </item>
        /// <item>
        /// ヘッダ情報の key と value が区切られていません。
        /// </item>
        /// <item>
        /// ヘッダ情報の key が重複しています。
        /// </item>
        /// </list>
        /// </exception>
        public virtual IMultipartItem CreateItem(Stream partStream)
        {
            string contentType = null;

            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 (!partStream.CanRead)
            {
                InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_NOT_READ_STREAM);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            // ヘッダ部を解析し、headerListを作成する。
            IDictionary<string, string> headerList = CreateHeaderList(partStream);

            if (!headerList.ContainsKey(CONTENT_DISPOSITION))
            {
                InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_NOT_FOUND_CONTENTDISPOSITION);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            if (!headerList.ContainsKey(CONTENT_TYPE))
            {
                InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_NOT_FOUND_CONTENTTYPE);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            IMultipartItem multipartItem = null;
            contentType = headerList[CONTENT_TYPE];

            if (IsText(contentType))
            {
                // テキストの場合
                multipartItem = new MultipartTextItem(this.Boundary, partStream, headerList, this.Encoding);
            }
            else
            {
                // ファイルの場合
                multipartItem = new MultipartFileItem(this.Boundary, partStream, headerList, this.Encoding);
            }
            return multipartItem;
        }

        /// <summary>
        /// ヘッダ情報のコレクションを作成します。
        /// </summary>
        /// <param name="partStream">アップロードされたデータ。</param>
        /// <returns>ヘッダ情報を格納した <see cref="IDictionary{String, String}"/>。</returns>
        /// <exception cref="InvalidRequestException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// ヘッダ情報の key と value が区切られていません。
        /// </item>
        /// <item>
        /// ヘッダ情報の key が重複しています。
        /// </item>
        /// </list>
        /// </exception>
        protected virtual IDictionary<string, string> CreateHeaderList(Stream partStream)
        {
            long positionStart = partStream.Position;

            StreamReader reader = new StreamReader(partStream, this.Encoding);
            IDictionary<string, string> headerList = new Dictionary<string, string>();

            while (true)
            {
                string line = reader.ReadLine();
                if (line.Length == 0)
                {
                    // ヘッダが終わった
                    break;
                }
                else
                {
                    KeyValuePair<string, string> header = CreateHeader(line);
                    if (headerList.ContainsKey(header.Key))
                    {
                        // ヘッダが重複したから、例外
                        InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_REPEATED_KEY);
                        if (_log.IsErrorEnabled)
                        {
                            _log.Error(exception.Message, exception);
                        }
                        throw exception;
                    }
                    positionStart = positionStart + line.Length + CRLF_CODE.Length;
                    headerList.Add(header);
                }
            }

            // positionをボディ部の始めまで進めます。
            partStream.Position = positionStart + CRLF_CODE.Length;
            return headerList;
        }

        /// <summary>
        /// ヘッダ情報をキー値ペアに格納します。
        /// </summary>
        /// <param name="headerText">アップロードされた情報です。</param>
        /// <returns>アップロードされた情報を分割してキー値ペアに格納したものです。</returns>
        /// <exception cref="InvalidRequestException">
        /// ヘッダ情報の key と value が区切られていません。
        /// </exception>
        protected virtual KeyValuePair<string, string> CreateHeader(string headerText)
        {
            int headerSeparate = headerText.IndexOf(':');
            if (headerSeparate == -1)
            {
                // key と value に区切られていなかった場合
                InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_INVALID_HEADER); 
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            string headerKey
                = headerText.Substring(0, headerSeparate).Trim().ToUpper();
            string headerValue
                = headerText.Substring(headerSeparate + 1, headerText.Length - 1 - headerSeparate).Trim();

            KeyValuePair<string, string> header
                = new KeyValuePair<string, string>(headerKey, headerValue);

            return header;
        }

        /// <summary>
        /// アップロードデータがテキストかどうかを判断します。
        /// </summary>
        /// <param name="contentType">Content-Type です。</param>
        /// <returns>アップロードデータがテキストかどうかを表すフラグ。</returns>
        /// <exception cref="InvalidRequestException">
        /// ヘッダ情報の <seealso cref="CONTENT_TYPE"/> の値が不正です。
        /// </exception>
        protected virtual bool IsText(string contentType)
        {
            bool result = false;
            if (string.Compare(UPLOADTYPE_TEXT, contentType, true) == 0)
            {
                result = true;
            }
            else if (string.Compare(UPLOADTYPE_FILE, contentType, true) == 0)
            {
                result = false;
            }
            else
            {
                // content-Typeの値が不正な場合
                InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_INVALID_CONTENTTYPE);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            return result;
        }
    }
}