/*******************************************************************************
 * Copyright (c) 2000, 2015 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
 *     James Blackburn (Broadcom Corp.) - ongoing development
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
 *******************************************************************************/
package org.eclipse.core.internal.resources;

import java.io.*;
import java.util.*;
import org.eclipse.core.internal.localstore.SafeChunkyInputStream;
import org.eclipse.core.internal.localstore.SafeFileInputStream;
import org.eclipse.core.internal.utils.Messages;
import org.eclipse.core.internal.utils.Policy;
import org.eclipse.core.internal.watson.*;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;

/**
 * A marker manager stores and retrieves markers on resources in the workspace.
 */
public class MarkerManager implements IManager {

	//singletons
	private static final MarkerInfo[] NO_MARKER_INFO = new MarkerInfo[0];
	private static final IMarker[] NO_MARKERS = new IMarker[0];
	protected MarkerTypeDefinitionCache cache = new MarkerTypeDefinitionCache();
	private long changeId = 0;
	protected Map<IPath, MarkerSet> currentDeltas = null;
	protected final MarkerDeltaManager deltaManager = new MarkerDeltaManager();

	protected Workspace workspace;
	protected MarkerWriter writer = new MarkerWriter(this);

	/**
	 * Creates a new marker manager
	 */
	public MarkerManager(Workspace workspace) {
		this.workspace = workspace;
	}

	/**
	 * Adds the given markers to the given resource.
	 *
	 * @see IResource#createMarker(String)
	 */
	public void add(IResource resource, MarkerInfo newMarker) throws CoreException {
		Resource target = (Resource) resource;
		ResourceInfo info = workspace.getResourceInfo(target.getFullPath(), false, false);
		target.checkExists(target.getFlags(info), false);
		info = workspace.getResourceInfo(resource.getFullPath(), false, true);
		//resource may have been deleted concurrently -- just bail out if this happens
		if (info == null)
			return;
		// set the M_MARKERS_SNAP_DIRTY flag to indicate that this
		// resource's markers have changed since the last snapshot
		if (isPersistent(newMarker))
			info.set(ICoreConstants.M_MARKERS_SNAP_DIRTY);
		//Concurrency: copy the marker set on modify
		MarkerSet markers = info.getMarkers(true);
		if (markers == null)
			markers = new MarkerSet(1);
		basicAdd(resource, markers, newMarker);
		if (!markers.isEmpty())
			info.setMarkers(markers);
	}

	/**
	 * Adds the new markers to the given set of markers.  If added, the markers
	 * are associated with the specified resource.IMarkerDeltas for Added markers
	 * are generated.
	 */
	private void basicAdd(IResource resource, MarkerSet markers, MarkerInfo newMarker) throws CoreException {
		// should always be a new marker.
		if (newMarker.getId() != MarkerInfo.UNDEFINED_ID) {
			String message = Messages.resources_changeInAdd;
			throw new ResourceException(new ResourceStatus(IResourceStatus.INTERNAL_ERROR, resource.getFullPath(), message));
		}
		newMarker.setId(workspace.nextMarkerId());
		markers.add(newMarker);
		IMarkerSetElement[] changes = new IMarkerSetElement[1];
		changes[0] = new MarkerDelta(IResourceDelta.ADDED, resource, newMarker);
		changedMarkers(resource, changes);
	}

	/**
	 * Returns the markers in the given set of markers which match the given type.
	 */
	protected MarkerInfo[] basicFindMatching(MarkerSet markers, String type, boolean includeSubtypes) {
		int size = markers.size();
		if (size <= 0)
			return NO_MARKER_INFO;
		List<MarkerInfo> result = new ArrayList<>(size);
		IMarkerSetElement[] elements = markers.elements();
		for (IMarkerSetElement element : elements) {
			MarkerInfo marker = (MarkerInfo) element;
			// if the type is null then we are looking for all types of markers
			if (type == null)
				result.add(marker);
			else {
				if (includeSubtypes) {
					if (cache.isSubtype(marker.getType(), type))
						result.add(marker);
				} else {
					if (marker.getType().equals(type))
						result.add(marker);
				}
			}
		}
		size = result.size();
		if (size <= 0)
			return NO_MARKER_INFO;
		return result.toArray(new MarkerInfo[size]);
	}

