package it.unibz.inf.ontop.query.resultset.impl;

import com.google.common.collect.ImmutableList;
import it.unibz.inf.ontop.answering.logging.QueryLogger;
import it.unibz.inf.ontop.query.resultset.OntopBindingSet;
import it.unibz.inf.ontop.query.resultset.TupleResultSet;
import it.unibz.inf.ontop.exception.OntopConnectionException;
import it.unibz.inf.ontop.exception.OntopResultConversionException;
import it.unibz.inf.ontop.model.term.Variable;
import it.unibz.inf.ontop.utils.ImmutableCollectors;

import javax.annotation.Nullable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.NoSuchElementException;


public abstract class AbstractTupleResultSet implements TupleResultSet {

    protected final ResultSet rs;
    protected final ImmutableList<Variable> signature;
    private final QueryLogger queryLogger;

    /**
     * Provided when the closing the result set should involve closing the OBDA statement.
     * Needed because binding libraries do not always use OBDAStatement as initially intended.
     */
    @Nullable
    private final OntopConnectionCloseable statementClosingCB;

    /**
     * Flag used to emulate the expected behavior of next() and hasNext()
     * This workaround is due to the fact that java.sql.ResultSet does not have a hasNext() method.
     * Keeps track of whether next() or hasNext() has been called last.
     */
    private boolean lastCalledIsHasNext = false;

    /* Set to false iff the moveCursor() method returned false (at least once) */
    private boolean foundNextElement = true;

    private long rowCount = 0;

    AbstractTupleResultSet(ResultSet rs, ImmutableList<Variable> signature, QueryLogger queryLogger,
                           @Nullable OntopConnectionCloseable statementClosingCB){
        this.rs = rs;
        this.signature = signature;
        this.queryLogger = queryLogger;
        this.statementClosingCB = statementClosingCB;
    }

    @Override
    public int getColumnCount() {
        return signature.size();
    }

    @Override
    public ImmutableList<String> getSignature() {
        return signature.stream()
                .map(Variable::getName)
                .collect(ImmutableCollectors.toList());
    }


    @Override
    public OntopBindingSet next() throws OntopConnectionException, OntopResultConversionException {

        if (!lastCalledIsHasNext) {
            try {
                // Moves cursor one result ahead
                foundNextElement = moveCursor();
            } catch (Exception e) {
                throw buildConnectionException(e);
            }
        }
        lastCalledIsHasNext = false;
        if (!foundNextElement) {
            throw new NoSuchElementException("No next OntopBindingSet in this TupleResultSet");
        }
        return readCurrentRow();
    }

    @Override
    public boolean hasNext() throws OntopConnectionException {
        if (!lastCalledIsHasNext) {
            lastCalledIsHasNext = true;
            try {
                // Moves cursor one result ahead
                foundNextElement = moveCursor();
                if (foundNextElement)
                    rowCount++;
            } catch (Exception e) {
                throw buildConnectionException(e);
            }
        }
        if (!foundNextElement) {
            queryLogger.declareLastResultRetrievedAndSerialize(rowCount);
            close();
        }
        return foundNextElement;
    }

    /* This method can be overwritten to ensure distinct rows */
    protected boolean moveCursor() throws SQLException, OntopConnectionException {
        return rs.next();
    }

    @Override
    public void close() throws OntopConnectionException {
        try {
            rs.close();
            if (statementClosingCB != null)
                statementClosingCB.close();
        } catch (Exception e) {
            throw buildConnectionException(e);
        }
    }
    @Override
    public boolean isConnectionAlive() throws OntopConnectionException {
        try {
            return !rs.isClosed();
        } catch (SQLException e) {
            throw buildConnectionException(e);
        }
    }

    protected abstract OntopBindingSet readCurrentRow() throws OntopConnectionException, OntopResultConversionException;

    protected OntopConnectionException buildConnectionException(Exception e) {
        queryLogger.declareConnectionException(e);
        return new OntopConnectionException(e);
    }
}
