/* $Id$ */

/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.manifoldcf.core.jdbcpool;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Set;

import org.apache.manifoldcf.core.system.Logging;

/** The class that defines a connection pool.
*/
public class ConnectionPool
{
  public static final String _rcsid = "@(#)$Id$";

  protected final String dbURL;
  protected final String userName;
  protected final String password;
  protected volatile int freePointer;
  protected volatile int activeConnections;
  protected volatile boolean closed;
  protected final Connection[] freeConnections;
  protected final long[] connectionCleanupTimeouts;
  protected final long expiration;
  
  protected final boolean debug;
  
  protected final Set<WrappedConnection> outstandingConnections = new HashSet<WrappedConnection>();
  
  /** Constructor */
  public ConnectionPool(String dbURL, String userName, String password, int maxConnections, long expiration, boolean debug)
  {
    this.dbURL = dbURL;
    this.userName = userName;
    this.password = password;
    this.freeConnections = new Connection[maxConnections];
    this.connectionCleanupTimeouts = new long[maxConnections];
    this.freePointer = 0;
    this.activeConnections = 0;
    this.closed = false;
    this.expiration = expiration;
    this.debug = debug;
  }
  
  /** Obtain a connection from the pool.
  * This will wait until a connection is free, if the pool is already completely tapped.
  * The connection is returned by the "close" operation, executed on the connection.
  * (This requires us to wrap the actual connection object).
  */
  public WrappedConnection getConnection()
    throws SQLException, InterruptedException
  {
    Exception instantiationException;
    if (debug)
      instantiationException = new Exception("Possibly leaked db connection");
    else
      instantiationException = null;
    Connection rval = null;
    boolean returnedValue = true;
    try
    {
      while (true)
      {
        synchronized (this)
        {
          if (freePointer > 0)
          {
            if (closed)
              throw new InterruptedException("Pool already closed");
            rval = freeConnections[--freePointer];
            freeConnections[freePointer] = null;
            boolean isValid = true;
            try
            {
              isValid = rval.isValid(1);
            }
            catch (SQLException e)
            {
              // Ignore this; we just can't check if handle is valid I guess.
              // (Postgresql doesn't implement this method so it fails always)
            }
            catch (java.lang.AbstractMethodError e)
            {
              // Ignore this; we just can't check if handle is valid I guess.
              // (SQLServer doesn't implement this method so it fails always)
            }
            if (!isValid) {
              // If the connection is invalid, drop it on the floor, and get a new one.
              // Note: Order of operations is terribly important here!!
              final Connection closeValue = rval;
              rval = null;
              activeConnections--;
              try
              {
                closeValue.close();
              }
              catch (SQLException e)
              {
                // Ignore SQL errors on close, and drop the connection on the floor
              }
              continue;
            }
            break;
          }
          if (activeConnections == freeConnections.length)
          {
            // If properly configured, we really shouldn't be getting here.
            if (debug)
            {
              synchronized (outstandingConnections)
              {
                Logging.db.warn("Out of db connections, list of outstanding ones follows.");
                for (WrappedConnection c : outstandingConnections)
                {
                  Logging.db.warn("Found a possibly leaked db connection",c.getInstantiationException());
                }
              }
            }
            // Wait until kicked; we hope something will free up...
            this.wait();
            continue;
          }
          // Increment active connection counter, because we're about to mint a new connection, and break out of our loop
          // Note: order is terribly important here!
          activeConnections++;
          if (userName != null)
            rval = DriverManager.getConnection(dbURL, userName, password);
          else
            rval = DriverManager.getConnection(dbURL);
          break;
        }
      }

      WrappedConnection wc = new WrappedConnection(this,rval,instantiationException);
      if (debug)
      {
        synchronized (outstandingConnections)
        {
          outstandingConnections.add(wc);
        }
      }
      return wc;
    }
    catch (Error e)
    {
      returnedValue = false;
      throw e;
    }
    catch (RuntimeException e)
    {
      returnedValue = false;
      throw e;
    }
    catch (SQLException e)
    {
      returnedValue = false;
      throw e;
    }
    finally
    {
      if (!returnedValue)
      {
        // We didn't finish.  Restore the pool to the correct form.
        // Note: We should always be able to just return any current connection to the pool.  This is
        // safe because we reserved a slot when we decided to create the connection (if that's what
        // we did), or we just used a connection that was already allocated.  Either way, we can put
        // it into the pool.
        if (rval != null)
        {
          // We have a handle, so just free it and leave activeConnections alone
          release(rval);
        }
        else
        {
          // We didn't manage to create the handle, so decrement active connections.
          synchronized (this)
          {
            activeConnections--;
          }
        }
      }
    }
  }
  
  /** Flush the pool.
  */
  public synchronized void flushPool()
  {
    for (int i = 0 ; i < freePointer ; i++)
    {
      try
      {
        freeConnections[i].close();
      }
      catch (SQLException e)
      {
        Logging.db.warn("Error closing pooled connection: "+e.getMessage(),e);
      }
      freeConnections[i] = null;
      activeConnections--;
    }
    freePointer = 0;
    notifyAll();
  }
  
  /** Close down the pool.
  */
  public synchronized void closePool()
  {
    for (int i = 0 ; i < freePointer ; i++)
    {
      try
      {
        freeConnections[i].close();
      }
      catch (SQLException e)
      {
        Logging.db.warn("Error closing pooled connection: "+e.getMessage(),e);
      }
      freeConnections[i] = null;
    }
    freePointer = 0;
    closed = true;
    notifyAll();
  }
  
  /** Clean up expired connections.
  */
  public synchronized void cleanupExpiredConnections(long currentTime)
  {
    int i = 0;
    while (i < freePointer)
    {
      if (connectionCleanupTimeouts[i] <= currentTime)
      {
        Connection c = freeConnections[i];
        freeConnections[i] = null;
        freePointer--;
        activeConnections--;
        if (freePointer == i)
        {
          freeConnections[i] = null;
        }
        else
        {
          freeConnections[i] = freeConnections[freePointer];
          connectionCleanupTimeouts[i] = connectionCleanupTimeouts[freePointer];
          freeConnections[freePointer] = null;
        }
        try
        {
         c.close();
        }
        catch (SQLException e)
        {
          Logging.db.warn("Error closing pooled connection: "+e.getMessage(),e);
        }
      }
      else
        i++;
    }
  }
  
  public void releaseConnection(WrappedConnection connection)
  {

    if (debug)
    {
      synchronized (outstandingConnections)
      {
        if (!outstandingConnections.contains(connection))
          Logging.db.warn("Released a connection that wasn't tracked!!");
        outstandingConnections.remove(connection);
      }
    }

    release(connection.getConnection());
  }
  
  protected void release(Connection c)
  {
    synchronized (this)
    {
      freeConnections[freePointer] = c;
      connectionCleanupTimeouts[freePointer] = System.currentTimeMillis() + expiration;
      freePointer++;
      notifyAll();
    }
    
  }
  
}


