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

namespace TERASOLUNA.Fw.Client.Communication
{
    /// <summary>
    /// 通信機能を提供する基底クラスです。
    /// </summary>
    /// <remarks>
    /// <see cref="IHttpSender{TParam}"/>、<see cref="IHttpReceiver"/> を用いた通信処理の基本的な機能を提供します。
    /// 通信を行うために利用する <see cref="IHttpSender{TParam}"/>、<see cref="IHttpReceiver"/> が進行状況を通知する
    /// ために用いる <see cref="IProgressChangeReporter"/> を実装しています。
    /// </remarks>
    /// <typeparam name="TParam">サーバに送信するデータオブジェクトの型。</typeparam>
    public abstract class CommunicatorBase<TParam> :
            ICommunicator<TParam>, IProgressChangeReporter
        where TParam : class
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(CommunicatorBase<TParam>));

        /// <summary>
        /// 通信先の URL をアプリケーション構成ファイルから取得するためのキーとなる文字列です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "BaseUrl" です。
        /// </remarks>
        protected static readonly string BASE_URL = "BaseUrl";

        /// <summary>
        /// URI のスキーム名です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "http" です。
        /// </remarks>
        protected static readonly string HTTP_SCHEME = "http";

        /// <summary>
        /// URI のスキーム名です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "https" です。
        /// </remarks>
        protected static readonly string HTTPS_SCHEME = "https";

        /// <summary>
        /// 同期用のオブジェクトです。
        /// </summary>
        private object _syncRoot = new object();

        /// <summary>
        /// 通信に用いるリクエストオブジェクトです。
        /// </summary>
        private HttpWebRequest _request = null;

        /// <summary>
        /// 接続先 URL 文字列です。
        /// </summary>
        private string _address = null;

        /// <summary>
        /// リクエストタイムアウト時間(ミリ秒)です。
        /// </summary>
        /// <remarks>
        /// 既定値は100,000(100秒)です。
        /// </remarks>
        private int _requestTimeout = 100000;

        /// <summary>
        /// 送信処理に用いる <see cref="IHttpSender{TParam}"/> 実装クラスのインスタンスです。
        /// </summary>
        private IHttpSender<TParam> _sender = null;

        /// <summary>
        /// 受信処理に用いる <see cref="IHttpReceiver"/> 実装クラスのインスタンスです。
        /// </summary>
        private IHttpReceiver _receiver = null;

        /// <summary>
        /// 通信がキャンセルされたかどうかを示す値です。
        /// </summary>
        private bool _cancelled = false;

        /// <summary>
        /// 送信処理を行う <see cref="IHttpSender{TParam}"/> 実装クラスのインスタンスを取得または設定します。
        /// </summary>
        /// <value>
        /// 送信処理を行う <see cref="IHttpSender{TParam}"/>。
        /// </value>
        protected IHttpSender<TParam> Sender
        {
            get { return _sender; }
            set { _sender = value; }
        }

        /// <summary>
        /// 受信処理を行う <see cref="IHttpReceiver"/> 実装クラスのインスタンスを取得または設定します。
        /// </summary>
        /// <value>
        /// 受信処理を行う <see cref="IHttpReceiver"/>。
        /// </value>
        protected IHttpReceiver Receiver
        {
            get { return _receiver; }
            set { _receiver = value; }
        }

        /// <summary>
        /// 接続先 URL 文字列を取得または設定します。
        /// </summary>
        public string Address
        {
            get { return _address; }
            set { _address = value; }
        }

        /// <summary>
        /// リクエストタイムアウト時間(ミリ秒)を取得または設定します。
        /// </summary>
        public int RequestTimeout
        {
            get { return _requestTimeout; }
            set { _requestTimeout = value; }
        }

        /// <summary>
        /// 通信がキャンセルされたかどうかを示す値を取得または設定します。
        /// </summary>
        /// <remarks>
        /// 通信がキャンセルされた場合は true。それ以外の場合は false。既定値は false です。
        /// </remarks>
        /// <value>
        /// キャンセルフラグ。
        /// </value>
        public bool Cancelled
        {
            get { return _cancelled; }
            set { _cancelled = value; }
        }

        /// <summary>
        /// 通信の進行状況を表すイベントです。
        /// </summary>
        public event ExecuteProgressChangedEventHandler ProgressChanged;

        /// <summary>
        /// 通信処理を実行します。このメソッドの戻り値が <c>null</c> 参照となることはありません。
        /// </summary>
        /// <remarks>
        /// 以下の順序で処理を実行します。
        /// <list type="number">
        /// <item>リクエストの生成 <seealso cref="CreateRequest"/></item>
        /// <item>リクエストの初期化 <seealso cref="PrepareRequest"/></item>
        /// <item>送信処理 <seealso cref="Send"/></item>
        /// <item>受信処理 <seealso cref="Receive"/></item>
        /// </list>
        /// 各処理のメソッドをオーバーライドすることで、送信処理や受信処理の拡張ができます。
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="paramData"/> が <c>null</c> 参照です。
        /// </item>
        /// <item>
        /// <paramref name="requestHeaders"/> が <c>null</c> 参照です。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="ArgumentException">
        /// 通信処理で必要なデータが不正です。
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item><see cref="CommunicatorBase{TParam}.Sender"/> が <c>null</c> 参照です。</item>
        /// <item><see cref="CommunicatorBase{TParam}.Receiver"/> が <c>null</c> 参照です。</item>
        /// </list>
        /// </exception>
        /// <exception cref="ServerException">
        /// 通信先のサーバでエラーが発生しました。
        /// </exception>
        /// <exception cref="CommunicationException">
        /// サーバとの通信中に内部処理でエラーが発生しました。または通信がキャンセルされました。
        /// </exception>
        /// <exception cref="TerasolunaException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="_address"/>が、 <c>null</c> 参照または空文字の場合で、アプリケーション構成
        /// ファイルから URL 文字列が取得できなかった場合。
        /// </item>
        /// <item>
        /// <paramref name="_address"/> もしくは、<see cref="BASE_URL"/>が <c>null</c> 参照または空文字の場合で、アプリケーション構成
        /// ファイルから URL 文字列が取得できなかった場合。
        /// </item>
        /// <item>
        /// <paramref name="_address"/> が取得した URL 文字列がhttp形式でなかった場合。
        /// </item>
        /// </list>
        /// </exception>
        /// <param name="paramData">送信データを格納したオブジェクト。</param>
        /// <param name="requestHeaders">送信時に HTTP ヘッダとして利用するヘッダ文字列のコレクション。</param>
        /// <returns>通信処理結果を格納した <see cref="CommunicationResult"/>。</returns>
        public virtual CommunicationResult Communicate(TParam paramData,
                                                       IDictionary<string, string> requestHeaders)
        {
            //前提の確認
            if (paramData == null)
            {
                string message = string.Format(Properties.Resources.E_NULL_ARGUMENT, "paramData");
                ArgumentNullException exception = new ArgumentNullException("paramData");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

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

            // リクエスト作成
            _request = CreateRequest(_address);
            PrepareRequest(_request);

            // 通信処理
            CommunicationResult result = null;
            try
            {
                Send(_request, paramData, requestHeaders);

                result = Receive(_request);
            }
            catch (WebException e)
            {
                CommunicationException exception =
                    new CommunicationException(Properties.Resources.E_COMMUNICATION_EXCEPTION, e);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            finally
            {
                lock (_syncRoot)
                {
                    _request = null;
                }
            }

            return result;
        }

        /// <summary>
        /// 送信処理を行います。
        /// </summary>
        /// <remarks>
        /// <see cref="CommunicatorBase{TParam}.Sender"/> を用いて送信処理を実行します。
        /// 送信処理を拡張する場合、このメソッドをオーバーライドします。
        /// </remarks>
        /// <param name="request">送信処理に用いる <see cref="HttpWebRequest"/>。</param>
        /// <param name="paramData">送信データを格納したオブジェクト。</param>
        /// <param name="requestHeaders">送信時に HTTP ヘッダとして利用するヘッダ文字列のコレクション。</param>
        /// <exception cref="ArgumentException">
        /// 通信処理で必要なデータが不正です。
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// <see cref="CommunicatorBase{TParam}.Sender"/> が <c>null</c> 参照です。
        /// </exception>
        /// <exception cref="WebException">
        /// 通信エラーが発生しました。または通信がキャンセルされました。
        /// </exception>
        /// <exception cref="CommunicationException">
        /// 送信中に内部処理でエラーが発生しました。
        /// </exception>
        protected virtual void Send(HttpWebRequest request, TParam paramData, IDictionary<string, string> requestHeaders)
        {
            if (Sender == null)
            {
                InvalidOperationException exception =
                    new InvalidOperationException(Properties.Resources.E_SENDER_IS_NOT_HOLD);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            Sender.Send(request, paramData, requestHeaders, this);
        }

        /// <summary>
        /// 受信処理を行います。
        /// </summary>
        /// <remarks>
        /// <see cref="CommunicatorBase{TParam}.Receiver"/> を用いて受信処理を実行します。
        /// 受信処理を拡張する場合、このメソッドをオーバーライドします。
        /// </remarks>
        /// <param name="request">受信処理に用いる <see cref="HttpWebRequest"/>。</param>
        /// <returns>通信処理結果を格納した <see cref="CommunicationResult"/>。</returns>
        /// <exception cref="InvalidOperationException">
        /// <see cref="CommunicatorBase{TParam}.Receiver"/> が <c>null</c> 参照です。
        /// </exception>
        /// <exception cref="WebException">
        /// 通信エラーが発生しました。または通信がキャンセルされました。
        /// </exception>
        /// <exception cref="ServerException">
        /// 通信先のサーバでエラーが発生しました。
        /// </exception>
        /// <exception cref="CommunicationException">
        /// 受信中に内部処理でエラーが発生しました。
        /// </exception>
        protected virtual CommunicationResult Receive(HttpWebRequest request)
        {
            if (Receiver == null)
            {
                InvalidOperationException exception =
                    new InvalidOperationException(Properties.Resources.E_RECEIVER_IS_NOT_HOLD);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            return Receiver.Receive(request, this);
        }

        /// <summary>
        /// <paramref name="str"/> を接続先 URL として <see cref="HttpWebRequest"/> を生成します。
        /// </summary>
        /// <remarks>
        /// <paramref name="str"/> が <c>null</c> 参照の場合、アプリケーション構成ファイルの 
        ///  appSettings セクションより <see cref="BASE_URL"/> をキーとして URL 文字列を取得します。
        /// </remarks>
        /// <exception cref="TerasolunaException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="str"/>が、 <c>null</c> 参照または空文字の場合で、アプリケーション構成
        /// ファイルから URL 文字列が取得できなかった場合。
        /// </item>
        /// <item>
        /// <paramref name="str"/> もしくは、<see cref="BASE_URL"/>が <c>null</c> 参照または空文字の場合で、アプリケーション構成
        /// ファイルから URL 文字列が取得できなかった場合。
        /// </item>
        /// <item>
        /// <paramref name="str"/> が取得した URL 文字列が http 形式、または https 形式ではなかった場合。
        /// </item>
        /// </list>
        /// </exception>
        /// <param name="str">接続先 URL 文字列。</param>
        /// <returns><paramref name="str"/> を接続先 URL とした <see cref="HttpWebRequest"/>。</returns>
        protected virtual HttpWebRequest CreateRequest(string str)
        {
            // 送信するUrl文字列の取得
            string address = str;

            // BASE_URLの取得
            if (string.IsNullOrEmpty(address))
            {
                address = ConfigurationManager.AppSettings[BASE_URL];

                if (string.IsNullOrEmpty(address))
                {
                    TerasolunaException exception =
                        new TerasolunaException(Properties.Resources.E_COMMUNICATION_BASE_URL_NOT_FOUND);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }
            }

            Uri uri = null;
            try
            {
                uri = new Uri(address);
            }
            catch (UriFormatException e)
            {
                TerasolunaException exception = new TerasolunaException(string.Format(
                    Properties.Resources.E_COMMUNICATION_INVALID_URL, address), e);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            bool isHttpUri = HTTP_SCHEME.Equals(uri.Scheme) || HTTPS_SCHEME.Equals(uri.Scheme);
            if (!isHttpUri)
            {
                TerasolunaException exception = new TerasolunaException(string.Format(
                    Properties.Resources.E_COMMUNICATION_NOT_HTTP_URL, address));
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            HttpWebRequest result = (HttpWebRequest)HttpWebRequest.Create(uri);

            return result;
        }

        /// <summary>
        /// リクエストを初期化します。
        /// </summary>
        /// <remarks>
        /// <para>デフォルトではリクエストタイムアウト時間と、リダイレクト自動応答の無効化のみを
        /// 設定します。</para>
        /// <para>リクエストの初期化処理を変更する場合には、このメソッドをサブクラスで
        /// オーバーライドします。</para>
        /// <paramref name="RequestTimeout"/> に-1を設定するとリクエストタイムアウトが無効になります。
        /// </remarks>
        /// <param name="request">初期化対象の <see cref="HttpWebRequest"/>。</param>
        protected virtual void PrepareRequest(HttpWebRequest request)
        {
            request.Timeout = RequestTimeout;
            request.AllowAutoRedirect = false;
        }

        /// <summary>
        /// 通信が実行中であれば、キャンセルします。
        /// </summary>
        public virtual void Cancel()
        {
            if (_request != null)
            {
                lock (_syncRoot)
                {
                    if (_request != null)
                    {
                        _request.Abort();
                        Cancelled = true;
                    }
                }
            }
        }

        /// <summary>
        /// <see cref="CommunicatorBase{TParam}.ProgressChanged"/> イベントを発生させます。
        /// </summary>
        /// <param name="e">
        /// 進行状況の情報を格納している <see cref="ExecuteProgressChangedEventArgs"/>。
        /// </param>
        public virtual void ReportProgressChanged(ExecuteProgressChangedEventArgs e)
        {
            if (ProgressChanged != null)
            {
                ProgressChanged(this, e);
            }
        }
    }
}
