/*
 * Hibernate Search, full-text search for your domain model
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.search.test.spatial;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import org.apache.lucene.search.ConstantScoreQuery;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.resource.transaction.spi.TransactionStatus;
import org.hibernate.search.FullTextQuery;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.hibernate.search.query.engine.spi.FacetManager;
import org.hibernate.search.query.facet.Facet;
import org.hibernate.search.query.facet.FacetSortOrder;
import org.hibernate.search.query.facet.FacetingRequest;
import org.hibernate.search.spatial.impl.Point;
import org.hibernate.search.spatial.impl.Rectangle;
import org.hibernate.search.spatial.impl.SpatialQueryBuilderFromCoordinates;

/**
 * Hibernate Search spatial : Benchmarks with <a href="http://www.geonames.org">GeoNames</a>
 * as data source (nearly 8M points)
 *
 * Test many queries types :
 *
 * - pure distance post filtering
 * - double numeric range
 * - spatial hash
 * - double range + distance
 * - spatial hash + distance
 *
 * To test you must download <a href="http://download.geonames.org/export/dump/FR.zip">FR GeoBames file</a>
 * and extract it at the root directory of Hibernate Search
 *
 * @author Nicolas Helleringer
 */
public final class BenchWithGeonames {

	private BenchWithGeonames() {
		//not allowed
	}

	private static final String hibernateConfigurationFile = "/org/hibernate/search/test/spatial/hibernate.cfg.xml";
	private static final String geonamesDataFile = "FR.txt";

	public static void main(String args[]) {
		LoadGeonames();
		Bench();
		FacetTest();
	}

