/*
 * Copyright (c) 2007 David Crawshaw <david@zentus.com>
 * 
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
package org.sqlite;

import java.io.Reader;
import java.io.StringReader;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Calendar;

/**
 * Implements a JDBC ResultSet.
 */
final class RS extends Unused implements ResultSet, ResultSetMetaData, Codes
{
    private final Stmt stmt;
    private final DB   db;

    boolean            open     = false; // true means have results and can iterate them
    int                maxRows;         // max. number of rows as set by a Statement
    String[]           cols     = null; // if null, the RS is closed()
    String[]           colsMeta = null; // same as cols, but used by Meta interface
    boolean[][]        meta     = null;

    private int        limitRows;       // 0 means no limit, must check against maxRows
    private int        row      = 0;    // number of current row, starts at 1 (0 is for before loading data)
    private int        lastCol;         // last column accessed, for wasNull(). -1 if none

    RS(Stmt stmt) {
        this.stmt = stmt;
        this.db = stmt.db;
    }

    // INTERNAL FUNCTIONS ///////////////////////////////////////////

    boolean isOpen() {
        return open;
    }

    /* Throws SQLException if ResultSet is not open. */
    void checkOpen() throws SQLException {
        if (!open)
            throw new SQLException("ResultSet closed");
    }

    // takes col in [1,x] form, returns in [0,x-1] form
    private int checkCol(int col) throws SQLException {
        if (colsMeta == null)
            throw new IllegalStateException("SQLite JDBC: inconsistent internal state");
        if (col < 1 || col > colsMeta.length)
            throw new SQLException("column " + col + " out of bounds [1," + colsMeta.length + "]");
        return --col;
    }

    // takes col in [1,x] form, marks it as last accessed and returns [0,x-1]
    private int markCol(int col) throws SQLException {
        checkOpen();
        checkCol(col);
        lastCol = col;
        return --col;
    }

    private void checkMeta() throws SQLException {
        checkCol(1);
        if (meta == null)
            meta = db.column_metadata(stmt.pointer);
    }

    // ResultSet Functions //////////////////////////////////////////

    public void close() throws SQLException {
        cols = null;
        colsMeta = null;
        meta = null;
        open = false;
        limitRows = 0;
        row = 0;
        lastCol = -1;

        if (stmt == null)
            return;
        if (stmt != null && stmt.pointer != 0)
            db.reset(stmt.pointer);
    }

    // returns col in [1,x] form
    public int findColumn(String col) throws SQLException {
        checkOpen();
        int c = -1;
        for (int i = 0; i < cols.length; i++) {
            if (col.equalsIgnoreCase(cols[i])
                    || (cols[i].toUpperCase().endsWith(col.toUpperCase()) && cols[i].charAt(cols[i].length()
                            - col.length()) == '.')) {
                if (c == -1)
                    c = i;
                else
                    throw new SQLException("ambiguous column: '" + col + "'");
            }
        }
        if (c == -1)
            throw new SQLException("no such column: '" + col + "'");
        else
            return c + 1;
    }

    public boolean next() throws SQLException {
        if (!open)
            return false; // finished ResultSet
        lastCol = -1;

        // first row is loaded by execute(), so do not step() again
        if (row == 0) {
            row++;
            return true;
        }

        // check if we are row limited by the statement or the ResultSet
        if (maxRows != 0 && row > maxRows)
            return false;

        // do the real work
        int statusCode = db.step(stmt.pointer);
        switch (statusCode) {
        case SQLITE_DONE:
            close(); // agressive closing to avoid writer starvation
            return false;
        case SQLITE_ROW:
            row++;
            return true;
        case SQLITE_BUSY:
        default:
            db.throwex(statusCode);
            return false;
        }
    }

    public int getType() throws SQLException {
        return TYPE_FORWARD_ONLY;
    }

    public int getFetchSize() throws SQLException {
        return limitRows;
    }

    public void setFetchSize(int rows) throws SQLException {
        if (0 > rows || (maxRows != 0 && rows > maxRows))
            throw new SQLException("fetch size " + rows + " out of bounds " + maxRows);
        limitRows = rows;
    }

    public int getFetchDirection() throws SQLException {
        checkOpen();
        return ResultSet.FETCH_FORWARD;
    }

    public void setFetchDirection(int d) throws SQLException {
        checkOpen();
        if (d != ResultSet.FETCH_FORWARD)
            throw new SQLException("only FETCH_FORWARD direction supported");
    }

    public boolean isAfterLast() throws SQLException {
        return !open;
    }

    public boolean isBeforeFirst() throws SQLException {
        return open && row == 0;
    }

    public boolean isFirst() throws SQLException {
        return row == 1;
    }

