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

namespace TERASOLUNA.Fw.Client.Communication
{
    /// <summary>
    /// データ送信機能を提供する <see cref="IHttpSender{TPram}"/> 実装の基底クラスです。
    /// </summary>
    /// <remarks>
    /// <para>
    /// このクラスは抽象クラスです。基本的な処理フローと定数、リクエストの初期化ロジックを提供します。
    /// 派生クラスを作成する場合、<typeparamref name="TParam"/>で送信するデータの型を指定します。
    /// </para>
    /// </remarks>
    /// <typeparam name="TParam">
    /// 送信処理に渡すパラメータの型。
    /// </typeparam>
    public abstract class SenderBase<TParam> : IHttpSender<TParam> where TParam : class
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(SenderBase<TParam>));

        /// <summary>
        /// 一回に送受信するデータ量(byte)のデフォルト値です。
        /// </summary>
        /// <remarks>定数の値は 8192 です。</remarks>
        protected const int BUFFER_SIZE = 1024 * 8;

        /// <summary>
        /// HTTP リクエストの POST メソッドを指定する際に利用する文字列です。
        /// </summary>
        /// <remarks>定数の値は "POST" です。</remarks>
        protected static readonly string POST_METHOD_NAME = "POST";

        /// <summary>
        /// HTTP ヘッダに Content-Disposition ヘッダを設定する際、キーとして利用する文字列です。
        /// </summary>
        /// <remarks>
        /// <para>定数の値は "Content-Disposition" です。</para>
        /// </remarks>
        protected static readonly string CONTENT_DISPOSITION = "Content-Disposition";

        /// <summary>
        /// 一回に送受信するデータ量(byte)です。
        /// </summary>
        private int _bufferSize = BUFFER_SIZE;

        /// <summary>
        /// HTTP リクエストに指定する Content-Type ヘッダの値を取得します。
        /// </summary>
        public abstract string ContentType
        {
            get;
        }

        /// <summary>
        /// HTTP リクエストに指定する Method ヘッダの値を取得します。
        /// </summary>
        /// <remarks>
        /// このプロパティは通常 "POST" を返します。
        /// </remarks>
        public virtual string Method
        {
            get
            {
                return POST_METHOD_NAME;
            }
        }

        /// <summary>
        /// 一回に送受信するデータ量( byte )を表す値を取得または設定します。
        /// </summary>
        /// <remarks>
        /// 0 以下の値を設定した場合、 <see cref="SenderBase{TParam}.BUFFER_SIZE"/> の値が利用されます。
        /// </remarks>
        public int BufferSize
        {
            get
            {
                return _bufferSize;
            }
            set
            {
                if (value <= 0)
                {
                    _bufferSize = BUFFER_SIZE;
                }
                else
                {
                    _bufferSize = value;
                }
            }
        }

        /// <summary>
        /// <paramref name="request"/> で渡された <see cref="HttpWebRequest"/> を用いて、
        /// <paramref name="paramData"/> と <paramref name="headerCollection"/> の情報を
        /// 送信します。
        /// </summary>
        /// <remarks>
        /// <para>
        /// このメソッドは <see cref="SenderBase{TParam}.PrepareRequest"/> を用いてリクエストオブジェクトを準備します。
        /// その後、 <see cref="SenderBase{TParam}.SendRequest"/> を呼び出して送信処理を行います。
        /// </para>
        /// <para>
        /// 通常、このメソッドを直接オーバーライドすることは避けてください。 <see cref="Send"/> メソッドは
        /// <see cref="PrepareRequest"/> メソッドでリクエストを初期化した後、<see cref="SendRequest"/> 抽象
        /// メソッドへと処理を委譲します。この動作に問題がなければ、 <see cref="SendRequest"/> メソッドで処
        /// 理の本体を記述します。
        /// </para>
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="request"/> , <paramref name="paramData"/> , <paramref name="headerCollection"/> のいずれか
        /// が <c>null</c> 参照です。
        /// </exception>
        /// <exception cref="ArgumentException">
        /// 通信処理で必要なデータが不正です。
        /// </exception>
        /// <exception cref="WebException">
        /// 通信エラーが発生しました。または、リクエストがキャンセルされました。
        /// </exception>
        /// <exception cref="CommunicationException">
        /// 送信中に内部処理でエラーが発生しました。
        /// </exception>
        /// <param name="request">送信処理を行うリクエストオブジェクト。</param>
        /// <param name="paramData">送信データを格納したオブジェクト。</param>
        /// <param name="headerCollection">送信時に HTTP ヘッダとして利用するヘッダ文字列のコレクション。</param>
        /// <param name="reporter">進行状況通知を行う <see cref="IProgressChangeReporter"/> インスタンス。</param>
        public virtual void Send(HttpWebRequest request,
                                 TParam paramData,
                                 IDictionary<string, string> headerCollection,
                                 IProgressChangeReporter reporter)
        {
            // 入力チェック
            if (request == null)
            {
                ArgumentNullException exception = new ArgumentNullException("request");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(Properties.Resources.E_NULL_ARGUMENT, "request"), exception);
                }
                throw exception;

            }

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

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

            // リクエストデータを編集
            PrepareRequest(request, headerCollection);

            // 送信処理実行
            SendRequest(request, paramData, headerCollection, reporter);
        }


        /// <summary>
        /// <paramref name="request"/> で渡された <see cref="HttpWebRequest"/> を用いて、
        /// <paramref name="paramData"/> と <paramref name="headerCollection"/> の情報を
        /// 送信します。
        /// </summary>
        /// <remarks>
        /// <para>通信処理で発生した <see cref="WebException"/> はキャッチせずにスローします。また、内部処理で例外が
        /// 発生した場合、通常は <see cref="CommunicationException"/> にラップしてスローするようにします。コーディング
        /// ミスによる <see cref="ArgumentNullException"/> や <see cref="ArgumentException"/> 、ファイルシステムや設定
        /// ファイルの異常による例外をハンドリングする必要はありません。</para>
        /// <para>
        /// このメソッドを実装する際、内部で <paramref name="request"/> の <see cref="HttpWebRequest.GetRequestStream"/>
        /// メソッドを利用して書き込みストリームの取得を行います。 <see cref="HttpWebRequest.BeginGetRequestStream"/> 
        /// メソッドを利用して非同期書き込みを行うことは避けてください。非同期書き込み処理を実装する場合には、読み込み
        /// 処理も <see cref="HttpWebRequest.BeginGetResponse"/> を用いた非同期読み込みとする必要があります。
        /// </para>
        /// </remarks>
        /// <param name="request">送信処理を行うリクエストオブジェクト。</param>
        /// <param name="paramData">送信データを格納したオブジェクト。</param>
        /// <param name="headerCollection">送信時に HTTP ヘッダとして利用するヘッダ文字列のコレクション。</param>
        /// <param name="reporter">進行状況通知を行う <see cref="IProgressChangeReporter"/> インスタンス。</param>
        protected abstract void SendRequest(HttpWebRequest request,
                                            TParam paramData,
                                            IDictionary<string, string> headerCollection,
                                            IProgressChangeReporter reporter);

        /// <summary>
        /// データ送信時に利用する <see cref="HttpWebRequest"/> インスタンスの設定を行います。
        /// </summary>
        /// <remarks>
        /// <para> HTTP メソッド名と Content-Type を設定します。その他、<paramref name="requestHeaders"/>に
        /// 格納されたヘッダ文字列をリクエストヘッダとして追加します。</para>
        /// <para> <paramref name="requestHeaders"/> に "Method" キーまたは "Content-Type" キーが存在した場合でも、
        ///  <see cref="SenderBase{TParam}.ContentType"/> プロパティまたは <see cref="SenderBase{TParam}.Method"/> 
        /// プロパティが値を返す場合には、プロパティの値が優先されます。</para>
        /// </remarks>
        /// <param name="request">送信処理に利用する <see cref="HttpWebRequest"/> インスタンス。</param>
        /// <param name="requestHeaders">送信処理に利用するヘッダー文字列のコレクション。</param>
        protected virtual void PrepareRequest(WebRequest request, IDictionary<string, string> requestHeaders)
        {
            // メソッドとコンテントタイプを設定
            request.Method = Method;
            request.ContentType = ContentType;

            // リクエストヘッダの設定
            foreach (string key in requestHeaders.Keys)
            {
                request.Headers.Add(key, requestHeaders[key]);
            }
        }

        /// <summary>
        /// 送信サイズのパーセンテージを計算する。
        /// </summary>
        /// <param name="sendSize">送信サイズ</param>
        /// <param name="totalSize">合計サイズ</param>
        /// <returns>パーセンテージ</returns>
        /// <exception cref="OverflowException">
        /// 送信サイズのパーセンテージの計算でオーバーフローが発生しました。
        /// </exception>
        protected virtual int CalcSendPercentage(long sendSize, long totalSize)
        {
            int percentage = 0;
            checked
            {
                percentage = (int)(sendSize * 50 / totalSize);
            }
            return percentage;
        }
    }
}
