/*******************************************************************************
 * Copyright (c) 2004, 2008 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.core.runtime.internal.adaptor;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import org.eclipse.osgi.framework.internal.core.FrameworkProperties;
import org.eclipse.osgi.service.datalocation.Location;
import org.eclipse.osgi.util.NLS;

/**
 * Internal class.
 */
public class BasicLocation implements Location {
	static class MockLocker implements Locker {
		public boolean lock() throws IOException {
			// locking always successful
			return true;
		}

		public boolean isLocked() {
			// this lock is never locked
			return false;
		}

		public void release() {
			// nothing to release
		}

	}

	final private boolean isReadOnly;
	private URL location = null;
	private Location parent;
	final private URL defaultValue;
	final private String property;

	// locking related fields
	private File lockFile;
	private Locker locker;
	public static final String PROP_OSGI_LOCKING = "osgi.locking"; //$NON-NLS-1$
	private static String LOCK_FILENAME = ".metadata/.lock"; //$NON-NLS-1$
	public static boolean DEBUG;

	private static boolean isRunningWithNio() {
		try {
			Class.forName("java.nio.channels.FileLock"); //$NON-NLS-1$
		} catch (ClassNotFoundException e) {
			return false;
		}
		return true;
	}

	public static Locker createLocker(File lock, String lockMode) {
		if (lockMode == null)
			lockMode = FrameworkProperties.getProperty(PROP_OSGI_LOCKING);

		if ("none".equals(lockMode)) //$NON-NLS-1$
			return new MockLocker();

		if ("java.io".equals(lockMode)) //$NON-NLS-1$
			return new Locker_JavaIo(lock);

		if ("java.nio".equals(lockMode)) { //$NON-NLS-1$
			if (isRunningWithNio())
				return new Locker_JavaNio(lock);
			// TODO should we return null here.  NIO was requested but we could not do it...
			return new Locker_JavaIo(lock);
		}

		//	Backup case if an invalid value has been specified
		if (isRunningWithNio())
			return new Locker_JavaNio(lock);
		return new Locker_JavaIo(lock);

	}

	public BasicLocation(String property, URL defaultValue, boolean isReadOnly) {
		super();
		this.property = property;
		this.defaultValue = defaultValue;
		this.isReadOnly = isReadOnly;
	}

	public boolean allowsDefault() {
		return defaultValue != null;
	}

	public URL getDefault() {
		return defaultValue;
	}

	public synchronized Location getParentLocation() {
		return parent;
	}

	public synchronized URL getURL() {
		if (location == null && defaultValue != null)
			setURL(defaultValue, false);
		return location;
	}

	public synchronized boolean isSet() {
		return location != null;
	}

	public boolean isReadOnly() {
		return isReadOnly;
	}

	public boolean setURL(URL value, boolean lock) throws IllegalStateException {
		try {
			return set(value, lock);
		} catch (IOException e) {
			return false;
		}
	}

	public synchronized boolean set(URL value, boolean lock) throws IllegalStateException, IOException {
		if (location != null)
			throw new IllegalStateException(EclipseAdaptorMsg.ECLIPSE_CANNOT_CHANGE_LOCATION);
		File file = null;
		if (value.getProtocol().equalsIgnoreCase("file")) { //$NON-NLS-1$
			try {
				String basePath = new File(value.getFile()).getCanonicalPath();
				value = new URL("file:" + basePath); //$NON-NLS-1$
			} catch (IOException e) {
				// do nothing just use the original value
			}
			file = new File(value.getFile(), LOCK_FILENAME);
		}
		lock = lock && !isReadOnly;
		if (lock) {
			if (!lock(file, value))
				return false;
		}
		lockFile = file;
		location = LocationHelper.buildURL(value.toExternalForm(), true);
		if (property != null)
			FrameworkProperties.setProperty(property, location.toExternalForm());
		return lock;
	}

	public synchronized void setParent(Location value) {
		parent = value;
	}

	public synchronized boolean lock() throws IOException {
		if (!isSet())
			throw new IOException(EclipseAdaptorMsg.location_notSet);
		return lock(lockFile, location);
	}

	public synchronized boolean isLocked() throws IOException {
		if (!isSet())
			return false;
		return isLocked(lockFile);
	}

	/*
	 * This must be called while holding the synchronization lock for (this)
	 */
	private boolean lock(File lock, URL locationValue) throws IOException {
		if (isReadOnly)
			throw new IOException(NLS.bind(EclipseAdaptorMsg.location_folderReadOnly, lock));
		if (lock == null) {
			if (locationValue != null && !"file".equalsIgnoreCase(locationValue.getProtocol())) //$NON-NLS-1$
				throw new IOException(NLS.bind(EclipseAdaptorMsg.location_notFileProtocol, locationValue));
			throw new IllegalStateException(EclipseAdaptorMsg.location_noLockFile); // this is really unexpected
		}

		File parentFile = new File(lock.getParent());
		if (!parentFile.exists())
			if (!parentFile.mkdirs())
				throw new IOException(NLS.bind(EclipseAdaptorMsg.location_folderReadOnly, parentFile));

		setLocker(lock);
		if (locker == null)
			return true;
		boolean locked = false;
		try {
			locked = locker.lock();
			return locked;
		} finally {
			if (!locked)
				locker = null;
		}
	}

	/*
	 * This must be called while holding the synchronization lock for (this)
	 */
	private boolean isLocked(File lock) throws IOException {
		if (lock == null || isReadOnly)
			return true;
		if (!lock.exists())
			return false;
		setLocker(lock);
		return locker.isLocked();
	}

	/*
	 * This must be called while holding the synchronization lock for (this)
	 */
	private void setLocker(File lock) {
		if (locker != null)
			return;
		String lockMode = FrameworkProperties.getProperty(PROP_OSGI_LOCKING);
		locker = createLocker(lock, lockMode);
	}

	public synchronized void release() {
		if (locker != null)
			locker.release();
	}

	public Location createLocation(Location parentLocation, URL defaultLocation, boolean readonly) {
		BasicLocation result = new BasicLocation(null, defaultLocation, readonly);
		result.setParent(parentLocation);
		return result;
	}
}
