// 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.Net;
using System.Text;
using TERASOLUNA.Fw.Common.Logging;

namespace TERASOLUNA.Fw.Client.Communication
{
    /// <summary>
    /// マルチパート送信機能を提供するクラスです。
    /// </summary>
    public class MultipartSender : SenderBase<IList<MultipartElement>>
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(MultipartSender));

        /// <summary>
        /// マルチパートデータの境界線を設定する際に使用する文字列です。
        /// </summary>
        /// <remarks>
        /// <para>この定数の値は "--{0}" です。</para>
        /// </remarks>
        protected static readonly string STARTLINE = "--{0}";

        /// <summary>
        /// 送信データの終了を設定する際に使用する文字列です。
        /// </summary>
        /// <remarks>
        /// <para>この定数の値は "--{0}--" です。</para>
        /// </remarks>
        protected static readonly string ENDLINE = "--{0}--";

        /// <summary>
        /// リクエストヘッダにContent-Typeを設定する際に使用する文字列です。
        /// </summary>
        /// <remarks>
        /// <para>この定数の値は "multipart/form-data; boundary={0}; charset={1}" です。</para>
        /// <para>変数部分に、区切り文字列を設定して使用します。</para>
        /// </remarks>
        protected static readonly string CONTENT_TYPE_MULTIPART = "multipart/form-data; boundary={0}; charset={1}";

        /// <summary>
        /// 区切り文字列が送信対象に存在しないことをチェックするかどうかのフラグです。
        /// </summary>
        /// <remarks>
        /// <para>デフォルトの値は <c>true</c> です。</para>
        /// </remarks>
        private bool _checkBoundary = true;

        /// <summary>
        /// アップロード対象のテキストをエンコードするためのEncodingです。
        /// </summary>
        /// <remarks>
        /// <para>デフォルトの値は "Encoding.UTF8" です。</para>
        /// </remarks>
        private Encoding _encoding = Encoding.UTF8;

        /// <summary>
        /// 区切り文字列が送信対象に存在しないことをチェックするかどうかのフラグを取得または設定します。
        /// </summary>
        /// <remarks>
        /// <para> <c>true</c> の時は チェックします。</para>
        /// <para> <c>false</c> の時は チェックしません。</para>
        /// <para>既定値は true です。</para>
        /// </remarks>
        public bool CheckBoundary
        {
            get
            {
                return _checkBoundary;
            }
            set
            {
                _checkBoundary = value;
            }
        }

        /// <summary>
        /// アップロード対象のテキストをエンコードするための <see cref="Encoding"/> を取得または設定します。
        /// </summary>
        /// <remarks>
        /// <para>既定値は UTF-8 形式です。</para>
        /// </remarks>
        public Encoding Encoding
        {
            get
            {
                return _encoding;
            }
            set
            {
                if (value == null)
                {
                    _encoding = Encoding.UTF8;
                }
                else
                {
                    _encoding = value;
                }
            }
        }

        /// <summary>
        /// 送信時に利用する Content-Type ヘッダの値を取得します。このプロパティは必ず <c>null</c> 参照を返します。
        /// </summary>
        /// <remarks>
        /// 常に <c>null</c> 参照を返します。<see cref="MultipartSender"/> は Content-Type ヘッダにバウンダリ文字列
        /// を含んだ文字列を <see cref="SendRequest"/> メソッドで作成して設定します。
        /// </remarks>
        public override string ContentType
        {
            get
            {
                return null;
            }
        }

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

        /// <summary>
        /// <paramref name="request"/> で渡された <see cref="HttpWebRequest"/> を用いて、
        /// <paramref name="elements"/> と <paramref name="headerCollection"/> の情報を
        /// 送信します。
        /// </summary>
        /// <remarks>
        /// <para>
        /// 送信される HTTP リクエストボディは <paramref name="elements"/> の <see cref="IList{MultipartElement}"/> 
        /// に格納された <see cref="MultipartElement"/> のインスタンスをそれぞれ HTTP ヘッダ、HTTP ボディの
        /// 組み合わせとしてシリアライズし、重複しない区切り(バウンダリ)文字列によって分割したものです。
        /// </para>
        /// <para>
        /// マルチパート形式については、 RFC 1867 に準拠します。
        /// </para>
        /// <para>
        /// HTTP リクエストヘッダには <paramref name="headerCollection"/> に格納されたキー/値の情報が全て追加されます。
        /// <paramref name="headerCollection"/> に格納するキー/値はそれぞれ HTTP リクエストヘッダのキー/値として利用
        /// 可能な形式を満足する必要があります。それらは規定は、RFC 2616 "request-header" で定義されています。
        /// </para>
        /// <para>
        /// <paramref name="reporter"/> が <c>null</c> 参照ではない場合、
        /// <see cref="SenderBase{TParam}.BufferSize"/> プロパティに設定されたバッファサイズ分のデータ（デフォルトは8 KB ）をサーバに送信する毎に、
        /// <see cref="IProgressChangeReporter.ReportProgressChanged"/> イベントを発生させます。
        /// </para>
        /// </remarks>
        /// <param name="request">送信処理を行うリクエストオブジェクト。</param>
        /// <param name="paramData">送信データを格納したオブジェクト。</param>
        /// <param name="headerCollection">送信時に HTTP ヘッダとして利用するヘッダ文字列のコレクション。</param>
        /// <param name="reporter">進行状況通知を行う <see cref="IProgressChangeReporter"/> インスタンス。 </param> 
        /// <exception cref="ArgumentException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="paramData"/> が要素を含んでいません。
        /// </item>
        /// <item>
        /// <paramref name="paramData"/> に含まれる <see cref="MultipartElement"/> 内で 
        /// <see cref="MultipartElement.Name"/> の値が重複しています。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="System.IO.FileNotFoundException">
        /// 通信処理で必要なファイルが見つかりません。
        /// </exception>
        /// <exception cref="WebException">
        /// 通信エラーが発生しました。または、リクエストがキャンセルされました。
        /// </exception>
        protected override void SendRequest(HttpWebRequest request,
                                            IList<MultipartElement> paramData,
                                            IDictionary<string, string> headerCollection,
                                            IProgressChangeReporter reporter)
        {

            if (paramData.Count == 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_ILIST, "paramData");
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            // マルチパート要素の名前の重複をチェック
            string[] tempList = new string[paramData.Count];
            int i = 0;
            foreach (MultipartElement element in paramData)
            {
                if (element == null)
                {
                    string message = string.Format(Properties.Resources.E_NULL_PARAMETER, typeof(MultipartElement).Name);
                    ArgumentException exception = new ArgumentException(message);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(message, exception);
                    }
                    throw exception;
                }

                if (Array.IndexOf<string>(tempList, element.Name) != -1)
                {

                    ArgumentException exception = new ArgumentException(
                        string.Format(Properties.Resources.E_MULTIPART_NAME_REPEATED, element.Name));
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }
                tempList[i++] = element.Name;
            }

            // バウンダリ生成
            string boundaryKey = CreateBoundary();
            if (CheckBoundary)
            {
                while (ExistsBoundaryKey(paramData, boundaryKey))
                {
                    boundaryKey = CreateBoundary();
                }
            }

            // Content-Typeにバウンダリキーを格納
            request.ContentType = string.Format(CONTENT_TYPE_MULTIPART, boundaryKey, this.Encoding.HeaderName);

            // バウンダリ文字列から、バイト列とそのサイズ取得
            string boundaryString = string.Format(STARTLINE, boundaryKey);
            byte[] boundaryBytes = this.Encoding.GetBytes(boundaryString);
            int boundaryLength = boundaryBytes.Length;

            // バウンダリ文字列を元に終端子を作成し、バイト列とそのサイズ取得
            string endBoundaryString = string.Format(ENDLINE, boundaryKey);
            byte[] endBoundaryBytes = this.Encoding.GetBytes(endBoundaryString);
            int endBoundaryBytesLength = endBoundaryBytes.Length;

            // 改行文字から、バイト列とそのサイズを取得
            byte[] newLineBytes = this.Encoding.GetBytes("\r\n");
            int newLineLength = newLineBytes.Length;

            // 送信時に利用するバッファサイズと、バッファ配列を作成
            int localBufferSize = BufferSize;
            byte[] buffer = new byte[localBufferSize];

            // 送信処理
            using (Stream reqStream = request.GetRequestStream())
            {

                // 送信情報のログ出力
                if (_log.IsTraceEnabled)
                {
                    // URL
                    _log.Trace(string.Format(
                        Properties.Resources.T_REQUEST_SEND_ADDRESS, request.Address));

                    // HTTPヘッダ
                    StringBuilder requestHeaders = new StringBuilder();
                    requestHeaders.AppendLine(Properties.Resources.T_REQUEST_SEND_HEADER);
                    foreach (string key in request.Headers.AllKeys)
                    {
                        requestHeaders.AppendLine(string.Format(
                        Properties.Resources.T_DICTIONARY_KEY_VALUE, key, request.Headers[key]));
                    }
                    _log.Trace(requestHeaders.ToString().Trim());

                    StringBuilder requestBody = new StringBuilder();
                    requestBody.AppendLine(Properties.Resources.T_REQUEST_SEND_BODY);
                    foreach(MultipartElement element in paramData)
                    {
                        if (element.GetType().Name.Equals(typeof(MultipartValueElement).Name))
                        {
                            MultipartValueElement valueElement = element as MultipartValueElement;
                            requestBody.AppendLine(string.Format(Properties.Resources.T_MULTIPART_VALUE, valueElement.Name, valueElement.Value));
                        }
                        else
                        {
                            MultipartFileElement fileElement = element as MultipartFileElement;
                            requestBody.AppendLine(string.Format(Properties.Resources.T_MULTIPART_FILE, fileElement.Name, fileElement.UploadFilePath));
                        }
                    }
                    _log.Trace(requestBody.ToString().Trim());
                }

                int elementsSize = paramData.Count;
                for (int current = 0; current < elementsSize; current++)
                {
                    int percentage = 0; // 全体の進捗
                    long sendSize = 0L; // 個々のマルチパートデータの送信済みのバイト数
                    long totalSize = 0L;

                    MultipartElement element = paramData[current];

                    // マルチパートデータのヘッダの準備
                    string headerString = element.CreateHeader();
                    byte[] headerBytes = this.Encoding.GetBytes(headerString);
                    long headerBytesLength = headerBytes.Length;

                    // 区切り文字列を送信
                    reqStream.Write(boundaryBytes, 0, boundaryLength);
                    reqStream.Write(newLineBytes, 0, newLineLength);

                    // マルチパートデータのヘッダを送信
                    reqStream.Write(headerBytes, 0, headerBytes.Length);
                    reqStream.Write(newLineBytes, 0, newLineLength);
                    reqStream.Write(newLineBytes, 0, newLineLength);

                    sendSize =
                        headerBytesLength + boundaryLength + newLineLength + newLineLength + newLineLength;

                    using (Stream streamBody = element.CreateBodyStream(this.Encoding))
                    {

                        // 総送信データ量はボディサイズに加えて、送ったヘッダサイズとする
                        totalSize = streamBody.Length + sendSize;

                        // 最後のマルチパートデータであれば、終端子サイズも計算に含める
                        if (current >= paramData.Count - 1)
                        {
                            totalSize += endBoundaryBytesLength + newLineLength + newLineLength;
                        }

                        int readSize = streamBody.Read(buffer, 0, localBufferSize);
                        while (readSize > 0)
                        {
                            reqStream.Write(buffer, 0, readSize);
                            sendSize += readSize;
                            if (reporter != null)
                            {
                                if (totalSize > 0)
                                {
                                    percentage =
                                        CalcSendPercentage(current, paramData.Count, sendSize, totalSize);
                                }
                                reporter.ReportProgressChanged(new ExecuteProgressChangedEventArgs(percentage));
                            }
                            readSize = streamBody.Read(buffer, 0, localBufferSize);
                        }
                    }

                    reqStream.Write(newLineBytes, 0, newLineLength);
                    sendSize += newLineLength;
                }

                // 全てのマルチパートデータを送った後、終端子を出力
                reqStream.Write(endBoundaryBytes, 0, endBoundaryBytesLength);
                reqStream.Write(newLineBytes, 0, newLineLength);

                if (reporter != null)
                {
                    reporter.ReportProgressChanged(new ExecuteProgressChangedEventArgs(50));
                }
            }
        }

        /// <summary>
        /// マルチパートデータ送信状況の進捗率を計算します。
        /// </summary>
        /// <remarks>
        /// <para>マルチパートデータのアップロード処理の進捗率です。</para>
        /// <para>送信処理が全て完了すると、進捗率は 50% になります。</para>
        /// <para><paramref name="current"/> の値が 2147483 を超えることがないようにしてください。 また、
        /// <paramref name="max"/> の値が 1073741824 を超えることがないようにしてください。</para>
        /// </remarks>
        /// <param name="current">何件目のマルチパートデータかを表します。(※ 0 番目からスタート)</param>
        /// <param name="max">送信するマルチパートデータの件数です。</param>
        /// <param name="sendDataSize">送信しているマルチパートデータの送信済みのバイト数です。</param>
        /// <param name="length">送信しているマルチパートデータの総バイト数です。</param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="max"/> または <paramref name="length"/> が 0 です。
        /// </exception>
        /// <returns>
        /// 送信処理の進捗率。
        /// </returns>
        /// <exception cref="OverflowException">
        /// 送信サイズのパーセンテージの計算でオーバーフローが発生しました。
        /// </exception>
        protected virtual int CalcSendPercentage(int current, int max, long sendDataSize, long length)
        {
            if (max == 0 || length == 0)
            {
                ArgumentOutOfRangeException exception = new ArgumentOutOfRangeException();
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            int percentage = 0;
            checked
            {
                percentage = (int)(sendDataSize * 100L / length);
                percentage = (percentage + 100 * current) / (max * 2);
            }
            return percentage;
        }

        /// <summary>
        /// <paramref name="boundaryKey"/> が <paramref name="elements"/> のマルチパートデータ内に存在するか
        /// どうかを確認します。
        /// </summary>
        /// <remarks>
        /// <para><paramref name="elements"/> 内の全てのマルチパートデータのヘッダ、ボディをチェックします。
        /// </para>
        /// <para>存在する場合には true 、存在しない場合には false を返します。</para>
        /// </remarks>
        /// <param name="elements"><see cref="MultipartElement"/> を格納したリストです。</param>
        /// <param name="boundaryKey">区切り文字列です。</param>
        /// <exception cref="ArgumentNullException">
        /// <para><paramref name="boundaryKey"/> が <c>null</c> 参照です。</para>
        /// <para><paramref name="elements"/> が <c>null</c> 参照です。</para>
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <para><paramref name="boundaryKey"/> が空文字列です。</para>
        /// <para><paramref name="elements"/> が 0件です。</para>
        /// </exception>
        /// <returns><paramref name="boundaryKey"/> が <paramref name="elements"/> のマルチパートデータ内に
        /// 存在する場合に true 、存在しない場合には false 。
        /// </returns>
        protected virtual bool ExistsBoundaryKey(IList<MultipartElement> elements, string boundaryKey)
        {
            byte[] boundaryBytes = this.Encoding.GetBytes(boundaryKey);
            bool result = false;

            foreach (MultipartElement m in elements)
            {
                // ヘッダ内を確認します。
                string multipartHeader = m.CreateHeader();
                if (multipartHeader.Contains(boundaryKey))
                {
                    result = true;
                    break;
                }
                else
                {
                    using (Stream multipartBodyStream = m.CreateBodyStream(this.Encoding))
                    {
                        if (ExistsBytesFromStream(multipartBodyStream, boundaryBytes))
                        {
                            result = true;
                            break;
                        }
                    }
                }
            }
            return result;
        }


        /// <summary>
        /// <paramref name="stream"/> で与えられたストリームから、<paramref name="target"/> 
        /// と同等のバイト列が読みだされるかを確認します。
        /// </summary>
        /// <remarks>
        /// <para>
        /// このメソッドはマルチパートデータ内に区切り文字列が含まれるかどうかを確認するために用いられる
        /// ヘルパーメソッドです。
        /// </para>
        /// <para>
        /// <paramref name="stream"/>はシーク可能なストリームである必要があります。
        /// </para>
        /// </remarks>
        /// <param name="stream">検索対象のストリーム。</param>
        /// <param name="target">検索するバイト列。</param>
        /// <returns><paramref name="stream"/> から <paramref name="target"/> と同等のバイト列が読み出された
        /// 場合には true 、ストリームの最後まで読み出されなければ false。</returns>
        protected static bool ExistsBytesFromStream(Stream stream, byte[] target)
        {
            if (stream == null)
            {
                return false;
            }

            if (target.Length <= 0 || stream.Length < target.Length)
            {
                return false;
            }

            bool[] checkFlags = new bool[target.Length + 1];
            for (int i = 0; i < checkFlags.Length; i++)
            {
                checkFlags[i] = false;
            }

            checkFlags[0] = true;
            bool result = false;

            byte[] buf = new byte[1];
            int nextTargetPosition = 0;
            // 1 byteずつ取得
            while (stream.Read(buf, 0, 1) > 0)
            {
                int targetPosition = nextTargetPosition;
                nextTargetPosition = 0;
                for (int searchPosition = targetPosition; 0 <= searchPosition; searchPosition--)
                {
                    if (checkFlags[searchPosition])
                    {
                        // 前の値が一致した場合
                        if (buf[0] == target[searchPosition])
                        {
                            // 値が一致した場合、次の値を検査対象とする
                            checkFlags[searchPosition + 1] = true;
                            if (nextTargetPosition == 0)
                            {
                                // 検査対象カウンタを1つ上げる
                                nextTargetPosition = searchPosition + 1;
                            }
                        }
                        else
                        {
                            // 値が一致しない場合、次の値からの検査をやめる
                            checkFlags[searchPosition + 1] = false;
                        }
                    }
                    else
                    {
                        // 検査対象外の場合、次の値も検査対象から外す
                        checkFlags[searchPosition + 1] = false;
                    }
                }

                // 最後の値まで到達したら、ループを抜ける
                if (nextTargetPosition == target.Length)
                {
                    result = true;
                    break;
                }
            }
            return result;
        }

        /// <summary>区切り文字列を生成します。</summary>
        /// <remarks>
        /// グローバル一意識別子(GUID)を用いて、36文字のランダムな文字列を返します。
        /// </remarks>
        /// <returns>
        /// 区切り文字列として使用する文字列を返す。
        /// </returns>
        protected virtual string CreateBoundary()
        {
            // グローバル一意識別子(GUID)を用いて作成します。
            return System.Guid.NewGuid().ToString();
        }
    }
}
