/********************************************************
 * ADO.NET 2.0 Data Provider for SQLite Version 3.X
 * Written by Robert Simpson (robert@blackcastlesoft.com)
 *
 * Released to the public domain, use at your own risk!
 ********************************************************/

#if !PLATFORM_COMPACTFRAMEWORK
namespace System.Data.SQLite
{
    using System.Globalization;
    using System.Transactions;

  internal sealed class SQLiteEnlistment : IDisposable, IEnlistmentNotification
  {
    internal SQLiteTransaction _transaction;
    internal Transaction _scope;
    internal bool _disposeConnection;

    internal SQLiteEnlistment(
        SQLiteConnection cnn,
        Transaction scope,
        System.Data.IsolationLevel defaultIsolationLevel,
        bool throwOnUnavailable,
        bool throwOnUnsupported
        )
    {
      _transaction = cnn.BeginTransaction(GetSystemDataIsolationLevel(
          cnn, scope, defaultIsolationLevel, throwOnUnavailable,
          throwOnUnsupported));

      _scope = scope;

      _scope.EnlistVolatile(this, EnlistmentOptions.None);
    }

    ///////////////////////////////////////////////////////////////////////////

    #region Private Methods
    private System.Data.IsolationLevel GetSystemDataIsolationLevel(
        SQLiteConnection connection,
        Transaction transaction,
        System.Data.IsolationLevel defaultIsolationLevel,
        bool throwOnUnavailable,
        bool throwOnUnsupported
        )
    {
        if (transaction == null)
        {
            //
            // NOTE: If neither the transaction nor connection isolation
            //       level is available, throw an exception if instructed
            //       by the caller.
            //
            if (connection != null)
                return connection.GetDefaultIsolationLevel();

            if (throwOnUnavailable)
            {
                throw new InvalidOperationException(
                    "isolation level is unavailable");
            }

            return defaultIsolationLevel;
        }

        IsolationLevel isolationLevel = transaction.IsolationLevel;

        //
        // TODO: Are these isolation level mappings actually correct?
        //
        switch (isolationLevel)
        {
            case IsolationLevel.Unspecified:
                return System.Data.IsolationLevel.Unspecified;
            case IsolationLevel.Chaos:
                return System.Data.IsolationLevel.Chaos;
            case IsolationLevel.ReadUncommitted:
                return System.Data.IsolationLevel.ReadUncommitted;
            case IsolationLevel.ReadCommitted:
                return System.Data.IsolationLevel.ReadCommitted;
            case IsolationLevel.RepeatableRead:
                return System.Data.IsolationLevel.RepeatableRead;
            case IsolationLevel.Serializable:
                return System.Data.IsolationLevel.Serializable;
            case IsolationLevel.Snapshot:
                return System.Data.IsolationLevel.Snapshot;
        }

        //
        // NOTE: When in "strict" mode, throw an exception if the isolation
        //       level is not recognized; otherwise, fallback to the default
        //       isolation level specified by the caller.
        //
        if (throwOnUnsupported)
        {
            throw new InvalidOperationException(
                HelperMethods.StringFormat(CultureInfo.CurrentCulture,
                "unsupported isolation level {0}", isolationLevel));
        }

        return defaultIsolationLevel;
    }

    ///////////////////////////////////////////////////////////////////////////

    private void Cleanup(SQLiteConnection cnn)
    {
        if (_disposeConnection && (cnn != null))
            cnn.Dispose();

        _transaction = null;
        _scope = null;
    }
    #endregion

    ///////////////////////////////////////////////////////////////////////////

    #region IDisposable Members
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    #endregion

    ///////////////////////////////////////////////////////////////////////////

    #region IDisposable "Pattern" Members
    private bool disposed;
    private void CheckDisposed() /* throw */
    {
#if THROW_ON_DISPOSED
        if (disposed)
            throw new ObjectDisposedException(typeof(SQLiteEnlistment).Name);
#endif
    }

    ///////////////////////////////////////////////////////////////////////////

    private /* protected virtual */ void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                ////////////////////////////////////
                // dispose managed resources here...
                ////////////////////////////////////

                if (_transaction != null)
                {
                    _transaction.Dispose();
                    _transaction = null;
                }

                if (_scope != null)
                {
                    // _scope.Dispose(); // NOTE: Not "owned" by us.
                    _scope = null;
                }
            }

            //////////////////////////////////////
            // release unmanaged resources here...
            //////////////////////////////////////

            disposed = true;
        }
    }
    #endregion

    ///////////////////////////////////////////////////////////////////////////

    #region Destructor
    ~SQLiteEnlistment()
    {
        Dispose(false);
    }
    #endregion

    ///////////////////////////////////////////////////////////////////////////

    #region IEnlistmentNotification Members
    public void Commit(Enlistment enlistment)
    {
        CheckDisposed();

        SQLiteConnection cnn = null;

        try
        {
            while (true)
            {
                cnn = _transaction.Connection;

                if (cnn == null)
                    break;

                lock (cnn._enlistmentSyncRoot) /* TRANSACTIONAL */
                {
                    //
                    // NOTE: This check is necessary to detect the case where
                    //       the SQLiteConnection.Close() method changes the
                    //       connection associated with our transaction (i.e.
                    //       to avoid a race (condition) between grabbing the
                    //       Connection property and locking its enlistment).
                    //
                    if (!Object.ReferenceEquals(cnn, _transaction.Connection))
                        continue;

                    cnn._enlistment = null;

                    _transaction.IsValid(true); /* throw */
                    cnn._transactionLevel = 1;
                    _transaction.Commit();

                    break;
                }
            }

            enlistment.Done();
        }
        finally
        {
            Cleanup(cnn);
        }
    }

    ///////////////////////////////////////////////////////////////////////////

    public void InDoubt(Enlistment enlistment)
    {
      CheckDisposed();
      enlistment.Done();
    }

    ///////////////////////////////////////////////////////////////////////////

    public void Prepare(PreparingEnlistment preparingEnlistment)
    {
      CheckDisposed();

      if (_transaction.IsValid(false) == false)
        preparingEnlistment.ForceRollback();
      else
        preparingEnlistment.Prepared();
    }

    ///////////////////////////////////////////////////////////////////////////

    public void Rollback(Enlistment enlistment)
    {
        CheckDisposed();

        SQLiteConnection cnn = null;

        try
        {
            while (true)
            {
                cnn = _transaction.Connection;

                if (cnn == null)
                    break;

                lock (cnn._enlistmentSyncRoot) /* TRANSACTIONAL */
                {
                    //
                    // NOTE: This check is necessary to detect the case where
                    //       the SQLiteConnection.Close() method changes the
                    //       connection associated with our transaction (i.e.
                    //       to avoid a race (condition) between grabbing the
                    //       Connection property and locking its enlistment).
                    //
                    if (!Object.ReferenceEquals(cnn, _transaction.Connection))
                        continue;

                    _transaction.Rollback();

                    break;
                }
            }

            enlistment.Done();
        }
        finally
        {
            Cleanup(cnn);
        }
    }
    #endregion
  }
}
#endif // !PLATFORM_COMPACT_FRAMEWORK
