/*
 * 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.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Transient;

import org.apache.lucene.search.Sort;
import org.hibernate.Session;
import org.hibernate.search.FullTextQuery;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Latitude;
import org.hibernate.search.annotations.Longitude;
import org.hibernate.search.annotations.Spatial;
import org.hibernate.search.annotations.SpatialMode;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.hibernate.search.query.dsl.Unit;
import org.hibernate.search.spatial.DistanceSortField;
import org.hibernate.search.test.SearchTestBase;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;
import org.hibernate.testing.TestForIssue;
import org.junit.Assert;
import org.junit.Test;

/**
 * Spatial search with sort by distance and paging orders entities
 *
 * @author PB
 */
@TestForIssue(jiraKey = "HSEARCH-1267")
public class SpatialSearchSortByDistanceAndPagingTest extends SearchTestBase {
	private static final Log log = LoggerFactory.make();

	private static final int EXPECTED_RESULTS_COUNT = 37;
	private static final double CENTER_LAT = 54.0;
	private static final double CENTER_LON = 18.0;
	private static final double SEARCH_DISTANCE = 20.0;
	private Map<Long, Integer> entitiesIdsSet;
	private int idx;

	@Test
	public void testSortWithoutPaging_isOk() {
		Assert.assertEquals( EXPECTED_RESULTS_COUNT, doSearch( SEARCH_DISTANCE, 50, true ) );
	}

	@Test
	public void testSortWithPageSize5_notOk() {
		Assert.assertEquals(
				"sorting by distance and paging error",
				EXPECTED_RESULTS_COUNT, doSearch( SEARCH_DISTANCE, 5, true )
		);
	}

	@Test
	public void testSortWithPageSize10_notOk() {
		Assert.assertEquals(
				"sorting by distance and paging error",
				EXPECTED_RESULTS_COUNT, doSearch( SEARCH_DISTANCE, 10, true )
		);
	}

	@Test
	public void testNoSortWithPageSize5_isOk() {
		Assert.assertEquals( EXPECTED_RESULTS_COUNT, doSearch( SEARCH_DISTANCE, 5, false ) );
	}

	@Test
	public void testNoSortWithPageSize10_isOk() {
		Assert.assertEquals( EXPECTED_RESULTS_COUNT, doSearch( SEARCH_DISTANCE, 10, false ) );
	}

	@Override
	public void setUp() throws Exception {
		super.setUp();
		prepareTestData();
	}

	/**
	 * Search and iterate result pages.
	 *
	 * @return unique result count
	 */
	public int doSearch(double distance, int pageSize, boolean sortByDistance) {
		log.debug(
				String.format( Locale.ROOT,
						"distance %.2f pageSize %d, sortByDistance %s",
						distance, pageSize, sortByDistance
				)
		);

		entitiesIdsSet = new HashMap<>();
		idx = 0;
		int firstResult = 0;
		List result;

		while ( (result = distanceSearch( CENTER_LAT, CENTER_LON, distance, firstResult, pageSize, sortByDistance ))
				!= null && !result.isEmpty() ) {

			printResults( result );

			firstResult += pageSize;
		}
		return entitiesIdsSet.size();
	}

	/**
	 * Show result.
	 */
	private void printResults(List<GeoEntity> list) {
		for ( GeoEntity entity : list ) {
			log.debug(
					String.format( Locale.ROOT,
							"%d %f %d%s",
							idx, entity.getDistance(), entity.getId(),
							entitiesIdsSet.containsKey( entity.getId() )
									? " was at index " + entitiesIdsSet.get( entity.getId() ) : ""
					)
			);
			if ( !entitiesIdsSet.containsKey( entity.getId() ) ) {
				entitiesIdsSet.put( entity.getId(), idx );
			}
			idx++;
		}
	}