	protected int basicFindMaxSeverity(MarkerSet markers, String type, boolean includeSubtypes) {
		int max = -1;
		int size = markers.size();
		if (size <= 0)
			return max;
		IMarkerSetElement[] elements = markers.elements();
		for (IMarkerSetElement element : elements) {
			MarkerInfo marker = (MarkerInfo) element;
			// if the type is null then we are looking for all types of markers
			if (type == null)
				max = Math.max(max, getSeverity(marker));
			else {
				if (includeSubtypes) {
					if (cache.isSubtype(marker.getType(), type))
						max = Math.max(max, getSeverity(marker));
				} else {
					if (marker.getType().equals(type))
						max = Math.max(max, getSeverity(marker));
				}
			}
			if (max >= IMarker.SEVERITY_ERROR) {
				break;
			}
		}
		return max;
	}

	private int getSeverity(MarkerInfo marker) {
		Object o = marker.getAttribute(IMarker.SEVERITY);
		if (o instanceof Integer) {
			Integer i = (Integer) o;
			return i.intValue();
		}
		return -1;
	}

	/**
	 * Removes markers of the specified type from the given resource.
	 * Note: this method is protected to avoid creation of a synthetic accessor (it
	 * is called from an anonymous inner class).
	 */
	protected void basicRemoveMarkers(ResourceInfo info, IPathRequestor requestor, String type, boolean includeSubtypes) {
		MarkerSet markers = info.getMarkers(false);
		if (markers == null)
			return;
		IMarkerSetElement[] matching;
		IPath path;
		if (type == null) {
			// if the type is null, all markers are to be removed.
			//now we need to crack open the tree
			path = requestor.requestPath();
			info = workspace.getResourceInfo(path, false, true);
			info.setMarkers(null);
			matching = markers.elements();
		} else {
			matching = basicFindMatching(markers, type, includeSubtypes);
			// if none match, there is nothing to remove
			if (matching.length == 0)
				return;
			//now we need to crack open the tree
			path = requestor.requestPath();
			info = workspace.getResourceInfo(path, false, true);
			//Concurrency: copy the marker set on modify
			markers = info.getMarkers(true);
			// remove all the matching markers and also the whole
			// set if there are no remaining markers
			if (markers.size() == matching.length) {
				info.setMarkers(null);
			} else {
				markers.removeAll(matching);
				info.setMarkers(markers);
			}
		}
		info.set(ICoreConstants.M_MARKERS_SNAP_DIRTY);
		IMarkerSetElement[] changes = new IMarkerSetElement[matching.length];
		IResource resource = workspace.getRoot().findMember(path);
		for (int i = 0; i < matching.length; i++)
			changes[i] = new MarkerDelta(IResourceDelta.REMOVED, resource, (MarkerInfo) matching[i]);
		changedMarkers(resource, changes);
		return;
	}

	/**
	 * Adds the markers on the given target which match the specified type to the list.
	 */
	protected void buildMarkers(IMarkerSetElement[] markers, IPath path, int type, ArrayList<IMarker> list) {
		if (markers.length == 0)
			return;
		IResource resource = workspace.newResource(path, type);
		list.ensureCapacity(list.size() + markers.length);
		for (IMarkerSetElement marker : markers) {
			list.add(new Marker(resource, ((MarkerInfo) marker).getId()));
		}
	}

	/**
	 * Markers have changed on the given resource.  Remember the changes for subsequent notification.
	 */
	protected void changedMarkers(IResource resource, IMarkerSetElement[] changes) {
		if (changes == null || changes.length == 0)
			return;
		changeId++;
		if (currentDeltas == null)
			currentDeltas = deltaManager.newGeneration(changeId);
		IPath path = resource.getFullPath();
		MarkerSet previousChanges = currentDeltas.get(path);
		MarkerSet result = MarkerDelta.merge(previousChanges, changes);
		if (result.size() == 0)
			currentDeltas.remove(path);
		else
			currentDeltas.put(path, result);
		ResourceInfo info = workspace.getResourceInfo(path, false, true);
		if (info != null)
			info.incrementMarkerGenerationCount();
	}

