/**
 * SqlJetRawTable.java
 * Copyright (C) 2009-2010 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.
 *
 * For information on how to redistribute this software under
 * the terms of a license other than GNU General Public License
 * contact TMate Software at support@sqljet.com
 */
package org.tmatesoft.sqljet.core.internal.vdbe;

import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.tmatesoft.sqljet.core.SqlJetEncoding;
import org.tmatesoft.sqljet.core.SqlJetErrorCode;
import org.tmatesoft.sqljet.core.SqlJetException;
import org.tmatesoft.sqljet.core.internal.ISqlJetBtreeCursor;
import org.tmatesoft.sqljet.core.internal.ISqlJetLimits;
import org.tmatesoft.sqljet.core.internal.ISqlJetMemoryPointer;
import org.tmatesoft.sqljet.core.internal.ISqlJetVdbeMem;
import org.tmatesoft.sqljet.core.internal.SqlJetUtility;
import org.tmatesoft.sqljet.core.internal.memory.SqlJetMemoryPointer;
import org.tmatesoft.sqljet.core.internal.table.ISqlJetBtreeRecord;
import org.tmatesoft.sqljet.core.table.ISqlJetOptions;

/**
 * Implements {@link ISqlJetBtreeRecord}.
 * 
 * @author TMate Software Ltd.
 * @author Sergey Scherbina (sergey.scherbina@gmail.com)
 * 
 */
public class SqlJetBtreeRecord implements ISqlJetBtreeRecord {

    private ISqlJetBtreeCursor cursor;
    private boolean isIndex;

    private int fieldsCount = 0;
    private List<Integer> aType = new ArrayList<Integer>();
    private List<Integer> aOffset = new ArrayList<Integer>();
    private List<ISqlJetVdbeMem> fields = new ArrayList<ISqlJetVdbeMem>();

    private int file_format = ISqlJetOptions.SQLJET_DEFAULT_FILE_FORMAT;

    /**
     * @return the fields
     */
    public List<ISqlJetVdbeMem> getFields() {
        return Collections.unmodifiableList(fields);
    }

    public SqlJetBtreeRecord(ISqlJetBtreeCursor cursor, boolean isIndex, int fileFormat) throws SqlJetException {
        this.cursor = cursor;
        this.isIndex = isIndex;
        this.file_format = fileFormat;
        read();
    }

    public SqlJetBtreeRecord(List<ISqlJetVdbeMem> values) {
        fields.addAll(values);
        fieldsCount = values.size();
    }

    public SqlJetBtreeRecord(ISqlJetVdbeMem[] values, int file_format) {
        this.file_format = file_format;
        initFields(values);
    }

    public SqlJetBtreeRecord(ISqlJetVdbeMem... values) {
        initFields(values);
    }

    private void initFields(ISqlJetVdbeMem[] values) {
        fields.addAll(Arrays.asList(values));
        fieldsCount = values.length;
    }

