/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2011 Bull S.A.S
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */
package org.ow2.jonas.resource.internal.cm.sql.cache;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.ow2.jonas.resource.internal.cm.sql.PreparedStatementWrapper;

/**
 * Map cache implementation
 * A Pstmt key is used to index the entries
 * A LinkedHashMap is used to implement the LRU algorithm when entries must be freed (no more space)
 * @author pelletib
 */
public class PreparedStatementMapCacheImpl extends AbstractPreparedStatementCacheImpl implements PreparedStatementCache {

    /**
     * Cache
     */
    public Map<PreparedStatementMapCacheKey,List<PreparedStatementWrapper>> pStmts = null;

    /**
     * Constructor
     * @param maxCacheSize max cache size
     * @param logger trace logger
     */
    public PreparedStatementMapCacheImpl(final int maxCacheSize, final PreparedStatementCacheStatistics pstmtStats, final Logger logger) {
        super(maxCacheSize,pstmtStats,logger);

        if (maxCacheSize > 0) {
            pStmts = Collections.synchronizedMap(new LinkedHashMap<PreparedStatementMapCacheKey,List<PreparedStatementWrapper>>(maxCacheSize) {
                // (an anonymous inner class)
                private static final long serialVersionUID = 1;

                @Override protected boolean removeEldestEntry (final Map.Entry<PreparedStatementMapCacheKey,List<PreparedStatementWrapper>> eldest) {
                    // always return false in our case to inhibit automatic deleting in map
                    return false;
                }});
        } else {
            pStmts = Collections.synchronizedMap(new LinkedHashMap<PreparedStatementMapCacheKey,List<PreparedStatementWrapper>>() {
                // (an anonymous inner class)
                private static final long serialVersionUID = 1;

                @Override protected boolean removeEldestEntry (final Map.Entry<PreparedStatementMapCacheKey,List<PreparedStatementWrapper>> eldest) {
                    // always return false in our case to inhibit automatic deleting in map
                    return false;
                }});
        }

    }

    @Override
    public synchronized PreparedStatementWrapper get(final PreparedStatementWrapper psw) {

        int jumpNb = 0; // number of jumper to find or not
        int found = 0; // number of entry found in cache

        PreparedStatementWrapper outputPsw = null;

        if (cacheLogger.isLoggable(BasicLevel.DEBUG)) {
            cacheLogger.log(BasicLevel.DEBUG, "MC pStmts: " + pStmts);
        }

        // update statistics
        stats.incrementPrepStmtCacheAccessCount();

        // extract key
        PreparedStatementMapCacheKey key = getPstmtKey(psw);

        List<PreparedStatementWrapper>  listPsw = pStmts.get(key);

        // if there're some element for this entry
        if (listPsw != null) {
            // goes across the list to find a not closed element
            Iterator<PreparedStatementWrapper> i = listPsw.iterator();
            while (i.hasNext()) {
                outputPsw = i.next();
                if (outputPsw.isClosed()) {
                    outputPsw.clearPstmtValues();
                    found++;
                    break;
                } else {
                    outputPsw = null;
                    jumpNb++;
                    if (cacheLogger.isLoggable(BasicLevel.DEBUG)) {
                        cacheLogger.log(BasicLevel.DEBUG, "Statement in cache but already used, search another one");
                        cacheLogger.log(BasicLevel.DEBUG, "sql=" + psw.getSql() + ", count=" + jumpNb);
                    }
                }
            }
        }

        if (found > 0) { // found

            if (cacheLogger.isLoggable(BasicLevel.DEBUG)) {
                cacheLogger.log(BasicLevel.DEBUG, "Statement found in cache " + outputPsw);
            }
            // update statistics
            stats.incrementPrepStmtCacheHitCount();

        } else { // not found
            if (cacheLogger.isLoggable(BasicLevel.DEBUG)) {
                cacheLogger.log(BasicLevel.DEBUG, "No statement in cache (or not closed)");
            }
            // update statistics
            stats.incrementPrepStmtCacheMissCount();
        }

        return outputPsw;
    }

    /**
     * Put a pstmt in the cache. If the cache is full, an entry is removed first according to the LRU algorithm
     * @param psw prepared statement to add
     */
    @Override
    public synchronized void put(final PreparedStatementWrapper psw) throws CacheException, SQLException {

        // error if cache is disabled
        if (maxPstmtCacheSize < 0) {
            throw new CacheException("Cache disabled");
        }

        // update statistics
        stats.incrementPrepStmtCacheAccessCount();

        // if cache is satured, no entries was freed by the linkedlist (probably all are open)
        if (pstmtCacheSize >= maxPstmtCacheSize && maxPstmtCacheSize != 0) {

            // try to free a pstmt in the cache
            if (!findFree()) {
                if (cacheLogger.isLoggable(BasicLevel.DEBUG)) {
                    cacheLogger.log(BasicLevel.DEBUG, "No more space in cache");
                }
                throw new CacheException("No more space in cache");
            }
        }

        // extract key
        PreparedStatementMapCacheKey key = getPstmtKey(psw);

        List<PreparedStatementWrapper>  listPsw = pStmts.get(key);

        // if no element for this entry, create the list
        if (listPsw == null) {
            listPsw = new ArrayList<PreparedStatementWrapper>();
            pStmts.put(key,listPsw);
        }
        pstmtCacheSize++;

        // add the element in the list at the end
        listPsw.add(psw);

        if (cacheLogger.isLoggable(BasicLevel.DEBUG)) {
            cacheLogger.log(BasicLevel.DEBUG, "Adding PStmt: " + psw);
        }

        // update statistics
        stats.incrementPrepStmtCacheAddCount();
        stats.incrementPrepStmtCacheCurrentSize();

    }

