package org.openjproxy.jdbc;

import com.openjproxy.grpc.LobReference;
import com.openjproxy.grpc.LobType;
import com.openjproxy.grpc.OpResult;
import io.grpc.StatusRuntimeException;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.openjproxy.constants.CommonConstants;
import org.openjproxy.grpc.ProtoConverter;
import org.openjproxy.grpc.client.StatementService;
import org.openjproxy.grpc.dto.OpQueryResult;
import org.openjproxy.jdbc.sqlserver.HydratedBlob;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.NClob;
import java.sql.Ref;
import java.sql.RowId;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import static org.openjproxy.grpc.client.GrpcExceptionHandler.handle;

@Slf4j
public class ResultSet extends RemoteProxyResultSet {

    @Getter
    private final Map<String, Integer> labelsMap;

    private Iterator<OpResult> itResults;//Iterator of blocks of data
    private List<Object[]> currentDataBlock;//Current block of data being processed.
    private AtomicInteger blockIdx = new AtomicInteger(-1);//Current block index
    private AtomicInteger blockCount = new AtomicInteger(1);//Current block count
    private java.sql.ResultSetMetaData resultSetMetadata;
    private boolean inProxyMode;
    private boolean closed;
    private AtomicInteger currentIdx = new AtomicInteger(0);
    private boolean inRowByRowMode;

    private Object lastValueRead;