    public static ISqlJetBtreeRecord getRecord(SqlJetEncoding encoding, Object... values) throws SqlJetException {
        final List<ISqlJetVdbeMem> fields = new ArrayList<ISqlJetVdbeMem>(values.length);
        for (int i = 0; i < values.length; i++) {
            final Object value = values[i];
            final ISqlJetVdbeMem mem = new SqlJetVdbeMem();
            if (null == value) {
                mem.setNull();
            } else if (value instanceof String) {
                mem.setStr(SqlJetUtility.fromString((String) value, encoding), encoding);
            } else if (value instanceof Boolean) {
                mem.setInt64(((Boolean) value) ? 1 : 0);
            } else if (value instanceof Byte) {
                mem.setInt64((Byte) value);
            } else if (value instanceof Short) {
                mem.setInt64((Short) value);
            } else if (value instanceof Integer) {
                mem.setInt64((Integer) value);
            } else if (value instanceof Long) {
                mem.setInt64((Long) value);
            } else if (value instanceof Float) {
                mem.setDouble((Float) value);
            } else if (value instanceof Double) {
                mem.setDouble((Double) value);
            } else if (value instanceof ByteBuffer) {
                mem.setStr(SqlJetUtility.fromByteBuffer((ByteBuffer) value), encoding);
                mem.setTypeFlag(SqlJetVdbeMemFlags.Blob);
            } else if (value instanceof InputStream) {
                mem.setStr(SqlJetUtility.streamToBuffer((InputStream) value), encoding);
                mem.setTypeFlag(SqlJetVdbeMemFlags.Blob);
            } else if ("byte[]".equalsIgnoreCase(value.getClass().getCanonicalName())) {
                mem.setStr(SqlJetUtility.wrapPtr((byte[]) value), encoding);
                mem.setTypeFlag(SqlJetVdbeMemFlags.Blob);
            } else if (value instanceof SqlJetMemoryPointer) {
                mem.setStr((SqlJetMemoryPointer) value, encoding);
                mem.setTypeFlag(SqlJetVdbeMemFlags.Blob);
            } else {
                throw new SqlJetException(SqlJetErrorCode.MISUSE, "Bad value #" + i + " " + value.toString());
            }
            fields.add(mem);
        }
        return new SqlJetBtreeRecord(fields);
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.sqljet.core.internal.vdbe.ISqlJetRecord#getFieldsCount()
     */
    public int getFieldsCount() {
        return fieldsCount;
    }

    /**
     * Read and parse the table header. Store the results of the parse into the
     * record header cache fields of the cursor.
     * 
     * @throws SqlJetException
     */
    private void read() throws SqlJetException {

        long payloadSize; /* Number of bytes in the record */

        cursor.enterCursor();
        try {
            /*
             * This block sets the variable payloadSize to be the total number
             * of* bytes in the record.
             */
            if (isIndex) {
                payloadSize = cursor.getKeySize();
            } else {
                payloadSize = cursor.getDataSize();
            }
            /* If payloadSize is 0, then just store a NULL */
            if (payloadSize == 0) {
                return;
            }

            int i; /* Loop counter */
            ISqlJetMemoryPointer zData; /* Part of the record being decoded */
            /* For storing the record being decoded */
            SqlJetVdbeMem sMem = new SqlJetVdbeMem();

            ISqlJetMemoryPointer zIdx; /* Index into header */
            ISqlJetMemoryPointer zEndHdr; /*
                                           * Pointer to first byte after the
                                           * header
                                           */
            int[] offset = { 0 }; /* Offset into the data */
            int szHdrSz; /* Size of the header size field at start of record */
            int[] avail = { 0 }; /* Number of bytes of available data */

            assert (aType != null);
            assert (aOffset != null);

            /* Figure out how many bytes are in the header */
            if (isIndex) {
                zData = cursor.keyFetch(avail);
            } else {
                zData = cursor.dataFetch(avail);
            }
            /*
             * The following assert is true in all cases accept when* the
             * database file has been corrupted externally.* assert( zRec!=0 ||
             * avail>=payloadSize || avail>=9 );
             */
            szHdrSz = SqlJetUtility.getVarint32(zData, offset);

            /*
             * The KeyFetch() or DataFetch() above are fast and will get the
             * entire* record header in most cases. But they will fail to get
             * the complete* record header if the record header does not fit on
             * a single page* in the B-Tree. When that happens, use
             * sqlite3VdbeMemFromBtree() to* acquire the complete header text.
             */
            if (avail[0] < offset[0]) {
                sMem.fromBtree(cursor, 0, offset[0], isIndex);
                zData = sMem.z;
            }
            zEndHdr = SqlJetUtility.pointer(zData, offset[0]);
            zIdx = SqlJetUtility.pointer(zData, szHdrSz);

            /*
             * Scan the header and use it to fill in the aType[] and aOffset[]*
             * arrays. aType[i] will contain the type integer for the i-th*
             * column and aOffset[i] will contain the offset from the beginning*
             * of the record to the start of the data for the i-th column
             */
            fieldsCount = 0;
            for (i = 0; i < ISqlJetLimits.SQLJET_MAX_COLUMN && zIdx.getPointer() < zEndHdr.getPointer()
                    && offset[0] <= payloadSize; i++, fieldsCount++) {
                aOffset.add(i, offset[0]);
                int[] a = { 0 };
                SqlJetUtility.movePtr(zIdx, SqlJetUtility.getVarint32(zIdx, a));
                aType.add(i, a[0]);
                offset[0] += SqlJetVdbeSerialType.serialTypeLen(a[0]);

                fields.add(i, getField(i));

            }
            sMem.release();
            sMem.flags = SqlJetUtility.of(SqlJetVdbeMemFlags.Null);

            /*
             * If we have read more header data than was contained in the
             * header,* or if the end of the last field appears to be past the
             * end of the* record, or if the end of the last field appears to be
             * before the end* of the record (when all fields present), then we
             * must be dealing* with a corrupt database.
             */
            if (zIdx.getPointer() > zEndHdr.getPointer() || offset[0] > payloadSize
                    || (zIdx.getPointer() == zEndHdr.getPointer() && offset[0] != payloadSize)) {
                throw new SqlJetException(SqlJetErrorCode.CORRUPT);
            }

        } finally {
            cursor.leaveCursor();
        }
    }

    /**
     * Opcode: Column P1 P2 P3 P4 *
     * 
     * Interpret the data that cursor P1 points to as a structure built using
     * the MakeRecord instruction. (See the MakeRecord opcode for additional
     * information about the format of the data.) Extract the P2-th column from
     * this record. If there are less that (P2+1) values in the record, extract
     * a NULL.
     * 
     * The value extracted is stored in register P3.
     * 
     * If the column contains fewer than P2 fields, then extract a NULL. Or, if
     * the P4 argument is a P4_MEM use the value of the P4 argument as the
     * result.
     * 
     * @param pCrsr
     *            The BTree cursor
     * @param fieldNum
     *            column number to retrieve
     * @param isIndex
     *            True if an index containing keys only - no data
     * @param aType
     *            Type values for all entries in the record
     * @param aOffset
     *            Cached offsets to the start of each columns data
     * @param pDest
     * @throws SqlJetException
     */
    private ISqlJetVdbeMem getField(int column) throws SqlJetException {

        long payloadSize; /* Number of bytes in the record */
        int len; /* The length of the serialized data for the column */
        ISqlJetMemoryPointer zData; /* Part of the record being decoded */
        /* For storing the record being decoded */
        SqlJetVdbeMem sMem = new SqlJetVdbeMem();
        SqlJetVdbeMem pDest = new SqlJetVdbeMem();
        pDest.flags = SqlJetUtility.of(SqlJetVdbeMemFlags.Null);

        cursor.enterCursor();
        try {
            /*
             * This block sets the variable payloadSize to be the total number
             * of* bytes in the record.
             */
            if (isIndex) {
                payloadSize = cursor.getKeySize();
            } else {
                payloadSize = cursor.getDataSize();
            }

            /* If payloadSize is 0, then just store a NULL */
            if (payloadSize == 0) {
                return pDest;
            }

            /*
             * Get the column information. If aOffset[p2] is non-zero, then*
             * deserialize the value from the record. If aOffset[p2] is zero,*
             * then there are not enough fields in the record to satisfy the*
             * request. In this case, set the value NULL or to P4 if P4 is* a
             * pointer to a Mem object.
             */
            final Integer aOffsetColumn = aOffset.get(column);
            final Integer aTypeColumn = aType.get(column);
            if (aOffsetColumn != null && aTypeColumn != null && aOffsetColumn != 0) {
                len = SqlJetVdbeSerialType.serialTypeLen(aTypeColumn);
                sMem.fromBtree(cursor, aOffset.get(column), len, isIndex);
                zData = sMem.z;
                SqlJetVdbeSerialType.serialGet(zData, aTypeColumn, pDest);
                pDest.enc = cursor.getCursorDb().getOptions().getEncoding();
            }

        } finally {
            cursor.leaveCursor();
        }

        /*
         * If we dynamically allocated space to hold the data (in the
         * sqlite3VdbeMemFromBtree() call above) then transfer control of that
         * dynamically allocated space over to the pDest structure. This
         * prevents a memory copy.
         */
        if (sMem.zMalloc != null) {
            assert (sMem.z == sMem.zMalloc);
            assert (!pDest.flags.contains(SqlJetVdbeMemFlags.Dyn));
            assert (!(pDest.flags.contains(SqlJetVdbeMemFlags.Blob) || pDest.flags.contains(SqlJetVdbeMemFlags.Str)) || pDest.z.getBuffer() == sMem.z.getBuffer());
            pDest.flags.remove(SqlJetVdbeMemFlags.Ephem);
            pDest.flags.remove(SqlJetVdbeMemFlags.Static);
            pDest.flags.add(SqlJetVdbeMemFlags.Term);
            //pDest.z = sMem.z;
            //pDest.zMalloc = sMem.zMalloc;
        }

        pDest.makeWriteable();

        return pDest;

    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.sqljet.core.ISqlJetRecord#getStringField(int)
     */
    public String getStringField(int field, SqlJetEncoding enc) throws SqlJetException {
        final ISqlJetVdbeMem f = fields.get(field);
        if (null == f)
            return null;
        final ISqlJetMemoryPointer v = f.valueText(enc);
        if (null == v)
            return null;
        return SqlJetUtility.toString(v, enc);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.sqljet.core.ISqlJetRecord#getIntField(int)
     */
    public long getIntField(int field) {
        final ISqlJetVdbeMem f = fields.get(field);
        if (null == f)
            return 0;
        return f.intValue();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.sqljet.core.internal.table.ISqlJetBtreeRecord#getRealField
     * (int)
     */
    public double getRealField(int field) {
        final ISqlJetVdbeMem f = fields.get(field);
        if (null == f)
            return 0;
        return f.realValue();
    }

    /**
     * Assuming the record contains N fields, the record format looks like this:
     * 
     * <table border="1">
     * <tr>
     * <td>hdr-size</td>
     * <td>type 0</td>
     * <td>type 1</td>
     * <td>...</td>
     * <td>type N-1</td>
     * <td>data0</td>
     * <td>...</td>
     * <td>data N-1</td>
     * </tr>
     * </table>
     * 
     * Each type field is a varint representing the serial type of the
     * corresponding data element (see sqlite3VdbeSerialType()). The hdr-size
     * field is also a varint which is the offset from the beginning of the
     * record to data0.
     */
    public ISqlJetMemoryPointer getRawRecord() {
        /* A buffer to hold the data for the new record */
        ISqlJetMemoryPointer zNewRecord;
        int nData = 0; /* Number of bytes of data space */
        int nHdr = 0; /* Number of bytes of header space */
        int nByte = 0; /* Data space required for this record */
        int nZero = 0; /* Number of zero bytes at the end of the record */
        int nVarint; /* Number of bytes in a varint */
        int serial_type; /* Type field */
        int i; /* Space used in zNewRecord[] */

        /*
         * Loop through the elements that will make up the record to figure* out
         * how much space is required for the new record.
         */
        for (ISqlJetVdbeMem value : fields) {
            SqlJetVdbeMem pRec = (SqlJetVdbeMem) value;

            int len;
            if (pRec.flags.contains(SqlJetVdbeMemFlags.Zero) && pRec.n > 0) {
                pRec.expandBlob();
            }
            serial_type = SqlJetVdbeSerialType.serialType(pRec, file_format);
            len = SqlJetVdbeSerialType.serialTypeLen(serial_type);
            nData += len;
            nHdr += SqlJetUtility.varintLen(serial_type);
            if (pRec.flags.contains(SqlJetVdbeMemFlags.Zero)) {
                /*
                 * Only pure zero-filled BLOBs can be input to this Opcode.* We
                 * do not allow blobs with a prefix and a zero-filled tail.
                 */
                nZero += pRec.nZero;
            } else if (len != 0) {
                nZero = 0;
            }
        }

        /* Add the initial header varint and total the size */
        nHdr += nVarint = SqlJetUtility.varintLen(nHdr);
        if (nVarint < SqlJetUtility.varintLen(nHdr)) {
            nHdr++;
        }
        nByte = nHdr + nData - nZero;

        /*
         * Make sure the output register has a buffer large enough to store* the
         * new record. The output register (pOp->p3) is not allowed to* be one
         * of the input registers (because the following call to*
         * sqlite3VdbeMemGrow() could clobber the value before it is used).
         */
        zNewRecord = SqlJetUtility.allocatePtr(nByte);

        /* Write the record */
        i = SqlJetUtility.putVarint32(zNewRecord, nHdr);
        for (ISqlJetVdbeMem value : fields) {
            SqlJetVdbeMem pRec = (SqlJetVdbeMem) value;
            serial_type = SqlJetVdbeSerialType.serialType(pRec, file_format);
            /* serial type */
            i += SqlJetUtility.putVarint32(SqlJetUtility.pointer(zNewRecord, i), serial_type);
        }
        for (ISqlJetVdbeMem value : fields) {
            SqlJetVdbeMem pRec = (SqlJetVdbeMem) value;
            /* serial data */
            i += SqlJetVdbeSerialType.serialPut(SqlJetUtility.pointer(zNewRecord, i), (int) (nByte - i), pRec,
                    file_format);
        }
        assert (i == nByte);

        return zNewRecord;
    }
}
