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

namespace TERASOLUNA.Fw.Client.Configuration.Conversion
{
    /// <summary>
    /// アプリケーション構成ファイル内の ConversionConfiguration セクションを表します。
    /// </summary>
    /// <remarks>
    /// アプリケーション構成ファイルに定義されたデータセット変換設定ファイルをマージし、
    /// <see cref="ConvertCollection"/> を生成します。
    /// <para><see cref="ConvertCollection"/> にアクセスする前に、
    /// <see cref="CreateConvertElements"/> メソッドを実行する必要があります。</para>
    /// 
    /// <para>【設定方法】</para>
    /// <para>①&lt;configSections&gt;要素に &lt;section&gt;子要素を追加する。</para>
    /// <para>name 属性 : "ConversionConfiguration"</para>
    /// <para>type 属性 : 本クラスの完全名 + "," + ＦＷのアセンブリ名</para>
    /// <para>②&lt;ConversionConfiguration&gt;&lt;files&gt; 内に、&lt;file&gt;要素を追加する。</para>
    /// <para>path 属性 : 使用するデータセット変換設定ファイルのパス</para>
    /// 
    /// <para>【設定例】</para>
    /// <code>
    /// &lt;configuration&gt;
    ///   &lt;configSections&gt;
    ///     &lt;section name="ConversionConfiguration" 
    ///        type="TERASOLUNA.Fw.Client.Configuration.Conversion.ConversionConfigurationSection, TERASOLUNA.Fw.Client"/&gt;
    ///   &lt;/configSections&gt;
    ///   &lt;ConversionConfiguration&gt;
    ///     &lt;files&gt;
    ///       &lt;file path="conversion1.xml" /&gt;
    ///       &lt;file path="conversion2.xml" /&gt;
    ///     &lt;/files&gt;
    ///   &lt;/ConversionConfiguration&gt;
    /// &lt;/configuration&gt;
    /// </code>
    /// </remarks>
    public class ConversionConfigurationSection : ConfigurationSection
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(ConversionConfigurationSection));

        /// <summary>
        /// file 要素のコレクションを表す名前です。
        /// </summary>
        /// <remarks>
        /// file 要素の親要素名を変更する場合は、この値を変更してください。
        /// この定数の値は "files" です。
        /// </remarks>
        private const string ELEMENT_NAME_FILES = "files";

        /// <summary>
        /// convert 要素のコレクションを表す名前です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "conversionConfiguration" です。
        /// </remarks>
        private const string ELEMENT_NAME_CONVERSION_CONFIGURATION = "conversionConfiguration";
        
        /// <summary>
        /// データセット変換設定ファイルの XML 名前空間です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "http://www.terasoluna.jp/schema/ConversionSchema.xsd" です。
        /// </remarks>
        private const string XML_NAMESPACE
            = "http://www.terasoluna.jp/schema/ConversionSchema.xsd";

        /// <summary>
        /// XML スキーマファイルの完全名です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "TERASOLUNA.Fw.Client.Configuration.Conversion.ConversionSchema.xsd" です。
        /// </remarks>
        private const string XML_SCHEMA_FILE_NAME
            = "TERASOLUNA.Fw.Client.Configuration.Conversion.ConversionSchema.xsd";

        /// <summary>
        /// conversionConfiguration セクションの要素名です。
        /// </summary>
        /// <remarks>
        /// MergeConfiguration で使用します。
        /// この定数の値は "conversionConfiguration" です。
        /// </remarks>
        private const string SECTION_NAME = "conversionConfiguration";

        /// <summary>
        /// データセット変換設定ファイルのXML名前空間プレフィックスです。
        /// </summary>
        /// <remarks>
        /// MergeConfiguration で使用します。
        /// この定数の値は "cv" です。
        /// </remarks>
        private const string XML_NAMESPACE_PREFIX = "cv";

        /// <summary>
        /// データセット変換設定ファイルマージ処理で基準とする要素の XPATH です。
        /// </summary>
        /// <remarks>
        /// MergeConfiguration で使用します。
        /// この定数の値は <see cref="XML_NAMESPACE_PREFIX"/> + ":conversionConfiguration" です。
        /// </remarks>
        private const string SECTION_XPATH = XML_NAMESPACE_PREFIX + ":conversionConfiguration";

        /// <summary>
        /// データセット変換設定ファイルマージ処理でマージする要素の XPATH です。
        /// </summary>
        /// <remarks>
        /// MergeConfiguration で使用します。
        /// この定数の値は <see cref="SECTION_XPATH"/> + "/" + <see cref="XML_NAMESPACE_PREFIX"/> + ":convert" です。
        /// </remarks>
        private const string ELEMENT_XPATH = SECTION_XPATH + "/" + XML_NAMESPACE_PREFIX + ":convert";

        /// <summary>
        /// convert 要素のキーとなる id を表す属性名です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "id" です。
        /// </remarks>
        private const string ATTRIBUTE_NAME_EVENT_ID = "id";

        /// <summary>
        /// convert 要素内の子要素 param を表す要素名です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "param" です。
        /// </remarks>
        private const string ELEMENT_NAME_PARAMS = "param";

        /// <summary>
        /// convert 要素内の子要素 result を表す要素名です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "result" です。
        /// </remarks>
        private const string ELEMENT_NAME_RESULT = "result";

        /// <summary>
        /// convert 要素内の子要素 clear-view を表す要素名です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "clear-view" です。
        /// </remarks>
        private const string ELEMENT_NAME_CLEAR_VIEW = "clear-view";

        /// <summary>
        /// param 要素内の子要素 column を表す要素名です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "column" です。
        /// </remarks>
        private const string ELEMENT_NAME_COLUMN = "column";

        /// <summary>
        /// clear-view 要素の、画面 <see cref="System.Data.DataSet"/> のテーブル名を表す属性名です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "table" です。
        /// </remarks>
        private const string ATTRIBUTE_NAME_TABLE_NAME = "table"; 

        /// <summary>
        /// column 要素のテーブル名とカラム名を表す属性名です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "src" です。
        /// </remarks>
        private const string ATTRIBUTE_NAME_SRC_NAME = "src";

        /// <summary>
        /// column 要素のテーブル名とカラム名を表す属性名です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "dest" です。
        /// </remarks>
        private const string ATTRIBUTE_NAME_DEST_NAME = "dest";

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

        /// <summary>
        /// files 要素を取得します。
        /// </summary>
        /// <remarks>
        /// ConversionConfiguration セクションから <see cref="FilesCollection"/> を取得します。
        /// </remarks>
        /// <value>
        /// files 要素のコレクション。
        /// </value>
        [ConfigurationProperty(ELEMENT_NAME_FILES, IsDefaultCollection = false, IsRequired = true)]
        public FilesCollection Files
        {
            get
            {
                FilesCollection filesCollection = (FilesCollection)base[ELEMENT_NAME_FILES];
                return filesCollection;
            }
        }
        
        /// <summary>
        /// conversionConfiguration セクションから convert 要素のコレクションを取得します。
        /// </summary>
        /// <remarks>
        /// conversionConfiguration セクションから <see cref="ConvertCollection"/> を取得します。
        /// </remarks>
        /// <value>
        /// convert 要素のコレクション。
        /// </value>
        [ConfigurationProperty(ELEMENT_NAME_CONVERSION_CONFIGURATION, IsDefaultCollection = false, IsRequired = true)]
        public ConvertCollection ConvertCollection
        {
            get
            {
                ConvertCollection convertCollection = (ConvertCollection)base[ELEMENT_NAME_CONVERSION_CONFIGURATION];
                return convertCollection;
            }
        }

        /// <summary>
        /// files 要素の設定内容から convert 要素を生成して conversionConfiguration 要素に追加します。
        /// </summary>
        /// <remarks>
        /// <para><see cref="MergeConfiguration"/> クラスのインスタンスを生成し、データセット変換設定ファイルの設定情報をマージします。</para>
        /// <para>マージしたデータセット変換設定ファイルの内容から <see cref="ConvertElement"/> クラスのインスタンスを生成し、
        /// <see cref="ConvertCollection"/> に追加します。</para>
        /// </remarks>
        public virtual void CreateConvertElements()
        {
            // データセット変換設定ファイルの設定情報をマージする
            MergeConfiguration mgConfig
                = new MergeConfiguration(XML_NAMESPACE, XML_NAMESPACE_PREFIX, XML_SCHEMA_FILE_NAME);
            mgConfig.LoadConfig(SECTION_NAME, Files);
            XmlDocument xmlDoc = mgConfig.MergeXmlDocument(SECTION_XPATH, ELEMENT_XPATH);

            // データセット変換設定ファイルの設定情報をconversionConfiguration要素に追加する
            this.AddConvertElements(xmlDoc);
        }

        /// <summary>
        /// データセット変換設定ファイルの内容から convert 要素を生成し、
        /// conversionConfiguration 要素に追加します。
        /// </summary>
        /// <remarks>
        /// <see cref="XmlDocument"/> から <see cref="ConvertElement"/> クラスのインスタンスを生成し、<see cref="ConvertCollection"/> に追加します。
        /// </remarks>
        /// <param name="xmlDoc">データセット変換設定ファイルの内容を保持する
        /// <see cref="System.Xml.XmlDocument" /> 。</param>
        protected virtual void AddConvertElements(XmlDocument xmlDoc)
        {
            XmlNodeList list = xmlDoc.DocumentElement.ChildNodes;

            // <convert>要素分繰り返す
            foreach (XmlNode node in list)
            {
                if (node is XmlElement)
                {
                    ParamElement blogicParam = null;
                    ResultElement blogicResult = null;
                    
                    string id;
                    id = node.Attributes[ATTRIBUTE_NAME_EVENT_ID].Value;

                    // node から <param> ノードを抽出
                    XmlElement paramElement = node[ELEMENT_NAME_PARAMS];
                    if (paramElement != null)
                    {
                        // param 要素内の column 要素を作成して、columns コレクションに追加
                        ColumnCollection paramCollection = new ColumnCollection();
                        AddParamElement(paramCollection, paramElement);

                        blogicParam = new ParamElement(paramCollection);
                    }
                    
                    // node から <result> ノードを抽出
                    XmlElement resultElement = node[ELEMENT_NAME_RESULT];
                    if (resultElement != null)
                    {
                        // result 要素内の clear-view要素と column 要素を作成して、
                        // それぞれ clear-views コレクション、columns コレクションに追加。
                        ClearViewCollection clearViewCollection = new ClearViewCollection();
                        ColumnCollection paramCollection = new ColumnCollection();
                        AddResultElements(clearViewCollection, paramCollection, resultElement);

                        blogicResult = new ResultElement(clearViewCollection, paramCollection);
                    }

                    // convert 要素を作成
                    ConvertElement element = new ConvertElement(id, blogicParam, blogicResult);
                    this.ConvertCollection.Add(element);
                }
            }
        }

        /// <summary>
        /// <see cref="XmlNode"/> から、 result 要素の子要素を表す
        /// <see cref="ClearViewElement"/> と <see cref="ColumnElement"/> を作成し、
        /// それぞれ <see cref="ClearViewCollection"/> 、<see cref="ColumnCollection"/> に追加します。
        /// </summary>
        /// <param name="clearViews">clear-view 要素を追加する <see cref="ClearViewCollection"/> 。</param>
        /// <param name="columns">column 要素を追加する <see cref="ColumnCollection"/> 。</param>
        /// <param name="resultElement">clear-view ノード、 column ノードを保持する <see cref="XmlNode"/> 。</param>
        protected virtual void AddResultElements(ClearViewCollection clearViews, ColumnCollection columns, XmlNode resultElement)
        {
            foreach (XmlNode xmlElement in resultElement.ChildNodes)
            {
                if (xmlElement is XmlElement)
                {
                    if (ELEMENT_NAME_CLEAR_VIEW.Equals(xmlElement.Name))
                    {
                        // clear-view 要素だった場合
                        ClearViewElement clearViewElement = CreateClearViewElement(xmlElement);
                        clearViews.Add(clearViewElement);
                    }
                    else
                    {
                        // column 要素だった場合
                        ColumnElement columnElement = CreateColumnElement(xmlElement);
                        columns.Add(columnElement);
                    }
                }
            }
        }

        /// <summary>
        /// <see cref="XmlNode"/> から、 param 要素の子要素を表す 
        /// <see cref="ColumnElement"/> を作成し、<see cref="ColumnCollection"/> に追加します。
        /// </summary>
        /// <param name="columns">column 要素を追加する <see cref="ColumnCollection"/> 。</param>
        /// <param name="xmlParams">column ノードを保持する <see cref="XmlNode"/> 。</param>
        protected virtual void AddParamElement(ColumnCollection columns, XmlNode xmlParams)
        {
            foreach (XmlNode xmlParam in xmlParams.ChildNodes)
            {
                if (xmlParam is XmlElement)
                {
                    ColumnElement columnElement = CreateColumnElement(xmlParam);
                    columns.Add(columnElement);
                }
            }
        }

        /// <summary>
        /// XML ノードから新しい column 要素を作成します。
        /// </summary>
        /// <param name="xmlColumn">column ノード。例：&lt;column src="foo.col1" /&gt; 。</param>
        /// <returns>新しい <see cref="ColumnElement"/> 。</returns>
        /// <exception cref="TerasolunaException">column 要素の必須属性が設定されていません。</exception>
        protected virtual ColumnElement CreateColumnElement(XmlNode xmlColumn)
        {
            string srcColName = null;
            string destColName = null;

            if (xmlColumn.Attributes[ATTRIBUTE_NAME_SRC_NAME] == null)
            {
                string message = string.Format(
                    Client.Properties.Resources.E_CONVERSION_ATTRIBUTE_NOT_FOUND,
                    ELEMENT_NAME_COLUMN, ATTRIBUTE_NAME_SRC_NAME);
                TerasolunaException exception = new TerasolunaException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }
            
            srcColName = xmlColumn.Attributes[ATTRIBUTE_NAME_SRC_NAME].InnerText;

            if (xmlColumn.Attributes[ATTRIBUTE_NAME_DEST_NAME] != null)
            {
                destColName = xmlColumn.Attributes[ATTRIBUTE_NAME_DEST_NAME].InnerText;
            }

            ColumnElement ret = new ColumnElement(srcColName, destColName);
            return ret;
        }

        /// <summary>
        /// XML ノードから新しい clear-view 要素を作成します。
        /// </summary>
        /// <param name="xmlClearTable"><c>clear-view</c> ノード。</param>
        /// <returns>新しい <see cref="ClearViewElement"/> 。</returns>
        /// <exception cref="TerasolunaException">clear-view 要素の必須属性が設定されていません。</exception>
        protected virtual ClearViewElement CreateClearViewElement(XmlNode xmlClearTable)
        {
            string viewTableName = null;

            if (xmlClearTable.Attributes[ATTRIBUTE_NAME_TABLE_NAME] == null)
            {
                string message = string.Format(
                    Client.Properties.Resources.E_CONVERSION_ATTRIBUTE_NOT_FOUND,
                    ELEMENT_NAME_CLEAR_VIEW, ATTRIBUTE_NAME_TABLE_NAME);
                TerasolunaException exception = new TerasolunaException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            viewTableName = xmlClearTable.Attributes[ATTRIBUTE_NAME_TABLE_NAME].InnerText;

            ClearViewElement clearViewElement = new ClearViewElement(viewTableName);
            return clearViewElement;
        }
    }
}
