// 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.Data;
using System.IO;
using System.Net;
using System.Xml;
using TERASOLUNA.Fw.Common;
using TERASOLUNA.Fw.Common.Logging;
using TERASOLUNA.Fw.Common.Validation;
using System.Text;
using System.Net.Mime;
using System.Threading;

namespace TERASOLUNA.Fw.Client.Communication
{
    /// <summary>
    /// データ受信機能を提供する <see cref="IHttpReceiver"/> 実装の基底クラスです。
    /// </summary>
    /// <remarks>
    /// <para>
    /// このクラスは抽象クラスです。基本的な処理フローと定数、HTTP レスポンスのエンティティボディを
    /// XML形式で取得するメソッドと、TERASOLUNA Server Framework に対応したエラー処理の実装を提供し
    /// ます。
    /// </para>
    /// </remarks>
    public abstract class ReceiverBase : IHttpReceiver
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(ReceiverBase));

        /// <summary>
        /// 通信データが XML 電文であることを表す文字列です。
        /// </summary>
        protected static readonly string TEXT_XML = "text/xml";

        /// <summary>
        /// エラー発生箇所を表す XPath を保持するXMLノードのノード名を表す文字列です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "error-field" です。
        /// </remarks>
        protected static readonly string ERROR_FIELD = "error-field";

        /// <summary>
        /// サーバで発生したエラー種別を取得する際にキーとして利用する文字列です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "exception" です。
        /// </remarks>
        protected static readonly string ERROR_TYPE = "exception";

        /// <summary>
        /// 通信時にサーバから想定しない例外が発生したことを示す結果文字列です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "serverException" です。
        /// </remarks>
        public static readonly string SERVER_EXCEPTION = "serverException";

        /// <summary>
        /// 通信時にサーバから入力チェックエラーが発生したことを示す結果文字列です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "serverValidateException" です。
        /// </remarks>
        public static readonly string SERVER_VALIDATE_EXCEPTION = "serverValidateException";

        /// <summary>
        /// 通信時にサーバからのシステムエラーを含むその他のエラーを示す結果コードを表す文字列です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "exception" です。
        /// </remarks>
        protected static readonly string EXCEPTION = "exception";

        /// <summary>
        /// 通信時にサーバからの入力チェックエラーを示す結果コードを表す文字列です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "validateException" です。
        /// </remarks>
        protected static readonly string VALIDATE_EXCEPTION = "validateException";

        /// <summary>
        /// エラー情報を保持する XML ノードのノード名を表す文字列です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "error" です。
        /// </remarks>
        protected static readonly string ERROR_ELEMENT = "error";

        /// <summary>
        /// エラーコードを保持する XML ノードのノード名を表す文字列です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "error-code" です。
        /// </remarks>
        protected static readonly string ERROR_CODE = "error-code";

        /// <summary>
        /// エラーメッセージを保持する XML ノードのノード名を表す文字列です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "error-message" です。
        /// </remarks>
        protected static readonly string ERROR_MESSAGE = "error-message";

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

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

        /// <summary>
        /// 一回に送受信するデータ量( byte )を表す値を取得または設定します。
        /// </summary>
        /// <remarks>
        /// 0 以下の値を設定した場合、 <see cref="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="HttpWebResponse"/>を用いて受信処理を行います。
        /// </summary>
        /// <remarks>
        /// <para>通信処理で発生した <see cref="WebException"/> はキャッチせずにスローします。また、内部処理で例外が
        /// 発生した場合、通常は <see cref="CommunicationException"/> にラップしてスローするようにします。コーディング
        /// ミスによる <see cref="ArgumentNullException"/> や <see cref="ArgumentException"/> 、ファイルシステムや
        /// 設定ファイルの異常による例外をラップする必要はありません。</para>
        /// <para>
        /// このメソッドを実装する際、内部で <paramref name="response"/> の <see cref="HttpWebResponse.GetResponseStream"/> 
        /// メソッドを利用して読み込みストリームの取得を行います。ストリームは <see cref="ReceiverBase.ReceiveResponse"/> 
        /// メソッド内で閉じる必要はありません。呼び出し元がストリームを閉じる責任を負います。</para>
        /// <para>通常、 <see cref="CommunicationResult.ResultData"/> には電文データを格納したデータセットを格納します。
        /// バイナリファイルを受信した場合など、格納する電文がない場合でも、<c>null</c>参照を設定することは
        /// 避けてください。そうした場合には空の<see cref="DataSet"/>インスタンスを生成して設定します。
        /// </para>
        /// </remarks>
        /// <exception cref="WebException">
        /// 通信エラーが発生しました。または、リクエストがキャンセルされました。
        /// </exception>
        /// <exception cref="CommunicationException">
        /// 受信中に内部処理でエラーが発生しました。
        /// </exception>
        /// <param name="response">受信処理を行うレスポンスオブジェクト。</param>
        /// <param name="reporter">進行状況通知を行う<see cref="IProgressChangeReporter"/>インスタンス。</param>
        /// <returns>受信結果を保持した <see cref="CommunicationResult"/>インスタンス。</returns>
        protected abstract CommunicationResult ReceiveResponse(HttpWebResponse response,
                                                               IProgressChangeReporter reporter);

        /// <summary>
        /// <paramref name="request"/> で渡された <see cref="HttpWebRequest"/> インスタンスから
        /// <see cref="HttpWebResponse"/> を取得し、 <see cref="ReceiveResponse"/> メソッドを用いて受信処理を行います。
        /// </summary>
        /// <para>
        /// 通常、このメソッドを直接オーバーライドすることは避けてください。 <see cref="Receive"/> メソッドは
        /// <see cref="HttpWebResponse"/> を取得した後、、<see cref="ReceiveResponse"/> 抽象メソッドへと処理を
        /// 委譲します。<see cref="HttpWebResponse"/> の解放はこのメソッドの終了時に行われます。この動作に問題
        /// がなければ、 <see cref="ReceiveResponse"/> メソッドで処理の本体を記述します。
        /// </para>
        /// <param name="request">
        /// 受信処理を行うリクエストオブジェクト。
        /// </param>
        /// <param name="reporter">
        /// 進行状況通知を行う <see cref="IProgressChangeReporter"/> 実装クラスのインスタンス。
        /// </param>
        /// <returns>受信結果を格納した <see cref="CommunicationResult"/>。</returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="request"/> が null 参照です。
        /// </exception>
        /// <exception cref="WebException">
        /// サーバとの通信中にエラーが発生しました。または通信がキャンセルされました。
        /// </exception>
        /// <exception cref="ServerException">
        /// 通信先のサーバでエラーが発生しました。
        /// </exception>
        /// <exception cref="CommunicationException">
        /// 受信中に内部処理でエラーが発生しました。
        /// </exception>
        public virtual CommunicationResult Receive(HttpWebRequest request,
                                                   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;
            }

            CommunicationResult result = null;

            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                CheckResponseError(response);

                result = ReceiveResponse(response, reporter);
            }

            return result;
        }

        /// <summary>
        /// <paramref name="response"/> から HTTP レスポンスのボディを取得し、<see cref="XmlDocument"/> を
        /// 生成します。
        /// </summary>
        /// <remarks>
        /// <para>
        /// <paramref name="response"/> から HTTP レスポンスのボディを取得し、<see cref="XmlDocument"/> を
        /// 生成します。 XML データではない場合、例外となります。
        /// </para>
        /// <para>
        /// <paramref name="reporter"/> が <c>null</c> 参照ではない場合、
        /// <see cref="ReceiverBase.BufferSize"/> プロパティに設定されたバッファサイズ分のデータ（デフォルトは8 KB ）をサーバに送信する毎に、
        /// <see cref="IProgressChangeReporter.ReportProgressChanged"/> イベントを発生させます。
        /// </para>
        /// </remarks>
        /// <param name="response">受信処理を行うレスポンスオブジェクト。</param>
        /// <param name="reporter">
        /// 進行状況通知を行う <see cref="IProgressChangeReporter"/> 実装クラスのインスタンス。
        /// </param>
        /// <returns>
        /// サーバから受信したXML電文を保持した <see cref="XmlDocument"/> インスタンス。
        /// </returns>
        /// <exception cref="CommunicationException">
        /// 受信中に内部処理でエラーが発生しました。
        /// </exception>
        protected virtual XmlDocument GetXmlFromResponseBody(WebResponse response, IProgressChangeReporter reporter)
        {
            // データがXMLでない(content-typeが"text/xml"でない)場合
            string contentType = response.ContentType;
            if (string.IsNullOrEmpty(contentType))
            {
                CommunicationException exception
                    = new CommunicationException(Properties.Resources.E_COMMUNICATION_NOT_XML);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            contentType = contentType.Split(';')[0];
            if (string.Compare(TEXT_XML, contentType, true) != 0)
            {
                CommunicationException exception
                    = new CommunicationException(Properties.Resources.E_COMMUNICATION_NOT_XML);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            int localBufferSize = BufferSize;
            XmlDocument receiveXml = new XmlDocument();
            byte[] buffer = new byte[localBufferSize];
            using (Stream responseStream = response.GetResponseStream())
            {
                int percentage = 0;
                long receiveSize = 0L;
                long totalSize = response.ContentLength;

                using (MemoryStream stream = new MemoryStream())
                {
                    int readSize = 0;

                    // バッファサイズ分受信する毎にイベント通知を行う
                    readSize = responseStream.Read(buffer, 0, localBufferSize);
                    while (readSize > 0)
                    {
                        stream.Write(buffer, 0, readSize);
                        receiveSize += readSize;

                        // レポーターが存在していれば
                        if (reporter != null)
                        {
                            if (totalSize > 0)
                            {
                                percentage = CalcReceivePercentage(receiveSize, totalSize);
                            }
                            reporter.ReportProgressChanged(new ExecuteProgressChangedEventArgs(percentage));

                        }

                        readSize = responseStream.Read(buffer, 0, localBufferSize);
                    }

                    stream.Position = 0;

                    try
                    {
                        receiveXml.Load(stream);
                    }
                    catch (XmlException e)
                    {
                        CommunicationException exception
                            = new CommunicationException(Properties.Resources.E_COMMUNICATION_NOT_XML, e);
                        if (_log.IsErrorEnabled)
                        {
                            _log.Error(exception.Message, exception);
                        }
                        throw exception;
                    }
                }
            }
            return receiveXml;
        }

        /// <summary>
        /// サーバのエラー情報を取得し、 <see cref="ServerException"/> へ格納して返します。
        /// </summary>
        /// <remarks>
        /// <para>
        /// サーバでエラーが発生している場合、 HTTP レスポンスのボディを XML 形式として解析し、
        /// <c>error</c>要素を取得してエラー情報を作成します。エラー情報は、<c>error</c> 要素に含まれる子要素の値を用いて
        /// 設定される <see cref="MessageInfo"/> または <see cref="ValidationMessageInfo"/> のインスタンスのリストです。
        /// エラー情報は戻り値である <see cref="ServerException"/>のインスタンスに格納されます。 
        /// </para>
        /// <para><paramref name="errorType"/> の値が "validateException" である場合、入力値検証エラーとします。</para>
        /// <para>入力値検証エラーの場合、<c>error</c> 要素に含まれる子要素の値を用いて、 <see cref="ValidationMessageInfo"/>
        /// のインスタンスが作成されます。<c>error</c> 要素の子要素と、 <see cref="ValidationMessageInfo"/> インスタンスに
        /// 格納される情報の関連については、以下の通りとなります。</para>
        /// <para>
        /// <list type="table">
        /// <listheader>
        ///     <term>ValidationMessageInfoのプロパティ名</term>
        ///     <description>格納される値</description>
        /// </listheader>
        /// <item>
        ///     <term>Key</term>
        ///     <term>error/error-code</term>
        /// </item>
        /// <item>
        ///     <term>Message</term>
        ///     <description>error/error-message</description>
        /// </item>
        /// <item>
        ///     <term>ErrorPath</term>
        ///     <description>error/error-field</description>
        /// </item>
        /// <item>
        ///     <term>Detail</term>
        ///     <description>error 要素を読み込んだ <see cref="XmlNode"/> インスタンス。</description>
        /// </item>
        /// </list>
        /// </para>
        /// <para>
        /// 入力値検証エラーの場合、 <see cref="ServerException.ErrorType"/> に格納される値は 
        /// "serverValidateException" です。
        /// </para>
        /// <para><paramref name="errorType"/> の値が "validateException" 以外である場合、<c>error</c> 要素に含まれる
        /// 子要素の値を用いて、 <see cref="MessageInfo"/> のインスタンスが作成されます。<c>error</c> 要素の子要素と、
        /// <see cref="MessageInfo"/> インスタンスに格納される情報の関連については、以下の通りとなります。</para>
        /// <para>
        /// <list type="table">
        /// <listheader>
        ///     <term>MessageInfoのプロパティ名</term>
        ///     <description>格納される値</description>
        /// </listheader>
        /// <item>
        ///     <term>Key</term>
        ///     <term>error/error-code</term>
        /// </item>
        /// <item>
        ///     <term>Message</term>
        ///     <description>error/error-message</description>
        /// </item>
        /// <item>
        ///     <term>Detail</term>
        ///     <description>error 要素を読み込んだ <see cref="XmlNode"/> インスタンス。</description>
        /// </item>
        /// </list>
        /// </para>
        /// <para><paramref name="errorType"/> の値が "exception" であるとき、<see cref="ServerException.ErrorType"/> 
        /// に格納される値は "serverException" です。<paramref name="errorType"/> の値が "validateException" 、
        /// "exception" のいずれでもないとき、<see cref="ServerException.ErrorType"/> には <paramref name="errorType"/>
        /// の値がそのまま格納されます。</para>
        /// </remarks>
        /// <param name="errorType">サーバで発生したエラー種別を表す文字列。</param>
        /// <param name="xmlDoc">応答電文となる <see cref="XmlDocument"/>。</param>
        /// <returns>
        /// サーバでのエラー結果を格納した <see cref="ServerException"/>。
        /// </returns>
        protected virtual ServerException HandleErrorResult(string errorType, XmlDocument xmlDoc)
        {
            // error要素取得
            XmlNodeList errorElementList = xmlDoc.DocumentElement.SelectNodes(ERROR_ELEMENT);

            IList<MessageInfo> messageInfoList = new List<MessageInfo>();

            string resultErrorType = null;

            // エラー種別を設定する
            if (VALIDATE_EXCEPTION.Equals(errorType))
            {            
                resultErrorType = SERVER_VALIDATE_EXCEPTION;
            }
            else
            {
                if (EXCEPTION.Equals(errorType))
                {
                    resultErrorType = SERVER_EXCEPTION;
                }
                else
                {
                    resultErrorType = errorType;
                }
            }

            // エラー内容を設定する
            foreach (XmlNode errorElement in errorElementList)
            {
                // error-code要素の値取得
                string errorCode = null;
                XmlNode currentElement = errorElement.SelectSingleNode(ERROR_CODE);
                if (currentElement != null)
                {
                    errorCode = currentElement.InnerText;
                }

                // error-message要素の値取得
                string errorMessage = null;
                currentElement = errorElement.SelectSingleNode(ERROR_MESSAGE);
                if (currentElement != null)
                {
                    errorMessage = currentElement.InnerText;
                }

                // 入力値検証エラーの場合
                if (VALIDATE_EXCEPTION.Equals(errorType))
                {
                    // error-field要素の値取得
                    string errorField = null;
                    currentElement = errorElement.SelectSingleNode(ERROR_FIELD);
                    if (currentElement != null)
                    {
                        errorField = currentElement.InnerText;
                    }

                    ValidationMessageInfo messageInfo = new ValidationMessageInfo(
                        errorCode, errorMessage, errorField);

                    messageInfo.Detail = errorElement;
                    messageInfoList.Add(messageInfo);
                }
                // 入力値検証エラー以外（システムエラーや業務エラー）の場合
                else
                {
                    MessageInfo messageInfo = new MessageInfo(errorCode, errorMessage);
                    messageInfo.Detail = errorElement;
                    messageInfoList.Add(messageInfo);
                }

            }
            return new ServerException(
                Properties.Resources.E_COMMUNICATION_SERVER_EXCEPTION, resultErrorType, messageInfoList);
        }

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

        /// <summary>
        /// レスポンスの内容にエラーがないかチェックする。
        /// </summary>
        /// <param name="response">HttpWebResponse</param>
        /// <remarks>
        /// ステータスコード300番以上の場合、WebExceptionをスローする。
        /// </remarks>
        /// <exception cref="WebException">
        /// リクエスト要求は失敗しました。
        /// </exception>
        protected virtual void CheckResponseError(HttpWebResponse response)
        {
            if ((int)response.StatusCode >= 300)
            {
                // 受信したHTTPヘッダ情報のログ出力
                if (_log.IsTraceEnabled)
                {
                    StringBuilder responseHeader = new StringBuilder();
                    responseHeader.AppendLine(Properties.Resources.T_RESPONSE_RECEIVE_HEADER);
                    foreach (string key in response.Headers.AllKeys)
                    {
                        responseHeader.AppendLine(string.Format(
                        Properties.Resources.T_DICTIONARY_KEY_VALUE,
                        key, response.Headers[key]));
                    }
                    _log.Trace(responseHeader.ToString().Trim());
                }

                WebException exception = new WebException(
                    CreateResponseExceptionString(response), null, WebExceptionStatus.ProtocolError, response);
                throw exception;
            }
        }

        /// <summary>
        /// 例外メッセージを生成する。
        /// </summary>
        /// <param name="response">受信処理を行うレスポンスオブジェクト。</param>
        /// <returns>例外メッセージ。</returns>
        protected virtual string CreateResponseExceptionString(HttpWebResponse response)
        {
            string exceptionString = null;

            int statusCode = (int)response.StatusCode;
            string responseString = ReadResponse(response);

            if (string.IsNullOrEmpty(responseString))
            {
                // 受信したHTTPボディがnullまたは空文字の場合
                exceptionString = string.Format(Properties.Resources.E_COMMUNICATION_INVALID_STATUS_WITH_EMPTY_BODY, statusCode);
            }
            else
            {
                // 受信したHTTPボディ情報のログ出力
                if (_log.IsTraceEnabled)
                {
                    StringBuilder responseBody = new StringBuilder();
                    responseBody.AppendLine(Properties.Resources.T_RESPONSE_RECEIVE_BODY);
                    responseBody.Append(responseString);
                    _log.Trace(responseBody.ToString());
                }
                exceptionString = string.Format(Properties.Resources.E_COMMUNICATION_INVALID_STATUS, statusCode, responseString);
            }

            return exceptionString;
        }

        /// <summary>
        /// レスポンスボディを読み込む。
        /// </summary>
        /// <param name="response">受信処理を行うレスポンスオブジェクト。</param>
        /// <returns>レスポンスボディの文字列。</returns>
        protected virtual string ReadResponse(HttpWebResponse response)
        {
            string charset = response.CharacterSet;
            Encoding encoding = GetEncoding(charset);
            Stream stream = response.GetResponseStream();

            using (StreamReader reader = new StreamReader(stream, encoding, true))
            {
                return reader.ReadToEnd();
            }
        }

        /// <summary>
        /// エンコーディングを取得する。
        /// </summary>
        /// <param name="charset">文字コード。</param>
        /// <returns>エンコード。</returns>
        protected virtual Encoding GetEncoding(string charset)
        {
            if (!string.IsNullOrEmpty(charset))
            {
                try
                {
                    return Encoding.GetEncoding(charset);
                }
                catch (ArgumentException)
                {
                    // 何もしない
                }
            }

            return new ASCIIEncoding();
        }

    }
}
