#region Licence

/*
 * Copyright © 2002-2007 the original author or authors.
 * 
 * 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.
 */

#endregion

#region Imports

using System;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using Common.Logging;
using NHibernate;
using Spring.Threading;
using Spring.Transaction.Support;
using Spring.Util;

#endregion

namespace Spring.Data.NHibernate.Support
{
    /// <summary>
    /// Implementation of SessionScope that associates a single session within the using scope.
    /// </summary>
    /// <remarks>
    /// <para>It is recommended to be used in the following type of scenario:
    /// <code>
    /// using (new SessionScope())
    /// {
    ///    ... do multiple operation, possibly in multiple transactions. 
    /// }
    /// </code>
    /// At the end of "using", the session is automatically closed. All transactions within the scope use the same session,
    /// if you are using Spring's HibernateTemplate or using Spring's implementation of NHibernate 1.2's 
    /// ICurrentSessionContext interface.  
    /// </para>
    /// <para>
    /// It is assumed that the session factory object name is called "SessionFactory". In case that you named the object 
    /// in different way you can specify your can specify it in the application settings using the key
    /// Spring.Data.NHibernate.Support.SessionScope.SessionFactoryObjectName.  Values for EntityInterceptorObjectName
    /// and SingleSessionMode can be specified similarly.
    /// </para>
    /// <para>
    /// <b>Note:</b>
    /// The session is managed on a per thread basis on the thread that opens the scope instance. This means that you must
    /// never pass a reference to a <see cref="SessionScope"/> instance over to another thread!
    /// </para>
    /// </remarks>
    /// <author>Robert M. (.NET)</author>
    /// <author>Harald Radi (.NET)</author>
    public class SessionScope : IDisposable
    {
        #region Fields

        /// <summary>
        /// The logging instance.
        /// </summary>        
        protected readonly ILog log = LogManager.GetLogger(MethodInfo.GetCurrentMethod().DeclaringType);

        private readonly SessionScopeSettings settings;

        // Keys into LogicalThreadContext for runtime values.
        private readonly string PARTICIPATE_KEY;
        private readonly string ISOPEN_KEY;

        #endregion

        #region Constructor (s)

        /// <summary>
        /// Initializes a new instance of the <see cref="SessionScope"/> class in single session mode, 
        /// associating a session with the thread.  The session is opened lazily on demand.
        /// </summary>
        public SessionScope() 
            : this(true)
        {
        }


