package com.sap.dbtech.jdbc;

import java.sql.*;

import com.sap.dbtech.jdbc.packet.*;
import com.sap.dbtech.jdbc.exceptions.*;
import com.sap.dbtech.util.*;
import com.sap.dbtech.vsp001.*;

/**
 * The outcome of a particular fetch operation.  A fetch operation
 * results in one (when the fetch size is 1) or more (when the fetch
 * size is >1) data rows returned from the database server. Depending on
 * the kind of the fetch, the positioning in the result at the database
 * server and the start and end index computation does differ.
 */
class FetchChunk
{
    /**
     * The fetch operation type of a <tt>FETCH FIRST</tt>.
     */
    static final int TYPE_FIRST = 1;

    /**
     * The fetch operation type of a <tt>FETCH LAST</tt>.
     */
    static final int TYPE_LAST = 2;

    /**
     * The fetch operation type of a <tt>FETCH ABSOLUTE</tt> with an argument >1.
     */
    static final int TYPE_ABSOLUTE_UP = 3;

    /**
     * The fetch operation type of a <tt>FETCH ABSOLUTE</tt> with an argument <1.
     */
    static final int TYPE_ABSOLUTE_DOWN = 4;

    /**
     * The fetch operation type of a <tt>FETCH RELATIVE</tt> with an argument >1.
     */
    static final int TYPE_RELATIVE_UP = 5;

    /**
     * The fetch operation type of a <tt>FETCH RELATIVE</tt> with an argument <1.
     */
    static final int TYPE_RELATIVE_DOWN = 6;

    /**
     * The data packet from the fetch operation.
     */
    private ReplyPacket    replyPacket;

    /**
     * The <i>data part</i> of <code>replyPacket</code>.
     */
    private StructuredMem  replyData;

    /**
     * The current record inside the data part (<code>replyData</code>).
     */
    private StructuredMem  currentRecord;

    /**
     * The type of the fetch operation (one of the <code>TYPE_XXX</code> constants).
     */
    private int type;                          // type of fetch chunk


    /**
     * The index of the first row in this chunk.
     */
    private int start_index;

    /**
     * The index of the last row in this chunk.
     */
    private int end_index;

    /**
     * The current index within this chunk, starting with 0.
     */
    private int         currentOffset;

    /**
     * A flag indicating that this chunk is the first chunk of the result set.
     */
    private boolean     first;

    /**
     * A flag indicating that this chunk is the last chunk of the result set.
     */
    private boolean     last;

    /**
     * The number of bytes in a row.
     */
    private int         recordSize;

    /**
     * The number of elements in this chunk.
     */
    private int         chunkSize;

    /**
     * The number of rows in the complete result set, or -1 if this is not known.
     */
    private int         rowsInResultSet;

    /**
     * Creates a new fetch chunk.
     * @param type the type of the fetch operation.
     * @param absoluteStartRow the start row of this chunk. If negative, this is calculated from
     *   the end of the result set.
     * @param replyPacket the database server reply of the fetch
     * @param recordSize the size of one row.
     * @param maxRows the <code>maxRows</code> property of the statement that created this result.
     * @param rowsInResultSet the number of rows in this result set, or -1 if not known.
     * @exception SQLException if the reply does not contain a data part.
     */
    FetchChunk(int type,
               int absoluteStartRow,
               ReplyPacket replyPacket,
               int recordSize,
               int maxRows,
               int rowsInResultSet)
        throws SQLException
    {
        this.replyPacket=replyPacket;
        this.type=type;
        this.recordSize=recordSize;
        this.rowsInResultSet=rowsInResultSet;
        try {
            replyPacket.firstSegment();
            replyPacket.findPart(PartKind.Data_C);
        } catch(PartNotFound pnf) {
            throw new InternalJDBCError("Fetch operation delivered no data part.");
        }
        this.chunkSize=replyPacket.partArguments();
        int dataPos=replyPacket.getPartDataPos();
        this.replyData=replyPacket.getPointer(dataPos);
        currentOffset=0;
        currentRecord=replyData.getPointer(currentOffset * this.recordSize);
        if(absoluteStartRow > 0) {
            start_index=absoluteStartRow;
            end_index=absoluteStartRow + chunkSize - 1;
        } else {
             if(rowsInResultSet!=-1) {
                 if(absoluteStartRow < 0) {
                     start_index = rowsInResultSet + absoluteStartRow + 1; // - 1 is last
             	 } else {
             	     start_index = rowsInResultSet - absoluteStartRow + chunkSize ;
             	 }
                 end_index = start_index + chunkSize -1;
             } else {
                 start_index=absoluteStartRow;
                 end_index=absoluteStartRow + chunkSize -1;
             }
        }
        determineFlags(maxRows);
    }

