/* Hibernate, Relational Persistence for Idiomatic Java
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 * Copyright: Red Hat Inc. and Hibernate Authors
 */
package org.hibernate.reactive.id.impl;

import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.MappingException;
import org.hibernate.boot.model.relational.QualifiedName;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.config.spi.StandardConverters;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.hibernate.id.Configurable;
import org.hibernate.id.enhanced.SequenceStyleGenerator;
import org.hibernate.id.enhanced.TableGenerator;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.config.ConfigurationHelper;
import org.hibernate.reactive.provider.Settings;
import org.hibernate.reactive.id.ReactiveIdentifierGenerator;
import org.hibernate.reactive.pool.ReactiveConnection;
import org.hibernate.reactive.session.ReactiveSession;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.Type;

import java.util.Collections;
import java.util.Properties;
import java.util.concurrent.CompletionStage;

import static org.hibernate.id.enhanced.TableGenerator.CONFIG_PREFER_SEGMENT_PER_ENTITY;
import static org.hibernate.id.enhanced.TableGenerator.DEF_SEGMENT_COLUMN;
import static org.hibernate.id.enhanced.TableGenerator.DEF_SEGMENT_VALUE;
import static org.hibernate.id.enhanced.TableGenerator.SEGMENT_COLUMN_PARAM;
import static org.hibernate.id.enhanced.TableGenerator.SEGMENT_VALUE_PARAM;
import static org.hibernate.id.enhanced.TableGenerator.TABLE;
import static org.hibernate.reactive.id.impl.IdentifierGeneration.determineSequenceName;
import static org.hibernate.reactive.id.impl.IdentifierGeneration.determineTableName;

/**
 * Support for JPA's {@link javax.persistence.TableGenerator}. This
 * generator functions in two different modes: as a table generator
 * where different logical sequences are represented by different
 * rows ("segments"), or as an emulated sequence generator with
 * just one row and one column.
 */
