/**
 * SqlJetTableWrapper.java
 * Copyright (C) 2009-2013 TMate Software Ltd
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program 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 General Public License for more details.
 */
package org.tmatesoft.sqljet.core.internal.table;

import java.util.List;
import java.util.Random;
import java.util.Stack;

import org.tmatesoft.sqljet.core.SqlJetEncoding;
import org.tmatesoft.sqljet.core.SqlJetErrorCode;
import org.tmatesoft.sqljet.core.SqlJetException;
import org.tmatesoft.sqljet.core.SqlJetValueType;
import org.tmatesoft.sqljet.core.internal.ISqlJetBtree;
import org.tmatesoft.sqljet.core.internal.ISqlJetBtreeCursor;
import org.tmatesoft.sqljet.core.internal.ISqlJetMemoryPointer;
import org.tmatesoft.sqljet.core.internal.ISqlJetVdbeMem;
import org.tmatesoft.sqljet.core.internal.SqlJetBtreeTableCreateFlags;
import org.tmatesoft.sqljet.core.internal.SqlJetUtility;
import org.tmatesoft.sqljet.core.internal.vdbe.SqlJetBtreeRecord;
import org.tmatesoft.sqljet.core.internal.vdbe.SqlJetKeyInfo;

/**
 * @author TMate Software Ltd.
 * @author Sergey Scherbina (sergey.scherbina@gmail.com)
 * 
 */
public class SqlJetBtreeTable implements ISqlJetBtreeTable {

    protected ISqlJetBtree btree;
    protected int rootPage;

    protected boolean write;
    protected boolean index;

    private long priorNewRowid = 0;

    private SqlJetBtreeRecord recordCache;
    private Object[] valueCache;
    private Object[] valuesCache;
    
    private Stack<State> states;
    
    protected static class State {

        private ISqlJetBtreeCursor cursor;
        private SqlJetKeyInfo keyInfo;
        
        public State(ISqlJetBtreeCursor cursor, SqlJetKeyInfo keyInfo) {
            this.cursor = cursor;
            this.keyInfo = keyInfo;
        }
        
        public ISqlJetBtreeCursor getCursor() {
            return cursor;
        }
        
        public SqlJetKeyInfo getKeyInfo() {
            return keyInfo;
        }
        
        public void close() throws SqlJetException {
            if (cursor != null) {
                cursor.closeCursor();
            }
        }
    }

    /**
     * @param db
     * @param btree
     * @param rootPage
     * @param write
     * @param index
     * @throws SqlJetException
     */
    public SqlJetBtreeTable(ISqlJetBtree btree, int rootPage, boolean write, boolean index) throws SqlJetException {

        init(btree, rootPage, write, index);

    }

    /**
     * @param db
     * @param btree
     * @param rootPage
     * @param write
     * @param index
     * @throws SqlJetException
     */
    private void init(ISqlJetBtree btree, int rootPage, boolean write, boolean index) throws SqlJetException {
        this.states = new Stack<State>();
        this.btree = btree;
        this.rootPage = rootPage;
        this.write = write;
        this.index = index;

        pushState();
        first();
    }
    
    private State getCurrentState() {
        assert !states.isEmpty();
        return states.peek();
    }
    
    protected ISqlJetBtreeCursor getCursor() {
        return getCurrentState().getCursor();
    }
    
    protected SqlJetKeyInfo getKeyInfo() {
        return getCurrentState().getKeyInfo();
    }
    
    public void pushState() throws SqlJetException {
        SqlJetKeyInfo keyInfo = null;
        if (index) {
            keyInfo = new SqlJetKeyInfo();
            keyInfo.setEnc(btree.getDb().getOptions().getEncoding());
        }
        ISqlJetBtreeCursor cursor = btree.getCursor(rootPage, write, index ? keyInfo : null);
        states.push(new State(cursor, keyInfo));
        clearRecordCache();
        adjustKeyInfo();
    }
    
