/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2011, Red Hat, Inc. and/or its affiliates 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, Inc.
 *
 * 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.search.query.dsl.impl;

import java.util.Date;

import org.hibernate.search.exception.AssertionFailure;
import org.hibernate.search.bridge.spi.ConversionContext;
import org.hibernate.search.bridge.util.impl.ContextualExceptionBridgeHelper;
import org.hibernate.search.engine.spi.DocumentBuilderIndexedEntity;

/**
 * @author Hardy Ferentschik
 */
public class FacetRange<T> {
	private static final String MIN_INCLUDED = "[";
	private static final String MIN_EXCLUDED = "(";
	private static final String MAX_INCLUDED = "]";
	private static final String MAX_EXCLUDED = ")";

	private final T min;
	private final T max;
	private final boolean includeMin;
	private final boolean includeMax;
	private final String rangeString;
	private final String fieldName;
	private final Class<?> rangeType;

	private String stringMin;
	private String stringMax;

	public FacetRange(Class<?> rangeType,
					T min,
					T max,
					boolean includeMin,
					boolean includeMax,
					String fieldName,
					DocumentBuilderIndexedEntity<?> documentBuilder) {
		if ( max == null && min == null ) {
			throw new IllegalArgumentException( "At least one end of the range has to be specified" );
		}

		if ( documentBuilder == null ) {
			throw new AssertionFailure(
					"null is not a valid document builder"
			);
		}

		this.min = min;
		this.max = max;
		this.includeMax = includeMax;
		this.includeMin = includeMin;
		this.fieldName = fieldName;
		this.rangeString = buildRangeString();
		this.rangeType = rangeType;

		if ( Date.class.equals( rangeType ) ) {
			final ConversionContext conversionContext = new ContextualExceptionBridgeHelper();
			stringMin = documentBuilder.objectToString( fieldName, min, conversionContext );
			stringMax = documentBuilder.objectToString( fieldName, max, conversionContext );
		}
	}

	public T getMin() {
		return min;
	}

	public T getMax() {
		return max;
	}

	public boolean isMinIncluded() {
		return includeMin;
	}

	public boolean isMaxIncluded() {
		return includeMax;
	}

	public boolean isInRange(T value) {
		if ( Number.class.isAssignableFrom( rangeType ) ) {
			return isInRangeNumber( (Number) value, (Number) min, (Number) max );
		}
		else if ( String.class.equals( rangeType ) ) {
			return isInRangeString( (String) value, (String) min, (String) max );
		}
		else if ( Date.class.equals( rangeType ) ) {
			return isInRangeString(
					(String) value,
					stringMin,
					stringMax
			);
		}
		else {
			throw new AssertionFailure( "Unexpected value type: " + value.getClass().getName() );
		}
	}

	public String getRangeString() {
		return rangeString;
	}

	private String buildRangeString() {
		StringBuilder builder = new StringBuilder();
		if ( includeMin ) {
			builder.append( MIN_INCLUDED );
		}
		else {
			builder.append( MIN_EXCLUDED );
		}
		if ( min != null ) {
			builder.append( min );
		}
		builder.append( ", " );
		if ( max != null ) {
			builder.append( max );
		}
		if ( includeMax ) {
			builder.append( MAX_INCLUDED );
		}
		else {
			builder.append( MAX_EXCLUDED );
		}
		return builder.toString();
	}

	private boolean isInRangeString(String value, String min, String max) {
		// below range
		if ( min == null ) {
			if ( isMaxIncluded() ) {
				return value.compareTo( max ) <= 0;
			}
			else {
				return value.compareTo( max ) < 0;
			}
		}

		// above range
		if ( max == null ) {
			if ( isMinIncluded() ) {
				return value.compareTo( min ) >= 0;
			}
			else {
				return value.compareTo( min ) > 0;
			}
		}

		// from .. to range
		int minCheck = min.compareTo( value );
		if ( isMinIncluded() && minCheck > 0 ) {
			return false;
		}
		else if ( !isMinIncluded() && minCheck >= 0 ) {
			return false;
		}

		int maxCheck = value.compareTo( max );
		if ( isMaxIncluded() && maxCheck > 0 ) {
			return false;
		}
		else if ( !isMaxIncluded() && maxCheck >= 0 ) {
			return false;
		}
		return true;
	}

	private boolean isInRangeNumber(Number value, Number min, Number max) {
		// below range
		if ( min == null ) {
			if ( isMaxIncluded() ) {
				return compare( value, max ) <= 0;
			}
			else {
				return compare( value, max ) < 0;
			}
		}

		// above range
		if ( max == null ) {
			if ( isMinIncluded() ) {
				return compare( value, min ) >= 0;
			}
			else {
				return compare( value, min ) > 0;
			}
		}

		// from .. to range
		int minCheck = compare( min, value );
		if ( isMinIncluded() && minCheck > 0 ) {
			return false;
		}
		else if ( !isMinIncluded() && minCheck >= 0 ) {
			return false;
		}

		int maxCheck = compare( value, max );
		if ( isMaxIncluded() && maxCheck > 0 ) {
			return false;
		}
		else if ( !isMaxIncluded() && maxCheck >= 0 ) {
			return false;
		}
		return true;
	}

	// todo - does this implementation of Number comparison hold?
	private int compare(Number number1, Number number2) {
		if ( !number2.getClass().equals( number1.getClass() ) ) {
			throw new IllegalStateException();
		}

		if ( number1 instanceof Comparable ) {
			return ( (Comparable) number1 ).compareTo( number2 );
		}

		if ( number1.doubleValue() < number2.doubleValue() ) {
			return -1;
		}
		if ( number1.doubleValue() > number2.doubleValue() ) {
			return 1;
		}
		return 0;
	}

	@Override
	public String toString() {
		final StringBuilder sb = new StringBuilder();
		sb.append( "FacetRange" );
		sb.append( "{min=" ).append( min );
		sb.append( ", max=" ).append( max );
		sb.append( ", includeMin=" ).append( includeMin );
		sb.append( ", includeMax=" ).append( includeMax );
		sb.append( ", fieldName='" ).append( fieldName ).append( '\'' );
		sb.append( ", rangeType=" ).append( rangeType );
		sb.append( '}' );
		return sb.toString();
	}
}


