/* Copyright (c) 2001-2008, 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.lib.DoubleIntIndex;
import org.hsqldb.lib.HashMappedList;
import org.hsqldb.lib.HsqlArrayList;
import org.hsqldb.lib.LongKeyIntValueHashMap;

/**
 * Manages rows involved in transactions
 *
 * @author fredt@users
 * @version  1.8.0
 * @since 1.8.0
 */
public class TransactionManager {

    LongKeyIntValueHashMap rowSessionMap;
    boolean                reWriteProtect;
    Database               database;

    TransactionManager(Database db) {
        database      = db;
        rowSessionMap = new LongKeyIntValueHashMap(true);
    }

    public void setReWriteProtection(boolean value) {
        reWriteProtect = value;
    }

    void checkDelete(Session session, Row row) throws HsqlException {}

    void checkDelete(Session session,
                     HashMappedList rowSet) throws HsqlException {

        if (!reWriteProtect) {
            return;
        }

        int sessionid = session.getId();

        for (int i = 0, size = rowSet.size(); i < size; i++) {
            Row  row   = (Row) rowSet.getKey(i);
            long rowid = row.getId();

            if (rowSessionMap.get(rowid, sessionid) != sessionid) {
                throw Trace.error(Trace.INVALID_TRANSACTION_STATE_NO_SUBCLASS,
                                  Trace.ITSNS_OVERWRITE);
            }
        }
    }

    void checkDelete(Session session,
                     HsqlArrayList rowSet) throws HsqlException {

        if (!reWriteProtect) {
            return;
        }

        int sessionid = session.getId();

        for (int i = 0, size = rowSet.size(); i < size; i++) {
            Row  row   = (Row) rowSet.get(i);
            long rowid = row.getId();

            if (rowSessionMap.get(rowid, sessionid) != sessionid) {
                throw Trace.error(Trace.INVALID_TRANSACTION_STATE_NO_SUBCLASS,
                                  Trace.ITSNS_OVERWRITE);
            }
        }
    }

    void commit(Session session) {

        Object[] list = session.rowActionList.getArray();
        int      size = session.rowActionList.size();

        for (int i = 0; i < size; i++) {
            Transaction tx    = (Transaction) list[i];
            long        rowid = tx.row.getId();

            tx.commit(session);
            rowSessionMap.remove(rowid);
        }

        session.rowActionList.clear();
        session.savepoints.clear();
    }

    synchronized void rollback(Session session) {
        rollbackTransactions(session, 0, false);
        session.savepoints.clear();
    }

    void rollbackSavepoint(Session session,
                           String name) throws HsqlException {

        int index = session.savepoints.getIndex(name);

        if (index < 0) {
            throw Trace.error(Trace.SAVEPOINT_NOT_FOUND, name);
        }

        Integer oi    = (Integer) session.savepoints.get(index);
        int     limit = oi.intValue();

        rollbackTransactions(session, limit, false);

        while (session.savepoints.size() > index) {
            session.savepoints.remove(session.savepoints.size() - 1);
        }
    }

    void rollbackTransactions(Session session, int limit, boolean log) {

        Object[] list = session.rowActionList.getArray();
        int      size = session.rowActionList.size();

        for (int i = size - 1; i >= limit; i--) {
            Transaction tx = (Transaction) list[i];

            tx.rollback(session, log);
        }

        for (int i = limit; i < size; i++) {
            Transaction tx    = (Transaction) list[i];
            long        rowid = tx.row.getId();

            rowSessionMap.remove(rowid);
        }

        session.rowActionList.setSize(limit);
    }

    void addTransaction(Session session, Transaction transaction) {

        if (reWriteProtect) {
            rowSessionMap.put(transaction.row.getId(), session.getId());
        }
    }

    private long globalActionTimestamp = 0;

    /**
     * gets the next timestamp for an action
     */
    long nextActionTimestamp() {

        globalActionTimestamp++;

        return globalActionTimestamp;
    }

    /**
     * Return an array of all transactions sorted by System Change No.
     */
    Transaction[] getTransactionList() {

        Session[]     sessions = database.sessionManager.getAllSessions();
        int[]         tIndex   = new int[sessions.length];
        Transaction[] transactions;
        int           transactionCount = 0;

        {
            int actioncount = 0;

            for (int i = 0; i < sessions.length; i++) {
                actioncount += sessions[i].getTransactionSize();
            }

            transactions = new Transaction[actioncount];
        }

        while (true) {
            boolean found        = false;
            long    minChangeNo  = Long.MAX_VALUE;
            int     sessionIndex = 0;

            // find the lowest available SCN across all sessions
            for (int i = 0; i < sessions.length; i++) {
                int tSize = sessions[i].getTransactionSize();

                if (tIndex[i] < tSize) {
                    Transaction current =
                        (Transaction) sessions[i].rowActionList.get(
                            tIndex[i]);

                    if (current.SCN < minChangeNo) {
                        minChangeNo  = current.SCN;
                        sessionIndex = i;
                    }

                    found = true;
                }
            }

            if (!found) {
                break;
            }

            HsqlArrayList currentList = sessions[sessionIndex].rowActionList;

            for (; tIndex[sessionIndex] < currentList.size(); ) {
                Transaction current =
                    (Transaction) currentList.get(tIndex[sessionIndex]);

                // if the next change no is in this session, continue adding
                if (current.SCN == minChangeNo + 1) {
                    minChangeNo++;
                }

                if (current.SCN == minChangeNo) {
                    transactions[transactionCount++] = current;

                    tIndex[sessionIndex]++;
                } else {
                    break;
                }
            }
        }

        return transactions;
    }

    /**
     * Return a lookup of all transactions ids for cached tables.
     */
    public DoubleIntIndex getTransactionIDList() {

        Session[]      sessions = database.sessionManager.getAllSessions();
        DoubleIntIndex lookup   = new DoubleIntIndex(10, false);

        lookup.setKeysSearchTarget();

        for (int i = 0; i < sessions.length; i++) {
            HsqlArrayList tlist = sessions[i].rowActionList;

            for (int j = 0, size = tlist.size(); j < size; j++) {
                Transaction tx = (Transaction) tlist.get(j);

                if (tx.tTable.getTableType() == Table.CACHED_TABLE) {
                    lookup.addUnique(tx.row.getPos(), 0);
                }
            }
        }

        return lookup;
    }

    /**
     * Convert row ID's for cached table rows in transactions
     */
    public void convertTransactionIDs(DoubleIntIndex lookup) {

        Session[] sessions = database.sessionManager.getAllSessions();

        for (int i = 0; i < sessions.length; i++) {
            HsqlArrayList tlist = sessions[i].rowActionList;

            for (int j = 0, size = tlist.size(); j < size; j++) {
                Transaction tx = (Transaction) tlist.get(j);

                if (tx.tTable.getTableType() == Table.CACHED_TABLE) {
                    int pos = lookup.lookupFirstEqual(tx.row.getPos());

                    tx.row.setPos(pos);
                }
            }
        }
    }
}
