/**
 * Copyright (c) 2000-2013 Liferay, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of the Liferay Enterprise
 * Subscription 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.systemevent;

import com.liferay.portal.kernel.lar.StagedModelType;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.systemevent.SystemEvent;
import com.liferay.portal.kernel.systemevent.SystemEventHierarchyEntry;
import com.liferay.portal.kernel.systemevent.SystemEventHierarchyEntryThreadLocal;
import com.liferay.portal.kernel.util.StringBundler;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.model.AuditedModel;
import com.liferay.portal.model.ClassedModel;
import com.liferay.portal.model.GroupedModel;
import com.liferay.portal.model.StagedModel;
import com.liferay.portal.model.SystemEventConstants;
import com.liferay.portal.model.TypedModel;
import com.liferay.portal.service.SystemEventLocalServiceUtil;
import com.liferay.portal.spring.aop.AnnotationChainableMethodAdvice;

import java.io.Serializable;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import org.aopalliance.intercept.MethodInvocation;

/**
 * @author Zsolt Berentey
 */
public class SystemEventAdvice
	extends AnnotationChainableMethodAdvice<SystemEvent> {

	@Override
	public void afterReturning(MethodInvocation methodInvocation, Object result)
		throws Throwable {

		SystemEvent systemEvent = findAnnotation(methodInvocation);

		if ((systemEvent == _nullSystemEvent) || !systemEvent.send()) {
			return;
		}

		if (!isValid(methodInvocation, PHASE_AFTER_RETURNING)) {
			return;
		}

		Object[] arguments = methodInvocation.getArguments();

		ClassedModel classedModel = (ClassedModel)arguments[0];

		long groupId = getGroupId(classedModel);

		String className = getClassName(classedModel);

		String referrerClassName = null;

		if (classedModel instanceof TypedModel) {
			TypedModel typedModel = (TypedModel)classedModel;

			referrerClassName = typedModel.getClassName();
		}

		long classPK = getClassPK(classedModel);

		SystemEventHierarchyEntry systemEventHierarchyEntry =
			SystemEventHierarchyEntryThreadLocal.peek();

		if ((systemEventHierarchyEntry != null) &&
			systemEventHierarchyEntry.hasTypedModel(className, classPK)) {

			if (groupId > 0) {
				SystemEventLocalServiceUtil.addSystemEvent(
					0, groupId, systemEventHierarchyEntry.getClassName(),
					classPK, systemEventHierarchyEntry.getUuid(),
					referrerClassName, systemEvent.type(),
					systemEventHierarchyEntry.getExtraData());
			}
			else {
				SystemEventLocalServiceUtil.addSystemEvent(
					getCompanyId(classedModel),
					systemEventHierarchyEntry.getClassName(), classPK,
					systemEventHierarchyEntry.getUuid(), referrerClassName,
					systemEvent.type(),
					systemEventHierarchyEntry.getExtraData());
			}
		}
		else if (groupId > 0) {
			SystemEventLocalServiceUtil.addSystemEvent(
				0, groupId, className, classPK, getUuid(classedModel),
				referrerClassName, systemEvent.type(), StringPool.BLANK);
		}
		else {
			SystemEventLocalServiceUtil.addSystemEvent(
				getCompanyId(classedModel), className, classPK,
				getUuid(classedModel), referrerClassName, systemEvent.type(),
				StringPool.BLANK);
		}
	}

	@Override
	public Object before(MethodInvocation methodInvocation) throws Throwable {
		SystemEvent systemEvent = findAnnotation(methodInvocation);

		if (systemEvent == _nullSystemEvent) {
			return null;
		}

		if (systemEvent.action() != SystemEventConstants.ACTION_NONE) {
			if (!isValid(methodInvocation, PHASE_BEFORE)) {
				return null;
			}

			Object[] arguments = methodInvocation.getArguments();

			ClassedModel classedModel = (ClassedModel)arguments[0];

			SystemEventHierarchyEntry systemEventHierarchyEntry =
				SystemEventHierarchyEntryThreadLocal.push(
					getClassName(classedModel), getClassPK(classedModel),
					systemEvent.action());

			if (systemEventHierarchyEntry != null) {
				systemEventHierarchyEntry.setUuid(getUuid(classedModel));
			}
		}

		return null;
	}

	@Override
	public void duringFinally(MethodInvocation methodInvocation) {
		SystemEvent systemEvent = findAnnotation(methodInvocation);

		if (systemEvent == _nullSystemEvent) {
			return;
		}

		if (!isValid(methodInvocation, PHASE_DURING_FINALLY)) {
			return;
		}

		if (systemEvent.action() == SystemEventConstants.ACTION_NONE) {
			return;
		}

		Object[] arguments = methodInvocation.getArguments();

		ClassedModel classedModel = (ClassedModel)arguments[0];

		long classPK = getClassPK(classedModel);

		if (classPK == 0) {
			return;
		}

		SystemEventHierarchyEntryThreadLocal.pop(
			getClassName(classedModel), classPK);
	}

	@Override
	public SystemEvent getNullAnnotation() {
		return _nullSystemEvent;
	}

	protected String getClassName(ClassedModel classedModel) {
		String className = classedModel.getModelClassName();

		if (classedModel instanceof StagedModel) {
			StagedModel stagedModel = (StagedModel)classedModel;

			StagedModelType stagedModelType = stagedModel.getStagedModelType();

			className = stagedModelType.getClassName();
		}

		return className;
	}

	protected long getClassPK(ClassedModel classedModel) {
		Serializable primaryKeyObj = classedModel.getPrimaryKeyObj();

		if (!(primaryKeyObj instanceof Long)) {
			return 0;
		}

		return (Long)primaryKeyObj;
	}

	protected long getCompanyId(ClassedModel classedModel) {
		if (classedModel instanceof AuditedModel) {
			AuditedModel auditedModel = (AuditedModel)classedModel;

			return auditedModel.getCompanyId();
		}

		if (classedModel instanceof GroupedModel) {
			GroupedModel groupedModel = (GroupedModel)classedModel;

			return groupedModel.getCompanyId();
		}

		if (classedModel instanceof StagedModel) {
			StagedModel stagedModel = (StagedModel)classedModel;

			return stagedModel.getCompanyId();
		}

		return 0;
	}

	protected long getGroupId(ClassedModel classedModel) {
		if (!(classedModel instanceof GroupedModel)) {
			return 0;
		}

		GroupedModel groupedModel = (GroupedModel)classedModel;

		return groupedModel.getGroupId();
	}

	protected String getUuid(ClassedModel classedModel) throws Exception {
		String uuid = null;

		if (classedModel instanceof StagedModel) {
			StagedModel stagedModel = (StagedModel)classedModel;

			uuid = stagedModel.getUuid();
		}
		else {
			Class<?> modelClass = classedModel.getClass();

			Method getUuidMethod = modelClass.getMethod(
				"getUuid", new Class[0]);

			if (getUuidMethod != null) {
				uuid = (String)getUuidMethod.invoke(
					classedModel, new Object[0]);
			}
			else {
				uuid = StringPool.BLANK;
			}
		}

		return uuid;
	}

	protected boolean isValid(MethodInvocation methodInvocation, int phase) {
		Method method = methodInvocation.getMethod();

		Class<?>[] parameterTypes = method.getParameterTypes();

		if (parameterTypes.length == 0) {
			if (_log.isDebugEnabled() && (phase == PHASE_BEFORE)) {
				_log.debug(
					"The method " + methodInvocation +
						" must have at least one parameter");
			}

			return false;
		}

		Class<?> parameterType = parameterTypes[0];

		if (!ClassedModel.class.isAssignableFrom(parameterType)) {
			if (_log.isDebugEnabled() && (phase == PHASE_BEFORE)) {
				_log.debug(
					"The first parameter of " + methodInvocation +
						" must implement ClassedModel");
			}

			return false;
		}

		Object[] arguments = methodInvocation.getArguments();

		ClassedModel classedModel = (ClassedModel)arguments[0];

		if ((classedModel == null) ||
			!(classedModel.getPrimaryKeyObj() instanceof Long)) {

			if (_log.isDebugEnabled() && (phase == PHASE_BEFORE)) {
				_log.debug(
					"The first parameter of " + methodInvocation +
						" must be a long");
			}

			return false;
		}

		if (phase != PHASE_AFTER_RETURNING) {
			return true;
		}

		if (!AuditedModel.class.isAssignableFrom(parameterType) &&
			!GroupedModel.class.isAssignableFrom(parameterType) &&
			!StagedModel.class.isAssignableFrom(parameterType)) {

			if (_log.isDebugEnabled()) {
				StringBundler sb = new StringBundler(4);

				sb.append("If send is true, the first parameter of ");
				sb.append(methodInvocation);
				sb.append(" must implement AuditedModel, GroupedModel, or ");
				sb.append("StagedModel");

				_log.debug(sb.toString());
			}

			return false;
		}

		return true;
	}

	private static final int PHASE_AFTER_RETURNING = 1;

	private static final int PHASE_BEFORE = 0;

	private static final int PHASE_DURING_FINALLY = 2;

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

	private static SystemEvent _nullSystemEvent = new SystemEvent() {

		@Override
		public Class<? extends Annotation> annotationType() {
			return SystemEvent.class;
		}

		@Override
		public int action() {
			return SystemEventConstants.ACTION_NONE;
		}

		@Override
		public boolean send() {
			return false;
		}

		@Override
		public int type() {
			return 0;
		}

	};

}