#region License

/*
 * Copyright 2002-2004 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.Data;
using Common.Logging;
using Spring.Data.Common;
using Spring.Data.Support;
using Spring.Objects.Factory;
using Spring.Transaction;
using Spring.Transaction.Support;

#endregion
namespace Spring.Data.Core
{
    /// <summary>
    /// ADO.NET based implementation of the <see cref="Spring.Transaction.IPlatformTransactionManager"/>
    /// interface.
    /// </summary>
    /// <author>Mark Pollack (.NET)</author>
    public class AdoPlatformTransactionManager : AbstractPlatformTransactionManager, IResourceTransactionManager, IInitializingObject
    {

        private IDbProvider dbProvider;

        #region Logging Definition

        private static readonly ILog LOG = LogManager.GetLogger(typeof (AdoPlatformTransactionManager));

        #endregion      

        public AdoPlatformTransactionManager()
        {
            NestedTransactionsAllowed = true;
        }

        public AdoPlatformTransactionManager(IDbProvider dbProvider) : this()
        {
            DbProvider = dbProvider;

        }

        #region Propeties

        public IDbProvider DbProvider
        {
            get { return dbProvider; }
            set { dbProvider = value; }
        }

        #endregion

        /// <summary>
        /// Return the current transaction object.
        /// </summary>
        /// <returns>The current transaction object.</returns>
        /// <exception cref="Spring.Transaction.CannotCreateTransactionException">
        /// If transaction support is not available.
        /// </exception>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of lookup or system errors.
        /// </exception>
        protected override object DoGetTransaction()
        {
            DbProviderTransactionObject txMgrStateObject =
                new DbProviderTransactionObject();
            txMgrStateObject.SavepointAllowed = NestedTransactionsAllowed;
            ConnectionHolder conHolder = 
                (ConnectionHolder) TransactionSynchronizationManager.GetResource(DbProvider);
            txMgrStateObject.SetConnectionHolder(conHolder, false);
            return txMgrStateObject;
        }

        /// <summary>
        /// Check if the given transaction object indicates an existing,
        /// i.e. already begun, transaction.
        /// </summary>
        /// <param name="transaction">
        /// Transaction object returned by
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.
        /// </param>
        /// <returns>True if there is an existing transaction.</returns>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of system errors.
        /// </exception>
        protected override bool IsExistingTransaction(object transaction)
        {
            DbProviderTransactionObject txMgrStateObject =
                (DbProviderTransactionObject)transaction;
            return (txMgrStateObject.ConnectionHolder != null
                    &&
                    txMgrStateObject.ConnectionHolder.TransactionActive);

        }

        /// <summary>
        /// Begin a new transaction with the given transaction definition.
        /// </summary>
        /// <param name="transaction">
        /// Transaction object returned by
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.
        /// </param>
        /// <param name="definition">
        /// <see cref="Spring.Transaction.ITransactionDefinition"/> instance, describing
        /// propagation behavior, isolation level, timeout etc.
        /// </param>
        /// <remarks>
        /// Does not have to care about applying the propagation behavior,
        /// as this has already been handled by this abstract manager.
        /// </remarks>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of creation or system errors.
        /// </exception>
        protected override void DoBegin(object transaction, ITransactionDefinition definition)
        {
            DbProviderTransactionObject txMgrStateObject =
                (DbProviderTransactionObject)transaction;
            IDbConnection con = null;
	        
            if (dbProvider == null)
            {
                throw new ArgumentException("DbProvider is required to be set on AdoPlatformTransactionManager");
            }
	        
            try
            {
                if (txMgrStateObject.ConnectionHolder == null || txMgrStateObject.ConnectionHolder.SynchronizedWithTransaction)
                {
                    IDbConnection newCon = DbProvider.CreateConnection();
                    if (log.IsDebugEnabled)
                    {
                        log.Debug("Acquired Connection [" + newCon + ", " + newCon.ConnectionString + "] for ADO.NET transaction");
                    }
                    newCon.Open();                    

                    //TODO isolation level mgmt - will need to abstract out SQL used to specify this in DbMetaData
                    //MSDN docs...
                    //With one exception, you can switch from one isolation level to another at any time during a transaction. The exception occurs when changing from any isolation level to SNAPSHOT isolation
                    
                    
                    //IsolationLevel previousIsolationLevel = 

                    IDbTransaction newTrans = newCon.BeginTransaction(definition.TransactionIsolationLevel);

                    txMgrStateObject.SetConnectionHolder(new ConnectionHolder(newCon, newTrans), true);

                }
                txMgrStateObject.ConnectionHolder.SynchronizedWithTransaction = true;
                con = txMgrStateObject.ConnectionHolder.Connection;


                txMgrStateObject.ConnectionHolder.TransactionActive = true;

                int timeout = DetermineTimeout(definition);
                if (timeout != DefaultTransactionDefinition.TIMEOUT_DEFAULT)
                {
                    txMgrStateObject.ConnectionHolder.TimeoutInSeconds = timeout;
                }
                    

                //Bind transactional resources to thread
                if (txMgrStateObject.NewConnectionHolder)
                {
                    TransactionSynchronizationManager.BindResource(DbProvider, 
                                                                   txMgrStateObject.ConnectionHolder);
                }

            }
                //TODO catch specific exception
            catch (Exception e)
            {
                ConnectionUtils.DisposeConnection(con, DbProvider);
                throw new CannotCreateTransactionException("Could not create ADO.NET connection for transaction", e);
            }
                
        }


        /// <summary>
        /// Suspend the resources of the current transaction.
        /// </summary>
        /// <param name="transaction">Transaction object returned by
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.</param>
        /// <returns>
        /// An object that holds suspended resources (will be kept unexamined for passing it into
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoResume"/>.)
        /// </returns>
        /// <remarks>
        /// Transaction synchronization will already have been suspended.
        /// </remarks>
        /// <exception cref="Spring.Transaction.IllegalTransactionStateException">
        /// If suspending is not supported by the transaction manager implementation.
        /// </exception>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// in case of system errors.
        /// </exception>
        protected override object DoSuspend(object transaction)
        {
            DbProviderTransactionObject txMgrStateObject = (DbProviderTransactionObject)transaction;
            txMgrStateObject.ConnectionHolder = null;
            ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.UnbindResource(DbProvider);
            return conHolder;
        }


        /// <summary>
        /// Resume the resources of the current transaction.
        /// </summary>
        /// <param name="transaction">Transaction object returned by
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.</param>
        /// <param name="suspendedResources">The object that holds suspended resources as returned by
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoSuspend"/>.</param>
        /// <remarks>
        /// Transaction synchronization will be resumed afterwards.
        /// </remarks>
        /// <exception cref="Spring.Transaction.IllegalTransactionStateException">
        /// If suspending is not supported by the transaction manager implementation.
        /// </exception>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of system errors.
        /// </exception>
        protected override void DoResume(object transaction, object suspendedResources)
        {
            ConnectionHolder conHolder = (ConnectionHolder)suspendedResources;
            TransactionSynchronizationManager.BindResource(DbProvider, conHolder);            
        }
	   

        /// <summary>
        /// Perform an actual commit on the given transaction.
        /// </summary>
        /// <param name="status">The status representation of the transaction.</param>
        /// <remarks>
        /// <p>
        /// An implementation does not need to check the rollback-only flag.
        /// </p>
        /// </remarks>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of system errors.
        /// </exception>
        protected override void DoCommit(DefaultTransactionStatus status)
        {
            DbProviderTransactionObject txMgrStateObject =
                (DbProviderTransactionObject)status.Transaction;
            IDbTransaction trans = txMgrStateObject.ConnectionHolder.Transaction;
            if (status.Debug)
            {
                IDbConnection conn = txMgrStateObject.ConnectionHolder.Connection;
                log.Debug("Committing ADO.NET transaction on Connection [" + conn + ", " + conn.ConnectionString + "]");
            }
            try
            {
                trans.Commit();
            }
            catch (Exception e)
            {
                throw new TransactionSystemException("Could not commit ADO.NET transaction", e);
            }

        }

        /// <summary>
        /// Perform an actual rollback on the given transaction.
        /// </summary>
        /// <param name="status">The status representation of the transaction.</param>
        /// <remarks>
        /// An implementation does not need to check the new transaction flag.
        /// </remarks>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of system errors.
        /// </exception>
        protected override void DoRollback(DefaultTransactionStatus status)
        {
            DbProviderTransactionObject txMgrStateObject =
                (DbProviderTransactionObject)status.Transaction;
            IDbConnection conn = txMgrStateObject.ConnectionHolder.Connection;
            IDbTransaction trans = txMgrStateObject.ConnectionHolder.Transaction;
            if (status.Debug)
            {
                log.Debug("Rolling back ADO.NET transaction on Connection [" + conn + ", " + conn.ConnectionString + "]" );
            }
            try
            {
                trans.Rollback();
            }
            catch (Exception e)
            {
                throw new TransactionSystemException("Could not rollback ADO.NET transaction", e);
            }
        }

        /// <summary>
        /// Set the given transaction rollback-only. Only called on rollback
        /// if the current transaction takes part in an existing one.
        /// </summary>
        /// <param name="status">The status representation of the transaction.</param>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of system errors.
        /// </exception>
        protected override void DoSetRollbackOnly(DefaultTransactionStatus status)
        {
            DbProviderTransactionObject txMgrStateObject =
                        (DbProviderTransactionObject)status.Transaction;
            if (status.Debug)
            {
                IDbConnection conn = txMgrStateObject.ConnectionHolder.Connection;
                log.Debug("Setting ADO.NET transaction [" + conn + ", " + conn.ConnectionString + "] rollback-only.");
            }
            txMgrStateObject.SetRollbackOnly();

        }

        protected override void DoCleanupAfterCompletion(object transaction)
        {
            DbProviderTransactionObject txMgrStateObject =
                (DbProviderTransactionObject)transaction;
            if (txMgrStateObject.NewConnectionHolder)
            {
                TransactionSynchronizationManager.UnbindResource(DbProvider);
            }
            IDbConnection con = txMgrStateObject.ConnectionHolder.Connection;

            if (log.IsDebugEnabled)
            {
                log.Debug("Releasing ADO.NET Connection [" + con + ", " + con.ConnectionString + "] after transaction");
            }

            ConnectionUtils.DisposeConnection(con, DbProvider);
            //TODO clear out IDbTransaction object?

            txMgrStateObject.ConnectionHolder.Clear();


        }



        /// <summary>
        /// DbProvider transaction (state) object, representing a ConnectionHolder.
        /// Used as a transaction object by AdoPlatformTransactionManager
        /// </summary>
        /// <remarks>Derives from AdoTransactionObjectSupport to inherit the capability
        /// to manage Savepoints.
        /// </remarks>
        /// <seealso cref="ConnectionHolder"/>
        private class DbProviderTransactionObject : AdoTransactionObjectSupport
        {
            private bool newConnectionHolder;

            public void SetConnectionHolder(ConnectionHolder connectionHolder,
                                            bool newConnection)
            {
                ConnectionHolder = connectionHolder;
                newConnectionHolder = newConnection;
            }

            public bool NewConnectionHolder
            {
                get
                {
                    return newConnectionHolder;
                }
            }

            public bool HasTransaction
            {
                get
                {
                    return (ConnectionHolder != null && ConnectionHolder.TransactionActive);                 
                }
            }

            /// <summary>
            /// Sets the rollback only.
            /// </summary>
            public void SetRollbackOnly()
            {                
                ConnectionHolder.RollbackOnly = true;
            }

            /// <summary>
            /// Return whether the transaction is internally marked as rollback-only.
            /// </summary>
            /// <value></value>
            /// <returns>True of the transaction is marked as rollback-only.</returns>
            public override bool RollbackOnly
            {
                get
                {
                    return ConnectionHolder.RollbackOnly;
                }
            }

        }

        /// <summary>
        /// Invoked by an <see cref="Spring.Objects.Factory.IObjectFactory"/>
        /// after it has injected all of an object's dependencies.
        /// </summary>
        /// <exception cref="System.ArgumentException">
        /// If DbProvider is null.
        /// </exception>
        public void AfterPropertiesSet()
        {
            if (dbProvider == null)
            {
                throw new ArgumentException("DbProvider is required");
            }
        }

        /// <summary>
        /// Gets the resource factory that this transaction manager operates on,
        /// For the AdoPlatformTransactionManager this is the DbProvider
        /// </summary>
        /// <value>The DbProvider.</value>
        public object ResourceFactory
        {
            get
            {
                return dbProvider;
            }
        }
    }
}
