// 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;
using System.Web;
using System.Web.Configuration;
using TERASOLUNA.Fw.Common.Logging;
using TERASOLUNA.Fw.Web.Configuration.ExceptionTransition;
using TERASOLUNA.Fw.Web.Configuration.Page;
using System.Web.SessionState;

namespace TERASOLUNA.Fw.Web.HttpModule
{
    /// <summary>
    /// アプリケーションからスローされた例外クラスの種類に応じて画面遷移を行う機能を提供します。
    /// </summary>
    /// <remarks>
    /// 本クラスを利用する際には、アプリケーションに Grobal.asax ファイルを追加する必要があります。
    /// <para>web.config に &lt;httpModules&gt; 設定が必要です。</para>
    /// </remarks>
    public class ExceptionTransitionListenerImpl : IHttpModule
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(ExceptionTransitionListenerImpl));

        /// <summary>
        /// 本モジュールが有効であるかどうかを保持する <see cref="Boolean"/>。
        /// </summary>
        /// <remarks>
        /// 本モジュールが有効の場合は true。無効の場合は false。
        /// </remarks>
        private bool _exceptionTransitionEnabled = false;
      
        /// <summary>
        /// IIS ログに例外に関する情報を出力するかどうかを保持する <see cref="Boolean"/>。
        /// </summary>
        /// <remarks>
        /// ログを出力する場合は true。出力しない場合は false。
        /// </remarks>
        private bool _exceptionLoggingEnabled = false;

        /// <summary>
        /// モジュールの有効可否を取得または設定します。
        /// </summary>
        /// <value>
        /// モジュールの有効可否。
        /// </value>
        protected bool ExceptionTransitionEnabled
        {
            get
            { 
                return _exceptionTransitionEnabled; 
            }
            set 
            {
                _exceptionTransitionEnabled = value; 
            }
        }

        /// <summary>
        /// ログ出力の可否を取得または設定します。
        /// </summary>
        /// <value>
        /// ログ出力の可否。
        /// </value>
        protected bool ExceptionLoggingEnabled
        {
            get
            { 
                return _exceptionLoggingEnabled; 
            }
            set 
            { 
                _exceptionLoggingEnabled = value; 
            }
        }

        /// <summary>
        /// ExceptionTransitionListenerImpl オブジェクトで使用されるリソース (メモリを除く) を解放します。 
        /// </summary>
        public virtual void Dispose()
        {            
        }

        /// <summary>
        /// ExceptionTransitionListenerImpl オブジェクトの作成時に初期化コードを呼び出します。 
        /// </summary>
        /// <remarks>
        /// エラー画面遷移モジュールが有効な場合、<see cref="HttpApplication"/> オブジェクトの
        /// Error イベントのイベントハンドラを登録します。
        /// </remarks>
        /// <param name="context">
        /// ASP.NET アプリケーション内のすべてのアプリケーションオブジェクトに共通のメソッド、
        /// プロパティ、およびイベントへのアクセスを提供する <see cref="HttpApplication"/> オブジェクト。
        /// </param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")]
        public virtual void Init(HttpApplication context)
        {
            ExceptionTransitionConfigurationSection etcs 
                = (ExceptionTransitionConfigurationSection)WebConfigurationManager.GetSection("exceptionTransitionConfiguration");

            if (String.Equals(etcs.Mode, "on", StringComparison.CurrentCultureIgnoreCase))
            {
                _exceptionTransitionEnabled = true;
                context.Error += new EventHandler(context_Error);
            }

            if (_exceptionTransitionEnabled && String.Equals(etcs.Logging, "on", StringComparison.CurrentCultureIgnoreCase))
            {
                _exceptionLoggingEnabled = true;
            }

        }

        /// <summary>
        /// HttpApplication.Error イベントのイベントハンドラ。
        /// </summary>
        /// <remarks>
        /// 設定ファイルから、発生した例外クラスに対する遷移先のURLを取得し、画面遷移を行います。
        /// </remarks>
        /// <param name="sender">イベントのソース。</param>
        /// <param name="e">イベントデータを格納している <see cref="EventArgs"/> オブジェクト。</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")]
        protected virtual void context_Error(object sender, EventArgs e)
        {
            HttpApplication app = sender as HttpApplication;
            HttpContext context = app.Context;

            if (WebUtils.IsInCustomErrorPages())
            {   // request が web.config 内の customErrors で定義されたページの場合は処理を行わない
                return;
            }

            try
            {
                Exception lastError = context.Server.GetLastError();

                if (lastError == null)
                {   // 例外情報がない場合はなにもしない
                    return;
                }

                if (lastError is HttpUnhandledException)
                {   // ユーザーコードによって処理されていない例外の場合
                    // InnerException にユーザコードで発生した例外オブジェクトが前回の例外となる
                    if (lastError.InnerException != null)
                    {
                        lastError = lastError.InnerException;
                    }
                }
                else
                {
                    HttpException he = lastError as HttpException;
                    if (he != null)
                    {
                        int errorCode = he.GetHttpCode();
                        string source = he.Source;
                        if (errorCode >= 400 && "System.Web".Equals(source))
                        {   // HTTP Status Code が
                            // 4XX  Client Error ／ 5XX  Server Error の場合で、
                            // ASP.NET内部の例外の場合はなにもしない
                            return;
                        }
                    }
                }

                string redirectPagePath = GetRedirectUrl(lastError);

                if (redirectPagePath.Length > 0)
                {
                    // 画面遷移保証機能が有効な場合はトークンを設定する
                    string appPath = context.Request.ApplicationPath;
                    string redirectUrl = null;
                    if (!("/".Equals(appPath)))
                    {
                        redirectUrl = appPath + redirectPagePath;
                    }
                    else
                    {
                        redirectUrl = redirectPagePath;
                    }

                    bool isCheckTokenEnabled = PageConfiguration.IsCheckTokenEnabled(redirectPagePath);
                    HttpSessionState session = context.Session;
                    if (isCheckTokenEnabled && session.IsNewSession)
                    {
                        // 次の画面にセッションを引き継ぐことができず、
                        // トークンチェックで落ちてしまうため、システムエラーとする。
                        // （ASP.NETのエラー処理機能に任せる。）
                        return;
                    }

                    string token = (string)context.Items["TERASOLUNA_Token"];
                    if (isCheckTokenEnabled && token != null)
                    {
                        // セッションにエラーページに対応するトークンを設定する
                        Hashtable tokens = (Hashtable)session["TERASOLUNA_Token"];
                        tokens[redirectUrl] = token;
                        session["TERASOLUNA_Token"] = tokens;
                        // トークンをクエリ文字列に付加する
                        redirectUrl = WebUtils.AppendParameter(redirectUrl, "__Token", token);
                    }

                    session["TERASOLUNA_LastError"] = lastError;
                    session["TERASOLUNA_ErrorPage"] = context.Request.Url.AbsoluteUri;

                    if (_exceptionLoggingEnabled)
                    {
                        string errMessage = string.Format(" Source :{0};  Message : {1}; StackTace : {2};",
                                                  lastError.Source,
                                                  lastError.Message,
                                                  lastError.StackTrace);
                        context.Response.AppendToLog(errMessage);
                    }

                    context.Server.ClearError();

                    // 画面遷移することを示すログを出力する。
                    if (_log.IsDebugEnabled)
                    {
                        _log.Debug(string.Format(Properties.Resources.D_EXCEPTION_TRANSIT,
                            lastError.GetType(), redirectUrl));
                    }
                    context.Response.Redirect(redirectUrl);

                    // エラー画面遷移時に TokenProcessorImpl.context_EndRequest 内で、
                    // RedirectLocation の再設定をさせないためのフラグ
                    context.Items["TERASOLUNA_ErrorTransition"] = true;
                }
                // else エラー画面遷移機能に設定がない場合は何もしない。（ASP.NETのエラー処理機能に任せる。）

            }
            catch (Exception ex)
            {
                string errMessage = string.Format(" Source :{0};  Message : {1}; StackTace : {2};",
                                                ex.Source,
                                                ex.Message,
                                                ex.StackTrace);
                context.Response.AppendToLog(errMessage);

                throw;
            }
        }

        /// <summary>
        /// 設定ファイルから、発生した例外クラスに対する遷移先ページを取得します。
        /// </summary>
        /// <remarks>
        /// 遷移先ページを取得できない場合は、空文字列を返します。
        /// </remarks>
        /// <param name="ex"><see cref="Exception"/> オブジェクト。</param>
        /// <returns>仮想アプリケーションルートパス以降の遷移先ページ。</returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="ex"/> が null 参照です。
        /// </exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings")]
        protected virtual string GetRedirectUrl(Exception ex)
        {
            if(ex == null)
            {
                ArgumentNullException exception = new ArgumentNullException("ex");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "ex"), exception);
                }
                throw exception;
            }
            Type t = ex.GetType();

            string nextPagePath = "";

            while (t != null)
            {
                string nextPage = ExceptionTransitionConfiguration.GetNextPage(t);

                if (nextPage != null)
                {
                    nextPagePath = PageConfiguration.GetPath(nextPage);
                    break;
                }

                t = t.BaseType;
            }

            return nextPagePath;
        }
    }
}