	/**
	 * Returns the marker with the given id or <code>null</code> if none is found.
	 */
	public IMarker findMarker(IResource resource, long id) {
		MarkerInfo info = findMarkerInfo(resource, id);
		return info == null ? null : new Marker(resource, info.getId());
	}

	/**
	 * Returns the marker with the given id or <code>null</code> if none is found.
	 */
	public MarkerInfo findMarkerInfo(IResource resource, long id) {
		ResourceInfo info = workspace.getResourceInfo(resource.getFullPath(), false, false);
		if (info == null)
			return null;
		MarkerSet markers = info.getMarkers(false);
		if (markers == null)
			return null;
		return (MarkerInfo) markers.get(id);
	}

	/**
	 * Returns all markers of the specified type on the given target, with option
	 * to search the target's children.
	 * Passing <code>null</code> for the type specifies a match
	 * for all types (i.e., <code>null</code> is a wildcard.
	 */
	public IMarker[] findMarkers(IResource target, final String type, final boolean includeSubtypes, int depth) {
		ArrayList<IMarker> result = new ArrayList<>();
		doFindMarkers(target, result, type, includeSubtypes, depth);
		if (result.size() == 0)
			return NO_MARKERS;
		return result.toArray(new IMarker[result.size()]);
	}

	/**
	 * Fills the provided list with all markers of the specified type on the given target,
	 * with option to search the target's children.
	 * Passing <code>null</code> for the type specifies a match
	 * for all types (i.e., <code>null</code> is a wildcard.
	 */
	public void doFindMarkers(IResource target, ArrayList<IMarker> result, final String type, final boolean includeSubtypes, int depth) {
		//optimize the deep searches with an element tree visitor
		if (depth == IResource.DEPTH_INFINITE && target.getType() != IResource.FILE)
			visitorFindMarkers(target.getFullPath(), result, type, includeSubtypes);
		else
			recursiveFindMarkers(target.getFullPath(), result, type, includeSubtypes, depth);
	}

	/**
	 * Finds the max severity across all problem markers on the given target,
	 * with option to search the target's children.
	 */
	public int findMaxProblemSeverity(IResource target, String type, boolean includeSubtypes, int depth) {
		//optimize the deep searches with an element tree visitor
		if (depth == IResource.DEPTH_INFINITE && target.getType() != IResource.FILE)
			return visitorFindMaxSeverity(target.getFullPath(), type, includeSubtypes);
		return recursiveFindMaxSeverity(target.getFullPath(), type, includeSubtypes, depth);
	}

	public long getChangeId() {
		return changeId;
	}

	/**
	 * Returns the map of all marker deltas since the given change Id.
	 */
	public Map<IPath, MarkerSet> getMarkerDeltas(long startChangeId) {
		return deltaManager.assembleDeltas(startChangeId);
	}

	/**
	 * Returns true if this manager has a marker delta record
	 * for the given marker id, and false otherwise.
	 */
	boolean hasDelta(IPath path, long id) {
		if (currentDeltas == null)
			return false;
		MarkerSet set = currentDeltas.get(path);
		if (set == null)
			return false;
		return set.get(id) != null;
	}

	/**
	 * Returns true if the given marker is persistent, and false
	 * otherwise.
	 */
	public boolean isPersistent(MarkerInfo info) {
		if (!cache.isPersistent(info.getType()))
			return false;
		Object isTransient = info.getAttribute(IMarker.TRANSIENT);
		return isTransient == null || !(isTransient instanceof Boolean) || !((Boolean) isTransient).booleanValue();
	}

	/**
	 * Returns true if the given marker type is persistent, and false
	 * otherwise.
	 */
	public boolean isPersistentType(String type) {
		return cache.isPersistent(type);
	}

