/*******************************************************************************
 * Copyright (c) 2019 Ecliptical Software Inc. 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:
 *     Ecliptical Software Inc. - initial API and implementation
 *******************************************************************************/
package org.eclipse.pde.ds.internal.annotations;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.MultiRule;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.pde.core.plugin.ModelEntry;
import org.eclipse.pde.internal.core.IPluginModelListener;
import org.eclipse.pde.internal.core.PluginModelDelta;
import org.eclipse.pde.internal.core.PluginModelManager;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;

@SuppressWarnings("restriction")
public class DSLibPluginModelListener implements IPluginModelListener {

	private static DSLibPluginModelListener INSTANCE;

	private final HashMap<IJavaProject, String> projects = new HashMap<>();

	private final HashMap<String, Integer> counts = new HashMap<>();

	private DSLibPluginModelListener() {
		PluginModelManager.getInstance().addPluginModelListener(this);
	}

	private synchronized static DSLibPluginModelListener getInstance(boolean create) {
		if (create && INSTANCE == null) {
			INSTANCE = new DSLibPluginModelListener();
		}

		return INSTANCE;
	}

	private void decrementCount(String modelId) {
		Integer oldCount = counts.get(modelId);
		if (oldCount != null) {
			if (oldCount.intValue() <= 1) {
				counts.remove(modelId);
			} else {
				counts.put(modelId, oldCount.intValue() - 1);
			}
		}
	}

	public static void addProject(IJavaProject project, String modelId) {
		DSLibPluginModelListener instance = getInstance(true);
		synchronized (instance.projects) {
			String oldModelId = instance.projects.put(project, modelId);
			Integer count = instance.counts.getOrDefault(modelId, Integer.valueOf(0));
			instance.counts.put(modelId, count.intValue() + 1);
			if (oldModelId != null) {
				instance.decrementCount(oldModelId);
			}
		}
	}

	public static void removeProject(IJavaProject project) {
		DSLibPluginModelListener instance = getInstance(false);
		if (instance != null) {
			synchronized (instance.projects) {
				String oldModelId = instance.projects.remove(project);
				if (oldModelId != null) {
					instance.decrementCount(oldModelId);
				}
			}
		}
	}

	private boolean containsModel(ModelEntry[] entries, String id) {
		for (ModelEntry entry : entries) {
			if ("org.eclipse.pde.ds.lib".equals(entry.getId())) {
				return true;
			}
		}

		return false;
	}

	@Override
	public void modelsChanged(PluginModelDelta delta) {
		synchronized (projects) {
			HashSet<String> modelIds = new HashSet<>(2);
		for (String modelId : counts.keySet()) {
			if (((delta.getKind() & PluginModelDelta.ADDED) != 0 && containsModel(delta.getAddedEntries(), modelId))
					|| ((delta.getKind() & PluginModelDelta.CHANGED) != 0
							&& containsModel(delta.getChangedEntries(), modelId))
					|| ((delta.getKind() & PluginModelDelta.REMOVED) != 0
							&& containsModel(delta.getRemovedEntries(), modelId))) {
				modelIds.add(modelId);
			}
		}

			ArrayList<IJavaProject> toUpdate = new ArrayList<>(projects.size());
			if (!modelIds.isEmpty()) {
				for (Map.Entry<IJavaProject, String> entry : projects.entrySet()) {
					IJavaProject project = entry.getKey();
					String modelId = entry.getValue();
					if (modelIds.contains(modelId)) {
						toUpdate.add(project);
					}
				}
			}

			if (!toUpdate.isEmpty()) {
				requestClasspathUpdate(toUpdate);
			}
		}
	}

	private void requestClasspathUpdate(final Collection<IJavaProject> changedProjects) {
		WorkspaceJob job = new WorkspaceJob(Messages.ProjectClasspathPreferenceChangeListener_jobName) {
			@Override
			public IStatus runInWorkspace(IProgressMonitor monitor) {
				SubMonitor progress = SubMonitor.convert(monitor, Messages.DSAnnotationPreferenceListener_taskName,
						changedProjects.size());
				for (IJavaProject project : changedProjects) {
					ProjectClasspathPreferenceChangeListener.updateClasspathContainer(project, progress.newChild(1));
				}

				return Status.OK_STATUS;
			}
		};

		job.setSystem(true);

		ISchedulingRule[] rules = changedProjects.stream().map(IJavaProject::getProject)
				.toArray(size -> new ISchedulingRule[size]);
		job.setRule(new MultiRule(rules));

		Display display = Display.getCurrent();
		if (display != null) {
			PlatformUI.getWorkbench().getProgressService().showInDialog(display.getActiveShell(), job);
		}

		job.schedule();
	}

	public static void dispose() {
		DSLibPluginModelListener instance = getInstance(false);
		if (instance != null) {
			PluginModelManager.getInstance().removePluginModelListener(instance);
		}
	}
}
