/*******************************************************************************
 * Copyright (c) 2000, 2016 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
 *     Francis Lynch (Wind River) - [301563] Save and load tree snapshots
 *     Broadcom Corporation - ongoing development
 *     Sergey Prigogin (Google) - [437005] Out-of-date .snap file prevents Eclipse from running
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
 *     Mickael Istria (Red Hat Inc.) - Bug 488937
 *******************************************************************************/
package org.eclipse.core.internal.resources;

import java.io.*;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.internal.localstore.SafeChunkyInputStream;
import org.eclipse.core.internal.localstore.SafeChunkyOutputStream;
import org.eclipse.core.internal.utils.Messages;
import org.eclipse.core.internal.utils.Policy;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;

public class LocalMetaArea implements ICoreConstants {
	/* package */static final String F_BACKUP_FILE_EXTENSION = ".bak"; //$NON-NLS-1$
	/* package */static final String F_DESCRIPTION = ".workspace"; //$NON-NLS-1$

	/* package */static final String F_HISTORY_STORE = ".history"; //$NON-NLS-1$
	/* package */static final String F_MARKERS = ".markers"; //$NON-NLS-1$
	/* package */static final String F_OLD_PROJECT = ".prj"; //$NON-NLS-1$
	/* package */static final String F_PROJECT_LOCATION = ".location"; //$NON-NLS-1$
	/* package */static final String F_PROJECTS = ".projects"; //$NON-NLS-1$
	/* package */static final String F_PROPERTIES = ".properties"; //$NON-NLS-1$
	/* package */static final String F_REFRESH = ".refresh"; //$NON-NLS-1$
	/* package */static final String F_ROOT = ".root"; //$NON-NLS-1$
	/* package */static final String F_SAFE_TABLE = ".safetable"; //$NON-NLS-1$
	/* package */static final String F_SNAP = ".snap"; //$NON-NLS-1$
	/* package */static final String F_SNAP_EXTENSION = "snap"; //$NON-NLS-1$
	/* package */static final String F_SYNCINFO = ".syncinfo"; //$NON-NLS-1$
	/* package */static final String F_TREE = ".tree"; //$NON-NLS-1$
	/* package */static final String URI_PREFIX = "URI//"; //$NON-NLS-1$
	/* package */static final String F_METADATA = ".metadata"; //$NON-NLS-1$

	protected final IPath metaAreaLocation;

	/**
	 * The project location is just stored as an optimization, to avoid recomputing it.
	 */
	protected final IPath projectMetaLocation;

	public LocalMetaArea() {
		super();
		metaAreaLocation = ResourcesPlugin.getPlugin().getStateLocation();
		projectMetaLocation = metaAreaLocation.append(F_PROJECTS);
	}

	/**
	 * For backwards compatibility, if there is a project at the old project
	 * description location, delete it.
	 */
	public void clearOldDescription(IProject target) {
		Workspace.clear(getOldDescriptionLocationFor(target).toFile());
	}

	/**
	 * Delete the refresh snapshot once it has been used to open a new project.
	 */
	public void clearRefresh(IProject target) {
		Workspace.clear(getRefreshLocationFor(target).toFile());
	}

	public void create(IProject target) {
		java.io.File file = locationFor(target).toFile();
		//make sure area is empty
		Workspace.clear(file);
		file.mkdirs();
	}

	/**
	 * Creates the meta area root directory.
	 */
	public synchronized void createMetaArea() throws CoreException {
		java.io.File workspaceLocation = metaAreaLocation.toFile();
		Workspace.clear(workspaceLocation);
		if (!workspaceLocation.mkdirs()) {
			String message = NLS.bind(Messages.resources_writeWorkspaceMeta, workspaceLocation);
			throw new ResourceException(IResourceStatus.FAILED_WRITE_METADATA, null, message, null);
		}
	}

