/*******************************************************************************
 * Copyright (c) 2000, 2011 QNX Software Systems 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:
 *     QNX Software Systems - Initial API and implementation
 *     Red Hat Inc. - add setTargets method
 *******************************************************************************/
package org.eclipse.cdt.make.internal.core;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Vector;

import org.eclipse.cdt.make.core.IMakeTarget;
import org.eclipse.cdt.make.core.IMakeTargetListener;
import org.eclipse.cdt.make.core.IMakeTargetManager;
import org.eclipse.cdt.make.core.MakeCorePlugin;
import org.eclipse.cdt.make.core.MakeTargetEvent;
import org.eclipse.core.resources.ICommand;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;

public class MakeTargetManager implements IMakeTargetManager, IResourceChangeListener {
	private static String TARGET_BUILD_EXT = "MakeTargetBuilder"; //$NON-NLS-1$

	private static String TARGETS_EXT = "targets"; //$NON-NLS-1$

	private final ListenerList listeners = new ListenerList();
	private final Map<IProject, ProjectTargets> projectMap = new HashMap<>();
	private HashMap<String, String> builderMap;
	protected Vector<IProject> fProjects = new Vector<>();

	public MakeTargetManager() {
	}

	@Override
	public IMakeTarget createTarget(IProject project, String name, String targetBuilderID) throws CoreException {
		return new MakeTarget(this, project, targetBuilderID, name);
	}

	@Override
	public void addTarget(IMakeTarget target) throws CoreException {
		addTarget(null, target);
	}

	@Override
	public void addTarget(IContainer container, IMakeTarget target) throws CoreException {
		if (container instanceof IWorkspaceRoot) {
			throw new CoreException(new Status(IStatus.ERROR, MakeCorePlugin.getUniqueIdentifier(), -1,
					MakeMessages.getString("MakeTargetManager.add_to_workspace_root"), null)); //$NON-NLS-1$
		}
		ProjectTargets projectTargets = projectMap.get(target.getProject());
		if (projectTargets == null) {
			projectTargets = readTargets(target.getProject());
		}
		target.setContainer(container == null ? target.getProject() : container);
		projectTargets.add(target);
		try {
			writeTargets(projectTargets);
		} catch (CoreException e) {
			projectTargets.remove(target);
			throw e;
		}
		notifyListeners(new MakeTargetEvent(this, MakeTargetEvent.TARGET_ADD, target));
	}

	@Override
	public void setTargets(IContainer container, IMakeTarget[] targets) throws CoreException {
		if (container instanceof IWorkspaceRoot) {
			throw new CoreException(new Status(IStatus.ERROR, MakeCorePlugin.getUniqueIdentifier(), -1,
					MakeMessages.getString("MakeTargetManager.add_to_workspace_root"), null)); //$NON-NLS-1$
		}
		ProjectTargets projectTargets = projectMap.get(targets[0].getProject());
		if (projectTargets == null) {
			projectTargets = readTargets(targets[0].getProject());
		}
		if (container == null)
			container = targets[0].getProject();
		IMakeTarget[] oldTargets = projectTargets.get(container);
		projectTargets.set(container, targets);
		try {
			writeTargets(projectTargets);
		} catch (CoreException e) {
			// we only need to reset the targets if writing of targets fails
			projectTargets.set(container, oldTargets);
			throw e;
		}
		notifyListeners(new MakeTargetEvent(this, MakeTargetEvent.TARGET_ADD, targets[0]));
	}

	@Override
	public boolean targetExists(IMakeTarget target) {
		ProjectTargets projectTargets = projectMap.get(target.getProject());
		if (projectTargets == null) {
			projectTargets = readTargets(target.getProject());
		}
		return projectTargets.contains(target);
	}

	@Override
	public void removeTarget(IMakeTarget target) throws CoreException {
		ProjectTargets projectTargets = projectMap.get(target.getProject());
		if (projectTargets == null) {
			projectTargets = readTargets(target.getProject());
		}
		if (projectTargets.remove(target)) {
			try {
				writeTargets(projectTargets);
			} catch (CoreException e) {
				projectTargets.add(target);
				throw e;
			}
			notifyListeners(new MakeTargetEvent(this, MakeTargetEvent.TARGET_REMOVED, target));
		}
	}

