/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 2000-2003
 *      Sleepycat Software.  All rights reserved.
 *
 * $Id: DataDb.java,v 1.21 2003/10/22 14:28:38 mhayes Exp $
 */

package com.sleepycat.bdb;

import com.sleepycat.db.Db;
import com.sleepycat.db.Dbc;
import com.sleepycat.db.DbException;
import com.sleepycat.db.Dbt;
import com.sleepycat.db.DbTxn;
import com.sleepycat.bdb.util.RuntimeExceptionWrapper;
import java.util.ArrayList;
import java.util.List;

/**
 * (<em>internal</em>) Wraps a Berkeley DB database (Db) object and adds
 * normalization of certain flags and environment modes.
 *
 * <p><b>NOTE:</b> This classes is internal and may be changed incompatibly or
 * deleted in the future.  It is public only so it may be used by
 * subpackages.</p>
 *
 * @author Mark Hayes
 */
public class DataDb {

    public static final int ENOMEM = 12; // from errorno.h
    public static final int EINVAL = 22; // from errorno.h

    public static final int FLAGS_POS_MASK = 0xff;            // flags position
    public static final int FLAGS_MOD_MASK = ~FLAGS_POS_MASK; // and modifiers

    DataEnvironment env;
    Db db;
    int type;
    boolean ordered;
    private ThreadLocal cdbContext;
    private String file;
    private String database;
    boolean recNumAccess;
    boolean keysRenumbered;
    boolean dupsAllowed;
    boolean dupsOrdered;
    boolean transactional;
    boolean dirtyReadAllowed;

    /**
     * Creates a database wrapper.
     *
     * @param db is the underlying database.
     */
    public DataDb(Db db) {

        try {
            this.env = DataEnvironment.getEnvironment(db.getDbEnv());
            this.file = db.getFileName();
            this.database = db.getDatabaseName();
            this.db = db;
            this.type = db.getDbType();
            this.ordered = (type == Db.DB_BTREE || type == Db.DB_QUEUE ||
                            type == Db.DB_RECNO);
            this.recNumAccess = (type != Db.DB_HASH &&
                                 (type != Db.DB_BTREE ||
                                  (db.getFlags() & Db.DB_RECNUM) != 0));
            this.keysRenumbered = (type == Db.DB_RECNO &&
                                   (db.getFlags() & Db.DB_RENUMBER) != 0);
            this.dupsOrdered = (db.getFlags() & Db.DB_DUPSORT) != 0;
            this.dupsAllowed = (this.dupsOrdered ||
                                ((db.getFlags() & Db.DB_DUP) != 0));
            this.dirtyReadAllowed = (db.getOpenFlags() & Db.DB_DIRTY_READ) != 0;
            this.transactional = env.isTxnMode() && db.isTransactional();
            if (env.isCdbMode()) {
                this.cdbContext = new ThreadLocal();
                this.cdbContext.set(new CdbThreadContext(this));
            }
        } catch (DbException e) {
            throw new RuntimeExceptionWrapper(e);
        }
    }

    /**
     * Closes the database.
     */
    public void close()
        throws DbException {

        db.close(0);
    }

    /**
     * Returns the environment.
     *
     * @return the environment.
     */
    public final DataEnvironment getEnv() {

        return env;
    }

    /**
     * Returns the underlying database.
     *
     * @return the underlying database.
     */
    public final Db getDb() {

        return db;
    }

    /**
     * Returns whether keys are ordered for the database.
     *
     * @return whether keys are ordered.
     */
    public final boolean isOrdered() {

        return ordered;
    }

    /**
     * Returns whether duplicates are allowed for the database.
     *
     * @return whether duplicates are allowed.
     */
    public final boolean areDuplicatesAllowed() {

        return dupsAllowed;
    }

    /**
     * Returns whether duplicates are ordered for the database.
     *
     * @return whether duplicates are ordered.
     */
    public final boolean areDuplicatesOrdered() {

        return dupsOrdered;
    }

    /**
     * Returns whether keys (record numbers) are renumbered for the database.
     *
     * @return whether keys are renumbered.
     */
    public final boolean areKeysRenumbered() {

        return keysRenumbered;
    }

    /**
     * Returns whether record number access is allowed.
     *
     * @return whether record number access is allowed.
     */
    public final boolean hasRecNumAccess() {

        return recNumAccess;
    }

    /**
     * Returns whether the database was opened in a transaction and therefore
     * must be written in a transaction.
     *
     * @return whether the database is transactional.
     */
    public final boolean isTransactional() {

        return transactional;
    }

    /**
     * Returns whether dirty-read is allowed for the database.
     *
     * @return whether dirty-read is allowed.
     */
    public final boolean isDirtyReadAllowed() {

        return dirtyReadAllowed;
    }