	/**
	 * Search GeoEntities starting from startLat and startLon within distance
	 */
	private List distanceSearch(
			double startLat,
			double startLon,
			double distance,
			int firstResult,
			int maxResult,
			boolean sortByDistance) {

		List resultList = new ArrayList();

		Session sessionHbn = openSession();
		sessionHbn.beginTransaction();

		FullTextSession fTxtSess = Search.getFullTextSession( sessionHbn );

		QueryBuilder builder = fTxtSess.getSearchFactory().buildQueryBuilder().forEntity( GeoEntity.class ).get();

		org.apache.lucene.search.Query luceneQuery = builder.spatial()
				.within( distance, Unit.KM )
				.ofLatitude( startLat )
				.andLongitude( startLon )
				.createQuery();

		FullTextQuery hibQuery = fTxtSess.createFullTextQuery( luceneQuery, GeoEntity.class );
		hibQuery.setProjection( FullTextQuery.THIS, FullTextQuery.SPATIAL_DISTANCE );
		hibQuery.setSpatialParameters( startLat, startLon, Spatial.COORDINATES_DEFAULT_FIELD );

		if ( sortByDistance ) {
			Sort distanceSort = new Sort(
					new DistanceSortField(
							startLat,
							startLon,
							Spatial.COORDINATES_DEFAULT_FIELD
					)
			);
			hibQuery.setSort( distanceSort );
		}

		hibQuery.setFirstResult( firstResult ).setMaxResults( maxResult );

		List tmpList = hibQuery.list();

		// copy distance from projection to entities
		for ( Object obj[] : (List<Object[]>) tmpList ) {
			GeoEntity entity = (GeoEntity) obj[0];
			entity.setDistance( (Double) obj[1] );
			resultList.add( entity );
		}

		sessionHbn.getTransaction().commit();
		sessionHbn.close();
		return resultList;
	}

	@Override
	public Class<?>[] getAnnotatedClasses() {
		return new Class<?>[] {
				GeoEntity.class
		};
	}

	@Entity(name = "GeoEntity")
	@Indexed
	@Spatial(spatialMode = SpatialMode.RANGE)
	public static class GeoEntity {

		@Id
		@GeneratedValue(strategy = GenerationType.AUTO)
		@DocumentId
		private Long id;
		@Latitude
		private Double lat;
		@Longitude
		private Double lon;
		@Transient
		private Double distance;
		private String value;

		public GeoEntity() {
		}

		public GeoEntity(Double lat, Double lon, String value) {
			super();
			this.value = value;
			this.lat = lat;
			this.lon = lon;
		}

		public Long getId() {
			return id;
		}

		public void setId(Long id) {
			this.id = id;
		}

		public Double getLat() {
			return lat;
		}

		public void setLat(Double lat) {
			this.lat = lat;
		}

		public Double getLon() {
			return lon;
		}

		public void setLon(Double lon) {
			this.lon = lon;
		}

		public Double getDistance() {
			return distance;
		}

		public void setDistance(Double distance) {
			this.distance = distance;
		}

		public String getValue() {
			return value;
		}

		public void setValue(String value) {
			this.value = value;
		}
	}

	private void prepareTestData() {
		Session sessionHbn = openSession();
		sessionHbn.beginTransaction();

		sessionHbn.saveOrUpdate( new GeoEntity( 54.021861, 18.048349, "v00" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.079361, 18.185003, "v01" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.197314, 18.158194, "v02" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.177621, 18.150250, "v03" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.125537, 18.075425, "v04" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.070479, 18.064328, "v05" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.169191, 18.025334, "v06" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.031556, 18.059142, "v07" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.068104, 18.064764, "v08" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.103417, 18.182243, "v09" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.002537, 18.037648, "v10" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.223375, 18.003011, "v11" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.061524, 18.071648, "v12" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.001563, 18.055457, "v13" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.032163, 18.138202, "v14" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.229162, 18.109120, "v15" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.089290, 18.146594, "v16" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.066932, 18.227589, "v17" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.231832, 18.186478, "v18" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.088117, 18.206227, "v19" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.055539, 18.216111, "v20" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.175193, 18.026838, "v21" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.171762, 18.215527, "v22" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.196967, 18.177212, "v23" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.028481, 18.084819, "v24" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.171071, 18.057752, "v25" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.062881, 18.117777, "v26" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.247034, 18.235728, "v27" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.014431, 18.220235, "v28" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.076813, 18.010077, "v29" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.103647, 18.170640, "v30" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.222894, 18.116137, "v31" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.134499, 18.137614, "v32" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.188910, 18.180216, "v33" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.157758, 18.125557, "v34" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.194089, 18.228482, "v35" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.138517, 18.014723, "v36" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.200781, 18.163288, "v37" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.103558, 18.146598, "v38" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.193829, 18.142770, "v39" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.003273, 18.228929, "v40" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.051599, 18.236313, "v41" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.008487, 18.197262, "v42" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.041120, 18.079304, "v43" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.174614, 18.071497, "v44" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.013968, 18.015511, "v45" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.094368, 18.036610, "v46" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.025288, 18.144333, "v47" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.075499, 18.206458, "v48" ) );
		sessionHbn.saveOrUpdate( new GeoEntity( 54.095646, 18.033243, "v49" ) );

		sessionHbn.getTransaction().commit();
		sessionHbn.close();
		log.debug( "test data saved" );
	}
}
