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

        /// <summary>
        /// ファイルパスを含むファイル名を取得する際、キーとして利用する文字列です。
        /// </summary>
        /// <remarks>
        /// <para>定数の値は "FILENAME" です。</para>
        /// </remarks>
        protected static readonly string FILENAME_CODE = "FILENAME";

        /// <summary>
        /// ファイルパスを含むファイル名です。
        /// </summary>
        /// <value>
        /// ファイルパスを含むファイル名。
        /// </value>
        private string _filename = null;

        /// <summary>
        /// すでに <seealso cref="Dispose(bool)"/> されているかどうかを表します。
        /// </summary>
        private bool _disposed = false;

        /// <summary>
        /// ファイルパスを含む一時ファイル名です。
        /// </summary>
        private string _temporaryFilePath = null;

        /// <summary>
        /// アップロードデータを<seealso cref="Stream"/>型にて保持します。
        /// </summary>
        private Stream _outputStream = null;

        /// <summary>
        /// 一回に処理するデータ量です。
        /// </summary>
        private long _bufferSize = 1024 * 8;

        /// <summary>
        /// ファイルパスを含むファイル名を取得します。
        /// </summary>
        /// <value>
        /// ファイルパスを含むファイル名。
        /// </value>
        public override string Filename
        {
            get
            {
                return _filename;
            }
        }

        /// <summary>
        /// ファイルパスを含む一時ファイル名を取得します。
        /// </summary>
        /// <value>
        /// ファイルパスを含む一時ファイル名。
        /// </value>
        protected virtual string TemporaryFilePath
        {
            get
            {
                return _temporaryFilePath;
            }
        }

        /// <summary>
        /// アップロードデータがテキストかどうかを取得します。
        /// </summary>
        /// <value>
        /// アップロードデータがテキストデータかどうかのフラグ。
        /// </value>
        /// <remarks>
        /// 必ず false です。
        /// </remarks>
        public override bool IsText
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// アップロードデータを <seealso cref="Stream"/> 型にて取得します。
        /// </summary>
        /// <value>
        /// コンストラクタで設定された値。
        /// </value>
        /// <exception cref="ObjectDisposedException">
        /// ストリームはすでに破棄されています。
        /// </exception>
        public override Stream OutputStream
        {
            get
            {
                if (_disposed)
                {
                    ObjectDisposedException exception = new ObjectDisposedException("MultipartFileItem");
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }
                return _outputStream;
            }
        }

        /// <summary>
        /// リソースが解放済みかどうかを取得または設定します。
        /// </summary>
        /// <value>
        /// リソースが解放済みかどうかを表します。
        /// </value>
        protected virtual bool Disposed
        {
            get
            {
                return _disposed;
            }
            set
            {
                _disposed = value;
            }
        }

        /// <summary>
        /// <see cref="MultipartFileItem"/> クラスの新しいインスタンスを初期化します
        /// </summary>
        /// <param name="boundary">アップロードデータを区切るための文字列。</param>
        /// <param name="partStream">アップロードされたデータ。</param>
        /// <param name="headerList">アップロードデータのヘッダ情報。</param>
        /// <param name="encoding">テキストをエンコードするための <seealso cref="Encoding"/> 。</param>
        /// <exception cref="InvalidRequestException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// ボディ部に終端文字列が存在しません。
        /// </item>
        /// <item>
        /// ヘッダ情報に content-disposition が存在しません。
        /// </item>
        /// <item>
        /// ヘッダ情報の key が重複しています。
        /// </item>
        /// <item>
        /// ヘッダ情報に <seealso cref="FILENAME_CODE"/> が存在しません。
        /// </item>
        /// </list>
        /// </exception>
        public MultipartFileItem(string boundary, 
                                 Stream partStream, 
                                 IDictionary<string, string> headerList, 
                                 Encoding encoding) 
            : base(boundary, partStream, headerList, encoding)
        {
            // ボディ部の終わり(区切り文字列)を見つける。
            FindBody(boundary, partStream);

            // 一時ファイルにデータを書き込む。
            _temporaryFilePath = Path.GetTempFileName();
            _outputStream = new FileStream(_temporaryFilePath, FileMode.Open, FileAccess.ReadWrite);
            WriteToOutputStream(partStream);

            _filename = GetFilename();
        }

        /// <summary>
        /// デストラクタです。
        /// </summary>
        ~MultipartFileItem()
        {
            this.Dispose(false);
        }

        /// <summary>
        /// リソースを解放します。
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly")]
        public override void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// リソースを解放します。
        /// </summary>
        /// <param name="disposing"><see cref="Stream"/> の解放を行うかどうかを表します。</param>
        protected virtual void Dispose(bool disposing)
        {
            lock (this)
            {
                if (!_disposed)
                {
                    if (_outputStream != null)
                    {
                        _outputStream.Close();
                        _outputStream = null;
                    }

                    if (_temporaryFilePath != null)
                    {
                        File.Delete(_temporaryFilePath);
                    }
                }
                _disposed = true;
            }
        }

        /// <summary>
        /// 一時ファイルにデータを書き込む。
        /// </summary>
        /// <param name="partStream">アップロードデータの <see cref="Stream"/> クラスのインスタンス。</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")]
        protected void WriteToOutputStream(Stream partStream)
        {
            long length = PositionEnd - PositionStart;
            byte[] buffer = new byte[_bufferSize];

            partStream.Position = PositionStart;

            while (length > 0)
            {
                long size = Math.Min(_bufferSize, length);
                partStream.Read(buffer, 0, (int)size);
                _outputStream.Write(buffer, 0, (int)size);
                length -= size;
            }
            _outputStream.Position = 0;
            // 次の区切り文字までPositionを進める。
            partStream.Position += CRLF_CODE.Length;
        }

        /// <summary>
        /// ヘッダ情報より filename 属性の値を取得します。
        /// </summary>
        /// <returns>ファイルパスを含むファイル名。</returns>
        /// <exception cref="InvalidRequestException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// ヘッダ情報に content-disposition が存在しません。
        /// </item>
        /// <item>
        /// ヘッダ情報の key が重複しています。
        /// </item>
        /// <item>
        /// ヘッダ情報に <seealso cref="FILENAME_CODE"/> が存在しません。
        /// </item>
        /// </list>
        /// </exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        protected string GetFilename()
        {
            IDictionary<string, string> separatedList = SeparateHeader();

            if (separatedList.ContainsKey(FILENAME_CODE))
            {
                return separatedList[FILENAME_CODE];
            }
            else
            {
                // ヘッダ情報にfilenameがなかった場合
                InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_NOT_FOUND_FILENAME);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
        }
    }
}