    /**
     * Performs a general database 'get' operation.
     *
     * @param key the key thang.
     *
     * @param data the data thang.
     *
     * @param flags the low-level get flags.
     *
     * @return an error or zero for success.
     *
     * @throws DbException if a database problem occurs.
     */
    public int get(DataThang key, DataThang data, int flags)
        throws DbException {

        int pos = flags & DataDb.FLAGS_POS_MASK;
        if (cdbContext != null) {
            Dbc cursor = openCursor(flags == Db.DB_CONSUME ||
                                    flags == Db.DB_CONSUME_WAIT);
            try {
                if (pos == 0)
                    flags |= Db.DB_SET;
                return get(cursor, key, data, flags);
            } finally {
                closeCursor(cursor);
            }
        } else {
            if (isRecnoKeyNonPositive(pos, key))
                return Db.DB_NOTFOUND;
            return db.get(currentTxn(), key, data, flags);
        }
    }

    /**
     * Performs a general database 'get' operation via a cursor.
     *
     * @param cursor the cursor to read.
     *
     * @param key the key thang.
     *
     * @param val the data thang.
     *
     * @param flags the low-level get flags.
     *
     * @return an error or zero for success.
     *
     * @throws DbException if a database problem occurs.
     */
    public int get(Dbc cursor, DataThang key, DataThang val, int flags)
        throws DbException {

        int pos = flags & DataDb.FLAGS_POS_MASK;
        if (isRecnoKeyNonPositive(pos, key))
            return Db.DB_NOTFOUND;
        int err = cursor.get(key, val, flags);
        return err;
    }