    /**
     * Determines whether this chunk is the first and/or last of
     * a result set. This is done by checking the index boundaries,
     * and also the LAST PART information of the reply packet.
     * A forward chunk is also the last if it contains the record at
     * the <code>maxRows</code> row, as the user decided to make
     * the limit here.
     * @param maxRows the <code>maxRows</code> limit of the statement
     */
    private void determineFlags(int maxRows)
    {
        boolean wasLastPart=replyPacket.wasLastPart();

        if(wasLastPart) {
            switch(this.type) {
            case TYPE_FIRST:
            case TYPE_LAST:
            case TYPE_RELATIVE_DOWN:
                this.first=true;
                this.last=true;
                break;
            case TYPE_ABSOLUTE_UP:
            case TYPE_ABSOLUTE_DOWN:
            case TYPE_RELATIVE_UP:
                this.last=true;
            }
        }

        if(start_index==1) {
            first=true;
        }
        if(end_index==-1) {
            last=true;
        }
        // one special last for maxRows set
        if(maxRows!=0 && isForward() && end_index >= maxRows) {
            // if we have fetched too much, we have to cut here ...
            end_index = maxRows;
            chunkSize = maxRows + 1 - start_index;
            last=true;
        }

    }


    /**
     * Gets the reply packet.
     *
     * @return the <code>replyPacket</code> property.
     */
    ReplyPacket getReplyPacket()
    {
        return this.replyPacket;
    }

    /**
     * Gets the current record.
     * @return the <code>currentRecord</code> property.
     */
    StructuredMem getCurrentRecord()
    {
        return this.currentRecord;
    }

    /**
     * Returns whether the given row is truly inside the chunk.
     * @param row the row to check. Rows <0 count from the end of the result.
     * @return <code>true</code> if the row is inside, <code>false</code> if it's not
     * or the condition could not be determined due to an unknown end of result set.
     */
    boolean containsRow(int row)
    {
        if(start_index <= row &&
           end_index >= row) {
            return true;
        }
        // some tricks depending on whether we are on last/first chunk
        if(isForward() && last && row < 0) {
            return row >= start_index - end_index - 1;
        }
        if(!isForward() && first && row > 0) {
            return row <= end_index - start_index + 1;
        }
        // if we know the number of rows, we can compute this anyway
        // by inverting the row
        if(rowsInResultSet != -1 &&
           ((start_index<0 && row>0) || (start_index>0 && row<0))) {
            int inverted_row = (row > 0)
                ? (row - rowsInResultSet - 1)
                : (row + rowsInResultSet + 1);
            return start_index <= inverted_row && end_index >= inverted_row;
        }

        return false;
    }

    /**
     * Moves the position inside the chunk by a relative offset.
     * @param relativepos the relative moving offset.
     * @return <code>true</code> if it was moved, <code>false</code> otherwise.
     */
    boolean move(int relativepos)
    {
        if(currentOffset + relativepos < 0 ||
           currentOffset + relativepos >= chunkSize )  {
            return false;
        } else {
            unsafeMove(relativepos);
            return true;
        }
    }