	public static void LoadGeonames() {
		Session session = null;
		FullTextSession fullTextSession = null;
		BufferedReader buffRead = null;
		SessionFactory sessionFactory = null;
		try {
			sessionFactory = new Configuration().configure( hibernateConfigurationFile ).buildSessionFactory();

			session = sessionFactory.openSession();
			session.createNativeQuery( "delete from MemberLevelTestPoI" ).executeUpdate();
			session.beginTransaction();
			fullTextSession = Search.getFullTextSession( session );

			File geonamesFile = new File( geonamesDataFile );
			buffRead = new BufferedReader( new InputStreamReader( new FileInputStream( geonamesFile ), "UTF-8" ) );
			String line = null;

			int line_number = 0;
			while ( ( line = buffRead.readLine() ) != null ) {
				String[] data = line.split( "\t" );
				POI current = new POI(
						Integer.parseInt( data[0] ),
						data[1],
						Double.parseDouble( data[4] ),
						Double.parseDouble( data[5] ),
						data[7]
				);
				session.save( current );
				if ( ( line_number % 10000 ) == 0 ) {
					fullTextSession.flushToIndexes();
					session.getTransaction().commit();
					session.close();
					session = sessionFactory.openSession();
					fullTextSession = Search.getFullTextSession( session );
					session.beginTransaction();
					System.out.println( Integer.toString( line_number ) );
				}
				line_number++;
			}
			session.getTransaction().commit();
			fullTextSession.close();
			sessionFactory.close();
			buffRead.close();
		}
		catch (Exception e) {
			e.printStackTrace();
		}
		finally {
			if ( fullTextSession != null && fullTextSession.isOpen() ) {
				Transaction transaction = fullTextSession.getTransaction();
				if ( transaction != null && transaction.getStatus() == TransactionStatus.ACTIVE ) {
					transaction.rollback();
				}
				fullTextSession.close();
			}
			if ( sessionFactory != null && !sessionFactory.isClosed() ) {
				sessionFactory.close();
			}
			try {
				if ( buffRead != null ) {
					buffRead.close();
				}
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	public static void Bench() {
		Session session = null;
		FullTextSession fullTextSession = null;
		SessionFactory sessionFactory = null;
		try {

			sessionFactory = new Configuration().configure( hibernateConfigurationFile ).buildSessionFactory();

			session = sessionFactory.openSession();
			session.beginTransaction();
			fullTextSession = Search.getFullTextSession( session );

			long spatialHashTotalDuration = 0;
			long spatialTotalDuration = 0;
			long doubleRangeTotalDuration = 0;
			long distanceDoubleRangeTotalDuration = 0;

			long spatialHashDocsFetched = 0;
			long spatialDocsFetched = 0;
			long doubleRangeDocsFetched = 0;
			long distanceDoubleRangeDocsFetched = 0;

			org.apache.lucene.search.Query luceneQuery;
			long startTime, endTime, duration;
			FullTextQuery hibQuery;
			List spatialHashResults, rangeResults;
			final QueryBuilder queryBuilder = fullTextSession.getSearchFactory().buildQueryBuilder().forEntity( POI.class ).get();
			org.apache.lucene.search.Query query;
			final Integer iterations = 2000;
			final Integer warmUp = 50;
			Random random = new Random( 42 );

			for ( int i = 0; i < iterations; i++ ) {
				Point center = Point.fromDegrees( random.nextDouble() * 2 + 44 , random.nextDouble() * 2 + 3 );
				double radius = 25.0d;
				Rectangle boundingBox = Rectangle.fromBoundingCircle( center, radius );

				query = queryBuilder.bool()
						.must(
								queryBuilder.range()
										.onField( "latitude" )
										.from( boundingBox.getLowerLeft().getLatitude() )
										.to( boundingBox.getUpperRight().getLatitude() )
										.createQuery()
						)
						.must(
								queryBuilder.range()
										.onField( "longitude" )
										.from( boundingBox.getLowerLeft().getLongitude() )
										.to( boundingBox.getUpperRight().getLongitude() )
										.createQuery()
						)
						.createQuery();
				hibQuery = fullTextSession.createFullTextQuery( query, POI.class );
				hibQuery.setProjection( "id", "name" );
				startTime = System.nanoTime();
				try {
					doubleRangeDocsFetched += hibQuery.getResultSize();
				}
				finally {
					endTime = System.nanoTime();
				}
				duration = endTime - startTime;
				if ( i > warmUp ) {
					doubleRangeTotalDuration += duration;
				}
				session.clear();

				query = queryBuilder.bool()
						.must(
								queryBuilder.range()
										.onField( "latitude" )
										.from( boundingBox.getLowerLeft().getLatitude() )
										.to( boundingBox.getUpperRight().getLatitude() )
										.createQuery()
						)
						.must(
								queryBuilder.range()
										.onField( "longitude" )
										.from( boundingBox.getLowerLeft().getLongitude() )
										.to( boundingBox.getUpperRight().getLongitude() )
										.createQuery()
						)
						.createQuery();
				org.apache.lucene.search.Query filteredQuery = new ConstantScoreQuery(
						SpatialQueryBuilderFromCoordinates.buildDistanceQuery(
								query,
								center,
								radius,
								"location"
						)
				);
				hibQuery = fullTextSession.createFullTextQuery( filteredQuery, POI.class );
				hibQuery.setProjection( "id", "name" );
				startTime = System.nanoTime();
				try {
					distanceDoubleRangeDocsFetched += hibQuery.getResultSize();
				}
				finally {
					endTime = System.nanoTime();
				}
				duration = endTime - startTime;
				if ( i > warmUp ) {
					distanceDoubleRangeTotalDuration += duration;
				}
				rangeResults = hibQuery.list();
				session.clear();

				luceneQuery = SpatialQueryBuilderFromCoordinates.buildSpatialHashQuery( center, radius, "location" );
				hibQuery = fullTextSession.createFullTextQuery( luceneQuery, POI.class );
				hibQuery.setProjection( "id", "name" );
				startTime = System.nanoTime();

				try {
					spatialHashDocsFetched += hibQuery.getResultSize();
				}
				finally {
					endTime = System.nanoTime();
				}
				duration = endTime - startTime;
				if ( i > warmUp ) {
					spatialHashTotalDuration += duration;
				}
				session.clear();

				luceneQuery = SpatialQueryBuilderFromCoordinates.buildSpatialQueryByHash( center, radius, "location" );
				hibQuery = fullTextSession.createFullTextQuery( luceneQuery, POI.class );
				hibQuery.setProjection( "id", "name" );
				startTime = System.nanoTime();

				try {
					spatialDocsFetched += hibQuery.getResultSize();
				}
				finally {
					endTime = System.nanoTime();
				}
				duration = endTime - startTime;
				if ( i > warmUp ) {
					spatialTotalDuration += duration;
				}
				spatialHashResults = hibQuery.list();
				session.clear();

				if ( rangeResults.size() != spatialHashResults.size() ) {
					luceneQuery = SpatialQueryBuilderFromCoordinates.buildDistanceQuery( center, radius, "location" );
					hibQuery = fullTextSession.createFullTextQuery( luceneQuery, POI.class );
					hibQuery.setProjection( "id", "name" );

					System.out.println(
							">>>>> Different numbers of documents fetched for point (" +
									Double.toString( center.getLatitude() ) +
									"," +
									Double.toString( center.getLongitude() ) +
									") and radius " +
									Double.toString( radius )
					);
					System.out.println( "Range results : " + rangeResults );
					System.out.println( "Spatial Hash results : " + spatialHashResults );
					System.out.println( "Pure distance results : " + hibQuery.getResultSize() );

					List<Integer> rangeIds = new ArrayList<Integer>();
					for ( int index = 0; index < rangeResults.size(); index++ ) {
						rangeIds.add( (Integer) ( (Object[]) rangeResults.get( index ) )[0] );
					}
					List<Integer> spatialHashIds = new ArrayList<Integer>();
					for ( int index = 0; index < spatialHashResults.size(); index++ ) {
						spatialHashIds.add( (Integer) ( (Object[]) spatialHashResults.get( index ) )[0] );
					}

					rangeIds.removeAll( spatialHashIds );

					System.out.println( "Missing Ids : " + rangeIds );
				}

			}
			session.getTransaction().commit();
			session.close();
			sessionFactory.close();

			System.out
					.println(
							"Mean time with Spatial Hash : " + Double.toString(
									spatialHashTotalDuration * Math.pow( 10, -6 ) / ( iterations - warmUp )
							) + " ms. Average number of docs fetched : " + Double.toString( spatialHashDocsFetched / ((iterations - warmUp) * 1.0d) )
					);
			System.out
					.println(
							"Mean time with Spatial Hash + Distance filter : " + Double.toString(
									spatialTotalDuration * Math.pow( 10, -6 ) / ( iterations - warmUp )
							) + " ms. Average number of docs fetched : " + Double.toString( spatialDocsFetched / ((iterations - warmUp) * 1.0d) )
					);
			System.out
					.println(
							"Mean time with DoubleRange : " + Double.toString(
									doubleRangeTotalDuration * Math.pow( 10, -6 ) / (iterations - warmUp)
							) + " ms. Average number of docs fetched : " + Double.toString( doubleRangeDocsFetched / ((iterations - warmUp) * 1.0d) )
					);
			System.out
					.println(
							"Mean time with DoubleRange + Distance filter : " + Double.toString(
									distanceDoubleRangeTotalDuration * Math.pow( 10, -6 ) / ( iterations - warmUp )
							) + " ms. Average number of docs fetched : " + Double.toString( distanceDoubleRangeDocsFetched / ((iterations - warmUp) * 1.0d) )
					);

		}
		catch (Exception e) {
			e.printStackTrace();
		}
		finally {
			if ( fullTextSession != null && fullTextSession.isOpen() ) {
				Transaction transaction = fullTextSession.getTransaction();
				if ( transaction != null && transaction.getStatus() == TransactionStatus.ACTIVE ) {
					transaction.rollback();
				}
				fullTextSession.close();
			}
			if ( sessionFactory != null && !sessionFactory.isClosed() ) {
				sessionFactory.close();
			}
		}
	}

	public static void FacetTest() {
		Session session = null;
		FullTextSession fullTextSession = null;

		SessionFactory sessionFactory = null;

		try {
			sessionFactory = new Configuration().configure( hibernateConfigurationFile ).buildSessionFactory();

			session = sessionFactory.openSession();
			session.beginTransaction();
			fullTextSession = Search.getFullTextSession( session );

			org.apache.lucene.search.Query luceneQuery;

			FullTextQuery hibQuery;

			Point center = Point.fromDegrees( 46, 4 );
			double radius = 50.0d;

			luceneQuery = SpatialQueryBuilderFromCoordinates.buildSpatialQueryByHash( center, radius, "location" );
			hibQuery = fullTextSession.createFullTextQuery( luceneQuery, POI.class );
			hibQuery.setProjection( "id", "name", "type" );

			FacetManager facetManager = hibQuery.getFacetManager();

			QueryBuilder queryBuilder = fullTextSession.getSearchFactory().buildQueryBuilder().forEntity( POI.class ).get();

			FacetingRequest facetingRequest = queryBuilder.facet()
					.name( "typeFacet" )
					.onField( "type" )
					.discrete()
					.orderedBy( FacetSortOrder.COUNT_DESC )
					.includeZeroCounts( false )
					.createFacetingRequest();

			facetManager.enableFaceting( facetingRequest );

			Integer size = hibQuery.getResultSize();

			hibQuery.list();

			List<Facet> facets = facetManager.getFacets( "typeFacet" );

			System.out.println( facets );

			session.getTransaction().commit();
			session.close();
			sessionFactory.close();
		}
		catch (Exception e) {
			e.printStackTrace();
		}
		finally {
			if ( fullTextSession != null && fullTextSession.isOpen() ) {
				Transaction transaction = fullTextSession.getTransaction();
				if ( transaction != null && transaction.getStatus() == TransactionStatus.ACTIVE ) {
					transaction.rollback();
				}
				fullTextSession.close();
			}
			if ( sessionFactory != null && !sessionFactory.isClosed() ) {
				sessionFactory.close();
			}
		}
	}
}