	@Override
	public void renameTarget(IMakeTarget target, String name) throws CoreException {
		IMakeTarget makeTarget = target;

		ProjectTargets projectTargets = projectMap.get(makeTarget.getProject());
		if (projectTargets == null) {
			projectTargets = readTargets(makeTarget.getProject());
		}

		makeTarget.setName(name);
		if (projectTargets.contains(makeTarget)) {
			updateTarget(makeTarget);
		}
	}

	@Override
	public IMakeTarget[] getTargets(IContainer container) throws CoreException {
		ProjectTargets projectTargets = projectMap.get(container.getProject());
		if (projectTargets == null) {
			projectTargets = readTargets(container.getProject());
		}
		return projectTargets.get(container);
	}

	@Override
	public IMakeTarget findTarget(IContainer container, String name) throws CoreException {
		ProjectTargets projectTargets = projectMap.get(container.getProject());
		if (projectTargets == null) {
			projectTargets = readTargets(container.getProject());
		}
		return projectTargets.findTarget(container, name);
	}

	@Override
	public IProject[] getTargetBuilderProjects() {
		return fProjects.toArray(new IProject[fProjects.size()]);
	}

	@Override
	public String[] getTargetBuilders(IProject project) {
		if (fProjects.contains(project) || hasTargetBuilder(project)) {
			try {
				Vector<String> ids = new Vector<>();
				IProjectDescription description = project.getDescription();
				ICommand commands[] = description.getBuildSpec();
				for (ICommand command : commands) {
					for (Entry<String, String> entry : builderMap.entrySet()) {
						if (entry.getValue().equals(command.getBuilderName())) {
							ids.add(entry.getKey());
						}
					}
				}
				return ids.toArray(new String[ids.size()]);
			} catch (CoreException e) {
			}
		}
		return new String[0];
	}

	@Override
	public boolean hasTargetBuilder(IProject project) {
		try {
			if (project.isAccessible()) {
				IProjectDescription description = project.getDescription();
				ICommand commands[] = description.getBuildSpec();
				for (ICommand command : commands) {
					if (builderMap.containsValue(command.getBuilderName())) {
						return true;
					}
				}
			}
		} catch (CoreException e) {
		}
		return false;
	}

	public void startup() {
		initializeBuilders();
		IProject projects[] = ResourcesPlugin.getWorkspace().getRoot().getProjects();
		for (IProject project : projects) {
			if (hasTargetBuilder(project)) {
				fProjects.add(project);
			}
		}
		ResourcesPlugin.getWorkspace().addResourceChangeListener(this);
	}

	public void shutdown() {
		ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
	}

	@Override
	public void resourceChanged(IResourceChangeEvent event) {
		IResourceDelta delta = event.getDelta();
		if (delta != null) {
			try {
				delta.accept(new MakeTargetVisitor());
			} catch (CoreException e) {
				MakeCorePlugin.log(e);
			}
		}
	}

