/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 2000-2003
 *      Sleepycat Software.  All rights reserved.
 *
 * $Id: DataCursor.java,v 1.20 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 java.io.IOException;

/**
 * (<em>internal</em>) Represents a Berkeley DB cursor and adds support for
 * indices, bindings and key ranges.
 *
 * <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>
 *
 * <p>This class operates on a view and takes care of reading and updating
 * indices, calling bindings, constraining access to a key range, etc.</p>
 *
 * @author Mark Hayes
 */
public final class DataCursor {

    private Dbc cursor;
    private DbTxn txn;
    private DataView view;
    private DataEnvironment env;
    private DataDb db;
    private KeyRange range;
    private boolean writeAllowed;
    private boolean dirtyRead;
    private DataThang keyThang;
    private DataThang valueThang;
    private DataThang primaryKeyThang;
    private DataThang otherThang;
    private int cursorDbType;
    private DataCursor[] indexCursorsToClose;
    private boolean closeDirect;
    private boolean isJoinCursor;

    /**
     * Creates a cursor for a given view.
     *
     * @param view the database view traversed by the cursor.
     *
     * @param writeAllowed whether the cursor can be used for writing.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public DataCursor(DataView view, boolean writeAllowed)
        throws DbException, IOException {

        init(view, writeAllowed, null, null);
    }

    /**
     * Creates a cursor for a given view and single key range.
     *
     * @param view the database view traversed by the cursor.
     *
     * @param writeAllowed whether the cursor can be used for writing.
     *
     * @param singleKey the single key value.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public DataCursor(DataView view, boolean writeAllowed, Object singleKey)
        throws DbException, IOException {

        init(view, writeAllowed, view.subRange(singleKey), null);
    }

    /**
     * Creates a cursor for a given view and key range.
     *
     * @param view the database view traversed by the cursor.
     *
     * @param writeAllowed whether the cursor can be used for writing.
     *
     * @param beginKey the lower bound.
     *
     * @param beginInclusive whether the lower bound is inclusive.
     *
     * @param endKey the upper bound.
     *
     * @param endInclusive whether the upper bound is inclusive.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public DataCursor(DataView view, boolean writeAllowed,
                      Object beginKey, boolean beginInclusive,
                      Object endKey, boolean endInclusive)
        throws DbException, IOException {

        init(view, writeAllowed,
             view.subRange(beginKey, beginInclusive, endKey, endInclusive),
             null);
    }

    /**
     * Clones a cursor preserving the current position.
     *
     * @param other the cursor to clone.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public DataCursor(DataCursor other)
        throws DbException, IOException {

        this.view = other.view;
        this.env = other.env;
        this.db  = other.db;
        this.writeAllowed = other.writeAllowed;
        this.dirtyRead = other.dirtyRead;

        initThangs();

        this.keyThang.copy(other.keyThang);
        this.valueThang.copy(other.valueThang);
        if (this.primaryKeyThang != this.keyThang) {
            this.primaryKeyThang.copy(other.primaryKeyThang);
        }
        this.range = other.range;

        this.cursor = db.dupCursor(other.cursor, writeAllowed, Db.DB_POSITION);

        this.txn = other.txn;
        this.closeDirect = false;
    }

    DataCursor(DataView view, DataCursor[] indexCursors,
               boolean presorted, boolean closeIndexCursors)
        throws DbException, IOException {

        this.isJoinCursor = true;

        if (view.index != null) {
            throw new IllegalArgumentException(
                "Primary view in join must not be indexed");
        }
        Dbc[] cursors = new Dbc[indexCursors.length];
        for (int i = 0; i < cursors.length; i += 1) {
            DataIndex index = indexCursors[i].view.index;
            if (index == null || index.store != view.store) {
                throw new IllegalArgumentException(
                    "Join cursor for view " + index +
                    " is not indexed on primary store " + view.store);
            }
            cursors[i] = indexCursors[i].cursor;
        }
        Dbc joinCursor = view.store.db.db.join(cursors,
                                           presorted ? Db.DB_JOIN_NOSORT : 0);
        init(view, false, null, joinCursor);
        if (closeIndexCursors) {
            this.indexCursorsToClose = indexCursors;
        }
    }

    private void init(DataView view, boolean writeAllowed, KeyRange range,
                      Dbc cursor)
        throws DbException, IOException {

        this.view = view;
        this.env = view.store.db.env;
        this.range = (range != null) ? range : new KeyRange(view.range);
        this.writeAllowed = writeAllowed && view.isWriteAllowed();
        this.dirtyRead = view.dirtyRead;

        initThangs();

        if (view.index != null) {
            this.db = view.index.db;
        } else {
            this.db = view.store.db;
        }
        this.cursorDbType = this.db.db.getDbType();

        if (cursor != null) {
            this.cursor = cursor;
            this.closeDirect = true;
        } else {
            this.cursor = this.db.openCursor(this.writeAllowed);
            this.closeDirect = false;
        }
    }

    private void initThangs()
        throws DbException, IOException {

        this.keyThang = new DataThang();
        this.primaryKeyThang = (view.index != null)
                                ? (new DataThang()) : keyThang;
        this.valueThang = new DataThang();
    }

    /**
     * Closes a cursor.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public void close()
        throws DbException, IOException {

        if (cursor != null) {
            Dbc toClose = cursor;
            cursor = null;
            if (closeDirect) {
                toClose.close();
            } else {
                db.closeCursor(toClose);
            }
        }
        if (indexCursorsToClose != null) {
            DataCursor[] toClose = indexCursorsToClose;
            indexCursorsToClose = null;
            for (int i = 0; i < toClose.length; i += 1) {
                toClose[i].close();
            }
        }
    }

    /**
     * Returns the view for this cursor, as specified to the constructor.
     *
     * @return the view.
     */
    public DataView getView() {

        return view;
    }