public class TableReactiveIdentifierGenerator
		implements ReactiveIdentifierGenerator<Long>, Configurable {

	private boolean storeLastUsedValue;

	private QualifiedName qualifiedTableName;
	private String renderedTableName;

	private String segmentColumnName;
	private String segmentValue;

	private String valueColumnName;
	private long initialValue;

	private String selectQuery;
	private String insertQuery;
	private String updateQuery;

	private boolean sequenceEmulator;

	@Override
	public CompletionStage<Long> generate(ReactiveSession session, Object entity) {
		Object[] param = segmentColumnName == null ? new Object[] {} : new Object[] {segmentValue};
		ReactiveConnection connection = session.getReactiveConnection();
		return connection.selectLong( selectQuery, param )
				.thenCompose( result -> {
					if ( result == null ) {
						long initializationValue = storeLastUsedValue ? initialValue - 1 : initialValue;
						Object[] params = segmentColumnName == null ?
								new Object[] {initializationValue} :
								new Object[] {segmentValue, initializationValue};
						return connection.update( insertQuery, params )
								.thenApply( v -> initialValue );
					}
					else {
						long currentValue = result;
						long updatedValue = currentValue + 1;
						Object[] params = segmentColumnName == null ?
								new Object[] {updatedValue, currentValue} :
								new Object[] {updatedValue, currentValue, segmentValue};
						return connection.update( updateQuery, params )
								.thenApply( v -> storeLastUsedValue ? updatedValue : currentValue );
					}
				});
	}

	TableReactiveIdentifierGenerator(boolean sequenceEmulator) {
		this.sequenceEmulator = sequenceEmulator;
	}

	@Override
	public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
		JdbcEnvironment jdbcEnvironment = serviceRegistry.getService( JdbcEnvironment.class );
		Dialect dialect = jdbcEnvironment.getDialect();

		if (sequenceEmulator) {
			//it's a SequenceStyleGenerator backed by a table
			qualifiedTableName = determineSequenceName( params, serviceRegistry );
			valueColumnName = determineValueColumnNameForSequenceEmulation( params, jdbcEnvironment );
			segmentColumnName = null;
			segmentValue = null;
			initialValue = determineInitialValueForSequenceEmulation( params );

			storeLastUsedValue = false;
		}
		else {
			//It's a regular TableGenerator
			qualifiedTableName = determineTableName( params, serviceRegistry );
			segmentColumnName = determineSegmentColumnName( params, jdbcEnvironment );
			valueColumnName = determineValueColumnNameForTable( params, jdbcEnvironment );
			segmentValue = determineSegmentValue( params );
			initialValue = determineInitialValueForTable( params );

			storeLastUsedValue = serviceRegistry.getService( ConfigurationService.class )
					.getSetting( Settings.TABLE_GENERATOR_STORE_LAST_USED, StandardConverters.BOOLEAN, true );
		}

		// allow physical naming strategies a chance to kick in
		renderedTableName = jdbcEnvironment.getQualifiedObjectNameFormatter()
				.format( qualifiedTableName, dialect );

		selectQuery = buildSelectQuery( dialect );
		updateQuery = buildUpdateQuery( dialect );
		insertQuery = buildInsertQuery( dialect );
	}

	protected String determineSegmentColumnName(Properties params, JdbcEnvironment jdbcEnvironment) {
		final String name = ConfigurationHelper.getString( SEGMENT_COLUMN_PARAM, params, DEF_SEGMENT_COLUMN );
		return jdbcEnvironment.getIdentifierHelper().toIdentifier( name ).render( jdbcEnvironment.getDialect() );
	}

	protected String determineValueColumnNameForTable(Properties params, JdbcEnvironment jdbcEnvironment) {
		final String name = ConfigurationHelper.getString(TableGenerator.VALUE_COLUMN_PARAM, params, TableGenerator.DEF_VALUE_COLUMN );
		return jdbcEnvironment.getIdentifierHelper().toIdentifier( name ).render( jdbcEnvironment.getDialect() );
	}

	static String determineValueColumnNameForSequenceEmulation(Properties params, JdbcEnvironment jdbcEnvironment) {
		final String name = ConfigurationHelper.getString( SequenceStyleGenerator.VALUE_COLUMN_PARAM, params, SequenceStyleGenerator.DEF_VALUE_COLUMN );
		return jdbcEnvironment.getIdentifierHelper().toIdentifier( name ).render( jdbcEnvironment.getDialect() );
	}

	protected String determineSegmentValue(Properties params) {
		String segmentValue = params.getProperty( SEGMENT_VALUE_PARAM );
		if ( StringHelper.isEmpty( segmentValue ) ) {
			segmentValue = determineDefaultSegmentValue( params );
		}
		return segmentValue;
	}

	protected String determineDefaultSegmentValue(Properties params) {
		final boolean preferSegmentPerEntity = ConfigurationHelper.getBoolean( CONFIG_PREFER_SEGMENT_PER_ENTITY, params, false );
		return preferSegmentPerEntity ? params.getProperty( TABLE ) : DEF_SEGMENT_VALUE;
	}

	protected int determineInitialValueForTable(Properties params) {
		return ConfigurationHelper.getInt( TableGenerator.INITIAL_PARAM, params, TableGenerator.DEFAULT_INITIAL_VALUE );
	}

	protected int determineInitialValueForSequenceEmulation(Properties params) {
		return ConfigurationHelper.getInt( SequenceStyleGenerator.INITIAL_PARAM, params, SequenceStyleGenerator.DEFAULT_INITIAL_VALUE );
	}

	protected String buildSelectQuery(Dialect dialect) {
		final String alias = "tbl";
		String query = "select " + StringHelper.qualify( alias, valueColumnName ) +
				" from " + renderedTableName + ' ' + alias;
		if (segmentColumnName != null) {
			query += " where " + StringHelper.qualify(alias, segmentColumnName) + "=?";
		}

		return dialect.applyLocksToSql( query,
						new LockOptions( LockMode.PESSIMISTIC_WRITE ).setAliasSpecificLockMode( alias, LockMode.PESSIMISTIC_WRITE ),
						Collections.singletonMap( alias, new String[] { valueColumnName } ) );
	}

	protected String buildUpdateQuery(Dialect dialect) {
		String update = "update " + renderedTableName
				+ " set " + valueColumnName + "=?"
				+ " where " + valueColumnName + "=?";
		if (segmentColumnName != null) {
			update += " and " + segmentColumnName + "=?";
		}
		return update;
	}

	protected String buildInsertQuery(Dialect dialect) {
		String insert = "insert into " + renderedTableName;
		if (segmentColumnName != null) {
			insert += " (" + segmentColumnName + ", " + valueColumnName + ") " + " values (?, ?)";
		}
		else {
			insert += " (" + valueColumnName + ") " + " values (?)";
		}
		return insert;
	}

}