/*
  Copyright (c) 2002, 2016, Oracle and/or its affiliates. All rights reserved.

  The MySQL Connector/J is licensed under the terms of the GPLv2
  <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most MySQL Connectors.
  There are special exceptions to the terms and conditions of the GPLv2 as it is applied to
  this software, see the FOSS License Exception
  <http://www.mysql.com/about/legal/licensing/foss-exception.html>.

  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.

  You should have received a copy of the GNU General Public License along with this
  program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth
  Floor, Boston, MA 02110-1301  USA

 */

package com.mysql.jdbc;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/**
 * Model for result set data backed by a cursor. Only works for forward-only result sets (but still works with updatable concurrency).
 */
public class RowDataCursor implements RowData {

    private final static int BEFORE_START_OF_ROWS = -1;

    /**
     * The cache of rows we have retrieved from the server.
     */
    private List<ResultSetRow> fetchedRows;

    /**
     * Where we are positionaly in the entire result set, used mostly to
     * facilitate easy 'isBeforeFirst()' and 'isFirst()' methods.
     */
    private int currentPositionInEntireResult = BEFORE_START_OF_ROWS;

    /**
     * Position in cache of rows, used to determine if we need to fetch more
     * rows from the server to satisfy a request for the next row.
     */
    private int currentPositionInFetchedRows = BEFORE_START_OF_ROWS;

    /**
     * The result set that we 'belong' to.
     */
    private ResultSetImpl owner;

    /**
     * Have we been told from the server that we have seen the last row?
     */
    private boolean lastRowFetched = false;

    /**
     * Field-level metadata from the server. We need this, because it is not
     * sent for each batch of rows, but we need the metadata to unpack the
     * results for each field.
     */
    private Field[] metadata;

    /**
     * Communications channel to the server
     */
    private MysqlIO mysql;

    /**
     * Identifier for the statement that created this cursor.
     */
    private long statementIdOnServer;

    /**
     * The prepared statement that created this cursor.
     */
    private ServerPreparedStatement prepStmt;

    /**
     * The server status for 'last-row-sent'...This might belong in mysqldefs,
     * but it it only ever referenced from here.
     */
    private static final int SERVER_STATUS_LAST_ROW_SENT = 128;

    /**
     * Have we attempted to fetch any rows yet?
     */
    private boolean firstFetchCompleted = false;

    private boolean wasEmpty = false;

    private boolean useBufferRowExplicit = false;

    /**
     * Creates a new cursor-backed row provider.
     * 
     * @param ioChannel
     *            connection to the server.
     * @param creatingStatement
     *            statement that opened the cursor.
     * @param metadata
     *            field-level metadata for the results that this cursor covers.
     */
    public RowDataCursor(MysqlIO ioChannel, ServerPreparedStatement creatingStatement, Field[] metadata) {
        this.currentPositionInEntireResult = BEFORE_START_OF_ROWS;
        this.metadata = metadata;
        this.mysql = ioChannel;
        this.statementIdOnServer = creatingStatement.getServerStatementId();
        this.prepStmt = creatingStatement;
        this.useBufferRowExplicit = MysqlIO.useBufferRowExplicit(this.metadata);

    }

    /**
     * Returns true if we got the last element.
     */
    public boolean isAfterLast() {
        return this.lastRowFetched && this.currentPositionInFetchedRows > this.fetchedRows.size();
    }

    /**
     * Only works on non dynamic result sets.
     * 
     * @param index
     *            row number to get at
     * @return row data at index
     * @throws SQLException
     *             if a database error occurs
     */
    public ResultSetRow getAt(int ind) throws SQLException {
        notSupported();

        return null;
    }

    /**
     * Returns if iteration has not occured yet.
     * 
     * @return true if before first row
     * @throws SQLException
     *             if a database error occurs
     */
    public boolean isBeforeFirst() throws SQLException {
        return this.currentPositionInEntireResult < 0;
    }

    /**
     * Moves the current position in the result set to the given row number.
     * 
     * @param rowNumber
     *            row to move to
     * @throws SQLException
     *             if a database error occurs
     */
    public void setCurrentRow(int rowNumber) throws SQLException {
        notSupported();
    }

    /**
     * Returns the current position in the result set as a row number.
     * 
     * @return the current row number
     * @throws SQLException
     *             if a database error occurs
     */
    public int getCurrentRowNumber() throws SQLException {
        return this.currentPositionInEntireResult + 1;
    }

    /**
     * Returns true if the result set is dynamic.
     * 
     * This means that move back and move forward won't work because we do not
     * hold on to the records.
     * 
     * @return true if this result set is streaming from the server
     */
    public boolean isDynamic() {
        return true;
    }

    /**
     * Has no records.
     * 
     * @return true if no records
     * @throws SQLException
     *             if a database error occurs
     */
    public boolean isEmpty() throws SQLException {
        return this.isBeforeFirst() && this.isAfterLast();
    }

    /**
     * Are we on the first row of the result set?
     * 
     * @return true if on first row
     * @throws SQLException
     *             if a database error occurs
     */
    public boolean isFirst() throws SQLException {
        return this.currentPositionInEntireResult == 0;
    }

    /**
     * Are we on the last row of the result set?
     * 
     * @return true if on last row
     * @throws SQLException
     *             if a database error occurs
     */
    public boolean isLast() throws SQLException {
        return this.lastRowFetched && this.currentPositionInFetchedRows == (this.fetchedRows.size() - 1);
    }

    /**
     * Adds a row to this row data.
     * 
     * @param row
     *            the row to add
     * @throws SQLException
     *             if a database error occurs
     */
    public void addRow(ResultSetRow row) throws SQLException {
        notSupported();
    }