    KeyRange getRange() {

        return range;
    }

    /**
     * Returns whether write is allowed for this cursor, as specified to the
     * constructor.
     *
     * @return whether write is allowed.
     */
    public boolean isWriteAllowed() {

        return writeAllowed;
    }

    /**
     * Returns the key object for the last record read.
     *
     * @return the current key object.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public Object getCurrentKey()
        throws DbException, IOException {

        if (view.keyBinding == null) {
            throw new UnsupportedOperationException(
                "getCurrentKey requires keyBinding");
        }
        return view.makeKey(keyThang);
    }

    /**
     * Returns the value object for the last record read.
     *
     * @return the current value object.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public Object getCurrentValue()
        throws DbException, IOException {

        return view.makeValue(primaryKeyThang, valueThang);
    }

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

        return db.hasRecNumAccess();
    }

    /**
     * Returns the record number for the last record read.
     *
     * @return the last read record number.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public int getCurrentRecordNumber()
        throws DbException, IOException {

        if (cursorDbType == Db.DB_BTREE) {
            if (otherThang == null) {
                otherThang = new DataThang();
            }
            Dbt discardThang = DataThang.getDiscardDataThang();
            cursor.get(discardThang, otherThang, Db.DB_GET_RECNO);
            return otherThang.get_recno_key_data();
        } else {
            // Assume QUEUE or RECNO database.
            return keyThang.get_recno_key_data();
        }
    }

    /**
     * Perform a database 'get' using the given key and value.
     *
     * @param key the key or null if none is required by the flag.
     *
     * @param value the value or null if none is required by the flag.
     *
     * @param flag a single flag value appropriate for cursor get.
     *
     * @param lockForWrite whether to set the RMW flag.
     *
     * @return an error or zero for success.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public int get(Object key, Object value, int flag, boolean lockForWrite)
        throws DbException, IOException {

        int err = 0;
        if (view.btreeRecNumAccess && flag == Db.DB_SET) {
            flag = Db.DB_SET_RECNO;
        }
        int indexCursorFlag = flag;
        int indexPrimaryFlag = 0;
        if (flag == Db.DB_GET_BOTH) {
            view.useValue(value, valueThang, null);
            err = view.useKey(key, value, keyThang, range);
            indexCursorFlag = Db.DB_SET;
            indexPrimaryFlag = flag;
        } else if (flag == Db.DB_SET || flag == Db.DB_SET_RANGE ||
                   flag == Db.DB_SET_RECNO) {
            err = view.useKey(key, value, keyThang, range);
        } else if (flag == Db.DB_GET_RECNO || flag == Db.DB_JOIN_ITEM) {
            throw new IllegalArgumentException("flag not supported: " + flag);
        } else {
            // NEXT,PREV,NEXT_DUP,NEXT_NODUP,PREV_NODUP,CURRENT,FIRST,LAST
            if (key != null || value != null) {
                throw new IllegalArgumentException(
                                                "key and value must be null");
            }
        }
        if (err == 0) {
            err = lowLevelGet(flag, indexCursorFlag, indexPrimaryFlag,
                              lockForWrite);
        }
        return err;
    }

    /**
     * Find the given value, using DB_GET_BOTH if possible, or a sequential
     * search otherwise.
     *
     * @param value the value to search for among duplicates at the current
     * position.
     * 
     * @param findFirst whether to find the first or last value.
     *
     * @return an error or zero for success.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public int find(Object value, boolean findFirst)
        throws DbException, IOException {

        if (isJoinCursor) throw new UnsupportedOperationException();
        view.useValue(value, valueThang, null);
        int err;
        if (view.entityBinding != null && view.index == null &&
            (findFirst || !view.areDuplicatesAllowed())) {
            err = view.useKey(null, value, keyThang, range);
            if (err == 0) {
                err = lowLevelGet(Db.DB_GET_BOTH, Db.DB_SET,
                                  Db.DB_GET_BOTH, false);
            }
        } else {
            if (otherThang == null) {
                otherThang = new DataThang();
            }
            otherThang.copy(valueThang);
            int flag = (findFirst ? Db.DB_FIRST : Db.DB_LAST);
            err = 0;
            while (err == 0) {
                err = get(null, null, flag, false);
                if (err == 0 && valueThang.compareTo(otherThang) == 0) {
                    break;
                } else {
                    flag = (findFirst ? Db.DB_NEXT : Db.DB_PREV);
                }
            }
        }
        return err;
    }

    /**
     * Return the number of duplicates for the current key.
     *
     * @return the number of duplicates.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public int count()
        throws DbException, IOException {

        if (isJoinCursor) throw new UnsupportedOperationException();
        return cursor.count(0);
    }

    /**
     * Perform an arbitrary database 'put' operation, optionally returning
     * the previous value.
     *
     * @param key the key to put.
     *
     * @param value the value to put.
     *
     * @param flag a single flag value appropriate for cursor put.
     *
     * @param oldValue holds the old value, or null if the old value should
     * not be returned.
     *
     * @return an error or zero for success.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public int put(Object key, Object value, int flag, Object[] oldValue)
        throws DbException, IOException {

        return put(key, value, flag, oldValue, false);
    }

    /**
     * Perform an arbitrary database 'put' operation, optionally using the
     * current key instead of the key parameter.
     *
     * @param key the key to put.
     *
     * @param value the value to put.
     *
     * @param flag a single flag value appropriate for cursor put.
     *
     * @param oldValue holds the old value, or null if the old value should
     * not be returned.
     *
     * @param useCurrentKey is true to use the current key rather than the
     * key parameter.
     *
     * @return an error or zero for success.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public int put(Object key, Object value, int flag, Object[] oldValue,
                   boolean useCurrentKey)
        throws DbException, IOException {

        if (isJoinCursor) throw new UnsupportedOperationException();
        if (view.index != null) {
            throw new UnsupportedOperationException(
                "put with index not allowed");
        }
        boolean doUpdate = false; // update vs insert
        int err = 0;
        if (flag == Db.DB_CURRENT) {
            doUpdate = true;
        } else if (flag == Db.DB_AFTER || flag == Db.DB_BEFORE) {
            // Do nothing.
        } else {
            if (!useCurrentKey) {
                err = view.useKey(key, value, keyThang, range);
                if (err != 0) {
                    throw new IllegalArgumentException("key out of range");
                }
            }
            if (flag == 0) {
                flag = view.areDuplicatesOrdered() ? Db.DB_NODUPDATA
                                                   : Db.DB_KEYFIRST;
            }
            if (flag == Db.DB_KEYFIRST || flag == Db.DB_KEYLAST) {
                if (!view.areDuplicatesAllowed()) {
                    err = lowLevelGet(Db.DB_SET, 0, 0, true);
                    if (err == 0) {
                        doUpdate = true;
                    }
                }
            } else if (flag != Db.DB_NODUPDATA) {
                throw new IllegalArgumentException("flag unknown: " + flag);
            }
        }
        DataThang oldValueThang;
        if (doUpdate) {
            if (oldValue != null) {
                oldValue[0] = getCurrentValue();
            }
            if (otherThang == null) {
                otherThang = new DataThang();
            }
            otherThang.copy(valueThang);
            oldValueThang = otherThang;
        } else {
            if (oldValue != null) {
                oldValue[0] = null;
            }
            oldValueThang = null;
        }
        DataThang checkKeyThang = (flag == Db.DB_AFTER) ? null : keyThang;
        view.useValue(value, valueThang, checkKeyThang);
        err = db.put(cursor, keyThang, valueThang, flag);
        if (err == 0) {
            view.store.applyChange(primaryKeyThang, oldValueThang, valueThang);
        }
        return err;
    }

    /**
     * Perform an arbitrary database 'delete' operation.
     *
     * @return an error or zero for success.
     *
     * @throws DbException if a database problem occurs.
     *
     * @throws IOException if an IO problem occurs.
     */
    public int delete()
        throws DbException, IOException {

        if (isJoinCursor || !writeAllowed) {
            throw new UnsupportedOperationException();
        }
        int err;
        if (view.index != null) {
            err = view.store.db.delete(primaryKeyThang, 0);
        } else {
            err = cursor.delete(0);
        }
        if (err == 0) {
            view.store.applyChange(primaryKeyThang, valueThang, null);
        }
        return err;
    }

