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

import com.liferay.portal.kernel.dao.orm.LockMode;
import com.liferay.portal.kernel.dao.orm.Session;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.transaction.TransactionAttribute;
import com.liferay.portal.kernel.transaction.TransactionInvokerUtil;
import com.liferay.portal.kernel.util.HashUtil;
import com.liferay.portal.kernel.util.SetUtil;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.service.PortalPreferencesLocalServiceUtil;
import com.liferay.portal.service.persistence.PortalPreferencesPersistence;
import com.liferay.portal.service.persistence.PortalPreferencesUtil;

import java.io.IOException;
import java.io.Serializable;

import java.util.Arrays;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;

import javax.portlet.ReadOnlyException;

/**
 * @author Brian Wing Shun Chan
 * @author Alexander Chow
 */
public class PortalPreferencesImpl
	extends BasePreferencesImpl
	implements Cloneable, PortalPreferences, Serializable {

	public static final TransactionAttribute TRANSACTION_ATTRIBUTE;

	static {
		TransactionAttribute.Builder builder =
			new TransactionAttribute.Builder();

		builder.setRollbackForClasses(
			PortalException.class, SystemException.class);

		TRANSACTION_ATTRIBUTE = builder.build();
	}

	public PortalPreferencesImpl() {
		this(0, 0, null, Collections.<String, Preference>emptyMap(), false);
	}

	public PortalPreferencesImpl(
		long ownerId, int ownerType, String xml,
		Map<String, Preference> preferences, boolean signedIn) {

		super(ownerId, ownerType, xml, preferences);

		_signedIn = signedIn;
	}

	@Override
	public PortalPreferencesImpl clone() {
		return new PortalPreferencesImpl(
			getOwnerId(), getOwnerType(), getOriginalXML(),
			new HashMap<String, Preference>(
				getOriginalPreferences()), isSignedIn());
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}

		if (!(obj instanceof PortalPreferencesImpl)) {
			return false;
		}

		PortalPreferencesImpl portalPreferences = (PortalPreferencesImpl)obj;

		if ((getOwnerId() == portalPreferences.getOwnerId()) &&
			(getOwnerType() == portalPreferences.getOwnerType()) &&
			getPreferences().equals(portalPreferences.getPreferences())) {

			return true;
		}
		else {
			return false;
		}
	}

	@Override
	public long getUserId() {
		return _userId;
	}

	@Override
	public String getValue(String namespace, String key) {
		return getValue(namespace, key, null);
	}

	@Override
	public String getValue(String namespace, String key, String defaultValue) {
		key = _encodeKey(namespace, key);

		return super.getValue(key, defaultValue);
	}

	@Override
	public String[] getValues(String namespace, String key) {
		return getValues(namespace, key, null);
	}

	@Override
	public String[] getValues(
		String namespace, String key, String[] defaultValue) {

		key = _encodeKey(namespace, key);

		return super.getValues(key, defaultValue);
	}

	@Override
	public int hashCode() {
		int hashCode = HashUtil.hash(0, getOwnerId());

		hashCode = HashUtil.hash(hashCode, getOwnerType());
		hashCode = HashUtil.hash(hashCode, getPreferences());

		return hashCode;
	}

	@Override
	public boolean isSignedIn() {
		return _signedIn;
	}

	@Override
	public void reset(final String key) throws ReadOnlyException {
		if (isReadOnly(key)) {
			throw new ReadOnlyException(key);
		}

		String[] values = super.getValues(key, null);

		if (values == null) {
			return;
		}

		try {
			validateStore(
				new Callable<Void>() {

					@Override
					public Void call() {
						Map<String, Preference> modifiedPreferences =
							getModifiedPreferences();

						modifiedPreferences.remove(key);

						return null;
					}

				}, key);
		}
		catch (ConcurrentModificationException cme) {
			throw cme;
		}
		catch (Throwable t) {
			_log.error(t, t);
		}
	}

	@Override
	public void resetValues(String namespace) {
		Map<String, Preference> preferences = getPreferences();

		try {
			for (Map.Entry<String, Preference> entry : preferences.entrySet()) {
				String key = entry.getKey();

				if (key.startsWith(namespace) && !isReadOnly(key)) {
					reset(key);
				}
			}
		}
		catch (ConcurrentModificationException cme) {
			throw cme;
		}
		catch (Exception e) {
			_log.error(e, e);
		}
	}

	@Override
	public void setSignedIn(boolean signedIn) {
		_signedIn = signedIn;
	}

	@Override
	public void setUserId(long userId) {
		_userId = userId;
	}

	@Override
	public void setValue(String namespace, String key, final String value) {
		if (Validator.isNull(key) || key.equals(_RANDOM_KEY)) {
			return;
		}

		final String encodedKey = _encodeKey(namespace, key);

		try {
			if (value == null) {
				reset(encodedKey);

				return;
			}

			String[] oldValues = super.getValues(encodedKey, null);

			if ((oldValues != null) && (oldValues.length == 1) &&
				value.equals(oldValues[0])) {

				return;
			}

			Callable<Void> callable = new Callable<Void>() {

				@Override
				public Void call() throws ReadOnlyException {
					PortalPreferencesImpl.super.setValue(encodedKey, value);

					return null;
				}

			};

			if (_signedIn) {
				validateStore(callable, encodedKey);
			}
			else {
				callable.call();
			}
		}
		catch (ConcurrentModificationException cme) {
			throw cme;
		}
		catch (Throwable t) {
			_log.error(t, t);
		}
	}

	@Override
	public void setValues(String namespace, String key, final String[] values) {
		if (Validator.isNull(key) || key.equals(_RANDOM_KEY)) {
			return;
		}

		final String encodedKey = _encodeKey(namespace, key);

		try {
			if (values == null) {
				reset(encodedKey);

				return;
			}

			if (values.length == 1) {
				setValue(namespace, key, values[0]);

				return;
			}

			String[] oldValues = super.getValues(encodedKey, null);

			if (oldValues != null) {
				Set<String> valuesSet = SetUtil.fromArray(values);
				Set<String> oldValuesSet = SetUtil.fromArray(oldValues);

				if (valuesSet.equals(oldValuesSet)) {
					return;
				}
			}

			Callable<Void> callable = new Callable<Void>() {

				@Override
				public Void call() throws ReadOnlyException {
					PortalPreferencesImpl.super.setValues(encodedKey, values);

					return null;
				}

			};

			if (_signedIn) {
				validateStore(callable, encodedKey);
			}
			else {
				callable.call();
			}
		}
		catch (ConcurrentModificationException cme) {
			throw cme;
		}
		catch (Throwable t) {
			_log.error(t, t);
		}
	}

	private Callable<Boolean> _createValidateCallable(
		final String[] originalValues, final String key) {

		return new Callable<Boolean>() {

			@Override
			public Boolean call() throws Exception {
				com.liferay.portal.model.PortalPreferences
					preferences = PortalPreferencesUtil.fetchByO_O(
						getOwnerId(), getOwnerType(), false);

				PortalPreferencesPersistence portalPreferencesPersistence =
					PortalPreferencesUtil.getPersistence();

				Session session = null;

				try {
					session = portalPreferencesPersistence.openSession();

					session.evict(preferences);

					preferences = (com.liferay.portal.model.PortalPreferences)
						session.get(
							com.liferay.portal.model.impl.
								PortalPreferencesImpl.class,
							preferences.getPrimaryKey(), LockMode.UPGRADE);

					PortalPreferencesImpl portalPreferencesImpl =
						(PortalPreferencesImpl)
							PortletPreferencesFactoryUtil.fromXML(
								getOwnerId(), getOwnerType(),
								preferences.getPreferences());

					String originalXML = getOriginalXML();

					if (originalXML.equals(preferences.getPreferences())) {
						store();

						return true;
					}

					if (!Arrays.equals(
							originalValues,
							portalPreferencesImpl.getValues(
								key, (String[])null))) {

						return false;
					}

					reset();

					setOriginalPreferences(
						portalPreferencesImpl.getOriginalPreferences());

					setOriginalXML(preferences.getPreferences());

					return null;
				}
				finally {
					portalPreferencesPersistence.closeSession(session);
				}
			}

		};
	}

	protected void validateStore(final Callable<?> callable, final String key)
		throws Throwable {

		while (true) {
			String[] originalValues = super.getValues(key, null);

			callable.call();

			Boolean result = TransactionInvokerUtil.invoke(
				TRANSACTION_ATTRIBUTE,
				_createValidateCallable(originalValues, key));

			if (result == null) {
				continue;
			}

			if (!result) {
				PortalPreferencesWrapperCacheUtil.remove(
					getOwnerId(), getOwnerType());

				throw new ConcurrentModificationException();
			}

			break;
		}
	}

	@Override
	public void store() throws IOException {
		try {
			PortalPreferencesLocalServiceUtil.updatePreferences(
				getOwnerId(), getOwnerType(), this);
		}
		catch (SystemException se) {
			throw new IOException(se.getMessage());
		}
	}

	private String _encodeKey(String namespace, String key) {
		if (Validator.isNull(namespace)) {
			return key;
		}
		else {
			return namespace.concat(StringPool.POUND).concat(key);
		}
	}

	private static final String _RANDOM_KEY = "r";

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

	private boolean _signedIn;
	private long _userId;

}