	/**
	 * The project is being deleted. Delete all meta-data associated with the
	 * project.
	 */
	public void delete(IProject target) throws CoreException {
		IPath path = locationFor(target);
		if (!Workspace.clear(path.toFile()) && path.toFile().exists()) {
			String message = NLS.bind(Messages.resources_deleteMeta, target.getFullPath());
			throw new ResourceException(IResourceStatus.FAILED_DELETE_METADATA, target.getFullPath(), message, null);
		}
	}

	public IPath getBackupLocationFor(IPath file) {
		return file.removeLastSegments(1).append(file.lastSegment() + F_BACKUP_FILE_EXTENSION);
	}

	public IPath getHistoryStoreLocation() {
		return metaAreaLocation.append(F_HISTORY_STORE);
	}

	/**
	 * Returns the local file system location which contains the META data for
	 * the resources plugin (i.e., the entire workspace).
	 */
	public IPath getLocation() {
		return metaAreaLocation;
	}

	/**
	 * Returns the path of the file in which to save markers for the given
	 * resource. Should only be called for the workspace root and projects.
	 */
	public IPath getMarkersLocationFor(IResource resource) {
		Assert.isNotNull(resource);
		Assert.isLegal(resource.getType() == IResource.ROOT || resource.getType() == IResource.PROJECT);
		return locationFor(resource).append(F_MARKERS);
	}

	/**
	 * Returns the path of the file in which to snapshot markers for the given
	 * resource. Should only be called for the workspace root and projects.
	 */
	public IPath getMarkersSnapshotLocationFor(IResource resource) {
		return getMarkersLocationFor(resource).addFileExtension(F_SNAP_EXTENSION);
	}

	/**
	 * The project description file is the only metadata file stored outside
	 * the metadata area. It is stored as a file directly under the project
	 * location. For backwards compatibility, we also have to check for a
	 * project file at the old location in the metadata area.
	 */
	public IPath getOldDescriptionLocationFor(IProject target) {
		return locationFor(target).append(F_OLD_PROJECT);
	}

	public IPath getOldWorkspaceDescriptionLocation() {
		return metaAreaLocation.append(F_DESCRIPTION);
	}

	public IPath getPropertyStoreLocation(IResource resource) {
		int type = resource.getType();
		Assert.isTrue(type != IResource.FILE && type != IResource.FOLDER);
		return locationFor(resource).append(F_PROPERTIES);
	}

	/**
	 * Returns the path of the file in which to save the refresh snapshot for
	 * the given project.
	 */
	public IPath getRefreshLocationFor(IProject project) {
		Assert.isNotNull(project);
		return locationFor(project).append(F_REFRESH);
	}

	public IPath getSafeTableLocationFor(String pluginId) {
		IPath prefix = metaAreaLocation.append(F_SAFE_TABLE);
		// if the plugin is the resources plugin, we return the master table
		// location
		if (pluginId.equals(ResourcesPlugin.PI_RESOURCES))
			return prefix.append(pluginId); // master table
		int saveNumber = getWorkspace().getSaveManager().getSaveNumber(pluginId);
		return prefix.append(pluginId + "." + saveNumber); //$NON-NLS-1$
	}

	/**
	 * Returns the path of the snapshot file. The name of the file is composed from a sequence
	 * number corresponding to the sequence number of tree file and ".snap" extension. Should
	 * only be called for the workspace root.
	 */
	public IPath getSnapshotLocationFor(IResource resource) {
		Assert.isNotNull(resource);
		Assert.isLegal(resource.getType() == IResource.ROOT);
		IPath key = resource.getFullPath().append(F_TREE);
		String sequenceNumber = getWorkspace().getSaveManager().getMasterTable().getProperty(key.toString());
		if (sequenceNumber == null)
			sequenceNumber = "0"; //$NON-NLS-1$
		return metaAreaLocation.append(sequenceNumber + F_SNAP);
	}

	/**
	 * Returns the legacy, pre-4.4.1, path of the snapshot file. The name of the legacy snapshot
	 * file is ".snap". Should only be called for the workspace root.
	 */
	public IPath getLegacySnapshotLocationFor(IResource resource) {
		Assert.isNotNull(resource);
		Assert.isLegal(resource.getType() == IResource.ROOT);
		return metaAreaLocation.append(F_SNAP);
	}

