/*
 * 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.query.facet;

import java.util.List;
import java.util.Map;

import org.apache.lucene.search.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
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.facet.Facet;
import org.hibernate.search.query.facet.FacetingRequest;
import org.hibernate.search.query.facet.RangeFacet;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;
import org.junit.Test;

import static org.hibernate.search.util.impl.CollectionHelper.newArrayList;
import static org.hibernate.search.util.impl.CollectionHelper.newHashMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

/**
 * Simulate a web-shop with basic search which can be refined by facet requests.
 *
 * @author Hardy Ferentschik
 */
public class WebShopTest extends AbstractFacetTest {

	private static final Log log = LoggerFactory.make( Log.class );

	@Test
	public void testSimulateClient() {
		// get hold of the search service
		SearchService searchService = new SearchService( getSessionFactory() );

		// execute the search and display main query results
		List<Car> cars = searchService.searchCar( "BMW" );
		assertEquals( "We should have matching cars", 12, cars.size() );

		// get the menu items for faceting
		Map<String, List<FacetMenuItem>> facetMenuItems = searchService.getMenuItems();

		List<FacetMenuItem> colorMenuItems = facetMenuItems.get( SearchService.colorFacetName );
		assertEquals( "Wrong number of menu entries", 4, colorMenuItems.size() );
		for ( FacetMenuItem item : colorMenuItems ) {
			assertFalse( item.isSelected() );
		}

		List<FacetMenuItem> ccsMenuItems = facetMenuItems.get( SearchService.cubicCapacityFacetName );
		assertEquals( "Wrong number of menu entries", 3, ccsMenuItems.size() );
		for ( FacetMenuItem item : ccsMenuItems ) {
			assertFalse( item.isSelected() );
		}

		// let the user select a facet menu
		FacetMenuItem selectedItem = facetMenuItems.get( SearchService.colorFacetName ).get( 0 );
		assertEquals( "Wrong facet count", 3, selectedItem.getCount() );

		cars = searchService.selectMenuItem( selectedItem );
		assertEquals( "We should have matching cars", 3, cars.size() );

		// get the new menu items
		facetMenuItems = searchService.getMenuItems();

		colorMenuItems = facetMenuItems.get( SearchService.colorFacetName );
		assertEquals( "Wrong number of menu entries", 1, colorMenuItems.size() );
		FacetMenuItem menuItem = colorMenuItems.get( 0 );
		assertEquals( "Wrong facet count", 3, menuItem.getCount() );
		assertTrue( menuItem.isSelected() );

		ccsMenuItems = facetMenuItems.get( SearchService.cubicCapacityFacetName );
		assertEquals( "Wrong number of menu entries", 3, ccsMenuItems.size() );
		for ( FacetMenuItem item : ccsMenuItems ) {
			assertFalse( item.isSelected() );
		}

		// deselect the menuitem again
		cars = searchService.deSelectMenuItem( menuItem );
		assertEquals( "We should have matching cars", 12, cars.size() );
	}


	@Override
	public void loadTestData(Session session) {
		Transaction tx = session.beginTransaction();
		List<Car> allCars = newArrayList();
		for ( String make : makes ) {
			for ( String color : colors ) {
				for ( int cc : ccs ) {
					Car car = new Car( make, color, cc );
					session.save( car );
					allCars.add( car );
				}
			}
		}
		log.infof( "Indexed cars: %s", allCars );
		tx.commit();
		session.clear();
	}

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

	public static class SearchService {
		public static final String colorFacetName = "color";
		public static final String cubicCapacityFacetName = "cubicCapacity_Numeric";
		private final SessionFactory factory;
		private FullTextQuery currentFullTextQuery;
		private Map<String, List<FacetMenuItem>> menuItems;
		private String queryString;
		private final List<Facet> selectedFacets = newArrayList();

		public SearchService(SessionFactory factory) {
			this.factory = factory;
		}

		/**
		 * Service method for a potential car search website
		 *
		 * @param queryString the query string specified by the user
		 *
		 * @return a list of cars matching the query
		 */
		private List<Car> searchCar(String queryString) {
			this.queryString = queryString;
			FullTextSession fullTextSession = Search.getFullTextSession( factory.openSession() );
			buildFullTextQuery( queryString, fullTextSession );

			Transaction tx = fullTextSession.beginTransaction();
			List<Car> cars = currentFullTextQuery.list();
			tx.commit();

			fullTextSession.close();

			return cars;
		}