    /**
     * Moves to after last.
     * 
     * @throws SQLException
     *             if a database error occurs
     */
    public void afterLast() throws SQLException {
        notSupported();
    }

    /**
     * Moves to before first.
     * 
     * @throws SQLException
     *             if a database error occurs
     */
    public void beforeFirst() throws SQLException {
        notSupported();
    }

    /**
     * Moves to before last so next el is the last el.
     * 
     * @throws SQLException
     *             if a database error occurs
     */
    public void beforeLast() throws SQLException {
        notSupported();
    }

    /**
     * We're done.
     * 
     * @throws SQLException
     *             if a database error occurs
     */
    public void close() throws SQLException {

        this.metadata = null;
        this.owner = null;
    }

    /**
     * Returns true if another row exists.
     * 
     * @return true if more rows
     * @throws SQLException
     *             if a database error occurs
     */
    public boolean hasNext() throws SQLException {

        if (this.fetchedRows != null && this.fetchedRows.size() == 0) {
            return false;
        }

        if (this.owner != null && this.owner.owningStatement != null) {
            int maxRows = this.owner.owningStatement.maxRows;

            if (maxRows != -1 && this.currentPositionInEntireResult + 1 > maxRows) {
                return false;
            }
        }

        if (this.currentPositionInEntireResult != BEFORE_START_OF_ROWS) {
            // Case, we've fetched some rows, but are not at end of fetched block
            if (this.currentPositionInFetchedRows < (this.fetchedRows.size() - 1)) {
                return true;
            } else if (this.currentPositionInFetchedRows == this.fetchedRows.size() && this.lastRowFetched) {
                return false;
            } else {
                // need to fetch to determine
                fetchMoreRows();

                return (this.fetchedRows.size() > 0);
            }
        }

        // Okay, no rows _yet_, so fetch 'em

        fetchMoreRows();

        return this.fetchedRows.size() > 0;
    }

    /**
     * Moves the current position relative 'rows' from the current position.
     * 
     * @param rows
     *            the relative number of rows to move
     * @throws SQLException
     *             if a database error occurs
     */
    public void moveRowRelative(int rows) throws SQLException {
        notSupported();
    }

    /**
     * Returns the next row.
     * 
     * @return the next row value
     * @throws SQLException
     *             if a database error occurs
     */
    public ResultSetRow next() throws SQLException {
        if (this.fetchedRows == null && this.currentPositionInEntireResult != BEFORE_START_OF_ROWS) {
            throw SQLError.createSQLException(Messages.getString("ResultSet.Operation_not_allowed_after_ResultSet_closed_144"),
                    SQLError.SQL_STATE_GENERAL_ERROR, this.mysql.getExceptionInterceptor());
        }

        if (!hasNext()) {
            return null;
        }

        this.currentPositionInEntireResult++;
        this.currentPositionInFetchedRows++;

        // Catch the forced scroll-passed-end
        if (this.fetchedRows != null && this.fetchedRows.size() == 0) {
            return null;
        }

        if ((this.fetchedRows == null) || (this.currentPositionInFetchedRows > (this.fetchedRows.size() - 1))) {
            fetchMoreRows();
            this.currentPositionInFetchedRows = 0;
        }

        ResultSetRow row = this.fetchedRows.get(this.currentPositionInFetchedRows);

        row.setMetadata(this.metadata);

        return row;
    }

    /**
     */
    private void fetchMoreRows() throws SQLException {
        if (this.lastRowFetched) {
            this.fetchedRows = new ArrayList<ResultSetRow>(0);
            return;
        }

        synchronized (this.owner.connection.getConnectionMutex()) {
            boolean oldFirstFetchCompleted = this.firstFetchCompleted;

            if (!this.firstFetchCompleted) {
                this.firstFetchCompleted = true;
            }

            int numRowsToFetch = this.owner.getFetchSize();

            if (numRowsToFetch == 0) {
                numRowsToFetch = this.prepStmt.getFetchSize();
            }

            if (numRowsToFetch == Integer.MIN_VALUE) {
                // Handle the case where the user used 'old' streaming result sets

                numRowsToFetch = 1;
            }

            this.fetchedRows = this.mysql.fetchRowsViaCursor(this.fetchedRows, this.statementIdOnServer, this.metadata, numRowsToFetch,
                    this.useBufferRowExplicit);
            this.currentPositionInFetchedRows = BEFORE_START_OF_ROWS;

            if ((this.mysql.getServerStatus() & SERVER_STATUS_LAST_ROW_SENT) != 0) {
                this.lastRowFetched = true;

                if (!oldFirstFetchCompleted && this.fetchedRows.size() == 0) {
                    this.wasEmpty = true;
                }
            }
        }
    }

    /**
     * Removes the row at the given index.
     * 
     * @param index
     *            the row to move to
     * @throws SQLException
     *             if a database error occurs
     */
    public void removeRow(int ind) throws SQLException {
        notSupported();
    }

    /**
     * Only works on non dynamic result sets.
     * 
     * @return the size of this row data
     */
    public int size() {
        return RESULT_SET_SIZE_UNKNOWN;
    }

    protected void nextRecord() throws SQLException {

    }

    private void notSupported() throws SQLException {
        throw new OperationNotSupportedException();
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.mysql.jdbc.RowProvider#setOwner(com.mysql.jdbc.ResultSet)
     */
    public void setOwner(ResultSetImpl rs) {
        this.owner = rs;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.mysql.jdbc.RowProvider#getOwner()
     */
    public ResultSetInternalMethods getOwner() {
        return this.owner;
    }

    public boolean wasEmpty() {
        return this.wasEmpty;
    }

    public void setMetadata(Field[] metadata) {
        this.metadata = metadata;
    }

}