        /// <summary>
        /// Initializes a new instance of the <see cref="SessionScope"/> class.
        /// </summary>
        /// <param name="open">
        /// If set to <c>true</c> associate a session with the thread.  If false, another
        /// collaborating class will associate the session with the thread, potentially by calling
        /// the Open method on this class.
        /// </param>
        public SessionScope(bool open)
            : this("appSettings", open)
        {
            // noop
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SessionScope"/> class.
        /// </summary>
        /// <param name="sectionName">
        /// The name of the configuration section to read configuration settings from. 
        /// See <see cref="ConfigSectionSessionScopeSettings"/> for more info.
        /// </param>
        /// <param name="open">
        /// If set to <c>true</c> associate a session with the thread.  If false, another
        /// collaborating class will associate the session with the thread, potentially by calling
        /// the Open method on this class.
        /// </param>
        public SessionScope(string sectionName, bool open)
            : this( sectionName, typeof(SessionScope), open)
        {
            // noop
        }


        /// <summary>
        /// Initializes a new instance of the <see cref="SessionScope"/> class.
        /// </summary>
        /// <param name="sectionName">
        /// The name of the configuration section to read configuration settings from. 
        /// See <see cref="ConfigSectionSessionScopeSettings"/> for more info.
        /// </param>
        /// <param name="namespaceType">The type, who's full name is used for prefixing appSetting keys</param>
        /// <param name="open">
        /// If set to <c>true</c> associate a session with the thread.  If false, another
        /// collaborating class will associate the session with the thread, potentially by calling
        /// the Open method on this class.
        /// </param>
        public SessionScope(string sectionName, Type namespaceType, bool open)
          : this(new ConfigSectionSessionScopeSettings(namespaceType, sectionName), open)
        {
          // noop
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SessionScope"/> class.
        /// </summary>
        /// <param name="sessionFactory">
        /// The <see cref="ISessionFactory"/> instance to be used for obtaining <see cref="ISession"/> instances.
        /// </param>
        /// <param name="open">
        /// If set to <c>true</c> associate a session with the thread.  If false, another
        /// collaborating class will associate the session with the thread, potentially by calling
        /// the Open method on this class.
        /// </param>
        public SessionScope(ISessionFactory sessionFactory, bool open)
            : this(new SessionScopeSettings(sessionFactory), open)
        {
            // noop
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SessionScope"/> class.
        /// </summary>
        /// <param name="sessionFactory">
        /// The <see cref="ISessionFactory"/> instance to be used for obtaining <see cref="ISession"/> instances.
        /// </param>
        /// <param name="entityInterceptor">
        /// Specify the <see cref="IInterceptor"/> to be set on each session provided by this <see cref="SessionScope"/> instance.
        /// </param>
        /// <param name="singleSession">
        /// Set whether to use a single session for each request. See <see cref="SingleSession"/> property for details.
        /// </param>
        /// <param name="defaultFlushMode">
        /// Specify the flushmode to be applied on each session provided by this <see cref="SessionScope"/> instance.
        /// </param>
        /// <param name="open">
        /// If set to <c>true</c> associate a session with the thread.  If false, another
        /// collaborating class will associate the session with the thread, potentially by calling
        /// the Open method on this class.
        /// </param>
        public SessionScope(ISessionFactory sessionFactory, IInterceptor entityInterceptor, bool singleSession, FlushMode defaultFlushMode, bool open)
            :this(new SessionScopeSettings(sessionFactory, entityInterceptor, singleSession, defaultFlushMode), open)
        {
            // noop
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SessionScope"/> class.
        /// </summary>
        /// <param name="settings">An <see cref="SessionScopeSettings"/> instance holding the scope configuration</param>
        /// <param name="open">
        /// If set to <c>true</c> associate a session with the thread.  If false, another
        /// collaborating class will associate the session with the thread, potentially by calling
        /// the Open method on this class.
        /// </param>
        public SessionScope(SessionScopeSettings settings, bool open)
        {
            log = LogManager.GetLogger(this.GetType());
            this.settings = settings;

            PARTICIPATE_KEY = UniqueKey.GetInstanceScopedString(this, "Participate");
            ISOPEN_KEY = UniqueKey.GetInstanceScopedString(this, "IsOpen");

            if (open)
            {
                Open();
            }
        }

        #endregion

        #region Properties

        /// <summary>
        /// Set whether to use a single session for each request. Default is "true".
        /// If set to false, each data access operation or transaction will use
        /// its own session (like without Open Session in View). Each of those
        /// sessions will be registered for deferred close, though, actually
        /// processed at request completion.
        /// </summary>
        public bool SingleSession
        {
            get { return settings.SingleSession; }
        }

        /// <summary>
        /// Gets the flushmode to be applied on each newly created session.
        /// </summary>
        /// <remarks>
        /// This property defaults to <see cref="FlushMode.Never"/> to ensure that modifying objects outside the boundaries 
        /// of a transaction will not be persisted. It is recommended to not change this value but wrap any modifying operation
        /// within a transaction.
        /// </remarks>
        public FlushMode DefaultFlushMode
        {
            get { return settings.DefaultFlushMode; }
        }

        /// <summary>
        /// Get or set the configured SessionFactory
        /// </summary>
        public ISessionFactory SessionFactory
        {
            get
            {
                return settings.SessionFactory;
            }
        }

        /// <summary>
        /// Get or set the configured EntityInterceptor
        /// </summary>
        public IInterceptor EntityInterceptor
        {
            get
            {
                return settings.EntityInterceptor;
            }
        }

        /// <summary>
        /// Gets a flag, whether this scope is in "open" state on the current logical thread.
        /// </summary>
        public bool IsOpen
        {
            get
            {
                return (null != LogicalThreadContext.GetData(ISOPEN_KEY));
            }
        }

        /// <summary>
        /// Gets a flag, whether this scope manages it's own session for the current logical thread or not.
        /// </summary>
        public bool IsParticipating
        {
            get
            {
                return (null != LogicalThreadContext.GetData(PARTICIPATE_KEY));
            }
        }

        /// <summary>
        /// Sets a flag, whether this scope is in "open" state on the current logical thread.
        /// </summary>
        private void SetOpen(bool isOpen)
        {
            if (isOpen)
            {
                LogicalThreadContext.SetData(ISOPEN_KEY, ISOPEN_KEY);
            }
            else
            {
                LogicalThreadContext.FreeNamedDataSlot(ISOPEN_KEY);
            }
        }

        /// <summary>
        /// Gets/Sets a flag, whether this scope manages it's own session for the current logical thread or not.
        /// </summary>
        /// <value><c>false</c> if session is managed by this module. <c>false</c> otherwise</value>
        private void SetParticipating(bool participating)
        {
            if (participating)
            {
                LogicalThreadContext.SetData(PARTICIPATE_KEY, PARTICIPATE_KEY);
            }
            else
            {
                LogicalThreadContext.FreeNamedDataSlot(PARTICIPATE_KEY);
            }
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Call <code>Close()</code>, 
        /// </summary>
        public virtual void Dispose()
        {
            Close();
        }

        #endregion

        #region Methods

        /// <summary>
        /// Opens a new session or participates in an existing session and 
        /// registers with spring's <see cref="TransactionSynchronizationManager"/>.
        /// </summary>
        public void Open()
        {
            if (IsParticipating || IsOpen)
            {
                throw new InvalidOperationException("This scope is already open");
            }

            bool isDebugEnabled = log.IsDebugEnabled;

            if (SingleSession)
            {
                // single session mode
                if (TransactionSynchronizationManager.HasResource(SessionFactory))
                {
                    // Do not modify the Session: just set the participate flag.
                    if (isDebugEnabled) log.Debug("Participating in existing Hibernate SessionFactory");
                    SetParticipating(true);
                }
                else
                {
                    if (isDebugEnabled) log.Debug("Opening single Hibernate Session in SessionScope");
                    TransactionSynchronizationManager.BindResource(SessionFactory, new LazySessionHolder(this));
                }
            }
            else
            {
                // deferred close mode
                if (SessionFactoryUtils.IsDeferredCloseActive(SessionFactory))
                {
                    // Do not modify deferred close: just set the participate flag.
                    if (isDebugEnabled) log.Debug("Participating in active deferred close mode");
                    SetParticipating(true);
                }
                else
                {
                    if (isDebugEnabled) log.Debug("Initializing deferred close mode");
                    SessionFactoryUtils.InitDeferredClose(SessionFactory);
                }
            }

            SetOpen(true);
        }

        /// <summary>
        /// Close the current view's session and unregisters 
        /// from spring's <see cref="TransactionSynchronizationManager"/>.
        /// </summary>
        public void Close()
        {
            bool isDebugEnabled = log.IsDebugEnabled;
            if (isDebugEnabled) log.Debug("Trying to close SessionScope");

            if (IsOpen)
            {
                try
                {
                    DoClose(isDebugEnabled);
                }
                finally
                {
                    SetOpen(false);
                    SetParticipating(false);                    
                }
            }
            else
            {
                if (isDebugEnabled) log.Debug("SessionScope is already closed - doing nothing");
            }
        }

        private void DoClose(bool isLogDebugEnabled)
        {
            if (!IsParticipating)
            {
                if (SingleSession)
                {
                    // single session mode
                    if (isLogDebugEnabled) log.Debug("Closing single Hibernate Session in SessionScope");
                    LazySessionHolder holder = (LazySessionHolder)TransactionSynchronizationManager.UnbindResource(SessionFactory);
                    holder.Close();
                }
                else
                {
                    // deferred close mode
                    if (isLogDebugEnabled) log.Debug("Closing all Hibernate Sessions");
                    SessionFactoryUtils.ProcessDeferredClose(SessionFactory);
                }
            }
            else
            {
                if (isLogDebugEnabled) log.Debug("Only participated Hibernate Session - doing nothing");
            }
        }

        private ISession DoOpenSession()
        {
            ISession session = SessionFactoryUtils.OpenSession(SessionFactory, EntityInterceptor);
            session.FlushMode = DefaultFlushMode;
            return session;
        }

        #endregion

        #region LazySessionHolder utility class

        /// <summary>
        /// This sessionHolder creates a default session only if it is needed.
        /// </summary>
        /// <remarks>
        /// Although a NHibernateSession deferes creation of db-connections until they are really
        /// needed, instantiation a session is imho still more expensive than this LazySessionHolder. (EE)
        /// </remarks>
        private class LazySessionHolder : SessionHolder
        {
            private readonly ILog log = LogManager.GetLogger(typeof(LazySessionHolder));
            private SessionScope owner;
            private ISession session;

            /// <summary>
            /// Initialize a new instance.
            /// </summary>
            public LazySessionHolder(SessionScope owner)
            {
                if (log.IsDebugEnabled) log.Debug("Created LazySessionHolder");
                this.owner = owner;
            }

            /// <summary>
            /// Create a new session on demand
            /// </summary>
            protected override void EnsureInitialized()
            {
                if (session == null)
                {
                    if (log.IsDebugEnabled) log.Debug("session instance requested - opening new session");
                    session = owner.DoOpenSession();
                    AddSession(session);
                }
            }

            /// <summary>
            /// Ensure session is closed (if any) and remove circular references to avoid memory leaks!
            /// </summary>
            public void Close()
            {
                owner = null;
                if (session != null)
                {
                    ISession tmpSession = session;
                    session = null;
                    SessionFactoryUtils.CloseSession(tmpSession);
                }
                if (log.IsDebugEnabled) log.Debug("Closed LazySessionHolder");
            }
        }

        #endregion
    }
}