	/**
	 * Returns the path of the file in which to save the sync information for
	 * the given resource. Should only be called for the workspace root and
	 * projects.
	 */
	public IPath getSyncInfoLocationFor(IResource resource) {
		Assert.isNotNull(resource);
		Assert.isLegal(resource.getType() == IResource.ROOT || resource.getType() == IResource.PROJECT);
		return locationFor(resource).append(F_SYNCINFO);
	}

	/**
	 * Returns the path of the file in which to snapshot the sync information
	 * for the given resource. Should only be called for the workspace root and
	 * projects.
	 */
	public IPath getSyncInfoSnapshotLocationFor(IResource resource) {
		return getSyncInfoLocationFor(resource).addFileExtension(F_SNAP_EXTENSION);
	}

	/**
	 * Returns the local file system location of the tree file for the given
	 * resource. This file does not follow the same save number as its plug-in.
	 * So, the number here is called "sequence number" and not "save number" to
	 * avoid confusion.
	 */
	public IPath getTreeLocationFor(IResource target, boolean updateSequenceNumber) {
		IPath key = target.getFullPath().append(F_TREE);
		String sequenceNumber = getWorkspace().getSaveManager().getMasterTable().getProperty(key.toString());
		if (sequenceNumber == null)
			sequenceNumber = "0"; //$NON-NLS-1$
		if (updateSequenceNumber) {
			int n = Integer.parseInt(sequenceNumber) + 1;
			n = n < 0 ? 1 : n;
			sequenceNumber = Integer.toString(n);
			getWorkspace().getSaveManager().getMasterTable().setProperty(key.toString(), sequenceNumber);
		}
		return locationFor(target).append(sequenceNumber + F_TREE);
	}

	public IPath getWorkingLocation(IResource resource, String id) {
		return locationFor(resource).append(id);
	}

	protected Workspace getWorkspace() {
		return (Workspace) ResourcesPlugin.getWorkspace();
	}

	public boolean hasSavedProject(IProject project) {
		//if there is a location file, then the project exists
		return getOldDescriptionLocationFor(project).toFile().exists() || locationFor(project).append(F_PROJECT_LOCATION).toFile().exists();
	}

	public boolean hasSavedWorkspace() {
		return metaAreaLocation.toFile().exists() || getBackupLocationFor(metaAreaLocation).toFile().exists();
	}

	/**
	 * Returns the local file system location in which the meta data for the
	 * resource with the given path is stored.
	 */
	public IPath locationFor(IPath resourcePath) {
		if (Path.ROOT.equals(resourcePath))
			return metaAreaLocation.append(F_ROOT);
		return projectMetaLocation.append(resourcePath.segment(0));
	}

	/**
	 * Returns the local file system location in which the meta data for the
	 * given resource is stored.
	 */
	public IPath locationFor(IResource resource) {
		if (resource.getType() == IResource.ROOT)
			return metaAreaLocation.append(F_ROOT);
		return projectMetaLocation.append(resource.getProject().getName());
	}

	/**
	 * Reads and returns the project description for the given project. Returns
	 * null if there was no project description file on disk. Throws an
	 * exception if there was any failure to read the project.
	 */
	public ProjectDescription readOldDescription(IProject project) throws CoreException {
		IPath path = getOldDescriptionLocationFor(project);
		if (!path.toFile().exists())
			return null;
		IPath tempPath = getBackupLocationFor(path);
		ProjectDescription description = null;
		try {
			description = new ProjectDescriptionReader(project).read(path, tempPath);
		} catch (IOException e) {
			String msg = NLS.bind(Messages.resources_readMeta, project.getName());
			throw new ResourceException(IResourceStatus.FAILED_READ_METADATA, project.getFullPath(), msg, e);
		}
		if (description == null) {
			String msg = NLS.bind(Messages.resources_readMeta, project.getName());
			throw new ResourceException(IResourceStatus.FAILED_READ_METADATA, project.getFullPath(), msg, null);
		}
		return description;
	}

