/* Copyright (c) 2001-2017, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb;

import org.hsqldb.HsqlNameManager.HsqlName;
import org.hsqldb.lib.IntKeyHashMap;
import org.hsqldb.lib.LongKeyHashMap;
import org.hsqldb.lib.LongKeyIntValueHashMap;
import org.hsqldb.lib.LongValueHashMap;
import org.hsqldb.result.Result;

/**
 * This class manages the reuse of Statement objects for prepared
 * statements for a Session instance.<p>
 *
 * A compiled statement is registered by a session to be managed. Once
 * registered, it is linked with one or more sessions.<p>
 *
 * The sql statement text distinguishes different compiled statements and acts
 * as lookup key when a session initially looks for an existing instance of
 * the compiled sql statement.<p>
 *
 * Once a session is linked with a statement, it uses the unique compiled
 * statement id for the sql statement to access the statement.<p>
 *
 * Changes to database structure via DDL statements, will result in all
 * registered Statement objects to become invalidated. This is done by
 * comparing the schema change and compile timestamps. When a session
 * subsequently attempts to use an invalidated Statement via its id, it will
 * reinstantiate the Statement using its sql statement still held by this class.<p>
 *
 * This class keeps count of the number of time each registered compiled
 * statement is linked to a session. It unregisters a compiled statement when
 * no session remains linked to it.<p>
 *
 * Modified by fredt@users from the original by campbell-burnet@users to simplify,
 * support multiple identical prepared statements per session, and avoid
 * memory leaks. Modified further to support schemas. Changed implementation
 * in 1.9 as a session object<p>
 *
 * @author Campbell Burnet (campbell-burnet@users dot sourceforge.net)
 * @author Fred Toussi (fredt@users dot sourceforge.net)
 *
 * @version 2.3.5
 * @since 1.7.2
 */
public final class StatementManager {

    /**
     * The Database for which this object is managing
     * CompiledStatement objects.
     */
    private Database database;

    /** Map: Schema id (int) => {Map: SQL String => Compiled Statement id (long)} */
    private IntKeyHashMap schemaMap;

    /** Map: Compiled statement id (int) => CompiledStatement object. */
    private LongKeyHashMap csidMap;

    /** Map: Compiled statement id (int) => number of uses of the statement */
    private LongKeyIntValueHashMap useMap;

    /**
     * Monotonically increasing counter used to assign unique ids to compiled
     * statements.
     */
    private long next_cs_id;

    /**
     * Constructs a new instance of <code>CompiledStatementManager</code>.
     *
     * @param database the Database instance for which this object is to
     *      manage compiled statement objects.
     */
    StatementManager(Database database) {

        this.database = database;
        schemaMap     = new IntKeyHashMap();
        csidMap       = new LongKeyHashMap();
        useMap        = new LongKeyIntValueHashMap();
        next_cs_id    = 0;
    }

    /**
     * Clears all internal data structures, removing any references to compiled statements.
     */
    synchronized void reset() {

        schemaMap.clear();
        csidMap.clear();
        useMap.clear();

        next_cs_id = 0;
    }

    /**
     * Retrieves the next compiled statement identifier in the sequence.
     *
     * @return the next compiled statement identifier in the sequence.
     */
    private long nextID() {

        next_cs_id++;

        return next_cs_id;
    }

    /**
     * Retrieves the registered compiled statement identifier associated with
     * the specified SQL String, or a value less than zero, if no such
     * statement has been registered.
     *
     * @param schema the schema id
     * @param sql the SQL String
     * @return the compiled statement identifier associated with the
     *      specified SQL String
     */
    private long getStatementID(HsqlName schema, String sql) {

        LongValueHashMap sqlMap =
            (LongValueHashMap) schemaMap.get(schema.hashCode());

        if (sqlMap == null) {
            return -1;
        }

        return sqlMap.get(sql, -1);
    }

    /**
     * Returns an existing CompiledStatement object with the given
     * statement identifier. Returns null if the CompiledStatement object
     * has been invalidated and cannot be recompiled
     *
     * @param session the session
     * @param csid the identifier of the requested CompiledStatement object
     * @return the requested CompiledStatement object
     */
    public synchronized Statement getStatement(Session session, long csid) {

        Statement cs = (Statement) csidMap.get(csid);

        if (cs == null) {
            return null;
        }

        if (cs.getCompileTimestamp()
                < database.schemaManager.getSchemaChangeTimestamp()) {
            Statement newStatement = recompileStatement(session, cs);

            if (newStatement == null) {
                freeStatement(csid);

                return null;
            }

            registerStatement(cs.getID(), newStatement);

            return newStatement;
        }

        return cs;
    }