    /**
     * Moves the position inside the chunk by a relative offset, but unchecked.
     * @param relativepos the relative moving offset.
     */
    private void unsafeMove(int relativepos)
    {
        this.currentOffset+=relativepos;
        this.currentRecord.moveBase((relativepos) * this.recordSize);
    }

    /**
     * Sets the current record to the supplied absolute position.
     * @param row the absolute row.
     * @return <code>true</code> if the row was set, <code>false</code> otherwise.
     */
    boolean setRow(int row)
    {
        if(start_index <= row  && end_index >= row) {
            unsafeMove(row - start_index - currentOffset);
            return true;
        }
        // some tricks depending on whether we are on last/first chunk
        if(isForward() && last && row < 0 && row >= start_index - end_index - 1 ) {
            // move backward to the row from the end index, but
            // honor the row number start at 1, make this
            // relative to chunk by subtracting start index
            // and relative for the move by subtracting the
            // current offset
            unsafeMove(end_index + row + 1 - start_index - currentOffset);
            return true;
        }
        if(!isForward() && first && row > 0 && row <= end_index - start_index + 1) {
            // simple. row is > 0. start_index if positive were 1 ...
            unsafeMove(row - 1 - currentOffset);
        }
        // if we know the number of rows, we can compute this anyway
        // by inverting the row
        if(rowsInResultSet != -1 &&
           ((start_index<0 && row>0) || (start_index>0 && row<0))) {
            int inverted_row = (row > 0)
                ? (row - rowsInResultSet - 1)
                : (row + rowsInResultSet + 1);
            return setRow(inverted_row);
        }

        return false;

    }

    /**
     * Called because there is a result set where the last element
     * is now interesting. This is the fact in a <code>FETCH LAST</code>
     * operation.
     */
    void moveToUpperBound()
    {
        int relativepos=chunkSize - currentOffset -1;
        this.currentRecord.moveBase(relativepos * this.recordSize);
        this.currentOffset= chunkSize - 1;
        return;
    }

    /**
     * Returns true if the internal position inside the chunk
     * is the greatest possible towards the end of this result
     * set.
     * @return <code>true</code> if our current position is equal
     *  to the end index of this chunk, <code>false</code> otherwise.
     */
    boolean isAtUpperBound()
    {
        return this.currentOffset==this.chunkSize-1;
    }

    /**
     * Returns true if the internal position inside the chunk
     * is the smallest possible
     * @return <code>true</code> if our current position is equal
     *  to the start index of this chunk, <code>false</code> otherwise.
     */
    boolean isAtLowerBound()
    {
        return this.currentOffset==0;
    }


    /**
     * Get the reply data.
     * @return the <code>replyData</code> property.
     */
    StructuredMem getReplyData()
    {
        return replyData;
    }

    /**
     * Returns whether this chunk is the first one.
     * <b>Take care, that this information may not be reliable.</b>
     * @return <code>true</code> if this is the first, and <code>false</code> if this
     *   is not first or the information is not known.
     */
    public boolean isFirst()
    {
        return this.first;
    }

    /**
     * Returns whether this chunk is the last one.
     * <b>Take care, that this information may not be reliable.</b>
     * @return <code>true</code> if this is the last, and <code>false</code> if this
     *   is not first or the information is not known.
     */
    public boolean isLast()
    {
        return this.last;
    }

    /**
     * Sets the <code>last</code> flag.
     * @param last the new value.
     */
    public void setLast(boolean last)
    {
        this.last=last;

    }

    /**
     * Sets the <code>first</code> flag.
     * @param first the new value.
     */
    public void setFirst(boolean first)
    {
        this.first=first;

    }

    /**
     * Gets the size of this chunk.
     * @return the number of rows in this chunk.
     */
    public int size()
    {
        return this.chunkSize;
    }

    /**
     * Gets whether the current position is the first in the result set.
     * @return <code>true</code> if the current position is the first row
     *   of the result set.
     */
    public boolean positionedAtFirst()
    {
        return(this.first && currentOffset==0);
    }