	/**
	 * Provides backward compatibility with existing workspaces based on
	 * descriptions.
	 */
	public WorkspaceDescription readOldWorkspace() {
		IPath path = getOldWorkspaceDescriptionLocation();
		IPath tempPath = getBackupLocationFor(path);
		try {
			WorkspaceDescription oldDescription = (WorkspaceDescription) new WorkspaceDescriptionReader().read(path, tempPath);
			// if one of those files exist, get rid of them
			Workspace.clear(path.toFile());
			Workspace.clear(tempPath.toFile());
			return oldDescription;
		} catch (IOException e) {
			return null;
		}
	}

	/**
	 * Returns the portions of the project description that are private, and
	 * adds them to the supplied project description. In particular, the
	 * project location, the project's dynamic references and build configurations
	 * are stored here.
	 * The project location will be set to <code>null</code> if the default
	 * location should be used. In the case of failure, log the exception and
	 * return silently, thus reverting to using the default location and no
	 * dynamic references. The current format of the location file is:
	 *    UTF - project location
	 *    int - number of dynamic project references
	 *    UTF - project reference 1
	 *    ... repeat for remaining references
	 * since 3.7:
	 *    int - number of build configurations
	 *      UTF - configuration name in order
	 *      ... repeated for N configurations
	 *    UTF - active build configuration name
	 *    int - number of build configurations with refs
	 *      UTF - build configuration name
	 *      int - number of referenced configuration
	 *        UTF - project name
	 *        bool - hasConfigName
	 *        UTF - configName if hasConfigName
	 *        ... repeat for number of referenced configurations
	 *      ... repeat for number of build configurations with references
	 */
	public void readPrivateDescription(IProject target, ProjectDescription description) {
		IPath locationFile = locationFor(target).append(F_PROJECT_LOCATION);
		java.io.File file = locationFile.toFile();
		if (!file.exists()) {
			locationFile = getBackupLocationFor(locationFile);
			file = locationFile.toFile();
			if (!file.exists())
				return;
		}
		try (DataInputStream dataIn = new DataInputStream(new SafeChunkyInputStream(file, 500))) {
			try {
				String location = dataIn.readUTF();
				if (location.length() > 0) {
					//location format < 3.2 was a local file system OS path
					//location format >= 3.2 is: URI_PREFIX + uri.toString()
					if (location.startsWith(URI_PREFIX))
						description.setLocationURI(URI.create(location.substring(URI_PREFIX.length())));
					else
						description.setLocationURI(URIUtil.toURI(Path.fromOSString(location)));
				}
			} catch (Exception e) {
				//don't allow failure to read the location to propagate
				String msg = NLS.bind(Messages.resources_exReadProjectLocation, target.getName());
				Policy.log(new ResourceStatus(IStatus.ERROR, IResourceStatus.FAILED_READ_METADATA, target.getFullPath(), msg, e));
			}
			//try to read the dynamic references - will fail for old location files
			int numRefs = dataIn.readInt();
			IProject[] references = new IProject[numRefs];
			IWorkspaceRoot root = getWorkspace().getRoot();
			for (int i = 0; i < numRefs; i++)
				references[i] = root.getProject(dataIn.readUTF());
			description.setDynamicReferences(references);

			// Since 3.7 -  Build Configurations
			String[] configs = new String[dataIn.readInt()];
			for (int i = 0; i < configs.length; i++)
				configs[i] = dataIn.readUTF();
			if (configs.length > 0)
				// In the future we may decide this is better stored in the
				// .project, so only set if configs.length > 0
				description.setBuildConfigs(configs);
			// Active configuration name
			description.setActiveBuildConfig(dataIn.readUTF());
			// Build configuration references?
			int numBuildConifgsWithRefs = dataIn.readInt();
			HashMap<String, IBuildConfiguration[]> m = new HashMap<>(numBuildConifgsWithRefs);
			for (int i = 0; i < numBuildConifgsWithRefs; i++) {
				String configName = dataIn.readUTF();
				numRefs = dataIn.readInt();
				IBuildConfiguration[] refs = new IBuildConfiguration[numRefs];
				for (int j = 0; j < numRefs; j++) {
					String projName = dataIn.readUTF();
					if (dataIn.readBoolean())
						refs[j] = new BuildConfiguration(root.getProject(projName), dataIn.readUTF());
					else
						refs[j] = new BuildConfiguration(root.getProject(projName), null);
				}
				m.put(configName, refs);
			}
			description.setBuildConfigReferences(m);
		} catch (IOException e) {
			//ignore - this is an old location file or an exception occurred
			// closing the stream
		}
	}

