/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.jdbc.results;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;

import org.apache.jena.jdbc.statements.JenaStatement;
import org.apache.jena.query.QueryExecution ;

/**
 * Represents a set of streamed results backed by some {@link QueryExecution},
 * streamed results are considered to be forward only
 * 
 * @param <T>
 *            Type of the underlying result rows
 * 
 */
public abstract class StreamedResults<T> extends QueryExecutionResults {

    private T currItem;
    private boolean finished = false;
    private int currRow = 0;

    /**
     * Creates new streamed results
     * 
     * @param statement
     *            Statement that created the result set
     * @param qe
     *            Query Execution
     * @param commit
     *            Whether a commit is necessary when the results are closed
     * @throws SQLException
     *             Thrown if the arguments are invalid
     */
    public StreamedResults(JenaStatement statement, QueryExecution qe, boolean commit) throws SQLException {
        super(statement, qe, commit);
    }

    /**
     * Gets the current result row (if any)
     * 
     * @return Result row, null if not at a row
     * @throws SQLException
     *             Thrown if the result set is closed
     */
    protected T getCurrentRow() throws SQLException {
        if (this.isClosed())
            throw new SQLException("Result set is closed");
        return this.currItem;
    }

    /**
     * Method which derived classes must implement to indicate whether they have
     * further rows available
     * 
     * @return True if further rows are available, false otherwise
     * @throws SQLException
     *             Thrown if an error determining whether rows are available
     */
    protected abstract boolean hasNext() throws SQLException;

    /**
     * Method which derived classes must implement to provide the next row
     * available
     * 
     * @return Next row available
     * @throws SQLException
     *             Thrown if this method is invoked when no further rows are
     *             available
     */
    protected abstract T moveNext() throws SQLException;

    @Override
    public final boolean absolute(int row) throws SQLException {
        if (this.isClosed()) {
            throw new SQLException("Cannot move to a row after the result set has been closed");
        } else if (row == 1) {
            // Try and move to the first row
            return this.first();
        } else if (row == -1) {
            // Try and move to the last row
            return this.last();
        } else if (row <= 0) {
            // Can't move to an arbitrary relative row from the end of the
            // results
            throw new SQLException(
                    "Jena JDBC result sets are forward only, cannot move to a row which is relative to the end of the result set since the number of result rows is not known in advance");
        } else if (row == this.currRow) {
            // Already at the desired row
            return true;
        } else if (row < this.currRow) {
            throw new SQLException("Jena JDBC result sets are forward only, cannot move backwards");
        } else {
            // Before the desired row
            while (this.hasNext() && this.currRow < row) {
                this.currItem = this.moveNext();
                this.currRow++;
            }
            // If we didn't reach it we hit the end of the result set
            if (this.currRow < row) {
                this.finished = true;
                this.currItem = null;
            }
            return (row == this.currRow);
        }
    }

    @Override
    public final void afterLast() throws SQLException {
        if (this.isClosed())
            throw new SQLException("Result Set is closed");
        if (finished)
            return;

        // Move to end of results
        while (this.hasNext()) {
            this.currItem = this.moveNext();
            this.currRow++;
        }
        this.currItem = null;
        this.finished = true;
    }

    @Override
    public final void beforeFirst() throws SQLException {
        if (this.isClosed())
            throw new SQLException("Result Set is closed");
        // If we've started throw an error as we can't move backwards
        if (this.currRow > 0)
            throw new SQLException(
                    "Jena JDBC result sets are forward only, can't move to before the start of the result set after navigation through the result set has begun");
        // Otherwise OK
        this.currItem = null;
    }

    @Override
    protected final void closeInternal() throws SQLException {
        this.currItem = null;
        this.finished = true;
        this.closeStreamInternal();
    }

    protected abstract void closeStreamInternal() throws SQLException;

    @Override
    public final boolean first() throws SQLException {
        if (this.isClosed())
            throw new SQLException("Result Set is closed");
        if (this.currRow == 1)
            return true;
        throw new SQLException(
                "Jena JDBC result sets are forward only, can't move backwards to the first row after the first row has been passed");
    }

    @Override
    public final int getFetchDirection() {
        return ResultSet.FETCH_FORWARD;
    }

    @Override
    public final int getFetchSize() {
        // TODO Need a buffering wrapper around ResultSet to make this
        // configurable
        return 0;
    }

    @Override
    public final int getRow() {
        return this.currRow;
    }

    @Override
    public final int getType() {
        return ResultSet.TYPE_FORWARD_ONLY;
    }

    @Override
    public final boolean isAfterLast() throws SQLException {
        if (this.isClosed())
            throw new SQLException("Result Set is closed");
        return this.finished;
    }

    @Override
    public final boolean isBeforeFirst() throws SQLException {
        if (this.isClosed())
            throw new SQLException("Result Set is closed");
        return this.currRow == 0;
    }

    @Override
    public final boolean isFirst() throws SQLException {
        if (this.isClosed())
            throw new SQLException("Result Set is closed");
        return this.currRow == 1;
    }

    @Override
    public final boolean isLast() throws SQLException {
        if (this.isClosed())
            throw new SQLException("Result Set is closed");
        return !this.hasNext();
    }

    @Override
    public final boolean last() throws SQLException {
        if (this.isClosed() || this.finished) {
            throw new SQLException("Jena JDBC Result Sets are forward-only");
        } else {
            while (this.hasNext()) {
                this.currItem = this.moveNext();
                this.currRow++;
            }
            return true;
        }
    }

    @Override
    public final boolean next() throws SQLException {
        if (this.isClosed()) {
            throw new SQLException("Cannot move to the next row in a closed result set");
        } else {
            if (this.hasNext()) {
                this.currItem = this.moveNext();
                this.currRow++;
                return true;
            } else {
                if (!this.finished)
                    this.currRow++;
                this.finished = true;
                return false;
            }
        }
    }

    @Override
    public final boolean relative(int rows) throws SQLException {
        if (this.isClosed()) {
            throw new SQLException("Cannot move to a row after the result set has been closed");
        } else if (rows == 0) {
            // Already at the desired row
            return true;
        } else if (rows < 0) {
            throw new SQLException("Jena JDBC result sets are forward only, cannot move backwards");
        } else {
            // Before the desired row
            int moved = 0;
            while (this.hasNext() && moved < rows) {
                this.currItem = this.moveNext();
                this.currRow++;
                moved++;
            }
            // If we didn't reach it we hit the end of the result set
            if (moved < rows) {
                this.finished = true;
                this.currItem = null;
            }
            return (rows == moved);
        }
    }

    @Override
    public final void setFetchDirection(int direction) throws SQLException {
        if (direction != ResultSet.FETCH_FORWARD)
            throw new SQLFeatureNotSupportedException("Jena JDBC Result Sets only support forward fetch");
    }

    @Override
    public final void setFetchSize(int rows) throws SQLException {
        // TODO Need to provide some buffering wrapper over a ResultSet to make
        // this possible
        throw new SQLFeatureNotSupportedException();
    }
}