    private boolean isRecnoKeyNonPositive(int pos, DataThang key) {

        if ((pos == Db.DB_SET_RECNO) ||
            ((type == Db.DB_RECNO ||
              type == Db.DB_QUEUE) &&
             (pos == Db.DB_SET ||
              pos == Db.DB_SET_RANGE ||
              pos == Db.DB_GET_BOTH))) {
            if (key.get_recno_key_data() <= 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * Performs a general database 'put' operation.
     *
     * @param key the key to put.
     *
     * @param data the data to put.
     *
     * @param flags the low-level put flags.
     *
     * @return an error or zero for success.
     *
     * @throws DbException if a database problem occurs.
     */
    public int put(DataThang key, DataThang data, int flags)
        throws DbException {

        // Dbc.put() cannot be used with QUEUE/RECNO types
        if (cdbContext != null && (type == Db.DB_HASH ||
                                   type == Db.DB_BTREE)) {
            Dbc cursor = openCursor(true);
            try {
                int pos = flags & DataDb.FLAGS_POS_MASK;
                if (pos == Db.DB_NODUPDATA && !areDuplicatesOrdered()) {
                    if (areDuplicatesAllowed()) {
                        int err = get(cursor, key, data, Db.DB_GET_BOTH);
                        if (err == 0) return Db.DB_KEYEXIST;
                        pos = 0;
                    } else {
                        pos = Db.DB_NOOVERWRITE;
                    }
                    flags = pos | (flags & DataDb.FLAGS_MOD_MASK);
                }
                return put(cursor, key, data, flags);
            } finally {
                closeCursor(cursor);
            }
        } else {
            if (cdbContext != null) { // QUEUE/RECNO
                CdbThreadContext context = (CdbThreadContext) cdbContext.get();
                if (context.writeCursors.size() > 0 ||
                    context.readCursors.size() > 0) {
                    throw new IllegalStateException(
                        "cannot put() with CDB write cursor open");
                }
            }
            int pos = flags & DataDb.FLAGS_POS_MASK;
            if (pos == Db.DB_NODUPDATA && !areDuplicatesOrdered()) {
                if (areDuplicatesAllowed()) {
                    int err = get(key, data, Db.DB_GET_BOTH);
                    if (err == 0) return Db.DB_KEYEXIST;
                    pos = 0;
                } else {
                    pos = Db.DB_NOOVERWRITE;
                }
                flags = pos | (flags & DataDb.FLAGS_MOD_MASK);
            }
            DbTxn txn = currentTxn();
            if (txn != null || !transactional)
                flags &= ~Db.DB_AUTO_COMMIT;
            int err = db.put(txn, key, data, flags);
            return err;
        }
    }

    /**
     * Performs a general database 'put' operation via a cursor.
     * This method works for HASH/BTREE types and all flags, or with all types
     * and DB_CURRENT only.
     *
     * @param cursor the cursor to write.
     *
     * @param key the key to put.
     *
     * @param data the data to put.
     *
     * @param flags the low-level put flags.
     *
     * @return an error or zero for success.
     *
     * @throws DbException if a database problem occurs.
     */
    public int put(Dbc cursor, DataThang key, DataThang data, int flags)
        throws DbException {

        if (flags == Db.DB_CURRENT && areDuplicatesOrdered()) {
            // Workaround for two Db issues: 1- with HASH type a put() with
            // different data is allowed, contrary to docs, 2- with non-HASH
            // types an erroror is printed to standard out.
            DataThang temp = new DataThang();
            cursor.get(key, temp, flags);
            if (data.equals(temp)) {
                return 0; // nothing to do, data is already as specified
            } else {
                throw new IllegalArgumentException(
                  "Current data differs from put data with sorted duplicates");
            }
        }
        if (flags == Db.DB_NOOVERWRITE) {
            int err = cursor.get(key, DataThang.getDiscardDataThang(),
                                 Db.DB_SET | env.getWriteLockFlag());
            if (err == 0) {
                return Db.DB_KEYEXIST;
            } else if (err != Db.DB_NOTFOUND) {
                return err;
            }
            flags = 0;
        }
        if (flags == 0) {
            if (areDuplicatesOrdered()) {
                flags = Db.DB_NODUPDATA;
            } else {
                flags = Db.DB_KEYLAST;
            }
        }
        int err = cursor.put(key, data, flags);
        return err;
    }

    /**
     * Performs a general database 'delete' operation.
     *
     * @param key the key to delete.
     *
     * @param flags the low-level delete flags.
     *
     * @return an error or zero for success.
     *
     * @throws DbException if a database problem occurs.
     */
    public int delete(DataThang key, int flags)
        throws DbException {

        Dbc cursor = openCursor(true);
        try {
            int err = cursor.get(key, DataThang.getDiscardDataThang(),
                                 Db.DB_SET | env.getWriteLockFlag());
            if (err == 0) {
                return cursor.delete(0);
            } else {
                return err;
            }
        } finally {
            closeCursor(cursor);
        }
    }

    /**
     * Opens a cursor for this database.
     *
     * @param writeCursor true to open a write cursor in a CDB environment, and
     * ignored for other environments.
     *
     * @return the open cursor.
     *
     * @throws DbException if a database problem occurs.
     */
    public Dbc openCursor(boolean writeCursor)
        throws DbException {

        if (cdbContext != null) {
            CdbThreadContext context = (CdbThreadContext) cdbContext.get();
            List cursors;
            int flags;

            if (writeCursor) {
                cursors = context.writeCursors;
                flags = Db.DB_WRITECURSOR;

                if (context.readCursors.size() > 0) {
                    // although CDB allows opening a write cursor when a read
                    // cursor is open, a self-deadlock will occur if a write is
                    // attempted for a record that is read-locked; we should
                    // avoid self-deadlocks at all costs
                    throw new IllegalStateException(
                      "cannot open CDB write cursor when read cursor is open");
                }
            } else {
                cursors = context.readCursors;
                flags = 0;
            }

            Dbc cursor;
            if (cursors.size() > 0) {
                Dbc other = ((Dbc) cursors.get(0));
                cursor = other.dup(0);
            } else {
                cursor = db.cursor(null, flags);
            }
            cursors.add(cursor);
            return cursor;
        } else {
            return db.cursor(currentTxn(), 0);
        }
    }

    /**
     * Duplicates a cursor for this database.
     *
     * @param writeCursor true to open a write cursor in a CDB environment, and
     * ignored for other environments.
     *
     * @param flags the low-level dup() flags.
     *
     * @return the open cursor.
     *
     * @throws DbException if a database problem occurs.
     */
    public Dbc dupCursor(Dbc cursor, boolean writeCursor, int flags)
        throws DbException {

        if (cdbContext != null) {
            CdbThreadContext context = (CdbThreadContext) cdbContext.get();
            List cursors = writeCursor ? context.writeCursors
                                       : context.readCursors;
            if (!cursors.contains(cursor))
                throw new IllegalStateException("cursor to dup not tracked");
            Dbc newCursor = cursor.dup(flags);
            cursors.add(newCursor);
            return newCursor;
        } else {
            return cursor.dup(flags);
        }
    }

    /**
     * Closes a cursor for this database.
     *
     * @param cursor the cursor to close.
     *
     * @throws DbException if a database problem occurs.
     */
    public void closeCursor(Dbc cursor)
        throws DbException {

        if (cursor == null) return;

        if (cdbContext != null) {
            CdbThreadContext context = (CdbThreadContext) cdbContext.get();

            if (!context.readCursors.remove(cursor) &&
                !context.writeCursors.remove(cursor)) {
                throw new IllegalStateException(
                  "closing CDB cursor that was not known to be open");
            }
        }

        cursor.close();
    }

    private final DbTxn currentTxn() {

        return transactional ? env.getTxn() : null;
    }

    /**
     * Returns a debugging string containing the database name.
     *
     * @return a debugging string.
     */
    public String toString() {

        String val = file;
        if (database != null) file += ' ' + database;
        return toString(this, val);
    }

    static String toString(Object o, String val) {

        String cls = null;
        if (o != null) {
            cls = o.getClass().getName();
            int i = cls.lastIndexOf('.');
            if (i >= 0) cls = cls.substring(i + 1);
        }
        return "[" + cls + ' ' + val + ']';
    }

    private static final class CdbThreadContext {

        private DataDb db;
        private List writeCursors = new ArrayList();
        private List readCursors = new ArrayList();

        CdbThreadContext(DataDb db) {
            this.db = db;
        }
    }
}
