package com.github.davidmoten.rx.jdbc;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Subscriber;
import rx.exceptions.Exceptions;
import rx.functions.Action0;
import rx.subscriptions.Subscriptions;

/**
 * OnSubscribe create method for a select query.
 */
final class QuerySelectOnSubscribe<T> implements OnSubscribe<T> {

    private static final Logger log = LoggerFactory.getLogger(QuerySelectOnSubscribe.class);

    /**
     * Returns an Observable of the results of pushing one set of parameters
     * through a select query.
     * 
     * @param params
     *            one set of parameters to be run with the query
     * @return
     */
    static <T> Observable<T> execute(QuerySelect query, List<Parameter> parameters,
            ResultSetMapper<? extends T> function) {
        return Observable.create(new QuerySelectOnSubscribe<T>(query, parameters, function));
    }

    private final ResultSetMapper<? extends T> function;
    private final QuerySelect query;
    private final List<Parameter> parameters;
    private final boolean stateProvided;

    /**
     * Constructor.
     * 
     * @param query
     * @param parameters
     */
    private QuerySelectOnSubscribe(QuerySelect query, List<Parameter> parameters,
            ResultSetMapper<? extends T> function) {
        this.query = query;
        this.parameters = parameters;
        this.function = function;
        this.stateProvided = query.sql().equals(QuerySelect.RETURN_GENERATED_KEYS);
    }

    @Override
    public void call(Subscriber<? super T> subscriber) {
        State state = null;
        try {
            if (stateProvided) {
                state = (State) parameters.get(0).value();
                setupUnsubscription(subscriber, state);
            } else {
                state = new State();
                connectAndPrepareStatement(subscriber, state);
                setupUnsubscription(subscriber, state);
                executeQuery(subscriber, state);
            }
            subscriber.setProducer(new QuerySelectProducer<T>(function, subscriber, state.con,
                    state.ps, state.rs));
        } catch (Throwable e) {
            query.context().endTransactionObserve();
            query.context().endTransactionSubscribe();
            try {
                if (state != null)
                    closeQuietly(state);
            } finally {
                handleException(e, subscriber);
            }
        }
    }

    private static <T> void setupUnsubscription(Subscriber<T> subscriber, final State state) {
        subscriber.add(Subscriptions.create(new Action0() {
            @Override
            public void call() {
                closeQuietly(state);
            }
        }));
    }

    /**
     * Obtains connection, creates prepared statement and assigns parameters to
     * the prepared statement.
     * 
     * @param subscriber
     * @param state
     * 
     * @throws SQLException
     */
    private void connectAndPrepareStatement(Subscriber<? super T> subscriber, State state)
            throws SQLException {
        log.debug("connectionProvider={}", query.context().connectionProvider());
        if (!subscriber.isUnsubscribed()) {
            log.debug("getting connection");
            state.con = query.context().connectionProvider().get();
            log.debug("preparing statement,sql={}", query.sql());
            state.ps = state.con.prepareStatement(query.sql(), ResultSet.TYPE_FORWARD_ONLY,
                    ResultSet.CONCUR_READ_ONLY);
            if (query.context().fetchSize() != null) {
                state.ps.setFetchSize(query.context().fetchSize());
            }
            log.debug("setting parameters");
            Util.setParameters(state.ps, parameters, query.names());
        }
    }

    /**
     * Executes the prepared statement.
     * 
     * @param subscriber
     * @param state
     * 
     * @throws SQLException
     */
    private void executeQuery(Subscriber<? super T> subscriber, State state) throws SQLException {
        if (!subscriber.isUnsubscribed()) {
            try {
                log.debug("executing sql={}, parameters {}", query.sql(), parameters);
                state.rs = query.resultSetTransform()
                        .call(query.context().resultSetTransform().call(state.ps.executeQuery()));
                log.debug("executed ps={}", state.ps);
            } catch (SQLException e) {
                throw new SQLException("failed to run sql=" + query.sql(), e);
            }
        }
    }

    /**
     * Tells observer about exception.
     * 
     * @param e
     * @param subscriber
     */
    private void handleException(Throwable e, Subscriber<? super T> subscriber) {
        log.debug("onError: {}", e.getMessage());
        Exceptions.throwOrReport(e, subscriber);
    }

    /**
     * Closes connection resources (connection, prepared statement and result
     * set).
     * 
     * @param state
     */
    private static void closeQuietly(State state) {
        // ensure only closed once and avoid race conditions
        if (state.closed.compareAndSet(false, true)) {
            // set the state fields to null after closing for garbage
            // collection purposes
            log.debug("closing rs");
            Util.closeQuietly(state.rs);
            log.debug("closing ps");
            Util.closeQuietly(state.ps);
            log.debug("closing con");
            Util.closeQuietlyIfAutoCommit(state.con);
            log.debug("closed");
        }
    }

}