	class MakeTargetVisitor implements IResourceDeltaVisitor {
		/**
		 * @see IResourceDeltaVisitor#visit(IResourceDelta)
		 */
		@Override
		public boolean visit(IResourceDelta delta) {
			if (delta == null) {
				return false;
			}
			IResource resource = delta.getResource();
			if (resource.getType() == IResource.PROJECT) {
				IProject project = (IProject) resource;
				int flags = delta.getFlags();
				int deltaKind = delta.getKind();
				if (deltaKind == IResourceDelta.ADDED) {
					if (hasTargetBuilder(project) && !fProjects.contains(project)) {
						fProjects.add(project);
						notifyListeners(
								new MakeTargetEvent(MakeTargetManager.this, MakeTargetEvent.PROJECT_ADDED, project));
					}
				} else if (deltaKind == IResourceDelta.REMOVED) {
					if (fProjects.contains(project)) {
						deleteTargets(project);
						fProjects.remove(project);
						notifyListeners(
								new MakeTargetEvent(MakeTargetManager.this, MakeTargetEvent.PROJECT_REMOVED, project));
					}
				} else if (deltaKind == IResourceDelta.CHANGED) {
					if (0 != (flags & IResourceDelta.DESCRIPTION)) {
						if (fProjects.contains(project) && !hasTargetBuilder(project)) {
							fProjects.remove(project);
							projectMap.remove(project);
							notifyListeners(new MakeTargetEvent(MakeTargetManager.this, MakeTargetEvent.PROJECT_REMOVED,
									project));
						} else if (!fProjects.contains(project) && hasTargetBuilder(project)) {
							fProjects.add(project);
							notifyListeners(new MakeTargetEvent(MakeTargetManager.this, MakeTargetEvent.PROJECT_ADDED,
									project));
						}
					}
					if (0 != (flags & IResourceDelta.OPEN)) {
						if (!project.isOpen() && fProjects.contains(project)) {
							fProjects.remove(project);
							projectMap.remove(project);
							notifyListeners(new MakeTargetEvent(MakeTargetManager.this, MakeTargetEvent.PROJECT_REMOVED,
									project));
						} else if (project.isOpen() && hasTargetBuilder(project) && !fProjects.contains(project)) {
							fProjects.add(project);
							notifyListeners(new MakeTargetEvent(MakeTargetManager.this, MakeTargetEvent.PROJECT_ADDED,
									project));
						}
					}
				}
				return false;
			}
			return resource instanceof IWorkspaceRoot;
		}
	}

	protected void updateTarget(IMakeTarget target) throws CoreException {
		if (target.getContainer() != null) { // target has not been added to manager.
			ProjectTargets projectTargets = projectMap.get(target.getProject());
			if (projectTargets == null || !projectTargets.contains(target)) {
				return; // target has not been added to manager.
			}
			writeTargets(projectTargets);
			notifyListeners(new MakeTargetEvent(this, MakeTargetEvent.TARGET_CHANGED, target));
		}
	}

	protected void writeTargets(ProjectTargets projectTargets) throws CoreException {
		projectTargets.saveTargets();
	}

	protected ProjectTargets readTargets(IProject project) {
		ProjectTargets projectTargets = new ProjectTargets(this, project);
		projectMap.put(project, projectTargets);
		return projectTargets;
	}

	protected void deleteTargets(IProject project) {
		//Historical: We clean up after all other parts.
		IPath targetFilePath = MakeCorePlugin.getDefault().getStateLocation().append(project.getName())
				.addFileExtension(TARGETS_EXT);
		File targetFile = targetFilePath.toFile();
		if (targetFile.exists()) {
			targetFile.delete();
		}
		projectMap.remove(project);
	}

	protected void initializeBuilders() {
		builderMap = new HashMap<>();
		IExtensionPoint point = Platform.getExtensionRegistry().getExtensionPoint(MakeCorePlugin.PLUGIN_ID,
				MakeTargetManager.TARGET_BUILD_EXT);
		IExtension[] extensions = point.getExtensions();
		for (IExtension extension : extensions) {
			IConfigurationElement[] cfgElements = extension.getConfigurationElements();
			for (IConfigurationElement cfgElement : cfgElements) {
				if (cfgElement.getName().equals("builder")) { //$NON-NLS-1$
					String builderID = cfgElement.getAttribute("builderID"); //$NON-NLS-1$
					String targetID = cfgElement.getAttribute("id"); //$NON-NLS-1$
					builderMap.put(targetID, builderID);
				}
			}
		}
	}

	protected void notifyListeners(MakeTargetEvent event) {
		for (Object listener : listeners.getListeners()) {
			((IMakeTargetListener) listener).targetChanged(event);
		}
	}

	@Override
	public void addListener(IMakeTargetListener listener) {
		listeners.add(listener);
	}

	@Override
	public void removeListener(IMakeTargetListener listener) {
		listeners.remove(listeners);
	}

	@Override
	public String getBuilderID(String targetBuilderID) {
		return builderMap.get(targetBuilderID);
	}
}
