/*
 *  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 groovy.sql;

import groovy.lang.GroovyObjectSupport;
import org.codehaus.groovy.runtime.ArrayGroovyMethods;
import org.codehaus.groovy.runtime.InvokerHelper;

import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/**
 * Class which delegates to a Statement but keeps track of a batch count size.
 * If the batch count reaches the predefined number, this Statement does an executeBatch()
 * automatically. If batchSize is zero, then no batching is performed.
 */
public class BatchingStatementWrapper extends GroovyObjectSupport implements AutoCloseable {
    private final Statement delegate;
    /** Automatic execution threshold; {@code 0} disables automatic partitioning. */
    protected int batchSize;
    /** Number of commands added since the last delegate batch execution. */
    protected int batchCount;
    /** Logger used for batch diagnostics. */
    protected Logger log;
    /** Accumulated update counts across delegate batch executions. */
    protected List<Integer> results;

    /**
     * Creates a batching wrapper for a statement.
     *
     * @param delegate the statement to wrap
     * @param batchSize the automatic execution threshold; {@code 0} disables automatic partitioning
     * @param log the logger to use for batch diagnostics
     */
    public BatchingStatementWrapper(Statement delegate, int batchSize, Logger log) {
        this.delegate = delegate;
        this.batchSize = batchSize;
        this.log = log;
        reset();
    }

    /**
     * Resets the wrapper's in-memory batch bookkeeping.
     */
    protected void reset() {
        batchCount = 0;
        results = new ArrayList<Integer>();
    }

    /**
     * Delegates unknown method calls to the wrapped {@link Statement}.
     *
     * @param name the method name
     * @param args the method arguments
     * @return the delegated result
     */
    @Override
    public Object invokeMethod(String name, Object args) {
        return InvokerHelper.invokeMethod(delegate, name, args);
    }

    /**
     * Adds a SQL command to the current batch.
     *
     * @param sql the SQL command to add
     * @throws SQLException if the command cannot be added
     */
    public void addBatch(String sql) throws SQLException {
        delegate.addBatch(sql);
        incrementBatchCount();
    }

    /**
     * Increments batch count (after addBatch(..) has been called)
     * and execute {@code delegate.executeBatch()} if batchSize has been reached.
     */
    protected void incrementBatchCount() throws SQLException {
        batchCount++;
        if (batchCount == batchSize /* never true for batchSize of 0 */) {
            int[] result = delegate.executeBatch();
            processResult(result);
            batchCount = 0;
        }
    }

    /**
     * Clears the current batch and resets this wrapper's batch bookkeeping.
     *
     * @throws SQLException if the wrapped statement fails to clear its batch
     */
    public void clearBatch() throws SQLException {
        if (batchSize != 0) {
            reset();
        }
        delegate.clearBatch();
    }

    /**
     * Executes any pending batched commands and returns the aggregated update counts.
     *
     * @return one update count per executed batch command
     * @throws SQLException if batch execution fails
     */
    public int[] executeBatch() throws SQLException {
        if (shouldCallDelegate()) {
            int[] lastResult = delegate.executeBatch();
            processResult(lastResult);
        }
        int[] result = new int[results.size()];
        for (int i = 0; i < results.size(); i++) {
            result[i] = results.get(i);
        }
        reset();
        return result;
    }

    private boolean shouldCallDelegate() {
        if (batchCount > 0) {
            return true;
        } else if (results.isEmpty()) {
            log.warning("Nothing has been added to batch. This might cause the JDBC driver to throw an exception.");
            return true;
        }
        // Nothing added since last delegate execution. No need to call the delegate this time.
        return false;
    }

    /**
     * Incorporates one delegate batch execution result into this wrapper's state.
     *
     * @param lastResult the update counts returned by the wrapped statement
     */
    protected void processResult(int[] lastResult) {
        boolean foundError = false;
        for (int i : lastResult) {
            if (i == Statement.EXECUTE_FAILED) foundError = true;
            results.add(i);
        }
        // A little bit of paranoid checking here? Most drivers will throw BatchUpdateException perhaps?
        if (batchCount != lastResult.length) {
            log.warning("Problem executing batch - expected result length of " + batchCount + " but got " + lastResult.length);
        } else if (foundError) {
            log.warning("Problem executing batch - at least one result failed in: " + ArrayGroovyMethods.toList(lastResult));
        } else {
            log.fine("Successfully executed batch with " + lastResult.length + " command(s)");
        }
    }

    /**
     * Closes the wrapped statement.
     *
     * @throws SQLException if the statement cannot be closed
     */
    @Override
    public void close() throws SQLException {
        delegate.close();
    }
}
