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

namespace TERASOLUNA.Fw.Common.BLogic
{
    /// <summary>
    /// <see cref="IBLogic"/> 実装クラスのインスタンスを生成します。
    /// </summary>
    /// <remarks>
    /// <see cref="IBLogic"/> 実装クラスのインスタンスを生成するためには、
    /// ビジネスロジック設定ファイルの blogic 要素の name 属性を指定し、
    ///  name 属性に対応する type 属性の型から <see cref="IBLogic"/> を生成します。
    /// </remarks>
    public class BLogicFactory
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(BLogicFactory));

        /// <summary>
        /// 利用する <see cref="BLogicFactory"/> クラスの型名をアプリケーション構成ファイルから
        /// 取得するためのキー文字列です。
        /// </summary>
        /// <remarks>
        /// <para>
        /// アプリケーション構成ファイルの appSettings 要素から、この文字列をキーとして、
        /// <see cref="BLogicFactory"/>クラスの型名を取得します。
        /// </para>
        /// <para>
        /// この定数の値は "BLogicFactoryTypeName" です。
        /// </para>
        /// </remarks>
        protected static readonly string BLOGIC_FACTORY_TYPENAME = "BLogicFactoryTypeName";

        /// <summary>
        /// <see cref="BLogicFactory"/> クラスのインスタンスのキャッシュです。
        /// </summary>
        private static volatile BLogicFactory _factory = null;

        /// <summary>
        /// ロック用のオブジェクトです。
        /// </summary>
        private static object _syncRoot = new Object();

        /// <summary>
        /// <see cref="IBLogic"/> 実装クラスに対応する <see cref="Type"/> インスタンスを
        /// 格納するキャッシュです。
        /// </summary>
        private IDictionary<string, Type> _blogicTypes = new Dictionary<string, Type>();

        /// <summary>
        /// <see cref="IBLogic"/> 実装クラスに対応する <see cref="Type"/> インスタンスの
        /// キャッシュを取得します。
        /// </summary>
        /// <value>
        /// <see cref="IBLogic"/> 実装クラスに対応する <see cref="Type"/> インスタンスのキャッシュです。
        /// </value>
        /// <remarks>
        /// デフォルト値は、空の <see cref="Dictionary{String, Type}"/> です。
        /// </remarks>
        protected IDictionary<string, Type> BLogicTypes
        {
            get
            {
                return _blogicTypes;
            }
        }

        /// <summary>
        /// <see cref="BLogicFactory"/> 型のクラスのインスタンスを取得します。
        /// </summary>
        /// <remarks>
        /// App.configに<see cref="BLOGIC_FACTORY_TYPENAME"/> をキーとして型名が
        /// 記述されていた場合、その型のインスタンスを生成します。記述がない場合、
        ///  <see cref="BLogicFactory"/> のインスタンスを生成します。
        /// </remarks>
        /// <value>
        /// <see cref="BLogicFactory"/> 型のクラスのインスタンスです。
        /// </value>
        /// <exception cref="ArgumentException">
        /// <see cref="BLogicFactory"/> 派生クラスのインスタンスを生成できません。
        /// </exception>
        /// <exception cref="TerasolunaException">
        /// <see cref="BLogicFactory"/> 派生クラスのインスタンスを生成できません。
        /// </exception>
        protected static BLogicFactory Factory
        {
            get
            {
                if (_factory == null)
                {
                    lock (_syncRoot)
                    {
                        if (_factory == null)
                        {
                            _factory = ClassUtil.CreateInstanceFromAppSettings<BLogicFactory>(
                              BLOGIC_FACTORY_TYPENAME, typeof(BLogicFactory));
                        }
                    }
                }
                return _factory;
            }
        }

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

        /// <summary>
        /// <see cref="IBLogic"/> 実装クラスのインスタンスを生成します。
        /// </summary>
        /// <param name="blogicName">生成する <see cref="IBLogic"/> 実装クラスを識別するための文字列。</param>
        /// <param name="genericTypeNames">ジェネリック型の型パラメータに渡される型名の可変長引数。</param>
        /// <returns><see cref="IBLogic"/> 実装クラスのインスタンス。
        /// 生成に失敗した場合は、例外をスローし、 null は返しません。</returns>
        /// <remarks>
        /// <para>
        /// <paramref name="blogicName"/> を元に、ビジネスロジック設定ファイルに設定された
        ///  <see cref="IBLogic"/> 実装クラスの型名を取得し、対応するインスタンスを生成します。
        /// </para>
        /// <para>
        /// <paramref name="blogicName"/> が null または空文字の場合は、何も処理を行わない <see cref="NopBLogic"/> を返却します。
        /// </para>
        /// <para>
        /// <paramref name="blogicName"/> はジェネリック型で型パラメータを持つクラスの型名も指定可能で、その場合は
        ///  <paramref name="genericTypeNames"/> に型パラメータに置き換えられる型の型名を指定する必要があります。
        /// </para>
        /// <para>
        /// 置き換える型パラメータが必要とする数よりも少ない場合は、例外をスローします。
        /// </para>
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="genericTypeNames"/> が null 参照です。
        /// </exception>
        /// <exception cref="TerasolunaException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <see cref="BLogicFactory"/> 派生クラスのインスタンスを生成できません。
        /// </item>
        /// <item>
        /// <paramref name="blogicName"/> をビジネスロジック名とするビジネスロジック設定ファイルの blogic 要素が未定義です。
        /// </item>
        /// <item>
        /// <paramref name="blogicName"/> をビジネスロジック名とするビジネスロジック設定ファイルの blogic 要素の type 属性の値から
        /// 型を解決できません。 
        /// </item>
        /// <item>
        /// <see cref="IBLogic"/> 実装クラスのインスタンスを生成できません。
        /// </item>
        /// <item>
        /// <paramref name="genericTypeNames"/> に設定されているアセンブリ修飾名の数が、
        /// <paramref name="blogicName"/> から生成されたジェネリック型に必要な型パラメータ数より少ないです。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="ConfigurationErrorsException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// 構成ファイルを読み込むことができません。
        /// </item>
        /// <item>
        /// 構成ファイルの appSettings 要素から "blogicConfiguration" をキーとして
        /// 取得した値から生成されたインスタンスが、 <see cref="BLogicConfigurationSection"/> 型ではありません。
        /// </item>
        /// </list>
        /// </exception>
        /// <example>
        /// ビジネスロジック設定ファイルの設定例を以下に示します。
        /// <code>
        /// &lt;blogicConfiguration xmlns="http://www.terasoluna.jp/schema/BLogicSchema.xsd"&gt;
        ///   &lt;!-- ジェネリック型で型パラメータを持つビジネスロジックの場合 --&gt;
        ///   &lt;blogic name="genericBLogicSample"
        ///              type="TERASOLUNA.Sample.GenericBLogicSample`1, TERASOLUNA.Sample" /&gt;
        ///   &lt;!-- ジェネリック型で型パラメータを持たないBLogicBaseの場合 --&gt;
        ///   &lt;blogic name="noGenericBLogicSample" 
        ///              type="TERASOLUNA.Sample.NoGenericBLogicSample, TERASOLUNA.Sample" /&gt;
        /// &lt;/blogicConfiguration&gt;
        /// </code>
        /// </example>
        public static IBLogic CreateBLogic(string blogicName, params string[] genericTypeNames)
        {
            if (genericTypeNames == null)
            {
                ArgumentNullException exception = new ArgumentNullException("genericTypeNames");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(Properties.Resources.E_NULL_ARGUMENT, "genericTypeNames"), exception);
                }
                throw exception;
            }

            IBLogic blogic = Factory.CreateInstance(blogicName, genericTypeNames);

            if (_log.IsDebugEnabled)
            {
                _log.Debug(string.Format(Common.Properties.Resources.D_CREATE_INSTANCE_SUCCESS,
                    typeof(IBLogic).FullName, blogic.GetType().FullName));
            }

            return blogic;
        }

        /// <summary>
        /// <see cref="IBLogic"/> 実装クラスのインスタンスを生成します。
        /// </summary>
        /// <param name="blogicName">生成する <see cref="IBLogic"/> 派生クラスを識別するための文字列。</param>
        /// <param name="genericTypeNames">ジェネリック型の型パラメータに置き換えられる型の型名の可変長引数。</param>
        /// <returns><see cref="IBLogic"/> 派生クラスのインスタンス。
        /// 生成に失敗した場合は、例外をスローし、 null は返しません。</returns>
        /// <remarks>
        /// <para>
        /// <paramref name="blogicName"/> を元に、ビジネスロジック設定ファイルに設定された
        /// <see cref="IBLogic"/> 実装クラスの型名を取得し、対応するインスタンスを生成します。
        /// </para>
        /// <para>
        /// <paramref name="blogicName"/> が null または空文字の場合は、何も処理を行わない <see cref="NopBLogic"/> を返却します。
        /// </para>
        /// <para>
        /// <paramref name="blogicName"/> はジェネリック型の型パラメータを持つクラスの型名も指定可能で、その場合は
        /// <paramref name="genericTypeNames"/> に型パラメータに置き換えられる型の型名を指定する必要があります。
        /// </para>
        /// <para>
        /// 置き換える型パラメータが必要とする数よりも少ない場合は、例外をスローします。
        /// </para>
        /// </remarks>
        /// <exception cref="TerasolunaException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="blogicName"/> をビジネスロジック名とするビジネスロジック設定ファイルの blogic 要素が未定義です。
        /// </item>
        /// <item>
        /// <paramref name="blogicName"/> をビジネスロジック名とするビジネスロジック設定ファイルの blogic 要素の type 属性の値から
        /// 型を解決できません。 
        /// </item>
        /// <item>
        /// <see cref="IBLogic"/> 実装クラスのインスタンスを生成できません。
        /// </item>
        /// <item>
        /// <paramref name="genericTypeNames"/> に設定されているアセンブリ修飾名の数が、
        /// <paramref name="blogicName"/> から生成されたジェネリック型に必要な型パラメータ数より少ないです。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="ConfigurationErrorsException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// 構成ファイルを読み込むことができません。
        /// </item>
        /// <item>
        /// 構成ファイルの appSettings 要素から blogicConfiguration をキーとして
        /// 取得した値から生成されたインスタンスが、 <see cref="BLogicConfigurationSection"/> 型ではありません。
        /// </item>
        /// </list>
        /// </exception>
        /// <example>
        /// ビジネスロジック設定ファイルの設定例を以下に示します。
        /// <code>
        /// &lt;blogicConfiguration xmlns="http://www.terasoluna.jp/schema/BLogicSchema.xsd"&gt;
        ///   &lt;!-- ジェネリック型で型パラメータを持つビジネスロジックの場合 --&gt;
        ///   &lt;blogic name="genericBLogicSample"
        ///              type="TERASOLUNA.Sample.GenericBLogicSample`1, TERASOLUNA.Sample" /&gt;
        ///   &lt;!-- ジェネリック型で型パラメータを持たないBLogicBaseの場合 --&gt;
        ///   &lt;blogic name="noGenericBLogicSample" type="TERASOLUNA.Sample.NoGenericBLogicSample, TERASOLUNA.Sample" /&gt;
        /// &lt;/blogicConfiguration&gt;
        /// </code>
        /// </example>
        protected virtual IBLogic CreateInstance(string blogicName, params string[] genericTypeNames)
        {
            Type blogicType = null;

            if (string.IsNullOrEmpty(blogicName))
            {
                return new NopBLogic();
            }

            bool canGetValue = false;
            ICollection blogicTypesCollection = (ICollection)_blogicTypes;
            lock (blogicTypesCollection.SyncRoot)
            {
                canGetValue = _blogicTypes.TryGetValue(blogicName, out blogicType);
            }

            if (!canGetValue)
            {
                // ビジネスロジック設定ファイルからBLogicのタイプ名を取得
                string blogicTypeName = BLogicConfiguration.GetBLogicTypeName(blogicName);

                if (string.IsNullOrEmpty(blogicTypeName))
                {
                    TerasolunaException exception = new TerasolunaException(string.Format(
                        Properties.Resources.E_BLOGIC_TYPE_NOT_FOUND, blogicName));
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }

                try
                {
                    blogicType = Type.GetType(blogicTypeName, true);
                }
                catch (Exception e)
                {
                    string message = string.Format(Properties.Resources.E_TYPE_LOAD_FAILED, blogicTypeName);
                    TerasolunaException exception = new TerasolunaException(message, e);

                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(message, exception);
                    }
                    throw exception;
                }

                lock (blogicTypesCollection.SyncRoot)
                {
                    _blogicTypes[blogicName] = blogicType;
                }
            }

            IBLogic blogic = ClassUtil.CreateInstanceWithGenericTypes<IBLogic>(blogicType.AssemblyQualifiedName, genericTypeNames);

            return blogic;
        }
    }
}