	/**
	 * Returns true if <code>type</code> is a sub type of <code>superType</code>.
	 */
	public boolean isSubtype(String type, String superType) {
		return cache.isSubtype(type, superType);
	}

	public void moved(final IResource source, final IResource destination, int depth) throws CoreException {
		final int count = destination.getFullPath().segmentCount();

		// we removed from the source and added to the destination
		IResourceVisitor visitor = resource -> {
			Resource r = (Resource) resource;
			ResourceInfo info = r.getResourceInfo(false, true);
			MarkerSet markers = info.getMarkers(false);
			if (markers == null)
				return true;
			info.set(ICoreConstants.M_MARKERS_SNAP_DIRTY);
			IMarkerSetElement[] removed = new IMarkerSetElement[markers.size()];
			IMarkerSetElement[] added = new IMarkerSetElement[markers.size()];
			IPath path = resource.getFullPath().removeFirstSegments(count);
			path = source.getFullPath().append(path);
			IResource sourceChild = workspace.newResource(path, resource.getType());
			IMarkerSetElement[] elements = markers.elements();
			for (int i = 0; i < elements.length; i++) {
				// calculate the ADDED delta
				MarkerInfo markerInfo = (MarkerInfo) elements[i];
				MarkerDelta delta = new MarkerDelta(IResourceDelta.ADDED, resource, markerInfo);
				added[i] = delta;
				// calculate the REMOVED delta
				delta = new MarkerDelta(IResourceDelta.REMOVED, sourceChild, markerInfo);
				removed[i] = delta;
			}
			changedMarkers(resource, added);
			changedMarkers(sourceChild, removed);
			return true;
		};
		destination.accept(visitor, depth, IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS | IContainer.INCLUDE_HIDDEN);
	}

	/**
	 * Adds the markers for a subtree of resources to the list.
	 */
	private void recursiveFindMarkers(IPath path, ArrayList<IMarker> list, String type, boolean includeSubtypes, int depth) {
		ResourceInfo info = workspace.getResourceInfo(path, false, false);
		if (info == null)
			return;
		MarkerSet markers = info.getMarkers(false);

		//add the matching markers for this resource
		if (markers != null) {
			IMarkerSetElement[] matching;
			if (type == null)
				matching = markers.elements();
			else
				matching = basicFindMatching(markers, type, includeSubtypes);
			buildMarkers(matching, path, info.getType(), list);
		}

		//recurse
		if (depth == IResource.DEPTH_ZERO || info.getType() == IResource.FILE)
			return;
		if (depth == IResource.DEPTH_ONE)
			depth = IResource.DEPTH_ZERO;
		for (IPath child : workspace.getElementTree().getChildren(path)) {
			recursiveFindMarkers(child, list, type, includeSubtypes, depth);
		}
	}

	/**
	 * Finds the max severity across problem markers for a subtree of resources.
	 */
	private int recursiveFindMaxSeverity(IPath path, String type, boolean includeSubtypes, int depth) {
		ResourceInfo info = workspace.getResourceInfo(path, false, false);
		if (info == null)
			return -1;
		MarkerSet markers = info.getMarkers(false);

		//add the matching markers for this resource
		int max = -1;
		if (markers != null) {
			max = basicFindMaxSeverity(markers, type, includeSubtypes);
			if (max >= IMarker.SEVERITY_ERROR) {
				return max;
			}
		}

		//recurse
		if (depth == IResource.DEPTH_ZERO || info.getType() == IResource.FILE)
			return max;
		if (depth == IResource.DEPTH_ONE)
			depth = IResource.DEPTH_ZERO;
		for (IPath child : workspace.getElementTree().getChildren(path)) {
			max = Math.max(max, recursiveFindMaxSeverity(child, type, includeSubtypes, depth));
			if (max >= IMarker.SEVERITY_ERROR) {
				break;
			}
		}
		return max;
	}

