package info.bukova.isspst.services.fulltext;

import info.bukova.isspst.ModuleUtils;
import info.bukova.isspst.dao.QueryDao;
import info.bukova.isspst.data.BaseData;
import info.bukova.isspst.data.User;
import info.bukova.isspst.services.ModuleNotActiveException;
import info.bukova.isspst.sort.ReflectionTools;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.hibernate.Hibernate;
import org.hibernate.search.FullTextQuery;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.hibernate.search.annotations.IndexedEmbedded;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FullTextServiceImpl implements FullTextService {
	
	@Autowired
	private QueryDao queryDao;
	private List<Class<?>> classesForSearch;
	private Map<Class<?>, String[]> fields;
	private List<Class<?>> nestedClasses;
	
	public FullTextServiceImpl() {
		nestedClasses = new ArrayList<Class<?>>();
		nestedClasses.add(User.class);
		nestedClasses.add(BaseData.class);
	}

	@Override
	@Transactional
	public void reindex() {
		Logger logger = LoggerFactory.getLogger(FullTextServiceImpl.class);
		logger.info("Indexing database for fulltext search");
		FullTextSession ftSession = Search.getFullTextSession(queryDao.getSession());
		ftSession.createIndexer().start();
	}

	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_SEARCH')")
	public List<?> search(Class<?> entityClass, String[] fields, String word) {
		checkActivity();
		FullTextSession session = Search.getFullTextSession(queryDao.getSession());
		QueryBuilder qb = session.getSearchFactory().buildQueryBuilder().forEntity(entityClass).get();
		
		Query luceneQuery = qb.keyword().onFields(fields).matching(word).createQuery();
		FullTextQuery hiberQuery = session.createFullTextQuery(luceneQuery, entityClass);
		hiberQuery.setSort(Sort.RELEVANCE);
		
		return hiberQuery.list();
	}

	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_SEARCH')")
	public List<?> globalSearch(String word) {
		checkActivity();
		List<Object> result = new ArrayList<Object>();
		
		for (Class<?> clazz : classesForSearch()) {
			result.addAll(search(clazz, fields.get(clazz), word));
		}
		
		for (Object o : result) {
			BaseData data = (BaseData) o;
			Hibernate.initialize(data.getModifiedBy());
			Hibernate.initialize(data.getOwnedBy());
		}
		
		return result;
	}
	
	private void checkActivity() {
		if (!ModuleUtils.getModule(this.getClass()).isActive()) {
			throw new ModuleNotActiveException();
		}
	}
	
	private List<Class<?>> classesForSearch() {
		if (classesForSearch != null) {
			return classesForSearch;
		}
		
		classesForSearch = new ArrayList<Class<?>>();
		fields = new HashMap<Class<?>, String[]>();
		FullTextSession session = Search.getFullTextSession(queryDao.getSession());
		
		for (Class<?> clazz : session.getSearchFactory().getIndexedTypes()) {
			if (nestedClasses.contains(clazz)) {
				continue;
			}
			
			classesForSearch.add(clazz);
			fields.put(clazz, fields(clazz, ""));
		}
		
		return classesForSearch;
	}
	
	private String[] fields(Class<?> clazz, String prefix) {
		List<String> res = new ArrayList<String>();
		
		for (Field field : ReflectionTools.getFields(clazz)) {
			for (Annotation a : field.getDeclaredAnnotations()) {
				if (a instanceof org.hibernate.search.annotations.Field) {
					res.add(prefix + field.getName());
				}
				
				if (a instanceof IndexedEmbedded) {
					Class<?> fieldClass;
					
					fieldClass = field.getType();
					if (fieldClass.isAssignableFrom(List.class)) {
						ParameterizedType type = (ParameterizedType) field.getGenericType();
						Type[] types = type.getActualTypeArguments();
						if (types.length == 1) {
							fieldClass = (Class<?>)types[0];
						}
					}
										
					classesForSearch.remove(fieldClass);
					fields.remove(fieldClass);
					nestedClasses.add(fieldClass);
					
					String nestedPrefix = prefix + field.getName() + ".";
					res.addAll(Arrays.asList(fields(fieldClass, nestedPrefix)));
				}
			}
		}
		
		String resArray[] = new String[res.size()];
		resArray = res.toArray(resArray);
		return resArray;
	}

}
