/*
 * Copyright 2004-2014 Brian McCallister
 * Copyright 2010-2014 Ning, Inc.
 * Copyright 2014-2020 Groupon, Inc
 * Copyright 2020-2020 Equinix, Inc
 * Copyright 2014-2020 The Billing Project, LLC
 *
 * The Billing Project 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 org.skife.jdbi.v2;

import org.skife.jdbi.v2.exceptions.UnableToCreateStatementException;
import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
import org.skife.jdbi.v2.tweak.RewrittenStatement;
import org.skife.jdbi.v2.tweak.SQLLog;
import org.skife.jdbi.v2.tweak.StatementBuilder;
import org.skife.jdbi.v2.tweak.StatementCustomizer;
import org.skife.jdbi.v2.tweak.StatementLocator;
import org.skife.jdbi.v2.tweak.StatementRewriter;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * Represents a prepared batch statement. That is, a sql statement compiled as a prepared
 * statement, and then executed multiple times in a single batch. This is, generally,
 * a very efficient way to execute large numbers of the same statement where
 * the statement only varies by the arguments bound to it.
 */
public class PreparedBatch extends SQLStatement<PreparedBatch>
{
    private final List<PreparedBatchPart> parts = new ArrayList<PreparedBatchPart>();
    private Binding currentBinding;

    PreparedBatch(StatementLocator locator,
                  StatementRewriter rewriter,
                  Handle handle,
                  StatementBuilder statementBuilder,
                  String sql,
                  ConcreteStatementContext ctx,
                  SQLLog log,
                  TimingCollector timingCollector,
                  Collection<StatementCustomizer> statementCustomizers,
                  Foreman foreman,
                  ContainerFactoryRegistry containerFactoryRegistry)
    {
        super(new Binding(), locator, rewriter, handle, statementBuilder, sql, ctx, log, timingCollector, statementCustomizers, foreman, containerFactoryRegistry);
        this.currentBinding = new Binding();
    }

    /**
     * Specify a value on the statement context for this batch
     *
     * @return self
     */
    @Override
    public PreparedBatch define(String key, Object value)
    {
        getContext().setAttribute(key, value);
        return this;
    }

    /**
     * Adds all key/value pairs in the Map to the {@link StatementContext}.
     *
     * @param values containing key/value pairs.
     * @return this
     */
    @Override
    public PreparedBatch define(final Map<String, ? extends Object> values)
    {
        if (values != null) {
            for (Map.Entry<String, ? extends Object> entry: values.entrySet())
            {
                getContext().setAttribute(entry.getKey(), entry.getValue());
            }
        }
        return this;
    }

    /**
     * Execute the batch
     *
     * @return the number of rows modified or inserted per batch part.
     */
    public int[] execute() {
        return (int[]) internalBatchExecute(null, null);
    }

    @SuppressWarnings("unchecked")
    public <GeneratedKeyType> GeneratedKeys<GeneratedKeyType> executeAndGenerateKeys(final ResultSetMapper<GeneratedKeyType> mapper,
                                                                                     String... columnNames) {
        return (GeneratedKeys<GeneratedKeyType>) internalBatchExecute(new QueryResultMunger<GeneratedKeys<GeneratedKeyType>>() {
            public GeneratedKeys<GeneratedKeyType> munge(Statement results) throws SQLException {
                return new GeneratedKeys<GeneratedKeyType>(mapper,
                                                           PreparedBatch.this,
                                                           results,
                                                           getContext(),
                                                           getContainerMapperRegistry());
            }
        }, columnNames);

    }

    private <Result> Object internalBatchExecute(QueryResultMunger<Result> munger, String[] columnNames) {
        boolean generateKeys = munger != null;
        // short circuit empty batch
        if (parts.size() == 0) {
            if (generateKeys) {
                throw new IllegalArgumentException("Unable generate keys for a not prepared batch");
            }
            return new int[]{};
        }

        PreparedBatchPart current = parts.get(0);
        final String my_sql ;
        try {
            my_sql = getStatementLocator().locate(getSql(), getContext());
        }
        catch (Exception e) {
            throw new UnableToCreateStatementException(String.format("Exception while locating statement for [%s]",
                                                                     getSql()), e, getContext());
        }
        final RewrittenStatement rewritten = getRewriter().rewrite(my_sql, current.getParams(), getContext());
        PreparedStatement stmt = null;
        try {
            try {
                Connection connection = getHandle().getConnection();
                if (generateKeys) {
                    if (columnNames != null && columnNames.length > 0) {
                        stmt = connection.prepareStatement(rewritten.getSql(), columnNames);
                    } else  {
                        stmt = connection.prepareStatement(rewritten.getSql(), Statement.RETURN_GENERATED_KEYS);
                    }
                } else {
                    stmt = connection.prepareStatement(rewritten.getSql(), Statement.NO_GENERATED_KEYS);
                }
                addCleanable(Cleanables.forStatement(stmt));
            }
            catch (SQLException e) {
                throw new UnableToCreateStatementException(e, getContext());
            }


            try {
                for (PreparedBatchPart part : parts) {
                    rewritten.bind(part.getParams(), stmt);
                    stmt.addBatch();
                }
            }
            catch (SQLException e) {
                throw new UnableToExecuteStatementException("Exception while binding parameters", e, getContext());
            }

            beforeExecution(stmt);

            try {
                final long start = System.nanoTime();
                final int[] rs =  stmt.executeBatch();
                final long elapsedTime = System.nanoTime() - start;
                getLog().logPreparedBatch(elapsedTime / 1000000L, rewritten.getSql(), parts.size());
                getTimingCollector().collect(elapsedTime, getContext());

                afterExecution(stmt);

                return generateKeys ? munger.munge(stmt) : rs;
            }
            catch (SQLException e) {
                throw new UnableToExecuteStatementException(e, getContext());
            }
        }
        finally {
            try {
                if (!generateKeys) {
                    cleanup();
                }
            }
            finally {
                this.parts.clear();
            }
        }
    }

    /**
     * Add a statement (part) to this batch. You'll need to bindBinaryStream any arguments to the
     * part.
     *
     * @return A part which can be used to bindBinaryStream parts to the statement
     */
    public PreparedBatchPart add()
    {
        PreparedBatchPart part = new PreparedBatchPart(this.currentBinding,
                                                       this,
                                                       getStatementLocator(),
                                                       getRewriter(),
                                                       getHandle(),
                                                       getStatementBuilder(),
                                                       getSql(),
                                                       getConcreteContext(),
                                                       getLog(),
                                                       getTimingCollector(),
                                                       getForeman(),
                                                       getContainerMapperRegistry());
        parts.add(part);
        this.currentBinding = new Binding();
        return part;
    }

    public PreparedBatch add(Object... args)
    {
        PreparedBatchPart part = add();
        for (int i = 0; i < args.length; ++i) {
            part.bind(i, args[i]);
        }
        return this;
    }


    /**
     * Create a new batch part by binding values looked up in <code>args</code> to
     * named parameters on the statement.
     *
     * @param args map to bind arguments from for named parameters on the statement
     *
     * @return the new batch part
     */
    public PreparedBatchPart add(Map<String, ? extends Object> args)
    {
        PreparedBatchPart part = add();
        part.bindFromMap(args);
        return part;
    }

    /**
     * The number of statements which are in this batch
     */
    public int getSize()
    {
        return parts.size();
    }

    /**
     * The number of statements which are in this batch
     */
    public int size()
    {
        return parts.size();
    }

    @Override
    protected Binding getParams()
    {
        return this.currentBinding;
    }
}
