// 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.Configuration;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.Schema;
using TERASOLUNA.Fw.Common.Logging;

namespace TERASOLUNA.Fw.Common.Configuration
{
    /// <summary>
    /// <see cref="FilesCollection"/> コレクションをマージし、<see cref="XmlDocument"/> クラスのインスタンスを生成します。
    /// </summary>
    /// <remarks>
    /// マージする場合には <see cref="MergeConfiguration.MergeXmlDocument"/>
    /// メソッドを実行する前に <see cref="MergeConfiguration.LoadConfig"/> メソッドを呼び出し、
    /// マージする <see cref="FilesCollection"/> コレクションを読み込む必要があります。
    /// </remarks>
    public class MergeConfiguration
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(MergeConfiguration));

        /// <summary>
        /// 読み込んだファイルを保持する <see cref="Hashtable"/> です。
        /// </summary>
        /// <remarks>
        /// デフォルトの値は、 null です。
        /// </remarks>
        private Hashtable xmls = null;

        /// <summary>
        /// 名前空間を管理する <see cref="XmlNamespaceManager"/> です。
        /// </summary>
        /// <remarks>
        /// デフォルトの値は、 null です。
        /// </remarks>
        private XmlNamespaceManager xmlNM = null;

        /// <summary>
        /// XML 検証に用いる <see cref="XmlDocumentValidator"/> です。
        /// </summary>
        /// <remarks>
        /// デフォルトの値は、 null です。
        /// </remarks>
        private XmlDocumentValidator xmlDocValidator = null;

        /// <summary>
        /// マージ結果を保持する <see cref="XmlDocument"/> です。
        /// </summary>
        /// <remarks>
        /// デフォルトの値は、 null です。
        /// </remarks>
        private XmlDocument mergedXmlDoc = null;

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

        /// <summary>
        /// <see cref="MergeConfiguration"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <param name="xmlNamespace">NameSpace 名。</param>
        /// <param name="xmlNameSpacePrefix">NameSpacePrefix 名。</param>
        /// <param name="schemaName">Schema 名。</param>
        /// <remarks>XML スキーマ検証に用いるスキーマを設定します。</remarks>
        /// <exception cref="ArgumentNullException">
        /// 次のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="xmlNamespace"/> が null 参照です。
        /// </item>
        /// <item>
        /// <paramref name="xmlNameSpacePrefix"/> が null 参照です。
        /// </item>
        /// <item>
        /// <paramref name="schemaName"/> が null 参照です。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="ArgumentException">
        /// 次のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="xmlNamespace"/> が空文字です。
        /// </item>
        /// <item>
        /// <paramref name="xmlNameSpacePrefix"/> が空文字です。
        /// </item>
        /// <item>
        /// <paramref name="schemaName"/> が空文字です。
        /// </item>
        /// </list>
        /// </exception>
        public MergeConfiguration(string xmlNamespace, string xmlNameSpacePrefix, string schemaName)
        {
            //引数のチェック
            if (xmlNamespace == null)
            {
                ArgumentNullException exception = new ArgumentNullException("xmlNamespace");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "xmlNamespace"), exception);
                }
                throw exception;
            }
            if (xmlNamespace.Length == 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_STRING, "xmlNamespace");
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

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

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

            //読み込んだファイルを保持するハッシュテーブルを用意する
            xmls = new Hashtable();

            //マージ結果を保持するXmlDocumentを用意する
            XmlDocument mergedXmlDoc = new XmlDocument();

            //名前空間の指定
            xmlNM = new XmlNamespaceManager(mergedXmlDoc.NameTable);
            xmlNM.AddNamespace(xmlNameSpacePrefix, xmlNamespace);

            // 外部設定ファイルをマージするXmlDocumentを用意する
            xmlDocValidator = new XmlDocumentValidator();

            xmlDocValidator.AddSchema(xmlNamespace, Assembly.GetExecutingAssembly(), schemaName);
        }

        /// <summary>
        /// <see cref="FilesCollection"/> の内容をスキーマチェックを行いながら、ハッシュテーブルに読み込みます。
        /// </summary>
        /// <param name="sectionName">セクション要素名。</param>
        /// <param name="files">Files コレクション。</param>
        /// <remarks>
        /// <para><see cref="FilesCollection"/> コレクションから <see cref="FileElement"/> を
        /// <see cref="XmlDocument"/> として読み取り、スキーマチェックを行いながらハッシュテーブルに格納します。</para>
        /// <para><see cref="MergeConfiguration.MergeXmlDocument"/> メソッドを使用することで、読み込んだ
        /// <see cref="XmlDocument"/> をマージすることができます。</para>
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// 次のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="sectionName"/> が null 参照です。
        /// </item>
        /// <item>
        /// <paramref name="files"/> が null 参照です。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="sectionName"/> が空文字です。
        /// </exception>
        /// <exception cref="ConfigurationErrorsException">
        /// 次のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// 構成ファイルの <paramref name="sectionName"/> 設定情報が不正です。
        /// </item>
        /// <item>
        /// ファイルの読み込み失敗です。
        /// </item>
        /// <item>
        /// <paramref name="sectionName"/> 設定ファイルの XML スキーマ検証エラーです。
        /// </item>
        /// </list>
        /// </exception>
        public void LoadConfig(string sectionName, FilesCollection files)
        {
            //引数のチェック
            if (sectionName == null)
            {
                ArgumentNullException exception = new ArgumentNullException("sectionName");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "sectionName"), exception);
                }
                throw exception;
            }
            if (sectionName.Length == 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_STRING, "sectionName");
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

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

            // 外部設定ファイルの基底のパスを保持する
            string filePath = null;
            // 外部設定ファイルの基底のパスからのパスを保持する
            string fileName = null;

            filePath = AppDomain.CurrentDomain.BaseDirectory;

            // 構成ファイルにfiles要素が無い場合は例外をスローする
            if (files.Count == 0)
            {
                string message = string.Format(Properties.Resources.E_CONFIGURATION_INVALID_CUSTOM_SECTION_HANDLER, sectionName);
                ConfigurationErrorsException exception = new ConfigurationErrorsException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            foreach (FileElement file in files)
            {
                XmlDocument xmlDoc = new XmlDocument();
                try
                {
                    fileName = Path.Combine(filePath, file.Path);
                }
                catch (ArgumentException ex)
                {
                    fileName = filePath + Path.DirectorySeparatorChar + file.Path;
                    string message = string.Format(Properties.Resources.E_CONFIGURATION_EXTERNAL_CONFIG_LOAD_FAILED, sectionName, fileName);
                    ConfigurationErrorsException exception = new ConfigurationErrorsException(message, ex);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(message, exception);
                    }
                    throw exception;
                }

                try
                {
                    //ファイルの読込
                    xmlDoc.Load(fileName);
                }
                catch (IOException ex)
                {
                    string message = string.Format(Properties.Resources.E_CONFIGURATION_EXTERNAL_CONFIG_LOAD_FAILED, sectionName, fileName);
                    ConfigurationErrorsException exception = new ConfigurationErrorsException(message, ex);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(message, exception);
                    }
                    throw exception;
                }
                catch (XmlException ex)
                {
                    string message = string.Format(Properties.Resources.E_CONFIGURATION_INVALID_EXTERNAL_CONFIG_FILE, sectionName, fileName);
                    ConfigurationErrorsException exception = new ConfigurationErrorsException(message, ex);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(message, exception);
                    }
                    throw exception;
                }

                try
                {
                    //スキーマチェック
                    xmlDocValidator.Validate(xmlDoc);
                }
                catch (XmlSchemaException ex)
                {
                    string message = string.Format(Properties.Resources.E_CONFIGURATION_INVALID_EXTERNAL_CONFIG_FILE, sectionName, fileName);
                    ConfigurationErrorsException exception = new ConfigurationErrorsException(message, ex);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(message, exception);
                    }
                    throw exception;
                }

                //ハッシュテーブルへ追加
                xmls[file.Path] = xmlDoc;
            }
        }

        /// <summary>
        /// 外部設定ファイルをマージします。
        /// </summary>
        /// <param name="sectionXpath">セクション要素取得用 XPath 。</param>
        /// <param name="elementXpath">要素取得用 XPath 。</param>
        /// <returns>マージした <see cref="XmlDocument"/> 。</returns>
        /// <remarks>
        /// <see cref="MergeConfiguration.LoadConfig"/> メソッドで保持した外部設定ファイルをマージします。
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// 次のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="sectionXpath"/> が null 参照です。
        /// </item>
        /// <item>
        /// <paramref name="elementXpath"/> が null 参照です。
        /// </item>
        /// <item>
        /// 外部設定ファイルの値が不正のです。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="ArgumentException">
        /// 次のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="sectionXpath"/> が空文字です。
        /// </item>
        /// <item>
        /// <paramref name="elementXpath"/> が空文字です。
        /// </item>
        /// <item>
        /// <paramref name="sectionXpath"/> が不正です。
        /// </item>
        /// <item>
        /// <paramref name="elementXpath"/> が不正です。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="ConfigurationErrorsException">
        /// <paramref name="sectionXpath"/> をマージ後に、 XML スキーマ検証エラーです。
        /// </exception>
        public XmlDocument MergeXmlDocument(string sectionXpath, string elementXpath)
        {
            //引数のチェック
            if (sectionXpath == null)
            {
                ArgumentNullException exception = new ArgumentNullException("sectionXpath");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(Properties.Resources.E_NULL_ARGUMENT, "sectionXpath"), exception);
                }
                throw exception;
            }
            if (sectionXpath.Length == 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_STRING, "sectionXpath");
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

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

            try
            {
                // ハッシュテーブルから読み込んだ外部設定ファイルのマージ
                foreach (XmlDocument value in xmls.Values)
                {
                    if (mergedXmlDoc == null)
                    {
                        // 1つ目の基準となる外部設定ファイルの指定
                        mergedXmlDoc = value;
                    }
                    else
                    {
                        // 2つ目以降の外部設定ファイルの指定
                        XmlDocument xmlDocSource = new XmlDocument();
                        xmlDocSource = value;

                        //マージ処理
                        XmlNodeList srcAppXmlNL = xmlDocSource.SelectNodes(elementXpath, xmlNM);
                        if (srcAppXmlNL.Count == 0)
                        {
                            ArgumentException exception = new ArgumentException(
                                string.Format(Properties.Resources.E_INVALID_ARGUMENT, "elementXpath"));
                            if (_log.IsErrorEnabled)
                            {
                                _log.Error(exception.Message, exception);
                            }
                            throw exception;
                        }
                        foreach (XmlNode xmlCommonNodeSource in srcAppXmlNL)
                        {
                            XmlNode appXmlNode = mergedXmlDoc.ImportNode(xmlCommonNodeSource, true);

                            //追加先のノードをチェックしてノードの追加を行う。
                            XmlNode mergeEle = mergedXmlDoc.SelectSingleNode(sectionXpath, xmlNM);
                            if (mergeEle == null)
                            {
                                string message = string.Format(Properties.Resources.E_INVALID_ARGUMENT, "sectionXpath");
                                ArgumentException exception = new ArgumentException(message);
                                if (_log.IsErrorEnabled)
                                {
                                    _log.Error(message, exception);
                                }
                                throw exception;
                            
                            }
                            mergedXmlDoc.SelectSingleNode(sectionXpath, xmlNM).AppendChild(appXmlNode);
                        }

                    }
                }

                // マージ後のXmlDocumentを検証する
                xmlDocValidator.Validate(mergedXmlDoc);
            }
            catch (ArgumentException)
            {
                throw;
            }
            catch (XmlSchemaException ex)
            {
                string message = string.Format(Properties.Resources.E_CONFIGURATION_INVALID_MERGED_CONFIG, sectionXpath);
                ConfigurationErrorsException exception = new ConfigurationErrorsException(message, ex);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }
            return mergedXmlDoc;
        }
    }
}
