/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Middleware LLC.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 *
 */
package org.hibernate.id.enhanced;

import java.io.Serializable;
import java.lang.reflect.Constructor;

import org.hibernate.HibernateException;
import org.hibernate.id.IdentifierGeneratorFactory;
import org.hibernate.util.ReflectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Factory for {@link Optimizer} instances.
 *
 * @author Steve Ebersole
 */
public class OptimizerFactory {
	private static final Logger log = LoggerFactory.getLogger( OptimizerFactory.class );

	public static final String NONE = "none";
	public static final String HILO = "hilo";
	public static final String POOL = "pooled";

	private static Class[] CTOR_SIG = new Class[] { Class.class, int.class };

	/**
	 * Marker interface for optimizer which wish to know the user-specified initial value.
	 * <p/>
	 * Used instead of constructor injection since that is already a public understanding and
	 * because not all optimizers care.
	 */
	public static interface InitialValueAwareOptimizer {
		/**
		 * Reports the user specified initial value to the optimizer.
		 * <p/>
		 * <tt>-1</tt> is used to indicate that the user did not specify.
		 *
		 * @param initialValue The initial value specified by the user, or <tt>-1</tt> to indicate that the
		 * user did not specify.
		 */
		public void injectInitialValue(long initialValue);
	}
	public static Optimizer buildOptimizer(String type, Class returnClass, int incrementSize) {
		String optimizerClassName;
		if ( NONE.equals( type ) ) {
			optimizerClassName = NoopOptimizer.class.getName();
		}
		else if ( HILO.equals( type ) ) {
			optimizerClassName = HiLoOptimizer.class.getName();
		}
		else if ( POOL.equals( type ) ) {
			optimizerClassName = PooledOptimizer.class.getName();
		}
		else {
			optimizerClassName = type;
		}

		try {
			Class optimizerClass = ReflectHelper.classForName( optimizerClassName );
			Constructor ctor = optimizerClass.getConstructor( CTOR_SIG );
			return ( Optimizer ) ctor.newInstance( new Object[] { returnClass, new Integer( incrementSize ) } );
		}
		catch( Throwable ignore ) {
			// intentionally empty
		}

		// the default...
		return new NoopOptimizer( returnClass, incrementSize );
	}
	/**
	 * Builds an optimizer
	 *
	 * @param type The optimizer type, either a short-hand name or the {@link Optimizer} class name.
	 * @param returnClass The generated value java type
	 * @param incrementSize The increment size.
	 * @param explicitInitialValue The user supplied initial-value (-1 indicates the user did not specify).
	 *
	 * @return The built optimizer
	 */
	@SuppressWarnings({ "UnnecessaryBoxing", "deprecation" })
	public static Optimizer buildOptimizer(String type, Class returnClass, int incrementSize, long explicitInitialValue) {
		final Optimizer optimizer = buildOptimizer( type, returnClass, incrementSize );
		if ( InitialValueAwareOptimizer.class.isInstance( optimizer ) ) {
			( (InitialValueAwareOptimizer) optimizer ).injectInitialValue( explicitInitialValue );
		}
		return optimizer;
	}
	/**
	 * Common support for optimizer implementations.
	 */
	public static abstract class OptimizerSupport implements Optimizer {
		protected final Class returnClass;
		protected final int incrementSize;

		/**
		 * Construct an optimizer
		 *
		 * @param returnClass The expected id class.
		 * @param incrementSize The increment size
		 */
		protected OptimizerSupport(Class returnClass, int incrementSize) {
			if ( returnClass == null ) {
				throw new HibernateException( "return class is required" );
			}
			this.returnClass = returnClass;
			this.incrementSize = incrementSize;
		}

		/**
		 * Take the primitive long value and "make" (or wrap) it into the
		 * {@link #getReturnClass id type}.
		 *
		 * @param value The primitive value to make/wrap.
		 * @return The wrapped value.
		 */
		protected final Serializable make(long value) {
			return IdentifierGeneratorFactory.createNumber( value, returnClass );
		}

		/**
		 * Getter for property 'returnClass'.  This is the Java
		 * class which is used to represent the id (e.g. {@link java.lang.Long}).
		 *
		 * @return Value for property 'returnClass'.
		 */
		public final Class getReturnClass() {
			return returnClass;
		}

		/**
		 * {@inheritDoc}
		 */
		public final int getIncrementSize() {
			return incrementSize;
		}
	}

	/**
	 * An optimizer that performs no optimization.  The database is hit for
	 * every request.
	 */
	public static class NoopOptimizer extends OptimizerSupport {
		private long lastSourceValue = -1;

		public NoopOptimizer(Class returnClass, int incrementSize) {
			super( returnClass, incrementSize );
		}