    public ResultSet(Iterator<OpResult> itOpResult, StatementService statementService, java.sql.Statement statement) throws SQLException {
        this.itResults = itOpResult;
        this.inProxyMode = false;
        this.closed = false;
        try {
            this.statement = statement;
            OpResult result = nextWithSessionUpdate(itOpResult.next());
            OpQueryResult opQueryResult = ProtoConverter.fromProto(result.getQueryResult());
            this.inRowByRowMode = CommonConstants.RESULT_SET_ROW_BY_ROW_MODE.equalsIgnoreCase(result.getFlag());
            this.setStatementService(statementService);
            this.setResultSetUUID(opQueryResult.getResultSetUUID());
            this.currentDataBlock = opQueryResult.getRows();
            this.labelsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);//During tests CockroachDB returned column names capital, this is so that the search for a column to be case insensitive.
            List<String> labels = opQueryResult.getLabels();
            for (int i = 0; i < labels.size(); i++) {
                labelsMap.put(labels.get(i).toUpperCase(), i);
            }
        } catch (StatusRuntimeException e) {
            throw handle(e);
        }
    }

    @Override
    public boolean next() throws SQLException {
        log.debug("next called");
        if (this.inProxyMode) {
            return super.next();
        }
        this.currentIdx.incrementAndGet();
        blockIdx.incrementAndGet();
        if (this.inRowByRowMode && blockIdx.get() > 0) {
            try {
                // Row by row mode is used in SQL Server and DB2 only when working with LOBs as per moving the cursor earlier
                // would invalidate the LOB object(s) and therefore for SQL Server and DB2 the read is only done when asked
                // by the client.
                OpResult result = this.nextWithSessionUpdate(
                        this.getStatementService().fetchNextRows(((Connection) this.statement.getConnection()).getSession(),
                        this.getResultSetUUID(), 1));
                this.setNextOpResult(result);
            } catch (StatusRuntimeException e) {
                throw handle(e);
            }
        }
        if (!this.inRowByRowMode && blockIdx.get() >= currentDataBlock.size() && itResults.hasNext()) {
            try {
                this.setNextOpResult(this.nextWithSessionUpdate(itResults.next()));
            } catch (StatusRuntimeException e) {
                throw handle(e);
            }
        }
        return blockIdx.get() < currentDataBlock.size();
    }

    private void setNextOpResult(OpResult result) {
        OpQueryResult opQueryResult = ProtoConverter.fromProto(result.getQueryResult());
        this.currentDataBlock = opQueryResult.getRows();
        this.blockCount.incrementAndGet();
        this.blockIdx.set(0);
    }

    private OpResult nextWithSessionUpdate(OpResult next) throws SQLException {
        log.debug("nextWithSessionUpdate called");
        ((Connection) this.statement.getConnection()).setSession(next.getSession());
        return next;
    }

    @Override
    public void close() throws SQLException {
        log.debug("close called");
        this.closed = true;
        this.blockIdx = null;
        this.itResults = null;
        this.currentDataBlock = null;
        //If the parent statement is closed the result set is closed already, attempting to close it again would produce an error.
        if (this.statement == null || !this.statement.isClosed()) {
            super.close();
        }
    }

    @Override
    public boolean wasNull() throws SQLException {
        log.debug("wasNull called");
        if (this.inProxyMode) {
            return super.wasNull();
        }
        return lastValueRead == null;
    }

    @Override
    public String getString(int columnIndex) throws SQLException {
        log.debug("getString: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getString(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return null;
        }
        if (String.valueOf(lastValueRead).startsWith(CommonConstants.OJP_CLOB_PREFIX)) {
            Clob clob = this.getClob(columnIndex);
            if (clob.length() > Integer.MAX_VALUE) {
                throw new SQLException("Attempt to read large CLOB (>2MB) via getString not allowed due to memory overflow danger.");
            }
            return clob.getSubString(1, (int) clob.length());
        }

        return lastValueRead.toString();
    }

    @Override
    public boolean getBoolean(int columnIndex) throws SQLException {
        log.debug("getBoolean: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getBoolean(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return false;
        }
        return (boolean) lastValueRead;
    }

    @Override
    public byte getByte(int columnIndex) throws SQLException {
        log.debug("getByte: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getByte(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return 0;
        } else if (lastValueRead instanceof byte[]) {
            return ((byte[]) lastValueRead)[0];
        } else if (lastValueRead instanceof Short) {
            return (byte)(short) lastValueRead;
        } else if (lastValueRead instanceof Integer) {
            return (byte)(int) lastValueRead;
        }
        return (byte) lastValueRead;
    }

    @Override
    public short getShort(int columnIndex) throws SQLException {
        log.debug("getShort: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getShort(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return 0;
        } else if (lastValueRead instanceof Integer) {
            return (short)(int) lastValueRead;
        }
        return (short) lastValueRead;
    }

    @Override
    public int getInt(int columnIndex) throws SQLException {
        log.debug("getInt: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getInt(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return 0;
        }
        Object value = lastValueRead;
        if (value instanceof Integer) {
            return (int) value;
        } else if (value instanceof Long) {
            Long lValue = (Long) value;
            return lValue.intValue();
        } else if (value instanceof Short) {
            Short sValue = (Short) value;
            return sValue.intValue();
        } else if (value instanceof Date) {
            Date dValue = (Date) value;
            LocalDate ld = LocalDate.ofEpochDay(dValue.getTime());
            if (ld.getDayOfMonth() > 0) {
                return ld.getDayOfMonth();
            } else if (ld.getMonth().getValue() > 0) {
                return ld.getMonth().getValue();
            } else if (ld.getYear() > 0) {
                return ld.getYear();
            }
            return (int) dValue.getTime();
        } else {
            return Integer.parseInt("" + value);
        }
    }

    @Override
    public long getLong(int columnIndex) throws SQLException {
        log.debug("getLong: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getLong(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return 0;
        }
        if (lastValueRead instanceof BigInteger) {
            return ((BigInteger) lastValueRead).longValue();
        }
        if (lastValueRead instanceof Integer) {
            return ((Integer) lastValueRead).longValue();
        }
        if (lastValueRead instanceof BigDecimal) {
            return ((BigDecimal) lastValueRead).longValue();
        }
        return (long) lastValueRead;
    }

    @Override
    public float getFloat(int columnIndex) throws SQLException {
        log.debug("getFloat: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getFloat(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return 0;
        }
        Object value = lastValueRead;
        if (value instanceof BigDecimal) {
            BigDecimal bdValue = (BigDecimal) value;
            return bdValue.floatValue();
        }
        return (float) value;
    }

    @Override
    public double getDouble(int columnIndex) throws SQLException {
        log.debug("getDouble: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getDouble(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return 0d;
        }
        Object value = lastValueRead;
        if (value instanceof BigDecimal) {
            BigDecimal bdValue = (BigDecimal) value;
            return bdValue.doubleValue();
        }
        return (double) value;
    }

    @Override
    public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException {
        log.debug("getBigDecimal: {}, {}", columnIndex, scale);
        if (this.inProxyMode) {
            return super.getBigDecimal(columnIndex, scale);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return null;
        }
        return (BigDecimal) lastValueRead;
    }

    @SneakyThrows
    @Override
    public byte[] getBytes(int columnIndex) throws SQLException {
        log.debug("getBytes: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getBytes(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead instanceof String) {// Means the server is treating it as a binary stream
            InputStream is = this.getBinaryStream(columnIndex);
            return is.readAllBytes();
        }
        return (byte[]) lastValueRead;
    }

    @Override
    public Date getDate(int columnIndex) throws SQLException {
        log.debug("getDate: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getDate(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return null;
        }
        Object result = lastValueRead;
        if (result instanceof Timestamp) {
            Timestamp timestamp = (Timestamp) result;
            return new Date(timestamp.getTime());
        }
        return (Date) result;
    }

    @Override
    public Time getTime(int columnIndex) throws SQLException {
        log.debug("getTime: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getTime(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return null;
        }
        return (Time) lastValueRead;
    }

    @Override
    public Timestamp getTimestamp(int columnIndex) throws SQLException {
        log.debug("getTimestamp: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getTimestamp(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return null;
        }
        return (Timestamp) lastValueRead;
    }

    @Override
    public InputStream getAsciiStream(int columnIndex) throws SQLException {
        log.debug("getAsciiStream: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getAsciiStream(columnIndex);
        }
        lastValueRead = null;
        throw new RuntimeException("Not implemented");
    }

    @Override
    public InputStream getUnicodeStream(int columnIndex) throws SQLException {
        log.debug("getUnicodeStream: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getUnicodeStream(columnIndex);
        }
        lastValueRead = null;
        throw new RuntimeException("Not implemented");
    }

    @Override
    public InputStream getBinaryStream(int columnIndex) throws SQLException {
        log.debug("getBinaryStream: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getBinaryStream(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return null;
        } else if (lastValueRead instanceof byte[]) {// Only used by SQL server
            return new ByteArrayInputStream((byte[]) lastValueRead);
        }
        Object objUUID = lastValueRead;
        String lobRefUUID = String.valueOf(objUUID);
        LobReference.Builder lobRefBuilder = LobReference.newBuilder()
                .setSession(this.getConnection().getSession())
                .setLobType(LobType.LT_BINARY_STREAM)
                .setUuid(lobRefUUID)
                .setColumnIndex(columnIndex);
        if (this.statement != null) {
            if (this.statement instanceof Statement) {
                Statement stmt = (Statement) this.statement;
                if (StringUtils.isNotBlank(stmt.getStatementUUID())) {
                    lobRefBuilder.setStmtUUID(stmt.getStatementUUID());
                }
            }
        }
        BinaryStream binaryStream = new BinaryStream((Connection) this.statement.getConnection(),
                new LobServiceImpl((Connection) this.statement.getConnection(), this.getStatementService()),
                this.getStatementService(), lobRefBuilder.build());
        return binaryStream.getBinaryStream();
    }

    @Override
    public String getString(String columnLabel) throws SQLException {
        log.debug("getString: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getString(columnLabel);
        }
        return this.getString(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public boolean getBoolean(String columnLabel) throws SQLException {
        log.debug("getBoolean: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getBoolean(columnLabel);
        }
        return this.getBoolean(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public byte getByte(String columnLabel) throws SQLException {
        log.debug("getByte: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getByte(columnLabel);
        }
        return this.getByte(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public short getShort(String columnLabel) throws SQLException {
        log.debug("getShort: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getShort(columnLabel);
        }
        return this.getShort(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public int getInt(String columnLabel) throws SQLException {
        log.debug("getInt: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getInt(columnLabel);
        }
        return this.getInt(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public long getLong(String columnLabel) throws SQLException {
        log.debug("getLong: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getLong(columnLabel);
        }
        return this.getLong(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public float getFloat(String columnLabel) throws SQLException {
        log.debug("getFloat: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getFloat(columnLabel);
        }
        return this.getFloat(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public double getDouble(String columnLabel) throws SQLException {
        log.debug("getDouble: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getDouble(columnLabel);
        }
        return this.getDouble(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public BigDecimal getBigDecimal(String columnLabel) throws SQLException {
        log.debug("getBigDecimal: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getBigDecimal(columnLabel);
        }
        return this.getBigDecimal(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public byte[] getBytes(String columnLabel) throws SQLException {
        log.debug("getBytes: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getBytes(columnLabel);
        }
        return this.getBytes(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public Date getDate(String columnLabel) throws SQLException {
        log.debug("getDate: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getDate(columnLabel);
        }
        return this.getDate(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public Time getTime(String columnLabel) throws SQLException {
        log.debug("getTime: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getTime(columnLabel);
        }
        return this.getTime(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public Timestamp getTimestamp(String columnLabel) throws SQLException {
        log.debug("getTimestamp: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getTimestamp(columnLabel);
        }
        return this.getTimestamp(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public Object getObject(String columnLabel) throws SQLException {
        log.debug("getObject: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getObject(columnLabel);
        }
        return this.getObject(this.labelsMap.get(columnLabel.toUpperCase()) + 1);
    }

    @Override
    public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException {
        log.debug("getBigDecimal: {}, {}", columnLabel, scale);
        if (this.inProxyMode) {
            return super.getBigDecimal(columnLabel, scale);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[this.labelsMap.get(columnLabel.toUpperCase())];
        if (lastValueRead == null) {
            return null;
        }
        return (BigDecimal) lastValueRead;
    }

    @Override
    public InputStream getAsciiStream(String columnLabel) throws SQLException {
        log.debug("getAsciiStream: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getAsciiStream(columnLabel);
        }
        lastValueRead = null;
        throw new RuntimeException("Not implemented");
    }

    @Override
    public InputStream getUnicodeStream(String columnLabel) throws SQLException {
        log.debug("getUnicodeStream: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getUnicodeStream(columnLabel);
        }
        lastValueRead = null;
        throw new RuntimeException("Not implemented");
    }

    @Override
    public InputStream getBinaryStream(String columnLabel) throws SQLException {
        log.debug("getBinaryStream: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getBinaryStream(columnLabel);
        }
        int colIdx = this.labelsMap.get(columnLabel.toUpperCase()) + 1;
        lastValueRead = currentDataBlock.get(blockIdx.get())[colIdx - 1];
        if (lastValueRead == null) {
            return null;
        }
        return this.getBinaryStream(colIdx);
    }

    @Override
    public SQLWarning getWarnings() throws SQLException {
        log.debug("getWarnings called");
        return super.getWarnings();
    }

    @Override
    public void clearWarnings() throws SQLException {
        log.debug("clearWarnings called");
        if (this.inProxyMode) {
            super.clearWarnings();
        }
    }

    @Override
    public String getCursorName() throws SQLException {
        log.debug("getCursorName called");
        if (this.inProxyMode) {
            return super.getCursorName();
        }
        return "";
    }

    @Override
    public java.sql.ResultSetMetaData getMetaData() throws SQLException {
        log.debug("getMetaData called");
        if (this.inProxyMode) {
            return super.getMetaData();
        }
        if (this.getResultSetUUID() == null) {
            throw new SQLException("No result set reference found.");
        }
        if (this.resultSetMetadata == null) {
            this.resultSetMetadata = new ResultSetMetaData(this, this.getStatementService());
        }
        return this.resultSetMetadata;
    }

    @Override
    public Object getObject(int columnIndex) throws SQLException {
        log.debug("getObject: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getObject(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        return lastValueRead;
    }

    @Override
    public int findColumn(String columnLabel) throws SQLException {
        log.debug("findColumn: {}", columnLabel);
        if (this.inProxyMode) {
            return super.findColumn(columnLabel);
        }
        return this.labelsMap.get(columnLabel) + 1;
    }

    @Override
    public Reader getCharacterStream(int columnIndex) throws SQLException {
        log.debug("getCharacterStream: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getCharacterStream(columnIndex);
        }
        lastValueRead = null;
        throw new RuntimeException("Not implemented");
    }

    @Override
    public Reader getCharacterStream(String columnLabel) throws SQLException {
        log.debug("getCharacterStream: {}", columnLabel);
        if (this.inProxyMode) {
            return super.getCharacterStream(columnLabel);
        }
        lastValueRead = null;
        throw new RuntimeException("Not implemented");
    }

    @Override
    public BigDecimal getBigDecimal(int columnIndex) throws SQLException {
        log.debug("getBigDecimal: {}", columnIndex);
        if (this.inProxyMode) {
            return super.getBigDecimal(columnIndex);
        }
        lastValueRead = currentDataBlock.get(blockIdx.get())[columnIndex - 1];
        if (lastValueRead == null) {
            return null;
        }
        return (BigDecimal) lastValueRead;
    }

    @Override
    public boolean isBeforeFirst() throws SQLException {
        log.debug("isBeforeFirst called");
        if (this.inProxyMode) {
            return super.isBeforeFirst();
        }
        return blockIdx.get() == -1;
    }

    @Override
    public boolean first() throws SQLException {
        log.debug("first called");
        this.inProxyMode = true;
        return super.first();
    }

    @Override
    public boolean isAfterLast() throws SQLException {
        log.debug("isAfterLast called");
        if (this.inProxyMode) {
            return super.isAfterLast();
        }
        return !itResults.hasNext() && blockIdx.get() >= currentDataBlock.size();
    }

    @Override
    public boolean isFirst() throws SQLException {
        log.debug("isFirst called");
        if (this.inProxyMode) {
            return super.isFirst();
        }
        return this.blockCount.get() == 1 && this.blockIdx.get() == 0;
    }

    @Override
    public boolean isLast() throws SQLException {
        log.debug("isLast called");
        if (this.inProxyMode) {
            return super.isLast();
        }
        return !itResults.hasNext() && blockIdx.get() == (currentDataBlock.size() - 1);
    }

    @Override
    public void beforeFirst() throws SQLException {
        log.debug("beforeFirst called");
        this.inProxyMode = true;
        super.beforeFirst();
    }

    @Override
    public void afterLast() throws SQLException {
        log.debug("afterLast called");
        this.inProxyMode = true;
        super.afterLast();
    }

    @Override
    public boolean last() throws SQLException {
        log.debug("last called");
        this.inProxyMode = true;
        return super.last();
    }

    @Override
    public int getRow() throws SQLException {
        log.debug("getRow called");
        if (this.inProxyMode) {
            return super.getRow();
        }
        return ((this.blockCount.get() - 1) * CommonConstants.ROWS_PER_RESULT_SET_DATA_BLOCK) + this.blockIdx.get() + 1;
    }

    @Override
    public boolean absolute(int row) throws SQLException {
        log.debug("absolute: {}", row);
        this.inProxyMode = true;
        return super.absolute(row);
    }

    @Override
    public boolean relative(int rows) throws SQLException {
        log.debug("relative: {}", rows);
        this.inProxyMode = true;
        return super.relative(rows);
    }

    @Override
    public boolean previous() throws SQLException {
        log.debug("previous called");
        if (this.inProxyMode) {
            return super.previous();
        }
        this.inProxyMode = true;
        return super.absolute(this.currentIdx.get() - 1);// Will reposition the cursor in the current row being processed.
    }

    @Override
    public void setFetchDirection(int direction) throws SQLException {
        log.debug("setFetchDirection: {}", direction);
        super.setFetchDirection(direction);
        this.inProxyMode = true;
    }

    @Override
    public int getFetchDirection() throws SQLException {
        log.debug("getFetchDirection called");
        return super.getFetchDirection();
    }

    @Override
    public void setFetchSize(int rows) throws SQLException {
        log.debug("setFetchSize: {}", rows);
        if (this.inProxyMode) {
            super.setFetchSize(rows);
        }
        throw new RuntimeException("Not implemented");
    }

    @Override
    public int getFetchSize() throws SQLException {
        log.debug("getFetchSize called");
        if (this.inProxyMode) {
            return super.getFetchSize();
        }
        throw new RuntimeException("Not implemented");
    }

    @Override
    public int getType() throws SQLException {
        log.debug("getType called");
        if (this.inProxyMode) {
            return super.getType();
        }
        return ResultSet.TYPE_FORWARD_ONLY;
    }

    @Override
    public int getConcurrency() throws SQLException {
        log.debug("getConcurrency called");
        if (this.inProxyMode) {
            return super.getConcurrency();
        }
        return ResultSet.CONCUR_READ_ONLY;
    }

    @Override
    public boolean rowUpdated() throws SQLException {
        log.debug("rowUpdated called");
        if (this.inProxyMode) {
            return super.rowUpdated();
        }
        throw new RuntimeException("Not implemented");
    }

    @Override
    public boolean rowInserted() throws SQLException {
        log.debug("rowInserted called");
        if (this.inProxyMode) {
            return super.rowInserted();
        }
        throw new RuntimeException("Not implemented");
    }

    @Override
    public boolean rowDeleted() throws SQLException {
        log.debug("rowDeleted called");
        if (this.inProxyMode) {
            return super.rowDeleted();
        }
        throw new RuntimeException("Not implemented");
    }

    @Override
    public void updateNull(int columnIndex) throws SQLException {
        log.debug("updateNull: {}", columnIndex);
        this.inProxyMode = true;
        super.absolute(this.currentIdx.get());
        super.updateNull(columnIndex);
    }

    @Override
    public void updateBoolean(int columnIndex, boolean x) throws SQLException {
        log.debug("updateBoolean: {}, {}", columnIndex, x);
        this.inProxyMode = true;
        super.absolute(this.currentIdx.get());
        super.updateBoolean(columnIndex, x);
    }

    @Override
    public void updateByte(int columnIndex, byte x) throws SQLException {
        log.debug("updateByte: {}, {}", columnIndex, x);
        if (this.inProxyMode) {
            super.updateByte(columnIndex, x);
            return;
        }
        throw new RuntimeException("Not implemented");
    }

    @Override
    public void updateShort(int columnIndex, short x) throws SQLException {
        log.debug("updateShort: {}, {}", columnIndex, x);
        if (this.inProxyMode) {
            super.updateShort(columnIndex, x);
            return;
        }
        throw new RuntimeException("Not implemented");
    }

    @Override
    public void updateInt(int columnIndex, int x) throws SQLException {
        log.debug("updateInt: {}, {}", columnIndex, x);
        if (this.inProxyMode) {
            super.updateInt(columnIndex, x);
            return;
        }
        throw new RuntimeException("Not implemented");
    }

    @Override
    public void updateLong(int columnIndex, long x) throws SQLException {
        log.debug("updateLong: {}, {}", columnIndex, x);
        if (this.inProxyMode) {
            super.updateLong(columnIndex, x);
            return;
        }
        throw new RuntimeException("Not implemented");
    }

    @Override
    public void updateFloat(int columnIndex, float x) throws SQLException {
        log.debug("updateFloat: {}, {}", columnIndex, x);
        if (this.inProxyMode) {
            super.updateFloat(columnIndex, x);
            return;
        }
        throw new RuntimeException("Not implemented");
    }

    @Override
    public void updateDouble(int columnIndex, double x) throws SQLException {
        log.debug("updateDouble: {}, {}", columnIndex, x);
        if (this.inProxyMode) {
            super.updateDouble(columnIndex, x);
            return;
        }
        throw new RuntimeException("Not implemented");
    }

    @Override
    public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException {
        log.debug("updateBigDecimal: {}, {}", columnIndex, x);
        if (this.inProxyMode) {
            super.updateBigDecimal(columnIndex, x);
            return;
        }
