/*******************************************************************************
 * Copyright (c) 2009, 2015 IBM Corporation and others.
 *
 * This
 * program and the accompanying materials are made available under the terms of
 * the Eclipse Public License 2.0 which accompanies this distribution, and is
 * available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 * 
 * Contributors: 
 *     IBM Corporation - initial API and implementation
 *     Red Hat Inc. - Bug 460967
 ******************************************************************************/
package org.eclipse.equinox.internal.p2.engine;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import org.eclipse.equinox.internal.p2.core.helpers.ServiceHelper;
import org.eclipse.osgi.service.datalocation.Location;
import org.eclipse.osgi.util.NLS;

/**
 * The purpose of this class is to enable cross process locking.
 * See 257654 for more details.
 */
public class ProfileLock {
	private static final String LOCK_FILENAME = ".lock"; //$NON-NLS-1$

	private final Location location;
	private final Object lock;
	private Thread lockHolder;
	private int waiting;

	public ProfileLock(Object lock, File profileDirectory) {
		this.lock = lock;
		location = createLockLocation(profileDirectory);
	}

	private static Location createLockLocation(File parent) {
		Location anyLoc = ServiceHelper.getService(EngineActivator.getContext(), Location.class);
		try {
			final URL url = parent.toURL();
			Location location = anyLoc.createLocation(null, url, false);
			location.set(url, false, LOCK_FILENAME);
			return location;
		} catch (MalformedURLException e) {
			throw new IllegalArgumentException(NLS.bind(Messages.SimpleProfileRegistry_Bad_profile_location, e.getLocalizedMessage()), e);
		} catch (IllegalStateException e) {
			throw e;
		} catch (IOException e) {
			throw new IllegalStateException(e.getLocalizedMessage(), e);
		}
	}

	/**
	 * Asserts that this thread currently holds the profile lock.
	 * @throws IllegalStateException If this thread does not currently hold the profile lock
	 */
	public void checkLocked() {
		synchronized (lock) {
			if (lockHolder == null)
				throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_not_locked);

			Thread current = Thread.currentThread();
			if (lockHolder != current)
				throw new IllegalStateException(Messages.thread_not_owner);
		}
	}

	/**
	 * Attempts to obtain an exclusive write lock on a profile. The profile lock must be
	 * owned by any process and thread that wants to modify a profile. If the lock
	 * is currently held by another thread in this process, this method will  block until
	 * the lock becomes available. If the lock is currently held by another process,
	 * this method returns <code>false</code>. Re-entrant attempts to acquire the
	 * same profile lock multiple times in the same thread is not allowed.
	 * 
	 * @return <code>true</code> if the lock was successfully obtained by this thread,
	 * and <code>false</code> if another process is currently holding the lock.
	 */
	public boolean lock() {
		synchronized (lock) {
			Thread current = Thread.currentThread();
			if (lockHolder == current)
				throw new IllegalStateException(Messages.profile_lock_not_reentrant);

			boolean locationLocked = (waiting != 0);
			while (lockHolder != null) {
				locationLocked = true;
				waiting++;
				boolean interrupted = false;
				try {
					lock.wait();
				} catch (InterruptedException e) {
					interrupted = true;
				} finally {
					waiting--;
					// if interrupted restore interrupt to thread state
					if (interrupted)
						current.interrupt();
				}
			}
			try {
				if (!locationLocked && !location.lock())
					return false;

				lockHolder = current;
			} catch (IOException e) {
				throw new IllegalStateException(NLS.bind(Messages.SimpleProfileRegistry_Profile_not_locked_due_to_exception, e.getLocalizedMessage()), e);
			}
			return true;
		}
	}

	/**
	 * Releases the exclusive write lock on a profile. This method must only be called
	 * by a thread that currently owns the lock.
	 */
	public void unlock() {
		synchronized (lock) {
			if (lockHolder == null)
				throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_not_locked);

			Thread current = Thread.currentThread();
			if (lockHolder != current)
				throw new IllegalStateException(Messages.thread_not_owner);

			lockHolder = null;
			if (waiting == 0)
				location.release();
			else
				lock.notify();
		}
	}

	/**
	 * Returns whether a thread in this process currently holds the profile lock.
	 * 
	 * @return <code>true</code> if a thread in this process owns the profile lock,
	 * and <code>false</code> otherwise
	 */
	public boolean processHoldsLock() {
		synchronized (lock) {
			return lockHolder != null;
		}
	}
}