    private int lowLevelGet(int flag, int indexCursorFlag,
                            int indexPrimaryFlag, boolean lockForWrite)
        throws DbException {

        int err;
        int otherFlags = 0;
        if (dirtyRead)
            otherFlags |= Db.DB_DIRTY_READ;
        // Don't use RMW with dirty-read, since RWM may cause blocking
        if (lockForWrite && !dirtyRead && !env.isDirtyRead())
            otherFlags |= env.getWriteLockFlag();

        // Always clear the cached data formations to prevent inconsistencies.
        keyThang.clearDataFormation();
        primaryKeyThang.clearDataFormation();
        valueThang.clearDataFormation();

        if (view.index != null) {
            if (isJoinCursor) throw new UnsupportedOperationException();
            if (view.btreeRecNumAccess && indexCursorFlag == Db.DB_SET) {
                indexCursorFlag = Db.DB_SET_RECNO;
            }
            err = range.get(db, cursor, keyThang, primaryKeyThang,
                            indexCursorFlag | otherFlags);
            if (err == 0) {
                err = view.store.db.get(primaryKeyThang, valueThang,
                                        indexPrimaryFlag | otherFlags);
            }
        } else {
            if (isJoinCursor) {
                if (flag != Db.DB_FIRST &&
                    flag != Db.DB_NEXT &&
                    flag != Db.DB_NEXT_NODUP) {
                    throw new UnsupportedOperationException();
                }
                flag = 0;
                otherFlags = 0;
            }
            if (view.btreeRecNumAccess && flag == Db.DB_SET) {
                flag = Db.DB_SET_RECNO;
            }
            err = range.get(db, cursor, keyThang, valueThang,
                            flag | otherFlags);
        }
        return err;
    }
}