	/**
	 * Adds the markers for a subtree of resources to the list.
	 */
	private void recursiveRemoveMarkers(final IPath path, String type, boolean includeSubtypes, int depth) {
		ResourceInfo info = workspace.getResourceInfo(path, false, false);
		if (info == null) //phantoms don't have markers
			return;
		IPathRequestor requestor = new IPathRequestor() {
			@Override
			public String requestName() {
				return path.lastSegment();
			}

			@Override
			public IPath requestPath() {
				return path;
			}
		};
		basicRemoveMarkers(info, requestor, type, includeSubtypes);
		//recurse
		if (depth == IResource.DEPTH_ZERO || info.getType() == IResource.FILE)
			return;
		if (depth == IResource.DEPTH_ONE)
			depth = IResource.DEPTH_ZERO;
		for (IPath child : workspace.getElementTree().getChildren(path)) {
			recursiveRemoveMarkers(child, type, includeSubtypes, depth);
		}
	}

	/**
	 * Removes the specified marker
	 */
	public void removeMarker(IResource resource, long id) {
		MarkerInfo markerInfo = findMarkerInfo(resource, id);
		if (markerInfo == null)
			return;
		ResourceInfo info = ((Workspace) resource.getWorkspace()).getResourceInfo(resource.getFullPath(), false, true);
		//Concurrency: copy the marker set on modify
		MarkerSet markers = info.getMarkers(true);
		int size = markers.size();
		markers.remove(markerInfo);
		// if that was the last marker remove the set to save space.
		info.setMarkers(markers.size() == 0 ? null : markers);
		// if we actually did remove a marker, post a delta for the change.
		if (markers.size() != size) {
			if (isPersistent(markerInfo))
				info.set(ICoreConstants.M_MARKERS_SNAP_DIRTY);
			IMarkerSetElement[] change = new IMarkerSetElement[] {new MarkerDelta(IResourceDelta.REMOVED, resource, markerInfo)};
			changedMarkers(resource, change);
		}
	}

	/**
	 * Remove all markers for the given resource to the specified depth.
	 */
	public void removeMarkers(IResource resource, int depth) {
		removeMarkers(resource, null, false, depth);
	}

	/**
	 * Remove all markers with the given type from the node at the given path.
	 * Passing <code>null</code> for the type specifies a match
	 * for all types (i.e., <code>null</code> is a wildcard.
	 */
	public void removeMarkers(IResource target, final String type, final boolean includeSubtypes, int depth) {
		if (depth == IResource.DEPTH_INFINITE && target.getType() != IResource.FILE)
			visitorRemoveMarkers(target.getFullPath(), type, includeSubtypes);
		else
			recursiveRemoveMarkers(target.getFullPath(), type, includeSubtypes, depth);
	}

	/**
	 * Reset the marker deltas up to but not including the given start Id.
	 */
	public void resetMarkerDeltas(long startId) {
		currentDeltas = null;
		deltaManager.resetDeltas(startId);
	}

	public void restore(IResource resource, boolean generateDeltas, IProgressMonitor monitor) throws CoreException {
		// first try and load the last saved file, then apply the snapshots
		restoreFromSave(resource, generateDeltas);
		restoreFromSnap(resource);
	}

	protected void restoreFromSave(IResource resource, boolean generateDeltas) throws CoreException {
		IPath sourceLocation = workspace.getMetaArea().getMarkersLocationFor(resource);
		IPath tempLocation = workspace.getMetaArea().getBackupLocationFor(sourceLocation);
		java.io.File sourceFile = new java.io.File(sourceLocation.toOSString());
		java.io.File tempFile = new java.io.File(tempLocation.toOSString());
		if (!sourceFile.exists() && !tempFile.exists())
			return;
		try {
			try (
				DataInputStream input = new DataInputStream(new SafeFileInputStream(sourceLocation.toOSString(), tempLocation.toOSString()));
			) {
				MarkerReader reader = new MarkerReader(workspace);
				reader.read(input, generateDeltas);
			}
		} catch (Exception e) {
			//don't let runtime exceptions such as ArrayIndexOutOfBounds prevent startup
			String msg = NLS.bind(Messages.resources_readMeta, sourceLocation);
			throw new ResourceException(IResourceStatus.FAILED_READ_METADATA, sourceLocation, msg, e);
		}
	}

