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

namespace TERASOLUNA.Fw.Web.Session
{
    /// <summary>
    /// セッションのライフサイクルレベルを管理します。
    /// </summary>
    public class SessionManager
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(SessionManager));

        /// <summary>
        /// レベル管理マスタをセッションに登録するときのセッション キーです。
        /// </summary>
        /// <remarks>
        /// この定数の値は "SESSION_MANAGER_LEVEL_MASTER" です。
        /// </remarks>
        public static readonly string SESSION_MANAGER_LEVEL_MASTER = "SESSION_MANAGER_LEVEL_MASTER";

        /// <summary>
        /// キー管理マスタをセッションに登録するときのセッション キーです。
        /// </summary>
        /// <remarks>
        /// この定数の値は "SESSION_MANAGER_KEY_MASTER" です。
        /// </remarks>
        public static readonly string SESSION_MANAGER_KEY_MASTER = "SESSION_MANAGER_KEY_MASTER";

        /// <summary>
        /// セッションです。
        /// </summary>
        private HttpSessionState _session = null;

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

        /// <summary>
        /// <see cref="HttpSessionState"/> を取得します。
        /// </summary>
        protected virtual HttpSessionState Session
        {
            get { return _session; }
        }

        /// <summary>
        /// レベル管理マスタをセッションから取得します。
        /// </summary>
        /// <value>ライフサイクル レベルごとの項目を管理するためのレベル管理マスタ。</value>
        protected virtual IDictionary<int, IList<string>> LevelMaster
        {
            get
            {
                if (Session[SESSION_MANAGER_LEVEL_MASTER] == null)
                {
                    lock (_syncRoot)
                    {
                        if (Session[SESSION_MANAGER_LEVEL_MASTER] == null)
                        {
                            Session[SESSION_MANAGER_LEVEL_MASTER] = new Dictionary<int, IList<string>>();
                        }
                    }

                }
                return (IDictionary<int, IList<string>>)Session[SESSION_MANAGER_LEVEL_MASTER];
            }
        }

        /// <summary>
        /// キー管理マスタをセッションから取得します。
        /// </summary>
        /// <value>セッション キーに対応したライフサイクル レベルを管理するためのキー管理マスタ。</value>
        protected virtual IDictionary<string, int> KeyMaster
        {
            get
            {
                if (Session[SESSION_MANAGER_KEY_MASTER] == null)
                {
                    lock (_syncRoot)
                    {
                        if (Session[SESSION_MANAGER_KEY_MASTER] == null)
                        {
                            Session[SESSION_MANAGER_KEY_MASTER] = new Dictionary<string, int>();
                        }
                    }
                }
                return (IDictionary<string, int>)Session[SESSION_MANAGER_KEY_MASTER];
            }
        }

        /// <summary>
        /// <see cref="SessionManager"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <param name="session">セッション。</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="session"/> が null 参照です。
        /// </exception>
        public SessionManager(HttpSessionState session)
        {
            if (session == null)
            {
                ArgumentNullException exception = new ArgumentNullException("session");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "session"), exception);
                }
                throw exception;
            }

            _session = session;
        }

        /// <summary>
        /// ライフサイクル管理されている個別のセッション値を取得します。
        /// </summary>
        /// <param name="name">セッション値のキー名。 </param>
        /// <returns>指定された名前のセッション状態値。 </returns>
        /// <remarks>
        /// <para>読み取り専用です。セッションへの登録はできません。</para>
        /// <para>指定された <paramref name="name"/> の要素がライフサイクル管理下にない場合は、null が返されます。</para>
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="name"/> が null 参照です。
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="name"/> が空文字列です。
        /// </exception>
        public virtual object this[string name]
        {
            get
            {
                if (name == null)
                {
                    ArgumentNullException exception = new ArgumentNullException("name");
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(string.Format(
                            Properties.Resources.E_NULL_ARGUMENT, "name"), exception);
                    }
                    throw exception;
                }
                if (name.Length == 0)
                {
                    string message = string.Format(Properties.Resources.E_EMPTY_STRING, "name");
                    ArgumentException exception = new ArgumentException(message);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(message, exception);
                    }
                    throw exception;
                }

                object value = null;
                IDictionary<string, int> keyMaster = KeyMaster;
                if (keyMaster.ContainsKey(name))
                {
                    value = Session[name];
                }

                return value;
            }
        }

        /// <summary>
        /// 新しい項目をセッションに追加し、ライフサイクル管理します。
        /// または、既存のライフサイクル管理されている項目を更新します。
        /// </summary>
        /// <param name="name">セッションに追加する項目の名前。</param>
        /// <param name="value">セッションに追加する項目の値。</param>
        /// <param name="lifeCycleLevel">セッションに追加する項目のライフサイクル レベル。</param>
        /// <remarks>
        /// <para>ライフサイクル レベルには0以上の整数を指定してください。</para>
        /// <para>ライフサイクル レベル0が最も浅い階層となります。</para>
        /// <para>
        /// <paramref name="name"/> が既存のライフサイクル レベル管理項目を参照する場合、
        /// 既存の項目が、指定された <paramref name="value"/> によって上書きされます。
        /// </para>
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="name"/> が null 参照です。
        /// </exception>
        /// <exception cref="ArgumentException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="name"/> が空文字列です。
        /// </item>
        /// <item>
        /// <paramref name="lifeCycleLevel"/> に 0 より小さい整数が指定されました。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="TerasolunaException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// ライフサイクル管理外のセッションで、すでに同じキーで項目が登録されています。
        /// </item>
        /// <item>
        /// <paramref name="name"/> は <paramref name="lifeCycleLevel"/> と異なる
        /// ライフサイクル レベルで、すでに管理されています。
        /// </item>
        /// </list>
        /// </exception>
        public virtual void Add(string name, Object value, int lifeCycleLevel)
        {
            if (name == null)
            {
                ArgumentNullException exception = new ArgumentNullException("name");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "name"), exception);
                }
                throw exception;
            }
            if (name.Length == 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_STRING, "name");
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }
            if (lifeCycleLevel < 0)
            {
                string message = string.Format(Properties.Resources.E_LIFE_CYCLE_LEVEL);
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            // セッション管理に登録
            RegisterItem(name, value, lifeCycleLevel);
        }

        /// <summary>
        /// 指定されたライフサイクル レベルより深いライフサイクル レベルの項目をセッションから削除し、
        /// ライフサイクル管理から外します。
        /// </summary>
        /// <param name="lifeCycleLevel"> セッションから削除する項目のライフサイクル レベル。</param>
        /// <remarks>
        /// <para>
        /// <paramref name="lifeCycleLevel"/> で 1 を指定した場合、ライフサイクル レベル 1,2,3,･･･ の項目を、
        /// セッションから削除し、ライフサイクル管理から外します。
        /// </para>
        /// <para>
        /// 削除しようとしたライフサイクル レベルがライフサイクル レベル管理下にない場合、例外はスローされません。
        /// </para>
        /// </remarks>
        /// <exception cref="ArgumentException">
        /// <paramref name="lifeCycleLevel"/> に 0 より小さい整数が指定されました。
        /// </exception>
        public virtual void Remove(int lifeCycleLevel)
        {
            if (lifeCycleLevel < 0)
            {
                string message = string.Format(Properties.Resources.E_LIFE_CYCLE_LEVEL);
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            // セッション管理からlifeCycleLevelより深い階層の項目を削除
            RemoveLevels(lifeCycleLevel);
        }

        /// <summary>
        /// セッションから項目を削除し、ライフサイクル管理から外します。 
        /// </summary>
        /// <param name="name">セッションから削除し、ライフサイクル管理から外す項目の名前。 </param>
        /// <remarks>
        /// <para>
        /// <paramref name="name"/> で指定された項目がライフサイクル レベル管理下にない場合、例外はスローされません。
        /// </para>
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="name"/> が null 参照です。
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="name"/> が空文字列です。
        /// </exception>
        public virtual void Remove(string name)
        {
            if (name == null)
            {
                ArgumentNullException exception = new ArgumentNullException("name");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "name"), exception);
                }
                throw exception;
            }
            if (name.Length == 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_STRING, "name");
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            // セッション管理からnameをキーとして登録されている項目を削除
            RemoveItem(name);
        }

        /// <summary>
        /// セッションからライフサイクル管理下にある全てのキーと値を削除します。 
        /// </summary>
        public virtual void RemoveAll()
        {
            Remove(0);
        }

        /// <summary>
        /// ライフサイクル管理されているすべての値のキーを取得します。
        /// </summary>
        /// <remarks>
        /// <para>
        /// ライフサイクル レベル管理されていない場合、要素数0の <see cref="IList{String}"/> が返されます。
        /// </para>
        /// </remarks>
        /// <returns>
        /// ライフサイクル管理されているすべての値のキーを格納した <see cref="IList{String}"/>。
        /// </returns>
        public virtual IList<string> GetKeys()
        {
            IDictionary<string, int> keyMaster = KeyMaster;            

            // 文字列のコピーをリストに詰め替える
            IList<string> keyList = new List<string>();
            foreach (string key in keyMaster.Keys)
            {
                keyList.Add(string.Copy(key));
            }

            return keyList;
        }

        /// <summary>
        /// 指定したライフサイクル レベルにあるすべての値のキーを取得します。
        /// </summary>
        /// <param name="lifeCycleLevel">取得したいキーのライフサイクル レベル。</param>
        /// <remarks>
        /// <para>
        /// 指定したライフサイクル レベルが管理されていない場合、要素数 0 の <see cref="IList{String}"/> が返されます。
        /// </para>
        /// </remarks>
        /// <returns>
        /// <paramref name="lifeCycleLevel"/> で指定したライフサイクル レベルにある
        /// すべての値のキーを格納した <see cref="IList{String}"/>。
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="lifeCycleLevel"/> に0より小さい整数が指定されました。
        /// </exception>
        public virtual IList<string> GetKeys(int lifeCycleLevel)
        {
            if (lifeCycleLevel < 0)
            {
                string message = string.Format(Properties.Resources.E_LIFE_CYCLE_LEVEL);
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            IDictionary<int, IList<string>> levelMaster = LevelMaster;
            IList<string> keyList = new List<string>();
            if (levelMaster.ContainsKey(lifeCycleLevel))
            {
                // 文字列のコピーをリストに詰め替える
                foreach (string key in levelMaster[lifeCycleLevel])
                {
                    keyList.Add(string.Copy(key));
                }
            }

            return keyList;
        }

        /// <summary>
        /// ライフサイクル管理している、すべてのライフサイクル レベルを取得します。
        /// </summary>
        /// <remarks>
        /// <para>
        /// ライフサイクル レベルが管理されていない場合、要素数 0 の <see cref="IList{String}"/> が返されます。
        /// </para>
        /// </remarks>
        /// <returns>ライフサイクル管理している、すべてのライフサイクル レベル。</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public virtual IList<int> GetLifeCycleLevels()
        {
            // レベル管理マスタを取得
            IDictionary<int, IList<string>> levelMaster = LevelMaster;

            // レベル管理マスタのキー(レベル)を取得してソート
            int[] levels = new int[levelMaster.Count];
            levelMaster.Keys.CopyTo(levels, 0);
            Array.Sort(levels);

            // ソートしたキー(レベル)をリストに詰め替える
            IList<int> levelList = new List<int>();
            for (int i = 0; i < levels.Length; i++)
            {
                levelList.Add(levels[i]);
            }

            return levelList;
        }

        /// <summary>
        /// 指定した項目名のライフサイクル レベルを取得します。
        /// </summary>
        /// <param name="name">ライフサイクル レベルを取得したい項目名。</param>
        /// <remarks>
        /// <para>
        /// 指定した項目名がライフサイクル管理されていない場合、-1 が返されます。
        /// </para>
        /// </remarks>
        /// <returns>ライフサイクル レベル。</returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="name"/> が null 参照です。
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="name"/> が空文字列です。
        /// </exception>
        public virtual int GetLifeCycleLevel(string name)
        {
            if (name == null)
            {
                ArgumentNullException exception = new ArgumentNullException("name");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "name"), exception);
                }
                throw exception;
            }
            if (name.Length == 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_STRING, "name");
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            IDictionary<string, int> keyMaster = KeyMaster;
            int level = -1;
            if (keyMaster.ContainsKey(name))
            {
                level = keyMaster[name];
            }

            return level;
        }

        /// <summary>
        /// 新しい項目をセッションに追加し、ライフサイクル管理します。
        /// </summary>
        /// <param name="name">セッションに追加する項目の名前。</param>
        /// <param name="value">セッションに追加する項目の値。</param>
        /// <param name="lifeCycleLevel">セッションに追加する項目のライフサイクル レベル。</param>
        /// <exception cref="TerasolunaException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// ライフサイクル管理外のセッションで、すでに同じキーで項目が登録されています。
        /// </item>
        /// <item>
        /// <paramref name="name"/> は <paramref name="lifeCycleLevel"/> と異なる
        /// ライフサイクル レベルで、すでに管理されています。
        /// </item>
        /// </list>
        /// </exception>
        protected virtual void RegisterItem(string name, object value, int lifeCycleLevel)
        {
            IDictionary<int, IList<string>> levelMaster = LevelMaster;
            IDictionary<string, int> keyMaster = KeyMaster;

            // セッションに項目を追加できるか確認
            MasterCheck(name, lifeCycleLevel);

            // ライフサイクルレベルを登録
            IList<string> keyList = null;
            if (levelMaster.ContainsKey(lifeCycleLevel))
            {
                keyList = (IList<string>)levelMaster[lifeCycleLevel];
            }
            else
            {
                keyList = new List<string>();
            }

            if (!keyList.Contains(name))
            {
                keyList.Add(name);
                levelMaster[lifeCycleLevel] = keyList;
                keyMaster[name] = lifeCycleLevel;
            }

            Session[name] = value;
        }

        /// <summary>
        /// 指定したライフサイクル レベルより深いライフサイクル レベルの項目をセッションから削除し、
        /// ライフサイクル管理から外します。
        /// </summary>
        /// <param name="shallowLevel">セッションから削除する項目のライフサイクル レベル。</param>
        /// <remarks>
        /// <paramref name="shallowLevel"/> で 1 を指定した場合、ライフサイクル レベル 1,2,3,･･･ の項目を、
        /// セッションから削除し、ライフサイクル管理から外します。
        /// </remarks>
        protected virtual void RemoveLevels(int shallowLevel)
        {
            IDictionary<int, IList<string>> levelMaster = LevelMaster;
            IDictionary<string, int> keyMaster = KeyMaster;

            // 最も階層の深いライフサイクルレベルを取得
            int[] levels = new int[levelMaster.Count];
            levelMaster.Keys.CopyTo(levels, 0);
            Array.Sort(levels);
            int deepLevel = -1;
            if (levels.Length != 0)
            {
                deepLevel = levels[levels.Length - 1];
            }

            // shallowLevelからdeepLevelまでの項目を削除
            for (int level = shallowLevel; level <= deepLevel; level++)
            {
                if (levelMaster.ContainsKey(level))
                {
                    IList<string> keyList = levelMaster[level];
                    foreach (string key in keyList)
                    {
                        keyMaster.Remove(key);
                        Session.Remove(key);
                    }
                    levelMaster.Remove(level);
                }
            }
        }

        /// <summary>
        /// セッションから項目を削除し、ライフサイクル管理から外します。 
        /// </summary>
        /// <param name="name">セッションから削除し、ライフサイクル管理から外す項目の名前。 </param>
        protected virtual void RemoveItem(string name)
        {
            IDictionary<int, IList<string>> levelMaster = LevelMaster;
            IDictionary<string, int> keyMaster = KeyMaster;

            // ライフサイクル管理している項目を削除
            if (keyMaster.ContainsKey(name))
            {
                int level = keyMaster[name];
                IList<string> keyList = levelMaster[level];
                keyList.Remove(name);
                if (keyList.Count != 0)
                {
                    levelMaster[level] = keyList;
                }
                else
                {
                    levelMaster.Remove(level);
                }

                Session.Remove(name);
                keyMaster.Remove(name);
            }
        }

        /// <summary>
        /// <see cref="SessionManager"/> でセッション管理できるかどうかを判断します。
        /// </summary>
        /// <param name="name">セッションに追加する項目の名前。</param>
        /// <param name="lifeCycleLevel">セッションに追加する項目のライフサイクル レベル。</param>
        /// <exception cref="TerasolunaException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// ライフサイクル管理外のセッションで、すでに <paramref name="name"/> と同じキーで項目が登録されています。
        /// </item>
        /// <item>
        /// <paramref name="name"/> は <paramref name="lifeCycleLevel"/> と異なるライフサイクル レベルで、
        /// すでに管理されています。
        /// </item>
        /// </list>
        /// </exception>
        protected virtual void MasterCheck(string name, int lifeCycleLevel)
        {
            IDictionary<string, int> keyMaster = KeyMaster;
            bool isLifeCycleManaged = keyMaster.ContainsKey(name);

            if (isLifeCycleManaged)
            {
                // ライフサイクル レベル管理されているが、引数のlifeCycleLevelと異なるレベルで管理されていた場合
                int managedLevel = keyMaster[name];
                if (managedLevel != lifeCycleLevel)
                {
                    TerasolunaException exception = new TerasolunaException(string.Format(Properties.Resources.E_MANAGED_LIFE_CYCLE_LEVEL, name, lifeCycleLevel));
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }
            }
            else
            {
                // ライフサイクル レベル管理外のセッションで、すでに同じキーでセッションに登録されていた場合
                bool isRegisteredSession = Session[name] != null;
                if (isRegisteredSession)
                {
                    TerasolunaException exception = new TerasolunaException(string.Format(Properties.Resources.E_REGISTERED_SESSION, name));
                    if(_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }
            }
        }
    }
}