		/**
		 * {@inheritDoc}
		 */
		public Serializable generate(AccessCallback callback) {
			if ( lastSourceValue == -1 ) {
				while( lastSourceValue <= 0 ) {
					lastSourceValue = callback.getNextValue();
				}
			}
			else {
				lastSourceValue = callback.getNextValue();
			}
			return make( lastSourceValue );
		}

		/**
		 * {@inheritDoc}
		 */
		public long getLastSourceValue() {
			return lastSourceValue;
		}

		/**
		 * {@inheritDoc}
		 */
		public boolean applyIncrementSizeToSourceValues() {
			return false;
		}
	}

	/**
	 * Optimizer which applies a 'hilo' algorithm in memory to achieve
	 * optimization.
	 */
	public static class HiLoOptimizer extends OptimizerSupport {
		private long lastSourceValue = -1;
		private long value;
		private long hiValue;

		public HiLoOptimizer(Class returnClass, int incrementSize) {
			super( returnClass, incrementSize );
			if ( incrementSize < 1 ) {
				throw new HibernateException( "increment size cannot be less than 1" );
			}
			if ( log.isTraceEnabled() ) {
				log.trace( "creating hilo optimizer with [incrementSize=" + incrementSize + "; returnClass="  + returnClass.getName() + "]" );
			}
		}

		/**
		 * {@inheritDoc}
		 */
		public synchronized Serializable generate(AccessCallback callback) {
			if ( lastSourceValue < 0 ) {
				lastSourceValue = callback.getNextValue();
				while ( lastSourceValue <= 0 ) {
					lastSourceValue = callback.getNextValue();
				}
				hiValue = ( lastSourceValue * incrementSize ) + 1;
				value = hiValue - incrementSize;
			}
			else if ( value >= hiValue ) {
				lastSourceValue = callback.getNextValue();
				hiValue = ( lastSourceValue * incrementSize ) + 1;
			}
			return make( value++ );
		}


		/**
		 * {@inheritDoc}
		 */
		public long getLastSourceValue() {
			return lastSourceValue;
		}

		/**
		 * {@inheritDoc}
		 */
		public boolean applyIncrementSizeToSourceValues() {
			return false;
		}

		/**
		 * Getter for property 'lastValue'.
		 *
		 * @return Value for property 'lastValue'.
		 */
		public long getLastValue() {
			return value - 1;
		}

		/**
		 * Getter for property 'hiValue'.
		 *
		 * @return Value for property 'hiValue'.
		 */
		public long getHiValue() {
			return hiValue;
		}
	}

	/**
	 * Optimizer which uses a pool of values, storing the next low value of the
	 * range in the database.
	 */
	public static class PooledOptimizer extends OptimizerSupport implements InitialValueAwareOptimizer{
		private long value;
		private long hiValue = -1;
		private long initialValue = -1;
		public PooledOptimizer(Class returnClass, int incrementSize) {
			super( returnClass, incrementSize );
			if ( incrementSize < 1 ) {
				throw new HibernateException( "increment size cannot be less than 1" );
			}
			if ( log.isTraceEnabled() ) {
				log.trace( "creating pooled optimizer with [incrementSize=" + incrementSize + "; returnClass="  + returnClass.getName() + "]" );
			}
		}

		/**
		 * {@inheritDoc}
		 */
		public synchronized Serializable generate(AccessCallback callback) {
			if ( hiValue < 0 ) {
				value = callback.getNextValue();
				if ( value < 1 ) {
					// unfortunately not really safe to normalize this
					// to 1 as an initial value like we do the others
					// because we would not be able to control this if
					// we are using a sequence...
					log.info( "pooled optimizer source reported [" + value + "] as the initial value; use of 1 or greater highly recommended" );
				}
				if ( ( initialValue == -1 && value < incrementSize ) || ( value == initialValue ) ) {
					// the call to obtain next-value just gave us the initialValue
					hiValue = callback.getNextValue();
				}
				else {
					hiValue = value;
					value = hiValue - incrementSize;
				}
			}
			else if ( value >= hiValue ) {
				hiValue = callback.getNextValue();
				value = hiValue - incrementSize;
			}
			return make( value++ );
		}

		/**
		 * {@inheritDoc}
		 */
		public long getLastSourceValue() {
			return hiValue;
		}

		/**
		 * {@inheritDoc}
		 */
		public boolean applyIncrementSizeToSourceValues() {
			return true;
		}

		/**
		 * Getter for property 'lastValue'.
		 *
		 * @return Value for property 'lastValue'.
		 */
		public long getLastValue() {
			return value - 1;
		}

		public void injectInitialValue( long initialValue ) {
			this.initialValue = initialValue;
		}
	}
}
