// 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.Data;
using System.Text.RegularExpressions;
using TERASOLUNA.Fw.Common.Logging;
using TERASOLUNA.Fw.Common.Properties;

namespace TERASOLUNA.Fw.Common.Validation
{
    /// <summary>
    /// 入力値検証機能内で利用する共通関数を定義します。
    /// </summary>
    public static class ValidationUtils
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(ValidationUtils));

        /// <summary>
        /// XPath の第一階層を取得します。
        /// </summary>
        /// <param name="xpath" >XPath 。</param>
        /// <returns>
        /// <para>
        /// 最初の'/'以前の文字列。 <paramref name="xpath"/> が'/'を含まない場合は、
        ///  xpath をそのまま返し、<paramref name="xpath"/> が空文字か null ならば空文字を返す。
        /// </para>
        /// <list type="table">
        /// <listheader>
        /// <term>xpath</term>
        /// <description>返り値</description>
        /// </listheader>
        /// <item>
        /// <term>null</term>
        /// <description>""</description>
        /// </item>
        /// <item>
        /// <term>""</term>
        /// <description>""</description>
        /// </item>
        /// <item>
        /// <term>aaa</term>
        /// <description>aaa</description>
        /// </item>
        /// <item>
        /// <term>aaa/bbb</term>
        /// <description>aaa</description>
        /// </item>
        /// </list>
        /// </returns>
        public static string GetFirstPath(string xpath)
        {
            string result;
            if (string.IsNullOrEmpty(xpath))
            {
                return string.Empty;
            }

            int slash = xpath.IndexOf('/');
            if (slash < 0)
            {
                // 一階層のみであれば、xpathをそのまま返す。
                result = xpath;
            }
            else
            {
                // 先頭からslashまでをcomplexTypeとみなす。
                result = xpath.Substring(0, slash);
            }

            return result;
        }

        /// <summary>
        /// XPath の第二階層以降を取得します。
        /// </summary>
        /// <param name="xpath">XPath 。</param>
        /// <returns><paramref name="xpath"/> の第二階層以降。
        /// 一階層しかなければ空文字を返します。
        /// <paramref name="xpath"/> が空文字か null ならば空文字を返します。
        /// <list type="table">
        /// <listheader>
        /// <term>xpath</term>
        /// <description>返り値</description>
        /// </listheader>
        /// <item>
        /// <term>null</term>
        /// <description>""</description>
        /// </item>
        /// <item>
        /// <term>""</term>
        /// <description>""</description>
        /// </item>
        /// <item>
        /// <term>aaa</term>
        /// <description>""</description>
        /// </item>
        /// <item>
        /// <term>aaa/bbb</term>
        /// <description>bbb</description>
        /// </item>
        /// <item>
        /// <term>aaa/bbb/ccc</term>
        /// <description>bbb/ccc</description>
        /// </item>
        /// </list>
        /// </returns>
        public static string GetFollowingPath(string xpath)
        {
            string result;

            if (string.IsNullOrEmpty(xpath))
            {
                return string.Empty;
            }

            int slash = xpath.IndexOf('/');

            if (slash < 0)
            {
                return null;
            }
            else
            {
                result = xpath.Substring(slash + 1);
            }

            return result;
        }

        /// <summary>
        /// XPath の最後のエレメント名を取得します。
        /// </summary>
        /// <param name="xpath">XPath 。</param>
        /// <returns>
        /// <para>
        /// 最後のエレメント名を返します。
        /// </para>
        /// <list type="table">
        /// <listheader>
        /// <description>xpath</description>
        /// <description>返り値</description>
        /// </listheader>
        /// <item>
        /// <description>null</description>
        /// <description>""(空文字列)</description>
        /// </item>
        /// <item>
        /// <description>""(空文字列)</description>
        /// <description>""(空文字列)</description>
        /// </item>
        /// <item>
        /// <description>aaa</description>
        /// <description>aaa</description>
        /// </item>
        /// <item>
        /// <description>aaa/bbb</description>
        /// <description>bbb</description>
        /// </item>
        /// <item>
        /// <description>aaa/bbb/ccc</description>
        /// <description>ccc</description>
        /// </item>
        /// </list>
        /// </returns>
        public static string GetLastPath(string xpath)
        {
            string result;
            if (string.IsNullOrEmpty(xpath))
            {
                return string.Empty;
            }

            int slash = xpath.LastIndexOf('/');
            if (slash < 0)
            {
                // 一階層のみであれば、rulePath をそのまま返す。
                result = xpath;
            }
            else
            {
                // slashから最後までを返す。
                result = xpath.Substring(slash + 1);
            }

            return result;
        }

        /// <summary>
        /// <see cref="DataSet"/> から xpath で指定した範囲の <see cref="DataRow"/> リストを取得します。
        /// </summary>
        /// <param name="dataSet">検索対象 <see cref="DataSet"/> 。</param>
        /// <param name="xpath"><paramref name="dataSet"/> 内の <see cref="DataRow"/> を特定する XPath 。</param>
        /// <returns>
        /// <para>
        /// <paramref name="dataSet"/> から <paramref name="xpath"/> に合致した <see cref="DataRow"/> のリストを返します。
        /// 該当がなければ長さ 0 のリストを返します。
        /// </para>
        ///  (以下の例は、 AAA テーブルと BBB テーブルには <see cref="DataRelation"/> が張られている場合です。
        ///  Ca は AAA テーブルのカラム名、 Cb は BBB テーブルのカラム名を表します。)
        /// <list type="table">
        /// <listheader>
        /// <description>xpath</description>
        /// <description>返り値</description>
        /// </listheader>
        /// <item>
        /// <description>null</description>
        /// <description>長さ0のリストを返します。</description>
        /// </item>
        /// <item>
        /// <description>""(空文字列)</description>
        /// <description>長さ0のリストを返します。</description>
        /// </item>
        /// <item>
        /// <description>AAA</description>
        /// <description>AAAテーブルの全行を返します。</description>
        /// </item>
        /// <item>
        /// <description>AAA/Ca</description>
        /// <description>xpath="AAA"の場合と同じです。</description>
        /// </item>
        /// <item>
        /// <description>AAA[x]</description>
        /// <description>AAAテーブルのx行目を返します。</description>
        /// </item>
        /// <item>
        /// <description>AAA[x]/Ca</description>
        /// <description>xpath="AAA[x]"の場合と同じです。</description>
        /// </item>
        /// <item>
        /// <description>AAA/BBB</description>
        /// <description>AAAの全行にひもづくBBBの全行を返します。</description>
        /// </item>
        /// <item>
        /// <description>AAA/BBB/Cb</description>
        /// <description>xpath="AAA/BBB"の場合と同じです。</description>
        /// </item>
        /// <item>
        /// <description>AAA[x]/BBB</description>
        /// <description>AAAのx行にひもづくBBBの全行を返します。</description>
        /// </item>
        /// <item>
        /// <description>AAA[x]/BBB/Cb</description>
        /// <description>xpath="AAA[x]/BBB"の場合と同じです。</description>
        /// </item>
        /// <item>
        /// <description>AAA[x]/BBB[y]</description>
        /// <description>AAAのx行にひもづくBBBの全行のy行目を返します。</description>
        /// </item>
        /// <item>
        /// <description>AAA[x]/BBB[y]/Cb</description>
        /// <description>xpath="AAA[x]/BBB[y]"の場合と同じです。</description>
        /// </item>
        /// <item>
        /// <description>AAA[x]/BBB[y]/Ca</description>
        /// <description>BBBテーブルにはCaカラムは存在しないので、長さ0のリストを返します。</description>
        /// </item>
        /// </list>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="dataSet"/> が null 参照です。
        /// </exception>
        public static IList<DataRow> GetRowList(DataSet dataSet, string xpath)
        {
            if (dataSet == null)
            {
                ArgumentNullException exception = new ArgumentNullException("dataSet");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "dataSet"), exception);
                }
                throw exception;
            }

            IList<DataRow> result = new List<DataRow>();

            string firstPath = GetFirstPath(xpath);
            string firstPathWithoutIndex = GetXPathWithoutIndex(firstPath);
            int index = GetIndex(firstPath);

            if (!dataSet.Tables.Contains(firstPathWithoutIndex))
            {
                return result;
            }

            DataTable table = dataSet.Tables[firstPathWithoutIndex];

            // インデックス指定が無い場合
            if (index == 0)
            {
                foreach (DataRow row in table.Rows)
                {
                    result.Add(row);
                }
            }
            else if ((index >= 1) && (table.Rows.Count >= index))
            {
                // 0以上のインデックス指定がある場合
                result.Add(table.Rows[index - 1]);
            }
            else
            {
                // 異常なインデックス指定がある場合。
                return result;
            }

            string secondPath = GetFollowingPath(xpath);

            if (secondPath == null)
            {
                return result;
            }

            result = GetRowListSub(result, secondPath);

            return result;
        }

        /// <summary>
        /// <paramref name="list"/> から、 <paramref name="xpath"/> に該当する <see cref="DataRow"/> を取得します。
        /// </summary>
        /// <remarks>
        /// <see cref="ValidationUtils.GetRowList"/> から呼び出す内部関数です。外部からの使用は行わないでください。
        /// </remarks>
        /// <param name="list"><see cref="DataRow"/> のリスト。 xpath の先頭のパスと同じテーブルにある行。</param>
        /// <param name="xpath">XPath 。</param>
        /// <returns>xpathに該当する <see cref="DataRow"/> のリスト。</returns>
        private static IList<DataRow> GetRowListSub(IList<DataRow> list, string xpath)
        {
            // リンク元が無い場合
            if (list.Count == 0)
            {
                return list;
            }

            // 検索終了(complexType)
            if (string.IsNullOrEmpty(xpath))
            {
                return list;
            }

            // 最後がsimpleTypeの場合          
            if (xpath.IndexOf('/') < 0 && list[0].Table.Columns.Contains(xpath))
            {
                return list;
            }

            string firstPath = GetFirstPath(xpath);

            // これ以降にくるものは、リレーションテーブル
            IList<DataRow> result = new List<DataRow>();
            string firstPathWithoutIndex = GetXPathWithoutIndex(firstPath);
            int firstPathIndex = GetIndex(firstPath);

            IList<DataRelation> childRelations = new List<DataRelation>();
            foreach (DataRelation relation in list[0].Table.ChildRelations)
            {
                // firstPathに合致するリレーションを抜き出す。
                if (relation.ChildTable.TableName.Equals(firstPathWithoutIndex))
                {
                    childRelations.Add(relation);
                }
            }

            // リレーションテーブルからデータを抜き出す。
            foreach (DataRow row in list)
            {
                foreach (DataRelation relation in childRelations)
                {
                    DataRow[] rowArray = row.GetChildRows(relation);
                    foreach (DataRow childRow in rowArray)
                    {
                        result.Add(childRow);
                    }
                }
            }

            if (firstPathIndex == 0)
            {
                // インデックス指定なしなので、そのままでよい。
            }
            else if ((firstPathIndex >= 1) && (firstPathIndex <= result.Count))
            {
                DataRow dataRow = (DataRow)result[firstPathIndex - 1];
                result = new List<DataRow>();
                result.Add(dataRow);
            }
            else
            {
                // 異常なインデックス指定は空で返す。
                result = new List<DataRow>();
                return result;
            }

            string secondPath = GetFollowingPath(xpath);

            return GetRowListSub(result, secondPath);
        }

        /// <summary>
        /// XPath 内のインデックス指定([ x ])を削除します。
        /// </summary>
        /// <param name="xpath">XPath 。</param>
        /// <returns>
        /// <para>
        /// インデックス削除後の XPath を返します。
        /// </para>
        /// <list type="table">
        /// <listheader>
        /// <description>xpath</description>
        /// <description>返り値</description>
        /// </listheader>
        /// <item>
        /// <description>null</description>
        /// <description>null</description>
        /// </item>
        /// <item>
        /// <description>""</description>
        /// <description>null</description>
        /// </item>
        /// <item>
        /// <description>aaa</description>
        /// <description>aaa</description>
        /// </item>
        /// <item>
        /// <description>aaa[x]</description>
        /// <description>aaa</description>
        /// </item>
        /// <item>
        /// <description>aaa/bbb</description>
        /// <description>aaa/bbb</description>
        /// </item>
        /// <item>
        /// <description>aaa[x]/bbb[y]</description>
        /// <description>aaa/bbb</description>
        /// </item>
        /// </list>
        /// </returns>
        public static string GetXPathWithoutIndex(string xpath)
        {
            if (string.IsNullOrEmpty(xpath))
            {
                return null;
            }
            Regex regex = new Regex(@"\[[^[]+\]");
            string xpathWithoutIndex = regex.Replace(xpath, "");
            return xpathWithoutIndex;
        }

        /// <summary>
        /// "[ x ]"で指定されるインデックスを取得する。
        /// </summary>
        /// <remarks>
        /// インデックスは必ず 1 以上でなければなりません。
        /// </remarks>
        /// <param name="xpath">インデックスがついている可能性のあるパス。</param>
        /// <returns>
        /// <para>
        /// インデックスが存在すれば、その値( 1 以上)を返します。
        /// インデックス指定が存在しなければ、 0 を返します。
        /// </para>
        /// <list type="table">
        /// <listheader>
        /// <description>xpath</description>
        /// <description>返り値</description>
        /// </listheader>
        /// <item>
        /// <description>null</description>
        /// <description>0</description>
        /// </item>
        /// <item>
        /// <description>""</description>
        /// <description>0</description>
        /// </item>
        /// <item>
        /// <description>aaa</description>
        /// <description>0</description>
        /// </item>
        /// <item>
        /// <description>aaa[1]</description>
        /// <description>1</description>
        /// </item>
        /// <item>
        /// <description>aaa[1]/bbb[2]</description>
        /// <description>2</description>
        /// </item>
        /// </list>
        /// </returns>
        /// <exception cref="FormatException">
        /// <paramref name="xpath"/> 内のインデックスに 0 以下の数値を指定した場合、
        /// またはインデックスに整数型でない値を設定した場合。
        /// </exception>
        public static int GetIndex(string xpath)
        {
            if (xpath == null)
            {
                return 0;
            }

            int leftBracket = xpath.LastIndexOf('[');
            int rightBracket = xpath.LastIndexOf(']');
            if (leftBracket < 0 || rightBracket < 0 || leftBracket > rightBracket)
            {
                return 0;
            }

            string index = xpath.Substring(leftBracket + 1, rightBracket - leftBracket - 1);
            int result = 0;
            if (int.TryParse(index, out result))
            {
                if (result <= 0)
                {
                    string message = string.Format(Resources.E_VALIDATION_XPATH_NEGATIVE_INDEX, xpath);
                    FormatException exception = new FormatException(message);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(message, exception);
                    }
                    throw exception;
                }
            }
            else
            {
                string message = string.Format(Resources.E_VALIDATION_XPATH_INVALID_INDEX, xpath);
                FormatException exception = new FormatException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            return result;
        }

    }
}