	protected void restoreFromSnap(IResource resource) {
		IPath sourceLocation = workspace.getMetaArea().getMarkersSnapshotLocationFor(resource);
		if (!sourceLocation.toFile().exists())
			return;
		try {
			DataInputStream input = new DataInputStream(new SafeChunkyInputStream(sourceLocation.toFile()));
			try {
				MarkerSnapshotReader reader = new MarkerSnapshotReader(workspace);
				while (true)
					reader.read(input);
			} catch (EOFException eof) {
				// ignore end of file
			} finally {
				input.close();
			}
		} catch (Exception e) {
			// only log the exception, we should not fail restoring the snapshot
			String msg = NLS.bind(Messages.resources_readMeta, sourceLocation);
			Policy.log(new ResourceStatus(IResourceStatus.FAILED_READ_METADATA, sourceLocation, msg, e));
		}
	}

	public void save(ResourceInfo info, IPathRequestor requestor, DataOutputStream output, List<String> list) throws IOException {
		writer.save(info, requestor, output, list);
	}

	@Override
	public void shutdown(IProgressMonitor monitor) {
		// do nothing
	}

	public void snap(ResourceInfo info, IPathRequestor requestor, DataOutputStream output) throws IOException {
		writer.snap(info, requestor, output);
	}

	@Override
	public void startup(IProgressMonitor monitor) {
		// do nothing
	}

	/**
	 * Adds the markers for a subtree of resources to the list.
	 */
	private void visitorFindMarkers(IPath path, final ArrayList<IMarker> list, final String type, final boolean includeSubtypes) {
		IElementContentVisitor visitor = (tree, requestor, elementContents) -> {
			ResourceInfo info = (ResourceInfo) elementContents;
			if (info == null)
				return false;
			MarkerSet markers = info.getMarkers(false);

			//add the matching markers for this resource
			if (markers != null) {
				IMarkerSetElement[] matching;
				if (type == null)
					matching = markers.elements();
				else
					matching = basicFindMatching(markers, type, includeSubtypes);
				buildMarkers(matching, requestor.requestPath(), info.getType(), list);
			}
			return true;
		};
		new ElementTreeIterator(workspace.getElementTree(), path).iterate(visitor);
	}

	/**
	 * Finds the max severity across problem markers for a subtree of resources.
	 */
	private int visitorFindMaxSeverity(IPath path, final String type, final boolean includeSubtypes) {
		class MaxSeverityVisitor implements IElementContentVisitor {
			int max = -1;

			@Override
			public boolean visitElement(ElementTree tree, IPathRequestor requestor, Object elementContents) {
				// bail if an earlier sibling already hit the max
				if (max >= IMarker.SEVERITY_ERROR) {
					return false;
				}
				ResourceInfo info = (ResourceInfo) elementContents;
				if (info == null)
					return false;
				MarkerSet markers = info.getMarkers(false);

				//add the matching markers for this resource
				if (markers != null) {
					max = Math.max(max, basicFindMaxSeverity(markers, type, includeSubtypes));
				}
				return max < IMarker.SEVERITY_ERROR;
			}
		}
		MaxSeverityVisitor visitor = new MaxSeverityVisitor();
		new ElementTreeIterator(workspace.getElementTree(), path).iterate(visitor);
		return visitor.max;
	}

	/**
	 * Adds the markers for a subtree of resources to the list.
	 */
	private void visitorRemoveMarkers(IPath path, final String type, final boolean includeSubtypes) {
		IElementContentVisitor visitor = (tree, requestor, elementContents) -> {
			ResourceInfo info = (ResourceInfo) elementContents;
			if (info == null)
				return false;
			basicRemoveMarkers(info, requestor, type, includeSubtypes);
			return true;
		};
		new ElementTreeIterator(workspace.getElementTree(), path).iterate(visitor);
	}
}
