// Copyright (C) 2004-2007 MySQL AB
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as published by
// the Free Software Foundation
//
// There are special exceptions to the terms and conditions of the GPL 
// as it is applied to this software. View the full text of the 
// exception in file EXCEPTIONS in the directory of this software 
// distribution.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using MySql.Data.MySqlClient.Properties;
namespace MySql.Data.MySqlClient
{
  /// 
  /// Summary description for MySqlPool.
  /// 
  internal sealed class MySqlPool
  {
#if NET20
        private List inUsePool;
        private Queue idlePool;
#else
      private ArrayList inUsePool;
      private Queue idlePool;
#endif
      private MySqlConnectionStringBuilder settings;
      private uint minSize;
      private uint maxSize;
        private ProcedureCache procedureCache;
        private bool beingCleared;
        private int available;
        private AutoResetEvent autoEvent;
      public MySqlPool(MySqlConnectionStringBuilder settings)
      {
         minSize = settings.MinimumPoolSize;
         maxSize = settings.MaximumPoolSize;
            available = (int)maxSize;
            autoEvent = new AutoResetEvent(false);
            if (minSize > maxSize)
                minSize = maxSize;
         this.settings = settings;
#if NET20
            inUsePool = new List((int)maxSize);
            idlePool = new Queue((int)maxSize);
#else
         inUsePool =new ArrayList((int)maxSize);
         idlePool = new Queue((int)maxSize);
#endif
         // prepopulate the idle pool to minSize
            for (int i = 0; i < minSize; i++)
                idlePool.Enqueue(CreateNewPooledConnection());
            procedureCache = new ProcedureCache((int)settings.ProcedureCacheSize);
            beingCleared = false;
        }
        #region Properties
        public MySqlConnectionStringBuilder Settings 
      {
         get { return settings; }
         set { settings = value; }
      }
        public ProcedureCache ProcedureCache
        {
            get { return procedureCache; }
        }
        /// 
        /// It is assumed that this property will only be used from inside an active
        /// lock.
        /// 
        private bool HasIdleConnections
        {
            get { return idlePool.Count > 0; }
        }
        private int NumConnections
        {
            get { return idlePool.Count + inUsePool.Count; }
        }
        /// 
        /// Indicates whether this pool is being cleared.
        /// 
        public bool BeingCleared
        {
            get { return beingCleared; }
        }
        #endregion
        /// 
        /// CheckoutConnection handles the process of pulling a driver
        /// from the idle pool, possibly resetting its state,
        /// and adding it to the in use pool.  We assume that this method is only
        /// called inside an active lock so there is no need to acquire a new lock.
        /// 
        /// An idle driver object
      private Driver CheckoutConnection()
      {
            Driver driver = (Driver)idlePool.Dequeue();
         return driver;
      }
        private void CheckConn(Driver driver)
        {
            // first check to see that the server is still alive           
            if (!driver.Ping())
            {
                driver.Close();
                driver = CreateNewPooledConnection();
            }
            // if the user asks us to ping/reset pooled connections
            // do so now
            if (settings.ConnectionReset)
                driver.Reset();
        }
        /// 
        /// It is assumed that this method is only called from inside an active lock.
        /// 
        private Driver GetPooledConnection()
      {
            Driver driver = null;
            // if we don't have an idle connection but we have room for a new
            // one, then create it here.
            bool isIdle;
            lock ((idlePool as ICollection).SyncRoot)
            {
                if (!HasIdleConnections)
                {
                    driver = CreateNewPooledConnection();
                    isIdle = false;
                }
                else
                {
                    driver = CheckoutConnection();
                    isIdle = true;
                }
            }
            if (isIdle)
                CheckConn(driver);
            Debug.Assert(driver != null);
            lock ((inUsePool as ICollection).SyncRoot)
            {
                inUsePool.Add(driver);
            }
            return driver;
        }
        /// 
        /// It is assumed that this method is only called from inside an active lock.
        /// 
      private Driver CreateNewPooledConnection()
      {
            Debug.Assert((maxSize - NumConnections) > 0, "Pool out of sync.");
            Driver driver = Driver.Create(settings);
            driver.Pool = this;
            return driver;
        }
      public void ReleaseConnection(Driver driver)
      {
            lock ((inUsePool as ICollection).SyncRoot)
            {
                if (inUsePool.Contains(driver))
                    inUsePool.Remove(driver);
            }
            if (driver.IsTooOld() || beingCleared)
            {
                driver.Close();
                Debug.Assert(!idlePool.Contains(driver));
            }
            else
            {
                lock ((idlePool as ICollection).SyncRoot)
                {
                    idlePool.Enqueue(driver);
                }
            }
            Interlocked.Increment(ref available);
            autoEvent.Set();
        }
        /// 
        /// Removes a connection from the in use pool.  The only situations where this method 
        /// would be called are when a connection that is in use gets some type of fatal exception
        /// or when the connection is being returned to the pool and it's too old to be 
        /// returned.
        /// 
        /// 
        public void RemoveConnection(Driver driver)
        {
            lock ((inUsePool as ICollection).SyncRoot)
            {
                if (inUsePool.Contains(driver))
                {
                    inUsePool.Remove(driver);
                    Interlocked.Increment(ref available);
                    autoEvent.Set();
                }
            }
            // if we are being cleared and we are out of connections then have
            // the manager destroy us.
            if (beingCleared && NumConnections == 0)
                MySqlPoolManager.RemoveClearedPool(this);
        }
        private Driver TryToGetDriver()
        {
            int count = Interlocked.Decrement(ref available);
            if (count < 0)
            {
                Interlocked.Increment(ref available);
                return null;
            }
            try
            {
                Driver driver = GetPooledConnection();
                return driver;
            }
            catch (Exception ex)
            {
                if (settings.Logging)
                    Logger.LogException(ex);
                Interlocked.Increment(ref available);
                throw;
            }
        }
      public Driver GetConnection() 
      {
         int fullTimeOut = (int)settings.ConnectionTimeout * 1000;
            int timeOut = fullTimeOut;
            DateTime start = DateTime.Now;
            while (timeOut > 0)
            {
                Driver driver = TryToGetDriver();
                if (driver != null) return driver;
                // We have no tickets right now, lets wait for one.
                if (!autoEvent.WaitOne(timeOut, false)) break;
                timeOut = fullTimeOut - (int)DateTime.Now.Subtract(start).TotalMilliseconds;
            }
            throw new MySqlException(Resources.TimeoutGettingConnection);
      }
        /// 
        /// Clears this pool of all idle connections and marks this pool and being cleared
        /// so all other connections are closed when they are returned.
        /// 
        internal void Clear()
        {
            lock ((idlePool as ICollection).SyncRoot)
            {
                // first, mark ourselves as being cleared
                beingCleared = true;
                // then we remove all connections sitting in the idle pool
                while (idlePool.Count > 0)
                {
                    Driver d = idlePool.Dequeue();
                    d.Close();
                }
                // there is nothing left to do here.  Now we just wait for all
                // in use connections to be returned to the pool.  When they are
                // they will be closed.  When the last one is closed, the pool will
                // be destroyed.
            }
        }
  }
}