/**
 * 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 materialized results backed by some
 * {@link QueryExecution}, materialized results permit scrolling but are not
 * sensitive to changes in the underlying data
 * 
 * @param <T>
 *            Type of the underlying result rows
 * 
 */
public abstract class MaterializedResults<T> extends QueryExecutionResults {

    private T currItem;
    private int currRow = 0;

    /**
     * Creates new materialized 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 MaterializedResults(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 to move forwards to
     * 
     * @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 indicate whether they have
     * further rows available to move backwards to
     * 
     * @return True if further rows are available, false otherwise
     * @throws SQLException
     *             Thrown if an error determining whether rows are available
     */
    protected abstract boolean hasPrevious() 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;

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

    protected abstract int getTotalRows() 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) {
            // Try and move to before first row
            this.beforeFirst();
            return false;
        } else if (row > this.getTotalRows()) {
            // Try and move to after last row
            this.afterLast();
            return false;
        } else if (row <= 0) {
            // Move to a row relative to the end of the result set
            int destRow = this.getTotalRows() + 1 + row;
            if (destRow < 1) {
                // Move to before first
                this.beforeFirst();
                return false;
            } else {
                // Call ourselves to avoid repeating logic
                return this.absolute(destRow);
            }
        } else if (row == this.currRow) {
            // Already at the desired row
            return true;
        } else if (row < this.currRow) {
            // After the desired row
            while (this.hasPrevious() && row < this.currRow) {
                this.currItem = this.movePrevious();
                this.currRow--;
            }
            // Since we already checked that the desired row is a valid absolute
            // row it is always possible to move backwards to the desired row
            return true;
        } else {
            // Before the desired row
            while (this.hasNext() && this.currRow < row) {
                this.currItem = this.moveNext();
                this.currRow++;
            }
            // Since we already checked that the desired row is a valid absolute
            // row it is always possible to move forwards to the desired row
            return true;
        }
    }

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

        // Move to end of results if necessary
        while (this.hasNext()) {
            this.currItem = this.moveNext();
            this.currRow++;
        }
        this.currItem = null;
        this.currRow = this.getTotalRows() + 1;
    }

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

        // Are we after the last row? If so reset current row or movePrevious()
        // may break
        if (this.isAfterLast())
            this.currRow = this.getTotalRows();

        // Move to start of results if necessary
        while (this.hasPrevious()) {
            this.currItem = this.movePrevious();
            this.currRow--;
        }
        this.currItem = null;
        this.currRow = 0;
    }

    @Override
    protected final void closeInternal() throws SQLException {
        this.currItem = null;
        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 no rows this should always return false
        if (this.getTotalRows() == 0)
            return false;

        // Are we already at the first row?
        if (this.currRow == 1)
            return true;

        // Are we after the last row? If so reset current row or movePrevious()
        // may break
        if (this.isAfterLast())
            this.currRow = this.getTotalRows();

        // Before first row?
        if (this.isBeforeFirst()) {
            // Need to move forwards to first row
            if (this.hasNext()) {
                this.currItem = this.moveNext();
                this.currRow = 1;
                return true;
            } else {
                return false;
            }
        } else {
            // Otherwise move backwards to it
            while (this.hasPrevious()) {
                this.currItem = this.movePrevious();
                this.currRow--;
            }
            return true;
        }
    }

    @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_SCROLL_INSENSITIVE;
    }

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

    @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.currRow == this.getTotalRows();
    }

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

        // If no rows this should always return false
        if (this.getTotalRows() == 0)
            return false;

        // Are we already at the last row?
        if (this.currRow == this.getTotalRows())
            return true;

        // Are we after the last row?
        if (this.isAfterLast()) {
            this.currRow = this.getTotalRows();

            // Move backwards to last row
            if (this.hasPrevious()) {
                this.currItem = this.movePrevious();
                this.currRow = this.getTotalRows();
                return true;
            } else {
                return false;
            }
        } else {
            // Otherwise move forwards to the last row
            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 {
                this.currItem = null;
                this.currRow = this.getTotalRows() + 1;
                return false;
            }
        }
    }

    @Override
    public final boolean previous() throws SQLException {
        if (this.isClosed()) {
            throw new SQLException("Cannot mvoe to the previous row in a closed result set");
        } else {
            if (this.hasPrevious()) {
                this.currItem = this.movePrevious();
                this.currRow--;
                return true;
            } else {
                this.currItem = null;
                this.currRow = 0;
                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) {
            // Calculate destination row and use absolute() to move there
            int destRow = this.currRow + rows;

            // Need to sanitize resulting values below zero as otherwise
            // absolute will move us to a row relative to end of results when
            // actually this means we should be moving to before the first row
            if (destRow < 0)
                destRow = 0;
            return this.absolute(destRow);
        } else if (this.currRow + rows > this.getTotalRows()) {
            // Would result in moving beyond the end of the results
            this.afterLast();
            return false;
        } else {
            // Before the desired row
            int moved = 0;
            while (this.hasNext() && moved < rows) {
                this.currItem = this.moveNext();
                this.currRow++;
                moved++;
            }
            // Since we already checked if the move would take us beyond the end
            // of the results we will always have reached the desired row
            return true;
        }
    }

    @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();
    }
}
