// 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 MultipartUpload
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(MultipartUpload));

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

        /// <summary>
        /// Content-Type ヘッダの値の一部として利用する文字列です。
        /// </summary>
        /// <remarks>
        /// <para>定数の値は "MULTIPART/FORM-DATA" です。</para>
        /// </remarks>
        protected static readonly string CONTENT_TYPE_VALUE = "MULTIPART/FORM-DATA";

        /// <summary>
        /// boundary を取得する際、キーとして利用する文字列です。
        /// </summary>
        /// <remarks>
        /// <para>定数の値は "BOUNDARY" です。</para>
        /// </remarks>
        protected static readonly string BOUNDARY_KEY = "BOUNDARY";

        /// <summary>
        /// charset を取得する際、キーとして利用する文字列です。
        /// </summary>
        /// <remarks>
        /// <para>定数の値は "CHARSET" です。</para>
        /// </remarks>
        protected static readonly string CHARSET_KEY = "CHARSET";

        /// <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>
        /// <see cref="System.Web.HttpContext"/> クラスをラップして保持するための <see cref="HttpContextWrapper"/> クラスです。
        /// </summary>
        private HttpContextWrapper _httpContextWrapper = null;

        /// <summary>
        /// <see cref="IMultipartItem"/> クラスを作成するための <see cref="IMultipartItemFactory"/> クラスです。
        /// </summary>
        private IMultipartItemFactory _factory = null;

        /// <summary>
        /// <see cref="System.Web.HttpContext"/> クラスをラップして保持するためのクラスを取得します。
        /// </summary>
        /// <value>
        /// <see cref="System.Web.HttpContext"/> クラスをラップして保持するための <see cref="HttpContextWrapper"/> 。
        /// </value>
        public HttpContextWrapper HttpContextWrapper
        {
            get
            {
                return _httpContextWrapper;
            }
        }

        /// <summary>
        /// <see cref="IMultipartItem"/> を作成するためのクラスを取得します。
        /// </summary>
        /// <value>
        /// <see cref="IMultipartItem"/> を作成するための <see cref="IMultipartItemFactory"/> 。
        /// </value>
        public virtual IMultipartItemFactory Factory
        {
            get
            { 
                return _factory; 
            }
        }

        /// <summary>
        /// コンストラクタです。
        /// </summary>
        /// <param name="httpContextWrapper"><seealso cref="System.Web.HttpContext"/> クラスをラップして保持するための <see cref="HttpContextWrapper"/> クラスのインスタンス。</param>
        /// <param name="factory"><see cref="IMultipartItem"/> を作成する <see cref="IMultipartItemFactory"/> 実装クラスのインスタンス。</param>
        /// <exception cref="ArgumentNullException">
        /// <list type="bullet">
        /// <item>
        /// <paramref name="httpContextWrapper"/> が null 参照です。
        /// </item>
        /// <item>
        /// <paramref name="factory"/> が null 参照です。
        /// </item>
        /// </list>
        /// </exception>
        public MultipartUpload(HttpContextWrapper httpContextWrapper, IMultipartItemFactory factory)
        {
            if (httpContextWrapper == null)
            {
                ArgumentNullException exception = new ArgumentNullException("httpContextWrapper");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "httpContextWrapper"), exception);
                }
                throw exception;
            }

            if (factory == null)
            {
                ArgumentNullException exception = new ArgumentNullException("factory");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "factory"), exception);
                }
                throw exception;
            }

            _httpContextWrapper = httpContextWrapper;
            _factory = factory;
        }

        /// <summary>
        /// 受け取ったデータを <see cref="IMultipartItem"/> に格納し、格納された <see cref="IMultipartItem"/> を配列に格納して返します。
        /// </summary>
        /// <returns>受け取ったデータを保持した <see cref="IMultipartItem"/> を格納した <see cref="IDictionary{String, IMultipartItem}"/> 実装クラスのインスタンス。</returns>
        /// <exception cref="InvalidOperationException">
        /// <list type="bullet">
        /// <item>
        /// <seealso cref="MultipartUpload.HttpContextWrapper"/> の <seealso cref="Controller.HttpContextWrapper.RequestStream"/> が null 参照です。
        /// </item>
        /// <item>
        /// <seealso cref="MultipartUpload.HttpContextWrapper"/> の <seealso cref="Controller.HttpContextWrapper.ContentType"/> が null 参照です。
        /// </item>
        /// <item>
        /// <seealso cref="MultipartUpload.HttpContextWrapper"/> の <seealso cref="Controller.HttpContextWrapper.ContentType"/> が空文字です。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="InvalidRequestException">
        /// <list type="bullet">
        /// <item>
        /// ヘッダ情報の key が重複しています。
        /// </item>
        /// <item>
        /// ヘッダ情報の <seealso cref="CONTENT_TYPE"/> に <seealso cref="CONTENT_TYPE_VALUE"/> が存在しません。
        /// </item>
        /// <item>
        /// ヘッダ情報の <seealso cref="CONTENT_TYPE"/> に <seealso cref="BOUNDARY_KEY"/> が存在しません。
        /// </item>
        /// <item>
        /// ヘッダ情報の <seealso cref="CONTENT_TYPE"/> に <seealso cref="CHARSET_KEY"/> が存在しません。
        /// </item>
        /// <item>
        /// ボディ部に終端文字列が存在しません。
        /// </item>
        /// </list>
        /// </exception>
        public virtual IDictionary<string, IMultipartItem> CreateMultipartItems()
        {
            if (_httpContextWrapper.RequestStream == null)
            {
                InvalidOperationException exception = new InvalidOperationException(Properties.Resources.E_INVALID_UPLOADDATA);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            if (_httpContextWrapper.ContentType == null)
            {
                InvalidOperationException exception = new InvalidOperationException(Properties.Resources.E_NOT_FOUND_CONTENTTYPE);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            if (_httpContextWrapper.ContentType.Length == 0)
            {
                InvalidOperationException exception = new InvalidOperationException(Properties.Resources.E_EMPTY_STRING);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            IDictionary<string, string> contentTypeValues =
                GetContentTypeValues(this.HttpContextWrapper.ContentType);

            // ヘッダ部からboundary文字列を取得する。
            if (!contentTypeValues.ContainsKey(BOUNDARY_KEY))
            {
                // boundaryが存在しなかった場合
                InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_NOT_FOUND_BOUNDARY);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            string boundary = contentTypeValues[BOUNDARY_KEY];

            // ヘッダ部からencodingを取得し、encodingオブジェクトを生成する。
            Encoding encoding = null;
            if (contentTypeValues.ContainsKey(CHARSET_KEY))
            {
                // ヘッダ部からencodingを取得し、encodingオブジェクトを生成する。
                string charset = contentTypeValues[CHARSET_KEY];

                try
                {
                    encoding = Encoding.GetEncoding(charset);
                }
                catch (ArgumentException ex)
                {
                    // ヘッダ情報のencodingが不正だった場合
                    InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_INVALID_CHARSET, ex);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }
            }
            else
            {
                // ヘッダ部にcharsetが存在しなかった場合は、"UTF8"を使用する。
                encoding = Encoding.UTF8;
            }

            IDictionary<string, IMultipartItem> items = new Dictionary<string, IMultipartItem>();
            // 終端子が見つかるまでループする
            while (!IsTerminated(boundary, this.HttpContextWrapper.RequestStream))
            {
                this.Factory.Boundary = boundary;
                this.Factory.Encoding = encoding;

                IMultipartItem multipartItem = Factory.CreateItem(this.HttpContextWrapper.RequestStream);
                if (items.ContainsKey(multipartItem.Name))
                {
                    // マルチパート要素名が重複している場合
                    InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_REPEATED_KEY); 
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }
                items.Add(multipartItem.Name, multipartItem);
            }
            return items;
        }

        /// <summary>
        /// アップロードデータの Content-Type の詳細な属性を配列に格納します。
        /// </summary>
        /// <param name="contentType"> Content-Type 。</param>
        /// <returns>Content-Type の値を分割して配列に格納したものです。</returns>
        /// <exception cref="InvalidRequestException">
        /// <list type="bullet">
        /// <item>
        /// ヘッダ情報の key が重複しています。
        /// </item>
        /// <item>
        /// ヘッダ情報の <seealso cref="CONTENT_TYPE"/> に <seealso cref="CONTENT_TYPE_VALUE"/> が存在しません。
        /// </item>
        /// </list>
        /// </exception>
        protected virtual IDictionary<string, string> GetContentTypeValues(string contentType)
        {
            string[] separetedValues = contentType.Split(';');
            IDictionary<string, string> list = new Dictionary<string, string>();

            foreach (string separatedValue in separetedValues)
            {
                int index = separatedValue.IndexOf('=');
                if (index == -1)
                {
                    if (!separatedValue.Trim().ToUpper().Equals(CONTENT_TYPE_VALUE))
                    {
                        InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_NOT_FOUND_MULTIPART);
                        if (_log.IsErrorEnabled)
                        {
                            _log.Error(exception.Message, exception);
                        }
                        throw exception;
                    }
                }
                else
                {
                    string key =
                        separatedValue.Substring(0, index).Trim().ToUpper();
                    string value =
                        separatedValue.Substring(index + 1, separatedValue.Length - 1 - index).Trim();
                    if (list.ContainsKey(key))
                    {
                        // keyが重複している場合
                        InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_REPEATED_KEY);
                        if (_log.IsErrorEnabled)
                        {
                            _log.Error(exception.Message, exception);
                        }
                        throw exception;
                    }
                    else
                    {
                        list.Add(key, value);
                    }
                }
            }
            return list;
        }

        /// <summary>
        /// 終端子が見つかったかどうかを判断します。
        /// </summary>
        /// <param name="boundary">アップロードデータ区切り文字列。</param>
        /// <param name="stream"><see cref="Stream"/> 型のアップロードデータ。</param>
        /// <returns>次の行に終端子が見つかったかどうかを表すフラグ。</returns>
        /// <exception cref="InvalidRequestException">
        /// ボディ部に終端文字列が存在しません。
        /// </exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope")]
        protected virtual bool IsTerminated(string boundary, Stream stream)
        {
            bool result = false;
            string boundaryString = SEPARATE_CODE + boundary;
            string terminalString = SEPARATE_CODE + boundary + SEPARATE_CODE;
            long positionStart = stream.Position;
            
            StreamReader reader = new StreamReader(stream);
            string line = null;

            while (true)
            {
                line = reader.ReadLine();

                // 次のアップロードデータが見つかった場合
                if (line == boundaryString)
                {
                    positionStart += line.Length + CRLF_CODE.Length;
                    result = false;
                    break;
                }
                // 終端子が見つかった場合
                else if (line == terminalString)
                {
                    result = true;
                    break;
                }
                // 最後まで次のアップロードデータも終端子も見つからなかった場合
                else if (line == null)
                {
                    InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_NOT_FOUND_ENDCODE);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }
                else
                {
                    positionStart += line.Length + CRLF_CODE.Length;
                }
            }
            if (!result)
            {
                stream.Position = positionStart;
            }
            return result;
        }
    }
}