    /**
     * Recompiles an existing statement
     *
     * @param session the session
     * @param statement the old CompiledStatement object
     * @return the requested CompiledStatement object
     */
    public synchronized Statement getStatement(Session session,
            Statement statement) {

        long      csid = statement.getID();
        Statement cs   = (Statement) csidMap.get(csid);

        if (cs != null) {
            return getStatement(session, csid);
        }

        cs = recompileStatement(session, statement);

        return cs;
    }

    private Statement recompileStatement(Session session, Statement cs) {

        HsqlName  oldSchema = session.getCurrentSchemaHsqlName();
        Statement newStatement;

        // revalidate with the original schema
        try {
            HsqlName schema = cs.getSchemaName();
            int      props  = cs.getCursorPropertiesRequest();

            if (schema != null) {

                // checks the old schema exists
                session.setSchema(schema.name);
            }

            boolean setGenerated = cs.generatedResultMetaData() != null;

            newStatement = session.compileStatement(cs.getSQL(), props);

            newStatement.setCursorPropertiesRequest(props);

            if (!cs.getResultMetaData().areTypesCompatible(
                    newStatement.getResultMetaData())) {
                return null;
            }

            if (!cs.getParametersMetaData().areTypesCompatible(
                    newStatement.getParametersMetaData())) {
                return null;
            }

            newStatement.setCompileTimestamp(
                database.txManager.getGlobalChangeTimestamp());

            if (setGenerated) {
                StatementDML si = (StatementDML) cs;

                newStatement.setGeneratedColumnInfo(si.generatedType,
                                                    si.generatedInputMetaData);
            }
        } catch (Throwable t) {
            return null;
        } finally {
            session.setCurrentSchemaHsqlName(oldSchema);
        }

        return newStatement;
    }

    /**
     * Registers a compiled statement to be managed.
     *
     * The only caller should be a Session that is attempting to prepare
     * a statement for the first time or process a statement that has been
     * invalidated due to DDL changes.
     *
     * @param csid existing id or negative if the statement is not yet managed
     * @param cs The CompiledStatement to add
     * @return The compiled statement id assigned to the CompiledStatement
     *  object
     */
    private long registerStatement(long csid, Statement cs) {

        cs.setCompileTimestamp(database.txManager.getGlobalChangeTimestamp());

        int              schemaid = cs.getSchemaName().hashCode();
        LongValueHashMap sqlMap   = (LongValueHashMap) schemaMap.get(schemaid);

        if (sqlMap == null) {
            sqlMap = new LongValueHashMap();

            schemaMap.put(schemaid, sqlMap);
        }

        if (csid < 0) {
            csid = nextID();
        }

        cs.setID(csid);
        sqlMap.put(cs.getSQL(), csid);
        csidMap.put(csid, cs);

        return csid;
    }

    /**
     * Removes one (or all) of the links between a session and a compiled
     * statement. If the statement is not linked with any other session, it is
     * removed from management.
     *
     * @param csid the compiled statement identifier
     */
    synchronized void freeStatement(long csid) {

        if (csid == -1) {

            // statement was never added
            return;
        }

        int useCount = useMap.get(csid, 1);

        if (useCount > 1) {
            useMap.put(csid, useCount - 1);

            return;
        }

        Statement cs = (Statement) csidMap.remove(csid);

        if (cs != null) {
            int schemaid = cs.getSchemaName().hashCode();
            LongValueHashMap sqlMap =
                (LongValueHashMap) schemaMap.get(schemaid);
            String sql = cs.getSQL();

            sqlMap.remove(sql);
        }

        useMap.remove(csid);
    }

    /**
     * Compiles an SQL statement and returns a CompiledStatement Object
     *
     * @param session the session
     * @throws Throwable
     * @return CompiledStatement
     */
    synchronized Statement compile(Session session,
                                   Result cmd) throws Throwable {

        int       props = cmd.getExecuteProperties();
        Statement cs    = null;
        String    sql   = cmd.getMainString();
        long      csid  = getStatementID(session.currentSchema, sql);

        if (csid >= 0) {
            cs = (Statement) csidMap.get(csid);
        }

        // generated result props still overwrite earlier version
        if (cs == null || !cs.isValid() || cs.getCompileTimestamp() < database
                .schemaManager.getSchemaChangeTimestamp() || cs
                .getCursorPropertiesRequest() != props) {
            cs = session.compileStatement(sql, props);

            cs.setCursorPropertiesRequest(props);

            csid = registerStatement(csid, cs);
        }

        int useCount = useMap.get(csid, 0) + 1;

        useMap.put(csid, useCount);
        cs.setGeneratedColumnInfo(cmd.getGeneratedResultType(),
                                  cmd.getGeneratedResultMetaData());

        return cs;
    }
}