    public boolean isLast() throws SQLException { // FIXME
        throw new SQLException("function not yet implemented for SQLite");
    }

    @Override
    protected void finalize() throws SQLException {
        close();
    }

    public int getRow() throws SQLException {
        return row;
    }

    public boolean wasNull() throws SQLException {
        return db.column_type(stmt.pointer, markCol(lastCol)) == SQLITE_NULL;
    }

    // DATA ACCESS FUNCTIONS ////////////////////////////////////////

    public boolean getBoolean(int col) throws SQLException {
        return getInt(col) == 0 ? false : true;
    }

    public boolean getBoolean(String col) throws SQLException {
        return getBoolean(findColumn(col));
    }

    public byte getByte(int col) throws SQLException {
        return (byte) getInt(col);
    }

    public byte getByte(String col) throws SQLException {
        return getByte(findColumn(col));
    }

    public byte[] getBytes(int col) throws SQLException {
        return db.column_blob(stmt.pointer, markCol(col));
    }

    public byte[] getBytes(String col) throws SQLException {
        return getBytes(findColumn(col));
    }

    public Reader getCharacterStream(int col) throws SQLException {
        return new StringReader(getString(col));
    }

    public Reader getCharacterStream(String col) throws SQLException {
        return getCharacterStream(findColumn(col));
    }

    public Date getDate(int col) throws SQLException {
        if (db.column_type(stmt.pointer, markCol(col)) == SQLITE_NULL)
            return null;
        return new Date(db.column_long(stmt.pointer, markCol(col)));
    }

    public Date getDate(int col, Calendar cal) throws SQLException {
        if (db.column_type(stmt.pointer, markCol(col)) == SQLITE_NULL)
            return null;
        if (cal == null)
            return getDate(col);
        cal.setTimeInMillis(db.column_long(stmt.pointer, markCol(col)));
        return new Date(cal.getTime().getTime());
    }

    public Date getDate(String col) throws SQLException {
        return getDate(findColumn(col), Calendar.getInstance());
    }

    public Date getDate(String col, Calendar cal) throws SQLException {
        return getDate(findColumn(col), cal);
    }

    public double getDouble(int col) throws SQLException {
        if (db.column_type(stmt.pointer, markCol(col)) == SQLITE_NULL)
            return 0;
        return db.column_double(stmt.pointer, markCol(col));
    }

    public double getDouble(String col) throws SQLException {
        return getDouble(findColumn(col));
    }

    public float getFloat(int col) throws SQLException {
        if (db.column_type(stmt.pointer, markCol(col)) == SQLITE_NULL)
            return 0;
        return (float) db.column_double(stmt.pointer, markCol(col));
    }

    public float getFloat(String col) throws SQLException {
        return getFloat(findColumn(col));
    }

    public int getInt(int col) throws SQLException {
        return db.column_int(stmt.pointer, markCol(col));
    }

    public int getInt(String col) throws SQLException {
        return getInt(findColumn(col));
    }

    public long getLong(int col) throws SQLException {
        return db.column_long(stmt.pointer, markCol(col));
    }

    public long getLong(String col) throws SQLException {
        return getLong(findColumn(col));
    }

    public short getShort(int col) throws SQLException {
        return (short) getInt(col);
    }

    public short getShort(String col) throws SQLException {
        return getShort(findColumn(col));
    }

    public String getString(int col) throws SQLException {
        return db.column_text(stmt.pointer, markCol(col));
    }

    public String getString(String col) throws SQLException {
        return getString(findColumn(col));
    }

    public Time getTime(int col) throws SQLException {
        if (db.column_type(stmt.pointer, markCol(col)) == SQLITE_NULL)
            return null;
        return new Time(db.column_long(stmt.pointer, markCol(col)));
    }

    public Time getTime(int col, Calendar cal) throws SQLException {
        if (cal == null)
            return getTime(col);
        if (db.column_type(stmt.pointer, markCol(col)) == SQLITE_NULL)
            return null;
        cal.setTimeInMillis(db.column_long(stmt.pointer, markCol(col)));
        return new Time(cal.getTime().getTime());
    }

    public Time getTime(String col) throws SQLException {
        return getTime(findColumn(col));
    }

    public Time getTime(String col, Calendar cal) throws SQLException {
        return getTime(findColumn(col), cal);
    }

    public Timestamp getTimestamp(int col) throws SQLException {
        if (db.column_type(stmt.pointer, markCol(col)) == SQLITE_NULL)
            return null;
        return new Timestamp(db.column_long(stmt.pointer, markCol(col)));
    }

