/**
 * 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.search.buffer;

import com.liferay.portal.NoSuchModelException;
import com.liferay.portal.kernel.lar.ExportImportThreadLocal;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.search.Indexer;
import com.liferay.portal.kernel.search.SearchEngineUtil;
import com.liferay.portal.kernel.util.MethodKey;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.model.BaseModel;
import com.liferay.portal.model.ClassedModel;
import com.liferay.portal.model.ResourcedModel;
import com.liferay.portal.security.auth.CompanyThreadLocal;
import com.liferay.portal.service.PersistedModelLocalService;
import com.liferay.portal.service.PersistedModelLocalServiceRegistry;
import com.liferay.portal.util.PropsValues;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

import java.util.Arrays;
import java.util.Collection;

/**
 * @author Michael C. Han
 */
public class BufferedIndexerInvocationHandler implements InvocationHandler {

	public BufferedIndexerInvocationHandler(
		Indexer indexer,
		PersistedModelLocalServiceRegistry persistedModelLocalServiceRegistry) {

		_indexer = indexer;
		_persistedModelLocalServiceRegistry =
			persistedModelLocalServiceRegistry;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
		throws Throwable {

		IndexerRequestBuffer indexerRequestBuffer = IndexerRequestBuffer.get();

		if ((!Validator.equals(method.getName(), "delete") &&
			 !Validator.equals(method.getName(), "reindex")) ||
			(args.length == 0) || (args.length > 2) ||
			(indexerRequestBuffer == null)) {

			return method.invoke(_indexer, args);
		}

		if (SearchEngineUtil.isIndexReadOnly()) {
			if (_log.isDebugEnabled()) {
				_log.debug(
					"Skipping indexer request buffer because index is read " +
						"only");
			}

			return null;
		}

		if (ExportImportThreadLocal.isImportInProcess() &&
			Validator.equals(method.getName(), "reindex")) {

			if (_log.isDebugEnabled()) {
				_log.debug(
					"Skipping indexer request buffer because import is in " +
						"process");
			}

			return null;
		}

		if (CompanyThreadLocal.isDeleteInProcess()) {
			if (_log.isDebugEnabled()) {
				_log.debug(
					"Skipping indexer request buffer because a company " +
						"delete is in process");
			}

			return null;
		}

		Class<?> args0Class = args[0].getClass();

		if (!(args[0] instanceof BaseModel) &&
			!(args[0] instanceof ClassedModel) &&
			!((args[0] instanceof Long) && (args.length == 1)) &&
			!(args[0] instanceof long[]) &&
			!(args0Class.isArray() ||
			  Collection.class.isAssignableFrom(args0Class)) &&
			!((args.length == 2) && (args[0] instanceof String) &&
			  Validator.equals(args[1].getClass(), Long.class))) {

			return method.invoke(_indexer, args);
		}

		if (args[0] instanceof ResourcedModel &&
			args[0] instanceof ClassedModel &&
			Validator.equals(method.getName(), "reindex")) {

			MethodKey methodKey = new MethodKey(
				Indexer.class, method.getName(), String.class, Long.TYPE);

			ClassedModel classedModel = (ClassedModel)args[0];
			ResourcedModel resourcedModel = (ResourcedModel)args[0];

			bufferRequest(
				methodKey, classedModel.getModelClassName(),
				resourcedModel.getResourcePrimKey(), indexerRequestBuffer);
		}
		else if (args[0] instanceof ClassedModel) {
			MethodKey methodKey = new MethodKey(
				Indexer.class, method.getName(), Object.class);

			bufferRequest(methodKey, args[0], indexerRequestBuffer);
		}
		else if (args.length == 2) {
			MethodKey methodKey = new MethodKey(
				Indexer.class, method.getName(), String.class, Long.TYPE);

			String className = (String)args[0];
			Long classPK = (Long)args[1];

			PersistedModelLocalService persistedModelLocalService =
				_persistedModelLocalServiceRegistry.
					getPersistedModelLocalService(className);

			try {
				Object obj = persistedModelLocalService.getPersistedModel(
					classPK);

				if (obj instanceof ResourcedModel) {
					ResourcedModel resourcedModel = (ResourcedModel)obj;

					classPK = resourcedModel.getResourcePrimKey();
				}
			}
			catch (Exception e) {
				if (_log.isDebugEnabled()) {
					_log.debug(
						"Unable to get resource primary key for class " +
							className + " with primary key " + classPK);

					if (!(e instanceof NoSuchModelException)) {
						_log.debug(e, e);
					}
				}
			}

			bufferRequest(methodKey, className, classPK, indexerRequestBuffer);
		}
		else if (args[0] instanceof Long) {
			MethodKey methodKey = new MethodKey(
				Indexer.class, method.getName(), String.class, Long.TYPE);

			String className = _indexer.getClassNames()[0];
			Long classPK = (Long)args[0];

			bufferRequest(methodKey, className, classPK, indexerRequestBuffer);
		}
		else if (args[0] instanceof long[]) {
			MethodKey methodKey = new MethodKey(
				Indexer.class, method.getName(), String.class, Long.TYPE);

			long[] primaryKeyArray = (long[])args[0];

			for (int i = 0; i<primaryKeyArray.length; i++) {
				String className = _indexer.getClassNames()[0];
				Long classPK = (Long)primaryKeyArray[i];

				bufferRequest(
					methodKey, className, classPK, indexerRequestBuffer);
			}
		}
		else {
			MethodKey methodKey = new MethodKey(
				Indexer.class, method.getName(), Object.class);

			Collection<Object> objects;

			try {
				if (args0Class.isArray()) {
					objects = Arrays.asList((Object[])args[0]);
				}
				else {
					objects = (Collection<Object>)args[0];
				}

				for (Object object : objects) {
					if (!(object instanceof ClassedModel)) {
						objects = null;

						break;
					}
				}
			}
			catch (Exception e) {
				if (_log.isDebugEnabled()) {
					if (_log.isDebugEnabled()) {
						_log.debug(
							"Unexpected error buffering indexing request", e);
					}
				}

				objects = null;
			}

			if (objects == null) {
				return method.invoke(_indexer, args);
			}

			for (Object object : objects) {
				bufferRequest(methodKey, object, indexerRequestBuffer);
			}
		}

		return null;
	}

	protected void bufferRequest(
			MethodKey methodKey, Object object,
			IndexerRequestBuffer indexerRequestBuffer)
		throws Exception {

		BaseModel<?> baseModel = (BaseModel<?>)object;

		ClassedModel classedModel = (ClassedModel)baseModel.clone();

		IndexerRequest indexerRequest = new IndexerRequest(
			methodKey.getMethod(), classedModel, _indexer);

		doBufferRequest(indexerRequest, indexerRequestBuffer);
	}

	protected void bufferRequest(
			MethodKey methodKey, String className, Long classPK,
			IndexerRequestBuffer indexerRequestBuffer)
		throws Exception {

		IndexerRequest indexerRequest = new IndexerRequest(
			methodKey.getMethod(), _indexer, className, classPK);

		doBufferRequest(indexerRequest, indexerRequestBuffer);
	}

	protected void doBufferRequest(
			IndexerRequest indexerRequest,
			IndexerRequestBuffer indexerRequestBuffer)
		throws Exception {

		if (BufferOverflowThreadLocal.isOverflowMode()) {
			indexerRequest.execute();

			return;
		}

		indexerRequestBuffer.add(indexerRequest);

		if (indexerRequestBuffer.size() >
				PropsValues.INDEX_REQUEST_BUFFER_MAX_SIZE) {

			IndexerRequestBufferUtil.bufferOverflowed(
				indexerRequestBuffer,
				PropsValues.INDEX_REQUEST_BUFFER_MAX_SIZE);
		}
	}

	private static Log _log = LogFactoryUtil.getLog(
		BufferedIndexerInvocationHandler.class);

	private final Indexer _indexer;
	private final PersistedModelLocalServiceRegistry
		_persistedModelLocalServiceRegistry;

}