    /**
     * @param prefix indentation
     * @return a string with a cache state (dump)
     */
    @Override
    public String getState(final String prefix) {

        String res = "";

        synchronized(this) {
            res += prefix + "size of pStmts:" + pstmtCacheSize + "\n";
            Iterator<List<PreparedStatementWrapper>> iter = pStmts.values().iterator();
            while (iter.hasNext()) {
                List<PreparedStatementWrapper> list = iter.next();
                int size = list.size();
                for (int i = 0; i < size; i++) {
                    res += prefix + "\t" + list.get(i) + "\n";
                }
            }
        }

        return res;
    }

    /**
     * Destroy all the cache entries
     */
    @Override
    public synchronized void destroy() {

        if (cacheLogger.isLoggable(BasicLevel.DEBUG)) {
            cacheLogger.log(BasicLevel.DEBUG, "Destroy the cache entries");
        }

        // update statistics
        stats.incrementPrepStmtCacheAccessCount();

        PreparedStatementWrapper pw = null;

        // Remove the PreparedStatements on the MC
        Iterator<List<PreparedStatementWrapper>> iter = pStmts.values().iterator();
        while (iter.hasNext()) {
            List<PreparedStatementWrapper> list = iter.next();
            int size = list.size();
            for (int i = 0; i < size; i++) {
                pw = list.get(i);
                try {
                    pw.destroy();
                } catch (Exception ex) {
                }
                stats.incrementPrepStmtCacheDeleteCount();
                stats.decrementPrepStmtCacheCurrentSize();

            }
            list.clear();
        }
        pStmts.clear();
        pstmtCacheSize = 0;
    }

    /**
     * Close all the cache entries
     */
    @Override
    public synchronized void reallyClose() {

        if (cacheLogger.isLoggable(BasicLevel.DEBUG)) {
            cacheLogger.log(BasicLevel.DEBUG, "Close physically the cache entries");
        }

        // update statistics
        stats.incrementPrepStmtCacheAccessCount();

        PreparedStatementWrapper pw = null;

        // Remove the PreparedStatements on the MC
        Iterator<List<PreparedStatementWrapper>> iter = pStmts.values().iterator();
        while (iter.hasNext()) {
            List<PreparedStatementWrapper> list = iter.next();
            int size = list.size();
            for (int i = 0; i < size; i++) {
                pw = list.get(i);
                try {
                    pw.closePstmt();
                } catch (Exception ex) {
                }
                stats.incrementPrepStmtCacheDeleteCount();
                stats.decrementPrepStmtCacheCurrentSize();
            }
            list.clear();
        }
        pStmts.clear();

        pstmtCacheSize = 0;
    }

    /**
     * Close all the cache entries
     */
    @Override
    public synchronized void close() {

        if (cacheLogger.isLoggable(BasicLevel.DEBUG)) {
            cacheLogger.log(BasicLevel.DEBUG, "Close the cache entries");
        }

        // update statistics
        stats.incrementPrepStmtCacheAccessCount();

        PreparedStatementWrapper pw = null;

        // Remove the PreparedStatements on the MC
        Iterator<List<PreparedStatementWrapper>> iter = pStmts.values().iterator();
        while (iter.hasNext()) {
            List<PreparedStatementWrapper> list = iter.next();
            int size = list.size();
            for (int i = 0; i < size; i++) {
                pw = list.get(i);
                try {
                    pw.close();
                } catch (Exception ex) {
                }
            }
        }
    }

    /**
     * Close the first entry
     */
    private boolean findFree() {

        if (cacheLogger.isLoggable(BasicLevel.DEBUG)) {
            cacheLogger.log(BasicLevel.DEBUG, "Close the first entry");
        }

        PreparedStatementWrapper pw = null;

        // the linked hashmap ensures to read the entries from the least recently accessed
        // to the most-recently accessed
        Iterator<List<PreparedStatementWrapper>> iter = pStmts.values().iterator();
        while (iter.hasNext()) {
            List<PreparedStatementWrapper> list = iter.next();
            int size = list.size();
            // in the list (duplicate element for an entry), search the first closed element
            for (int i = 0; i < size; i++) {
                pw = list.get(i);
                if (pw.isClosed()) {
                    if (cacheLogger.isLoggable(BasicLevel.DEBUG)) {
                        cacheLogger.log(BasicLevel.DEBUG, "removes " + pw);
                    }
                    list.remove(i);

                    pstmtCacheSize--;
                    // update statistic
                    stats.incrementPrepStmtCacheDeleteCount();
                    stats.decrementPrepStmtCacheCurrentSize();

                    return true;
                }
             }
        }
        return false;
    }

    /**
     * Build cache key from prepared statement wrapper
     */
    private PreparedStatementMapCacheKey getPstmtKey(final PreparedStatementWrapper psw) {
        PreparedStatementMapCacheKey key = null;
        // extract key
            key = new PreparedStatementMapCacheKey(
                    psw.getUser(),
                    psw.getSql(),
                    psw.getOriginalResultSetType(),
                    psw.getOriginalResultSetConcurrency(),
                    psw.getOriginalResultSetHoldability(),
                    psw.getAutoGeneratedKeys(),
                    psw.getColumnIndexes(),
                    psw.getColumnNames(),
                    psw.hashCode(),
                    cacheLogger);
        return key;

    }
}