    /**
     * Gets whether the current position is the last in the result set.
     * @return <code>true</code> if the current position is the last row
     *   of the result set.
     */
    public boolean positionedAtLast()
    {
        return(this.last && currentOffset==chunkSize-1);
    }

    /**
     * Get the current position within the result set.
     * @return the current position in the result set.
     */
    int getLogicalPos()
    {
        return start_index + currentOffset;
    }

    /**
     * Gets the current offset in this chunk.
     * @return the current position in this chunk (starts with 0).
     */
    int pos()
    {
        return currentOffset;
    }

    /**
     * Retrieves the position where the internal position is after the
     * fetch if this chunk is the current chunk.
     * @return the internal position - either the start or the end of this chunk.
     */
    int getKernelPos()
    {
        switch(this.type) {

        case TYPE_ABSOLUTE_DOWN:
        case TYPE_RELATIVE_UP:
        case TYPE_LAST:
            return start_index;
        case TYPE_FIRST:
        case TYPE_ABSOLUTE_UP:
        case TYPE_RELATIVE_DOWN:
        default:
            return end_index;
        }
    }

    boolean isForward()
    {
        return (type == TYPE_FIRST ||
                type == TYPE_ABSOLUTE_UP ||
                type == TYPE_RELATIVE_UP);
    }


    /**
     * Updates the number of rows in the result set.
     * @param rows the number of rows in the result set.
     */
    void setRowsInResultSet(int rows)
    {
        this.rowsInResultSet=rows;
    }

    /**
     * Gets the start index of the fetch chunk.
     *
     * @return The start index (smallest valid index).
     */
    int getStart()
    {
        return start_index;
    }

    /**
     * Gets the end index of the fetch chunk.
     *
     * @return The end index (largest valid index).
     */
    int getEnd()
    {
        return end_index;
    }

    /**
     * Gets a string with trace information. A sample output is
     * <pre>
     * FETCH CHUNK [
     *   TYPE           : FIRST
     *   START INDEX    : 1
     *   END INDEX      : 25
     *   FIRST          : TRUE
     *   LAST           : FALSE
     *   RECORD SIZE    : 35
     *   SIZE           : 25
     *   ROWS IN RESULT : -1
     * ]</pre>
     * @return The trace string.
     */
    public String traceString()
    {
        StringBuffer result=new StringBuffer();
        result.append("FETCH CHUNK [\n");
        switch(type) {
        case TYPE_FIRST:
            result.append("  TYPE           : FIRST\n");
            break;
        case TYPE_LAST:
            result.append("  TYPE           : LAST\n");
            break;
        case TYPE_ABSOLUTE_UP:
            result.append("  TYPE           : ABSOLUTE (UP)\n");
            break;
        case TYPE_ABSOLUTE_DOWN:
            result.append("  TYPE           : ABSOLUTE (DOWN)\n");
            break;
        case TYPE_RELATIVE_UP:
            result.append("  TYPE           : RELATIVE (UP)\n");
            break;
        case TYPE_RELATIVE_DOWN:
            result.append("  TYPE           : RELATIVE (DOWN)\n");
            break;
        default:
            result.append("  TYPE           : UNKNOWN\n");
            break;
        }
        result.append("  START INDEX    : " + start_index + "\n");
        result.append("  END INDEX      : " + end_index + "\n");
        result.append("  CURRENT        : " + currentOffset + "\n");
        result.append("  FIRST          : " + (first?"TRUE":"FALSE") + "\n");
        result.append("  LAST           : " + (last?"TRUE":"FALSE") + "\n");
        result.append("  RECORD SIZE    : " + recordSize + "\n");
        result.append("  SIZE           : " + chunkSize  + "\n");
        result.append("  ROWS IN RESULT : " + rowsInResultSet  + "\n");
        result.append("]");
        return result.toString();
    }
}
