/*
 * Copyright © 2014-2017 EntIT Software LLC, a Micro Focus company (L.P.)
 *
 * Licensed 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 io.cloudslang.engine.data;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;

import javax.sql.DataSource;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by IntelliJ IDEA.
 * User: froelica
 * Date: 4/25/13
 * Time: 11:03 AM
 */
public class SimpleHiloIdentifierGenerator implements IdentifierGenerator, IdentityGenerator {

    private final Logger logger = LogManager.getLogger(getClass());

    static final String TABLE_NAME = "OO_HILO";
    static final String SQL_SELECT = "SELECT NEXT_HI FROM " + TABLE_NAME;
    static final String SQL_UPDATE = "UPDATE " + TABLE_NAME + " SET NEXT_HI = NEXT_HI+1";
    static final String SQL_LOCK = "UPDATE " + TABLE_NAME + " SET NEXT_HI = NEXT_HI";
    static final long CHUNK_SIZE = 100000L;

    private static DataSource dataSource;
    private int currentChunk;
    private long currentId;
    private Lock lock = new ReentrantLock();

    // been initialized by Hibernate
    public SimpleHiloIdentifierGenerator() {
        updateCurrentChunk();
    }

    public static void setDataSource(DataSource injectedDataSource) {
        dataSource = injectedDataSource;
    }

    @Override
    public Long next() {
        return (Long) generate(null, null);
    }

    @Override
    public List<Long> bulk(int bulkSize) {
        List<Long> idsList = new ArrayList<>();
        for (int i = 0; i < bulkSize; i++) {
            idsList.add(next());
        }
        return idsList;
    }

    @Override
    public Serializable generate(SharedSessionContractImplementor session, Object object)
            throws HibernateException {
        lock.lock();
        try {
            long id = ++currentId;
            if (id > CHUNK_SIZE) {
                if (logger.isDebugEnabled()) logger.debug("ID has reached chunk size");
                updateCurrentChunk();
                id = ++currentId;
            }
            return currentChunk * CHUNK_SIZE + id;
        } finally {
            lock.unlock();
        }
    }

    private void updateCurrentChunk() {
        if (logger.isDebugEnabled()) {
            logger.debug("Updating HILO chunk...");
        }

        long t = System.currentTimeMillis();
        try (Connection conn = dataSource.getConnection()) {
            conn.setAutoCommit(false);
            JdbcTemplate jdbcTemplate = new JdbcTemplate(new SingleConnectionDataSource(conn, true));

            jdbcTemplate.update(SQL_LOCK);
            currentChunk = jdbcTemplate.queryForObject(SQL_SELECT, Integer.class);
            if (logger.isDebugEnabled())
                logger.debug("Current chunk: " + currentChunk);
            jdbcTemplate.execute(SQL_UPDATE);
            jdbcTemplate.execute("commit");

            if (logger.isDebugEnabled()) {
                logger.debug("Updating HILO chunk done in " + (System.currentTimeMillis() - t) + " ms");
            }
            currentId = 0;
        } catch (SQLException e) {
            logger.error("Unable to update current chunk", e);
            throw new IllegalStateException("Unable to update current chunk");
        }
    }
}
