/**
 * Copyright (c) 2000-2017 Liferay, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of the Liferay Enterprise
 * pSubscription License ("License"). You may not use this file except in
 * compliance with the License. You can obtain a copy of the License by
 * contacting Liferay, Inc. See the License for the specific language governing
 * permissions and limitations under the License, including but not limited to
 * distribution rights of the Software.
 *
 *
 *
 */

package com.liferay.portal.kernel.search;

import com.liferay.portal.kernel.dao.orm.QueryUtil;
import com.liferay.portal.kernel.dao.search.SearchPaginationUtil;
import com.liferay.portal.kernel.util.ArrayUtil;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.PropsKeys;
import com.liferay.portal.kernel.util.PropsUtil;
import com.liferay.portal.kernel.util.Time;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Tina Tian
 */
public abstract class BaseSearchResultPermissionFilter
	implements SearchResultPermissionFilter {

	@Override
	public Hits search(SearchContext searchContext) throws SearchException {
		int end = searchContext.getEnd();
		int start = searchContext.getStart();

		if ((end == QueryUtil.ALL_POS) && (start == QueryUtil.ALL_POS)) {
			Hits hits = getHits(searchContext);

			if (!isGroupAdmin(searchContext)) {
				filterHits(hits, searchContext);
			}

			return hits;
		}

		if ((start < 0) || (start > end)) {
			return new HitsImpl();
		}

		if (isGroupAdmin(searchContext)) {
			return getHits(searchContext);
		}

		double amplificationFactor = 1.0;
		int excludedDocsSize = 0;
		int hitsSize = 0;
		int offset = 0;
		long startTime = 0;

		List<Document> documents = new ArrayList<Document>();
		List<Float> scores = new ArrayList<Float>();

		while (true) {
			int count = end - documents.size();

			int amplifiedCount = (int)Math.ceil(count * amplificationFactor);

			int amplifiedEnd = offset + amplifiedCount;

			searchContext.setEnd(amplifiedEnd);
			searchContext.setStart(offset);

			Hits hits = getHits(searchContext);

			if (startTime == 0) {
				hitsSize = hits.getLength();
				startTime = hits.getStart();
			}

			Document[] oldDocs = hits.getDocs();

			filterHits(hits, searchContext);

			Document[] newDocs = hits.getDocs();

			excludedDocsSize += oldDocs.length - newDocs.length;

			collectHits(hits, documents, scores, count);

			if ((newDocs.length >= count) ||
				(oldDocs.length < amplifiedCount) ||
				(amplifiedEnd >= hitsSize)) {

				updateHits(
					hits, documents, scores, start, end,
					hitsSize - excludedDocsSize, startTime);

				return hits;
			}

			offset = amplifiedEnd;

			amplificationFactor = _getAmplificationFactor(
				documents.size(), offset);
		}
	}

	protected void collectHits(
		Hits hits, List<Document> documents, List<Float> scores, int count) {

		Document[] docs = hits.getDocs();

		if (docs.length < count) {
			count = docs.length;
		}

		for (int i = 0; i < count; i++) {
			documents.add(docs[i]);

			scores.add(hits.score(i));
		}
	}

	protected abstract void filterHits(Hits hits, SearchContext searchContext);

	protected abstract Hits getHits(SearchContext searchContext)
		throws SearchException;

	protected abstract boolean isGroupAdmin(SearchContext searchContext);

	protected void updateHits(
		Hits hits, List<Document> documents, List<Float> scores, int start,
		int end, int size, long startTime) {

		int[] startAndEnd = SearchPaginationUtil.calculateStartAndEnd(
			start, end, documents.size());

		start = startAndEnd[0];
		end = startAndEnd[1];

		documents = documents.subList(start, end);
		scores = scores.subList(start, end);

		hits.setDocs(documents.toArray(new Document[documents.size()]));
		hits.setScores(ArrayUtil.toFloatArray(scores));
		hits.setLength(size);
		hits.setSearchTime(
			(float)(System.currentTimeMillis() - startTime) / Time.SECOND);
	}

	private double _getAmplificationFactor(double totalViewable, double total) {
		if (totalViewable == 0) {
			return _INDEX_PERMISSION_FILTER_SEARCH_AMPLIFICATION_FACTOR;
		}

		return Math.min(
			1.0 / (totalViewable / total),
			_INDEX_PERMISSION_FILTER_SEARCH_AMPLIFICATION_FACTOR);
	}

	private static final double
		_INDEX_PERMISSION_FILTER_SEARCH_AMPLIFICATION_FACTOR =
			GetterUtil.getDouble(
				PropsUtil.get(
					PropsKeys.
						INDEX_PERMISSION_FILTER_SEARCH_AMPLIFICATION_FACTOR));

}