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

        /// <summary>
        /// コンテキストの種類をHTTPヘッダへ格納する際にキーとして利用する文字列です。
        /// </summary>
        /// <remarks><para>この定数の値は "application/octet-stream" です。</para></remarks>
        protected static readonly string CONTENT_TYPE_BINARY = "application/octet-stream";

        /// <summary>
        /// ファイル名のエンコード／デコードのタイプを表す文字列です。
        /// </summary>
        /// <remarks>
        /// <para>この定数の値は "B" です。"B" は、B エンコードを表します。</para>
        /// <para>
        /// B エンコードとは、7bit US-ASCII 以外のテキストを使用するための 7bit エンコーディングメカニズム
        /// の一つです。RFC 2047 で定義されています。エンコーディングアルゴリズムは Base64 と同じです。
        /// </para>
        /// </remarks>
        protected static readonly string ENCODE_TYPE = "B";

        /// <summary>
        /// Content-Disposition ヘッダでファイル名を設定する際に利用する文字列です。
        /// </summary>
        /// <remarks><para>この定数の値は "attachment; filename=?{0}?{1}?{2}?=" です。</para></remarks>
        protected static readonly string CONTENT_DISPOSITION_VALUE = "attachment; filename=?{0}?{1}?{2}?=";

        /// <summary>
        /// ファイル名をエンコード／デコードする際に利用する文字コードを表す文字列です。
        /// </summary>
        /// <remarks>
        /// <para>この定数の値は "ISO-2022-JP" です。</para>
        /// </remarks>
        protected static readonly string ENCODE_CHAR = "ISO-2022-JP";

        /// <summary>
        /// ファイル名をエンコード／デコードする際に利用する<see cref="Encoding"/>です。
        /// </summary>
        /// <remarks>
        /// デフォルトのエンコーディングは "ISO-2022-JP" です。
        /// </remarks>
        private Encoding _fileNameEncoding = Encoding.GetEncoding(ENCODE_CHAR);

        /// <summary>
        /// 送信時に利用する Content-Type ヘッダの値を取得します。
        /// このプロパティは必ず "application/octet-stream" を返します。
        /// </summary>
        public override string ContentType
        {
            get
            {
                return CONTENT_TYPE_BINARY;
            }
        }

        /// <summary>
        /// ファイル名をエンコード／デコードする際に利用する<see cref="Encoding"/>を取得または設定します。
        /// </summary>
        /// <remarks>
        /// このプロパティに <c>null</c> 参照を設定した場合、デフォルトのエンコーディングが利用されます。
        /// デフォルトのエンコーディングは "ISO-2022-JP" です。
        /// </remarks>
        public Encoding FileNameEncoding
        {
            get
            {
                return _fileNameEncoding;
            }
            set
            {
                if (value == null)
                {
                    _fileNameEncoding = Encoding.GetEncoding(ENCODE_CHAR);
                }
                else
                {
                    _fileNameEncoding = value;
                }
            }
        }

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

        /// <summary>
        /// <paramref name="request"/> で渡された <see cref="HttpWebRequest"/> を用いて、
        /// <paramref name="filePath"/> で表されるファイルと <paramref name="headerCollection"/> 
        /// の情報を送信します。
        /// </summary>
        /// <remarks>
        /// <para>
        /// 送信する HTTP リクエストボディは、<paramref name="filePath"/> で表されるファイルのバイナリ表現です。
        /// </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="filePath">送信データを格納したオブジェクト。</param>
        /// <param name="headerCollection">送信時にHTTPヘッダとして利用するヘッダ文字列のコレクション。</param>
        /// <param name="reporter">進行状況通知を行う <see cref="IProgressChangeReporter"/> インスタンス。 </param> 
        /// <exception cref="ArgumentException">
        /// <paramref name="filePath"/> が空文字です。
        /// </exception>
        /// <exception cref="System.IO.FileNotFoundException">
        /// 通信処理で必要なファイルが見つかりません。
        /// </exception>
        /// <exception cref="WebException">
        /// 通信エラーが発生しました。または、リクエストがキャンセルされました。
        /// </exception>
        protected override void SendRequest(HttpWebRequest request,
                                            string filePath,
                                            IDictionary<string, string> headerCollection,
                                            IProgressChangeReporter reporter)
        {
            if (filePath.Length == 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_STRING, "filePath");
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            // ファイル存在チェック
            if (!File.Exists(filePath))
            {
                string message = string.Format(Properties.Resources.E_COMMUNICATION_FILE_NOT_FOUND, filePath);
                FileNotFoundException exception = new FileNotFoundException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            // リクエストのヘッダにファイルネーム追加
            string fileName = Path.GetFileName(filePath);
            string encodedFileName = EncodeFileName(fileName);
            request.Headers.Add(CONTENT_DISPOSITION,
                string.Format(CONTENT_DISPOSITION_VALUE, _fileNameEncoding.WebName, ENCODE_TYPE, encodedFileName));

            // アップロードファイルの読み込み準備
            using (Stream upFileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
                int percentage = 0;
                long sendSize = 0L;
                long totalSize = upFileStream.Length;

                // ContentLengthの設定
                request.ContentLength = totalSize;

                // リクエストへ書き込み準備
                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());

                        // 送信ファイル名
                        _log.Trace(string.Format(Properties.Resources.T_UPLOAD_FILE_PATH, filePath));
                    }

                    int localBufferSize = BufferSize;
                    byte[] buffer = new byte[localBufferSize];
                    int readSize = upFileStream.Read(buffer, 0, localBufferSize);
                    while (readSize > 0)
                    {
                        // リクエストへバイナリデータを書き込む
                        reqStream.Write(buffer, 0, readSize);

                        // イベント通知
                        sendSize += readSize;
                        if (reporter != null)
                        {
                            if (totalSize > 0)
                            {
                                percentage = CalcSendPercentage(sendSize, totalSize);
                            }
                            reporter.ReportProgressChanged(new ExecuteProgressChangedEventArgs(percentage));
                        }

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

        /// <summary>
        /// <paramref name="fileName"/>を Base64 エンコードした結果を返します。
        /// </summary>
        /// <param name="fileName">エンコードするファイル名。</param>
        /// <returns>Base64 エンコードされた文字列。</returns>
        protected virtual string EncodeFileName(string fileName)
        {
            byte[] byteData = _fileNameEncoding.GetBytes(fileName);
            return Convert.ToBase64String(byteData);
        }
    }
}