	/**
	 * Writes the workspace description to the local meta area. This method is
	 * synchronized to prevent multiple current write attempts.
	 *
	 * @deprecated should not be called any more - workspace preferences are
	 *                     now maintained in the plug-in's preferences
	 */
	@Deprecated
	public synchronized void write(WorkspaceDescription description) throws CoreException {
		IPath path = getOldWorkspaceDescriptionLocation();
		path.toFile().getParentFile().mkdirs();
		IPath tempPath = getBackupLocationFor(path);
		try {
			new ModelObjectWriter().write(description, path, tempPath, System.getProperty("line.separator")); //$NON-NLS-1$
		} catch (IOException e) {
			String message = NLS.bind(Messages.resources_writeWorkspaceMeta, path);
			throw new ResourceException(IResourceStatus.FAILED_WRITE_METADATA, null, message, e);
		}
	}

	/**
	 * Write the private project description information, including the location
	 * and the dynamic project references.  See <tt>readPrivateDescription</tt>
	 * for details on the file format.
	 */
	public void writePrivateDescription(IProject target) throws CoreException {
		IPath location = locationFor(target).append(F_PROJECT_LOCATION);
		java.io.File file = location.toFile();
		//delete any old location file
		Workspace.clear(file);
		//don't write anything if there is no interesting private metadata
		ProjectDescription desc = ((Project) target).internalGetDescription();
		if (desc == null)
			return;
		final URI projectLocation = desc.getLocationURI();
		final IProject[] prjRefs = desc.getDynamicReferences(false);
		final String[] buildConfigs = desc.configNames;
		final Map<String, IBuildConfiguration[]> configRefs = desc.getBuildConfigReferences(false);
		if (projectLocation == null && prjRefs.length == 0 && buildConfigs.length == 0 && configRefs.isEmpty())
			return;
		//write the private metadata file
		try (SafeChunkyOutputStream output = new SafeChunkyOutputStream(file); DataOutputStream dataOut = new DataOutputStream(output);) {
			if (projectLocation == null)
				dataOut.writeUTF(""); //$NON-NLS-1$
			else
				dataOut.writeUTF(URI_PREFIX + projectLocation.toString());
			dataOut.writeInt(prjRefs.length);
			for (IProject prjRef : prjRefs)
				dataOut.writeUTF(prjRef.getName());

			// Since 3.7 - build configurations + references
			// Write out the build configurations
			dataOut.writeInt(buildConfigs.length);
			for (String buildConfig : buildConfigs) {
				dataOut.writeUTF(buildConfig);
			}
			// Write active configuration name
			dataOut.writeUTF(desc.getActiveBuildConfig());
			// Write out the configuration level references
			dataOut.writeInt(configRefs.size());
			for (Map.Entry<String, IBuildConfiguration[]> e : configRefs.entrySet()) {
				String refdName = e.getKey();
				IBuildConfiguration[] refs = e.getValue();

				dataOut.writeUTF(refdName);
				dataOut.writeInt(refs.length);
				for (IBuildConfiguration ref : refs) {
					dataOut.writeUTF(ref.getProject().getName());
					if (ref.getName() == null) {
						dataOut.writeBoolean(false);
					} else {
						dataOut.writeBoolean(true);
						dataOut.writeUTF(ref.getName());
					}
				}
			}
			output.succeed();
		} catch (IOException e) {
			String message = NLS.bind(Messages.resources_exSaveProjectLocation, target.getName());
			throw new ResourceException(IResourceStatus.INTERNAL_ERROR, null, message, e);
		}
	}
}