    protected void adjustKeyInfo() throws SqlJetException {
    }

    public boolean popState() throws SqlJetException {
        if (states.size() <= 1) {
            return false;
        }
        State oldState = states.pop();
        oldState.close();
        clearRecordCache();
        return true;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.sqljet.core.internal.btree.ISqlJetBtreeTable#close()
     */
    public void close() throws SqlJetException {
        while(popState()) {}

        clearRecordCache();
        getCurrentState().close();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.sqljet.core.ISqlJetBtreeTable#unlock()
     */
    public void unlock() {
        getCursor().leaveCursor();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.sqljet.core.ISqlJetBtreeTable#lock()
     */
    public void lock() throws SqlJetException {
        getCursor().enterCursor();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.sqljet.core.internal.btree.ISqlJetBtreeTable#eof()
     */
    public boolean eof() throws SqlJetException {
        hasMoved();
        return getCursor().eof();
    }

    /**
     * @throws SqlJetException
     */
    public boolean hasMoved() throws SqlJetException {
        getCursor().enterCursor();
        try {
            return getCursor().cursorHasMoved();
        } finally {
            getCursor().leaveCursor();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.sqljet.core.ISqlJetBtreeTable#first()
     */
    public boolean first() throws SqlJetException {
        lock();
        try {
            clearRecordCache();
            return !getCursor().first();
        } finally {
            unlock();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.sqljet.core.ISqlJetBtreeTable#last()
     */
    public boolean last() throws SqlJetException {
        lock();
        try {
            clearRecordCache();
            return !getCursor().last();
        } finally {
            unlock();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.sqljet.core.internal.btree.ISqlJetBtreeTable#next()
     */
    public boolean next() throws SqlJetException {
        lock();
        try {
            clearRecordCache();
            hasMoved();
            return !getCursor().next();
        } finally {
            unlock();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.sqljet.core.ISqlJetBtreeTable#previous()
     */
    public boolean previous() throws SqlJetException {
        lock();
        try {
            clearRecordCache();
            hasMoved();
            return !getCursor().previous();
        } finally {
            unlock();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.sqljet.core.internal.btree.ISqlJetBtreeTable#getRecord
     */
    public ISqlJetBtreeRecord getRecord() throws SqlJetException {
        if (eof())
            return null;
        if (null == recordCache) {
            lock();
            try {
                recordCache = new SqlJetBtreeRecord(getCursor(), index, btree.getDb().getOptions().getFileFormat());
            } finally {
                unlock();
            }
            valueCache = new Object[recordCache.getFieldsCount()];
        }
        return recordCache;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.sqljet.core.internal.table.ISqlJetBtreeTable#lockTable(
     * boolean)
     */
    public void lockTable(boolean write) {
        btree.lockTable(rootPage, write);
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.sqljet.core.internal.table.ISqlJetBtreeTable#getEncoding()
     */
    public SqlJetEncoding getEncoding() throws SqlJetException {
        return getCursor().getCursorDb().getOptions().getEncoding();
    }

    protected static boolean checkField(ISqlJetBtreeRecord record, int field) throws SqlJetException {
        return (field >= 0 && record != null && field < record.getFieldsCount());
    }

    protected ISqlJetVdbeMem getValueMem(int field) throws SqlJetException {
        final ISqlJetBtreeRecord r = getRecord();
        if (null == r)
            return null;
        if (!checkField(r, field))
            return null;
        final List<ISqlJetVdbeMem> fields = r.getFields();
        if (null == fields)
            return null;
        return fields.get(field);
    }

    public Object getValue(int field) throws SqlJetException {
        if (valueCache != null && field < valueCache.length) {
            final Object valueCached = valueCache[field];
            if (valueCached != null)
                return valueCached;
        }
        final Object valueUncached = getValueUncached(field);
        if (valueUncached != null) {
            valueCache[field] = valueUncached;
        }
        return valueUncached;
    }

    public Object getValueUncached(int field) throws SqlJetException {
        final ISqlJetVdbeMem value = getValueMem(field);
        if (value == null || value.isNull())
            return null;
        switch (value.getType()) {
        case INTEGER:
            return value.intValue();
        case FLOAT:
            return value.realValue();
        case TEXT:
            return SqlJetUtility.toString(value.valueText(getEncoding()), getEncoding());
        case BLOB:
            return value.valueBlob();
        case NULL:
            break;
        default:
            break;
        }
        return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.sqljet.core.internal.table.ISqlJetBtreeTable#getFieldsCount
     * ()
     */
    public int getFieldsCount() throws SqlJetException {
        final ISqlJetBtreeRecord r = getRecord();
        if (null == r)
            return 0;
        return r.getFieldsCount();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.sqljet.core.internal.table.ISqlJetBtreeTable#isNull(int)
     */
    public boolean isNull(int field) throws SqlJetException {
        final ISqlJetVdbeMem value = getValueMem(field);
        if (null == value)
            return true;
        return value.isNull();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.sqljet.core.internal.table.ISqlJetBtreeTable#getString(int)
     */
    public String getString(int field) throws SqlJetException {
        final ISqlJetVdbeMem value = getValueMem(field);
        if (value == null || value.isNull())
            return null;
        return SqlJetUtility.toString(value.valueText(getEncoding()), getEncoding());
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.sqljet.core.internal.table.ISqlJetBtreeTable#getInteger
     * (int)
     */
    public long getInteger(int field) throws SqlJetException {
        final ISqlJetVdbeMem value = getValueMem(field);
        if (value == null || value.isNull())
            return 0;
        return value.intValue();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.sqljet.core.internal.table.ISqlJetBtreeTable#getReal(int)
     */
    public double getFloat(int field) throws SqlJetException {
        final ISqlJetVdbeMem value = getValueMem(field);
        if (value == null || value.isNull())
            return 0;
        return value.realValue();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.sqljet.core.internal.table.ISqlJetBtreeTable#getFieldType
     * (int)
     */
    public SqlJetValueType getFieldType(int field) throws SqlJetException {
        final ISqlJetVdbeMem value = getValueMem(field);
        if (value == null)
            return SqlJetValueType.NULL;
        return value.getType();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.sqljet.core.internal.table.ISqlJetBtreeTable#getBlob(int)
     */
    public ISqlJetMemoryPointer getBlob(int field) throws SqlJetException {
        final ISqlJetVdbeMem value = getValueMem(field);
        if (value == null || value.isNull())
            return null;
        return value.valueBlob();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.sqljet.core.internal.table.ISqlJetBtreeTable#getValues()
     */
    public Object[] getValues() throws SqlJetException {
        if (valuesCache != null) {
            return valuesCache;
        } else {
            final ISqlJetBtreeRecord record = getRecord();
            final int fieldsCount = record.getFieldsCount();
            for (int i = 0; i < fieldsCount; i++) {
                valueCache[i] = getValue(i);
            }
            valuesCache = valueCache;
            return valueCache;
        }
    }

    public long newRowId() throws SqlJetException {
        return newRowId(0);
    }

    /**
     * Get a new integer record number (a.k.a "rowid") used as the key to a
     * table. The record number is not previously used as a key in the database
     * table that cursor P1 points to. The new record number is written written
     * to register P2.
     * 
     * Prev is the largest previously generated record number. No new record
     * numbers are allowed to be less than this value. When this value reaches
     * its maximum, a SQLITE_FULL error is generated. This mechanism is used to
     * help implement the AUTOINCREMENT feature.
     * 
     * @param prev
     * @return
     * @throws SqlJetException
     */
    public long newRowId(long prev) throws SqlJetException {
        /*
         * The next rowid or record number (different terms for the same thing)
         * is obtained in a two-step algorithm. First we attempt to find the
         * largest existing rowid and add one to that. But if the largest
         * existing rowid is already the maximum positive integer, we have to
         * fall through to the second probabilistic algorithm. The second
         * algorithm is to select a rowid at random and see if it already exists
         * in the table. If it does not exist, we have succeeded. If the random
         * rowid does exist, we select a new one and try again, up to 1000
         * times.For a table with less than 2 billion entries, the probability
         * of not finding a unused rowid is about 1.0e-300. This is a non-zero
         * probability, but it is still vanishingly small and should never cause
         * a problem. You are much, much more likely to have a hardware failure
         * than for this algorithm to fail.
         * 
         * To promote locality of reference for repetitive inserts, the first
         * few attempts at choosing a random rowid pick values just a little
         * larger than the previous rowid. This has been shown experimentally to
         * double the speed of the COPY operation.
         */

        lock();
        try {
            boolean useRandomRowid = false;
            long v = 0;
            int res = 0;
            int cnt = 0;

            if ((getCursor().flags() & (SqlJetBtreeTableCreateFlags.INTKEY.getValue() | SqlJetBtreeTableCreateFlags.ZERODATA
                    .getValue())) != SqlJetBtreeTableCreateFlags.INTKEY.getValue()) {
                throw new SqlJetException(SqlJetErrorCode.CORRUPT);
            }

            assert ((getCursor().flags() & SqlJetBtreeTableCreateFlags.INTKEY.getValue()) != 0);
            assert ((getCursor().flags() & SqlJetBtreeTableCreateFlags.ZERODATA.getValue()) == 0);

            long MAX_ROWID = 0x7fffffff;

            final boolean last = getCursor().last();

            if (last) {
                v = 1;
            } else {
                v = getCursor().getKeySize();
                if (v == MAX_ROWID) {
                    useRandomRowid = true;
                } else {
                    v++;
                }

                if (prev != 0) {
                    if (prev == MAX_ROWID || useRandomRowid) {
                        throw new SqlJetException(SqlJetErrorCode.FULL);
                    }
                    if (v < prev) {
                        v = prev + 1;
                    }
                }

                if (useRandomRowid) {
                    v = priorNewRowid;
                    Random random = new Random();
                    /* SQLITE_FULL must have occurred prior to this */
                    assert (prev == 0);
                    cnt = 0;
                    do {
                        if (cnt == 0 && (v & 0xffffff) == v) {
                            v++;
                        } else {
                            v = random.nextInt();
                            if (cnt < 5)
                                v &= 0xffffff;
                        }
                        if (v == 0)
                            continue;
                        res = getCursor().moveToUnpacked(null, v, false);
                        cnt++;
                    } while (cnt < 100 && res == 0);
                    priorNewRowid = v;
                    if (res == 0) {
                        throw new SqlJetException(SqlJetErrorCode.FULL);
                    }
                }
            }
            return v;
        } finally {
            unlock();
        }
    }

    protected void clearRecordCache() {
        if (recordCache != null) {
            recordCache.release();
        }
        recordCache = null;
        valuesCache = null;
        valueCache = null;
    }

    public void clear() throws SqlJetException {
        btree.clearTable(rootPage, null);
    }

    public long getKeySize() throws SqlJetException {
        return getCursor().getKeySize();
    }

    public int moveTo(ISqlJetMemoryPointer pKey, long nKey, boolean bias) throws SqlJetException {
        clearRecordCache();
        return getCursor().moveTo(pKey, nKey, bias);
    }

    /**
     * @param object
     * @param rowId
     * @param pData
     * @param remaining
     * @param i
     * @param b
     * @throws SqlJetException
     */
    public void insert(ISqlJetMemoryPointer pKey, long nKey, ISqlJetMemoryPointer pData, int nData, int nZero,
            boolean bias) throws SqlJetException {
        clearRecordCache();
        getCursor().insert(pKey, nKey, pData, nData, nZero, bias);
    }

    /**
     * @throws SqlJetException
     * 
     */
    public void delete() throws SqlJetException {
        clearRecordCache();
        getCursor().delete();
    }
}
