// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.


using System.Collections.Concurrent;
using System.Data.Common;
using System.Diagnostics;

namespace System.Data.ProviderBase
{
    // set_ConnectionString calls DbConnectionFactory.GetConnectionPoolGroup
    // when not found a new pool entry is created and potentially added
    // DbConnectionPoolGroup starts in the Active state

    // Open calls DbConnectionFactory.GetConnectionPool
    // if the existing pool entry is Disabled, GetConnectionPoolGroup is called for a new entry
    // DbConnectionFactory.GetConnectionPool calls DbConnectionPoolGroup.GetConnectionPool

    // DbConnectionPoolGroup.GetConnectionPool will return pool for the current identity
    // or null if identity is restricted or pooling is disabled or state is disabled at time of add
    // state changes are Active->Active, Idle->Active

    // DbConnectionFactory.PruneConnectionPoolGroups calls Prune
    // which will QueuePoolForRelease on all empty pools
    // and once no pools remain, change state from Active->Idle->Disabled
    // Once Disabled, factory can remove its reference to the pool entry

    sealed internal class DbConnectionPoolGroup
    {
        private readonly DbConnectionOptions _connectionOptions;
        private readonly DbConnectionPoolKey _poolKey;
        private readonly DbConnectionPoolGroupOptions _poolGroupOptions;
        private ConcurrentDictionary<DbConnectionPoolIdentity, DbConnectionPool> _poolCollection;

        private int _state;          // see PoolGroupState* below

        private DbConnectionPoolGroupProviderInfo _providerInfo;
        private DbMetaDataFactory _metaDataFactory;

        // always lock this before changing _state, we don't want to move out of the 'Disabled' state
        // PoolGroupStateUninitialized = 0;
        private const int PoolGroupStateActive = 1; // initial state, GetPoolGroup from cache, connection Open
        private const int PoolGroupStateIdle = 2; // all pools are pruned via Clear
        private const int PoolGroupStateDisabled = 4; // factory pool entry pruning method

        internal DbConnectionPoolGroup(DbConnectionOptions connectionOptions, DbConnectionPoolKey key, DbConnectionPoolGroupOptions poolGroupOptions)
        {
            Debug.Assert(null != connectionOptions, "null connection options");

            _connectionOptions = connectionOptions;
            _poolKey = key;
            _poolGroupOptions = poolGroupOptions;

            // always lock this object before changing state
            // HybridDictionary does not create any sub-objects until add
            // so it is safe to use for non-pooled connection as long as
            // we check _poolGroupOptions first
            _poolCollection = new ConcurrentDictionary<DbConnectionPoolIdentity, DbConnectionPool>();
            _state = PoolGroupStateActive;
        }

        internal DbConnectionOptions ConnectionOptions => _connectionOptions;

        internal DbConnectionPoolKey PoolKey => _poolKey;

        internal DbConnectionPoolGroupProviderInfo ProviderInfo
        {
            get
            {
                return _providerInfo;
            }
            set
            {
                _providerInfo = value;
                if (null != value)
                {
                    _providerInfo.PoolGroup = this;
                }
            }
        }

        internal bool IsDisabled => (PoolGroupStateDisabled == _state);

        internal DbConnectionPoolGroupOptions PoolGroupOptions => _poolGroupOptions;

        internal DbMetaDataFactory MetaDataFactory
        {
            get
            {
                return _metaDataFactory;
            }

            set
            {
                _metaDataFactory = value;
            }
        }

        internal int Clear()
        {
            // must be multi-thread safe with competing calls by Clear and Prune via background thread
            // will return the number of connections in the group after clearing has finished

            // First, note the old collection and create a new collection to be used
            ConcurrentDictionary<DbConnectionPoolIdentity, DbConnectionPool> oldPoolCollection = null;
            lock (this)
            {
                if (_poolCollection.Count > 0)
                {
                    oldPoolCollection = _poolCollection;
                    _poolCollection = new ConcurrentDictionary<DbConnectionPoolIdentity, DbConnectionPool>();
                }
            }

            // Then, if a new collection was created, release the pools from the old collection
            if (oldPoolCollection != null)
            {
                foreach (var entry in oldPoolCollection)
                {
                    DbConnectionPool pool = entry.Value;
                    if (pool != null)
                    {
                        DbConnectionFactory connectionFactory = pool.ConnectionFactory;
                        connectionFactory.QueuePoolForRelease(pool, true);
                    }
                }
            }

            // Finally, return the pool collection count - this may be non-zero if something was added while we were clearing
            return _poolCollection.Count;
        }