    public Timestamp getTimestamp(int col, Calendar cal) throws SQLException {
        if (cal == null)
            return getTimestamp(col);
        if (db.column_type(stmt.pointer, markCol(col)) == SQLITE_NULL)
            return null;
        cal.setTimeInMillis(db.column_long(stmt.pointer, markCol(col)));
        return new Timestamp(cal.getTime().getTime());
    }

    public Timestamp getTimestamp(String col) throws SQLException {
        return getTimestamp(findColumn(col));
    }

    public Timestamp getTimestamp(String c, Calendar ca) throws SQLException {
        return getTimestamp(findColumn(c), ca);
    }

    public Object getObject(int col) throws SQLException {
        switch (db.column_type(stmt.pointer, checkCol(col))) {
        case SQLITE_INTEGER:
            long val = getLong(col);
            if (val > Integer.MAX_VALUE || val < Integer.MIN_VALUE)
                return new Long(val);
            else
                return new Integer((int) val);
        case SQLITE_FLOAT:
            return new Double(getDouble(col));
        case SQLITE_BLOB:
            return getBytes(col);
        case SQLITE_NULL:
            return null;
        case SQLITE_TEXT:
        default:
            return getString(col);
        }
    }

    public Object getObject(String col) throws SQLException {
        return getObject(findColumn(col));
    }

    public Statement getStatement() {
        return stmt;
    }

    public String getCursorName() throws SQLException {
        return null;
    }

    public SQLWarning getWarnings() throws SQLException {
        return null;
    }

    public void clearWarnings() throws SQLException {}

    // ResultSetMetaData Functions //////////////////////////////////

    // we do not need to check the RS is open, only that colsMeta
    // is not null, done with checkCol(int).

    public ResultSetMetaData getMetaData() throws SQLException {
        return this;
    }

    public String getCatalogName(int col) throws SQLException {
        return db.column_table_name(stmt.pointer, checkCol(col));
    }

    public String getColumnClassName(int col) throws SQLException {
        checkCol(col);
        return "java.lang.Object";
    }

    public int getColumnCount() throws SQLException {
        checkCol(1);
        return colsMeta.length;
    }

    public int getColumnDisplaySize(int col) throws SQLException {
        return Integer.MAX_VALUE;
    }

    public String getColumnLabel(int col) throws SQLException {
        return getColumnName(col);
    }

    public String getColumnName(int col) throws SQLException {
        return db.column_name(stmt.pointer, checkCol(col));
    }

    public int getColumnType(int col) throws SQLException {
        switch (db.column_type(stmt.pointer, checkCol(col))) {
        case SQLITE_INTEGER:
            return Types.INTEGER;
        case SQLITE_FLOAT:
            return Types.FLOAT;
        case SQLITE_BLOB:
            return Types.BLOB;
        case SQLITE_NULL:
            return Types.NULL;
        case SQLITE_TEXT:
        default:
            return Types.VARCHAR;
        }
    }

    public String getColumnTypeName(int col) throws SQLException {
        switch (db.column_type(stmt.pointer, checkCol(col))) {
        case SQLITE_INTEGER:
            return "integer";
        case SQLITE_FLOAT:
            return "float";
        case SQLITE_BLOB:
            return "blob";
        case SQLITE_NULL:
            return "null";
        case SQLITE_TEXT:
        default:
            return "text";
        }
    }

    public int getPrecision(int col) throws SQLException {
        return 0;
    } // FIXME

    public int getScale(int col) throws SQLException {
        return 0;
    }

    public String getSchemaName(int col) throws SQLException {
        return "";
    }

    public String getTableName(int col) throws SQLException {
        return db.column_table_name(stmt.pointer, checkCol(col));
    }

    public int isNullable(int col) throws SQLException {
        checkMeta();
        return meta[checkCol(col)][1] ? columnNoNulls : columnNullable;
    }

    public boolean isAutoIncrement(int col) throws SQLException {
        checkMeta();
        return meta[checkCol(col)][2];
    }

    public boolean isCaseSensitive(int col) throws SQLException {
        return true;
    }

    public boolean isCurrency(int col) throws SQLException {
        return false;
    }

    public boolean isDefinitelyWritable(int col) throws SQLException {
        return true;
    } // FIXME: check db file constraints?

    public boolean isReadOnly(int col) throws SQLException {
        return false;
    }

    public boolean isSearchable(int col) throws SQLException {
        return true;
    }

    public boolean isSigned(int col) throws SQLException {
        return false;
    }

    public boolean isWritable(int col) throws SQLException {
        return true;
    }

    public int getConcurrency() throws SQLException {
        return CONCUR_READ_ONLY;
    }

    public boolean rowDeleted() throws SQLException {
        return false;
    }

    public boolean rowInserted() throws SQLException {
        return false;
    }

    public boolean rowUpdated() throws SQLException {
        return false;
    }
}