		private void buildFullTextQuery(String queryString, FullTextSession fullTextSession) {
			// get a query builder
			final QueryBuilder builder = fullTextSession.getSearchFactory()
					.buildQueryBuilder()
					.forEntity( Car.class )
					.get();

			// build a Lucene query
			final Query query = builder.keyword().onField( "make" ).matching( queryString ).createQuery();

			// create facets for navigation
			// discrete faceting
			final FacetingRequest colorFacet = builder.facet()
					.name( colorFacetName )
					.onField( "color" )
					.discrete()
					.createFacetingRequest();
			// range faceting
			final FacetingRequest priceFacet = builder.facet()
					.name( cubicCapacityFacetName )
					.onField( Car.CUBIC_CAPACITY_STRING_FACET_NUMERIC_ENCODING )
					.range()
					.below( 2500 ).excludeLimit()
					.from( 2500 ).to( 3000 )
					.above( 3000 ).excludeLimit()
					.createFacetingRequest();


			// create the fulltext query, enable the facets via the facet manager and return the results
			currentFullTextQuery = fullTextSession.createFullTextQuery( query, Car.class );
			currentFullTextQuery.setFirstResult( 0 ).setMaxResults( 20 );
			currentFullTextQuery.getFacetManager().enableFaceting( colorFacet ).enableFaceting( priceFacet );
		}

		public Map<String, List<FacetMenuItem>> getMenuItems() {
			menuItems = newHashMap();
			List<FacetMenuItem> items = newArrayList();

			int i = 0;
			for ( Facet facet : currentFullTextQuery.getFacetManager().getFacets( colorFacetName ) ) {
				items.add( new FacetMenuItem( facet, selectedFacets.contains( facet ), i ) );
				i++;
			}
			menuItems.put( colorFacetName, items );

			items = newArrayList();
			i = 0;
			for ( Facet facet : currentFullTextQuery.getFacetManager().getFacets( cubicCapacityFacetName ) ) {
				items.add( new FacetMenuItem( facet, selectedFacets.contains( facet ), i++ ) );
			}
			menuItems.put( cubicCapacityFacetName, items );
			return menuItems;
		}

		public List<Car> selectMenuItem(FacetMenuItem item) {
			// find the right facet using the facet manager
			List<Facet> facets = currentFullTextQuery.getFacetManager().getFacets( item.getFacetingName() );
			Facet facet = facets.get( item.getIndex() );
			selectedFacets.add( facet );

			// rer-run the query
			FullTextSession fullTextSession = Search.getFullTextSession( factory.openSession() );
			buildFullTextQuery( queryString, fullTextSession );

			Transaction tx = fullTextSession.beginTransaction();
			// use the facet to narrow down the query
			currentFullTextQuery.getFacetManager()
					.getFacetGroup( item.getFacetingName() )
					.selectFacets( selectedFacets.toArray( new Facet[selectedFacets.size()] ) );
			List<Car> cars = currentFullTextQuery.list();
			tx.commit();
			fullTextSession.close();
			return cars;
		}

		public List<Car> deSelectMenuItem(FacetMenuItem item) {
			// find the right facet using the facet manager
			List<Facet> facets = currentFullTextQuery.getFacetManager().getFacets( item.getFacetingName() );
			Facet facet = facets.get( item.getIndex() );
			selectedFacets.remove( facet );

			// rer-run the query
			FullTextSession fullTextSession = Search.getFullTextSession( factory.openSession() );
			buildFullTextQuery( queryString, fullTextSession );

			Transaction tx = fullTextSession.beginTransaction();
			// use the facet to narrow down the query
			currentFullTextQuery.getFacetManager()
					.getFacetGroup( item.getFacetingName() )
					.selectFacets( selectedFacets.toArray( new Facet[] { } ) );
			List<Car> cars = currentFullTextQuery.list();
			tx.commit();
			fullTextSession.close();
			return cars;
		}

	}

	// simulate some sort of frontend data structure for displaying the facet menu
	static class FacetMenuItem {
		private final boolean isSelected;
		private final int index;
		private final String facetingName;
		private final Facet facet;

		public FacetMenuItem(Facet facet, boolean selected, int index) {
			this.isSelected = selected;
			this.facet = facet;
			this.facetingName = facet.getFacetingName();
			this.index = index;
		}

		public boolean isSelected() {
			return isSelected;
		}

		public int getCount() {
			return facet.getCount();
		}

		public String getValue() {
			String value;
			if ( facet instanceof RangeFacet ) {
				// this could get more involved
				RangeFacet rangeFacet = (RangeFacet) facet;
				value = rangeFacet.getMin().toString() + " - " + rangeFacet.getMax().toString();
			}
			else {
				value = facet.getValue();
			}
			return value;
		}

		public int getIndex() {
			return index;
		}

		public String getFacetingName() {
			return facetingName;
		}

		@Override
		public String toString() {
			final StringBuilder sb = new StringBuilder();
			sb.append( "FacetMenuItem" );
			sb.append( "{isSelected=" ).append( isSelected );
			sb.append( ", facet=" ).append( facet );
			sb.append( '}' );
			return sb.toString();
		}
	}
}