        internal DbConnectionPool GetConnectionPool(DbConnectionFactory connectionFactory)
        {
            // When this method returns null it indicates that the connection
            // factory should not use pooling.

            // We don't support connection pooling on Win9x;
            // PoolGroupOptions will only be null when we're not supposed to pool
            // connections.
            DbConnectionPool pool = null;
            if (null != _poolGroupOptions)
            {
                DbConnectionPoolIdentity currentIdentity = DbConnectionPoolIdentity.NoIdentity;

                if (_poolGroupOptions.PoolByIdentity)
                {
                    // if we're pooling by identity (because integrated security is
                    // being used for these connections) then we need to go out and
                    // search for the connectionPool that matches the current identity.

                    currentIdentity = DbConnectionPoolIdentity.GetCurrent();

                    // If the current token is restricted in some way, then we must
                    // not attempt to pool these connections.
                    if (currentIdentity.IsRestricted)
                    {
                        currentIdentity = null;
                    }
                }

                if (null != currentIdentity)
                {
                    if (!_poolCollection.TryGetValue(currentIdentity, out pool)) // find the pool
                    {
                        lock (this)
                        {
                            // Did someone already add it to the list?
                            if (!_poolCollection.TryGetValue(currentIdentity, out pool))
                            {
                                DbConnectionPoolProviderInfo connectionPoolProviderInfo = connectionFactory.CreateConnectionPoolProviderInfo(this.ConnectionOptions);
                                DbConnectionPool newPool = new DbConnectionPool(connectionFactory, this, currentIdentity, connectionPoolProviderInfo);

                                if (MarkPoolGroupAsActive())
                                {
                                    // If we get here, we know for certain that we there isn't
                                    // a pool that matches the current identity, so we have to
                                    // add the optimistically created one
                                    newPool.Startup(); // must start pool before usage
                                    bool addResult = _poolCollection.TryAdd(currentIdentity, newPool);
                                    Debug.Assert(addResult, "No other pool with current identity should exist at this point");
                                    pool = newPool;
                                }
                                else
                                {
                                    // else pool entry has been disabled so don't create new pools
                                    Debug.Assert(PoolGroupStateDisabled == _state, "state should be disabled");

                                    // don't need to call connectionFactory.QueuePoolForRelease(newPool) because		
                                    // pool callbacks were delayed and no risk of connections being created		
                                    newPool.Shutdown();
                                }
                            }
                            else
                            {
                                // else found an existing pool to use instead
                                Debug.Assert(PoolGroupStateActive == _state, "state should be active since a pool exists and lock holds");
                            }
                        }
                    }
                    // the found pool could be in any state
                }
            }

            if (null == pool)
            {
                lock (this)
                {
                    // keep the pool entry state active when not pooling
                    MarkPoolGroupAsActive();
                }
            }
            return pool;
        }

        private bool MarkPoolGroupAsActive()
        {
            // when getting a connection, make the entry active if it was idle (but not disabled)
            // must always lock this before calling

            if (PoolGroupStateIdle == _state)
            {
                _state = PoolGroupStateActive;
            }
            return (PoolGroupStateActive == _state);
        }

        internal bool Prune()
        {
            // must only call from DbConnectionFactory.PruneConnectionPoolGroups on background timer thread
            // must lock(DbConnectionFactory._connectionPoolGroups.SyncRoot) before calling ReadyToRemove
            //     to avoid conflict with DbConnectionFactory.CreateConnectionPoolGroup replacing pool entry
            lock (this)
            {
                if (_poolCollection.Count > 0)
                {
                    var newPoolCollection = new ConcurrentDictionary<DbConnectionPoolIdentity, DbConnectionPool>();

                    foreach (var entry in _poolCollection)
                    {
                        DbConnectionPool pool = entry.Value;
                        if (pool != null)
                        {
                            // Actually prune the pool if there are no connections in the pool and no errors occurred.
                            // Empty pool during pruning indicates zero or low activity, but
                            //  an error state indicates the pool needs to stay around to
                            //  throttle new connection attempts.
                            if ((!pool.ErrorOccurred) && (0 == pool.Count))
                            {
                                // Order is important here.  First we remove the pool
                                // from the collection of pools so no one will try
                                // to use it while we're processing and finally we put the
                                // pool into a list of pools to be released when they
                                // are completely empty.
                                DbConnectionFactory connectionFactory = pool.ConnectionFactory;

                                connectionFactory.QueuePoolForRelease(pool, false);
                            }
                            else
                            {
                                newPoolCollection.TryAdd(entry.Key, entry.Value);
                            }
                        }
                    }
                    _poolCollection = newPoolCollection;
                }

                // must be pruning thread to change state and no connections
                // otherwise pruning thread risks making entry disabled soon after user calls ClearPool
                if (0 == _poolCollection.Count)
                {
                    if (PoolGroupStateActive == _state)
                    {
                        _state = PoolGroupStateIdle;
                    }
                    else if (PoolGroupStateIdle == _state)
                    {
                        _state = PoolGroupStateDisabled;
                    }
                }
                return (PoolGroupStateDisabled == _state);
            }
        }
    }
}
