/*******************************************************************************
 * Copyright (c) 2003, 2020 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *     Philipp Bumann <bumannp@gmail.com> - Bug 477602
 *******************************************************************************/
package org.eclipse.e4.ui.progress.internal;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.inject.Singleton;

import org.eclipse.core.commands.common.EventManager;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.e4.core.di.annotations.Creatable;
import org.eclipse.e4.ui.progress.IDisposableAction;
import org.eclipse.e4.ui.progress.IProgressConstants;

/**
 * This singleton remembers all JobTreeElements that should be preserved (e.g.
 * because their associated Jobs have the "keep" property set).
 */
@Creatable
@Singleton
public class FinishedJobs extends EventManager {

	/**
	 * Interface for notify listeners.
	 */
	static interface KeptJobsListener {

		/**
		 * A job to be kept has finished
		 *
		 * @param jte the element that finished
		 */
		void finished(JobTreeElement jte);

		/**
		 * A kept job has been removed.
		 *
		 * @param jte {@code null} if all elements were removed
		 */
		void removed(JobTreeElement jte);
	}

	final IJobProgressManagerListener listener;

	private final Set<JobTreeElement> keptjobinfos = new LinkedHashSet<>();

	private HashMap<Object, Long> finishedTime = new HashMap<>();

	private static final JobTreeElement[] EMPTY_INFOS = new JobTreeElement[0];

	public FinishedJobs() {
		listener = new IJobProgressManagerListener() {
			@Override
			public void addJob(JobInfo info) {
				removeDuplicates(info);
			}

			@Override
			public void addGroup(GroupInfo info) {
				removeDuplicates(info);
			}

			@Override
			public void refreshJobInfo(JobInfo info) {
				checkTasks(info);
			}

			@Override
			public void refreshGroup(GroupInfo info) {
				// no action
			}

			@Override
			public void refreshAll() {
				// no action
			}

			@Override
			public void removeJob(JobInfo info) {
				if (keep(info)) {
					synchronized (keptjobinfos) {
						removeDuplicates(info);
						add(info);
					}
				}
			}

			@Override
			public void removeGroup(GroupInfo group) {
				// no action
			}

			@Override
			public boolean showsDebug() {
				return false;
			}
		};
	}

	/**
	 * Returns true if JobInfo indicates that it must be kept.
	 */
	static boolean keep(JobInfo info) {
		Job job = info.getJob();
		if (job != null) {
			Object prop = job.getProperty(ProgressManagerUtil.KEEP_PROPERTY);
			if (prop instanceof Boolean) {
				if (((Boolean) prop).booleanValue()) {
					return true;
				}
			}

			prop = job.getProperty(ProgressManagerUtil.KEEPONE_PROPERTY);
			if (prop instanceof Boolean) {
				if (((Boolean) prop).booleanValue()) {
					return true;
				}
			}

			IStatus status = job.getResult();
			if (status != null && status.getSeverity() == IStatus.ERROR) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Register for notification.
	 *
	 * @param l listener to add. Not {@code null}.
	 */
	void addListener(KeptJobsListener l) {
		addListenerObject(l);
	}

	/**
	 * Deregister for notification.
	 *
	 * @param l listener to remove. Not {@code null}.
	 */
	void removeListener(KeptJobsListener l) {
		removeListenerObject(l);
	}

	private void removeDuplicates(GroupInfo info) {
		Object[] objects = info.getChildren();
		for (Object object : objects) {
			if (object instanceof JobInfo) {
				removeDuplicates((JobInfo) object);
			}
		}
	}

	private void removeDuplicates(JobTreeElement info) {
		JobTreeElement[] toBeRemoved = findJobsToRemove(info);
		if (toBeRemoved != null) {
			for (JobTreeElement element : toBeRemoved) {
				remove(element);
			}
		}
	}

	/**
	 * Add given Job to list of kept jobs.
	 */
	private void add(JobInfo info) {
		boolean fire = false;

		synchronized (keptjobinfos) {
			if (!keptjobinfos.contains(info)) {
				keptjobinfos.add(info);

				long now = System.currentTimeMillis();
				finishedTime.put(info, Long.valueOf(now));

				GroupInfo parent = info.getParent();
				if (!(parent == null || keptjobinfos.contains(parent))) {
					keptjobinfos.add(parent);
					finishedTime.put(parent, Long.valueOf(now));
				}

				fire = true;
			}
		}

		if (fire) {
			Object l[] = getListeners();
			for (Object element : l) {
				KeptJobsListener jv = (KeptJobsListener) element;
				jv.finished(info);
			}
		}
	}

	static void disposeAction(JobTreeElement jte) {
		if (jte.isJobInfo()) {
			JobInfo ji = (JobInfo) jte;
			Job job = ji.getJob();
			if (job != null) {
				Object prop = job
						.getProperty(IProgressConstants.ACTION_PROPERTY);
				if (prop instanceof IDisposableAction) {
					((IDisposableAction) prop).dispose();
				}
			}
		}
	}

	private JobTreeElement[] findJobsToRemove(JobTreeElement info) {

		if (info.isJobInfo()) {
			Job myJob = ((JobInfo) info).getJob();

			if (myJob != null) {

				Object prop = myJob
						.getProperty(ProgressManagerUtil.KEEPONE_PROPERTY);
				if (prop instanceof Boolean && ((Boolean) prop).booleanValue()) {
					ArrayList<JobTreeElement> found = null;
					JobTreeElement[] all = getKeptElements();
					for (JobTreeElement jte : all) {
						if (jte != info && jte.isJobInfo()) {
							Job job = ((JobInfo) jte).getJob();
							if (job != null && job != myJob
									&& job.belongsTo(myJob)) {
								if (found == null) {
									found = new ArrayList<>();
								}
								found.add(jte);
							}
						}
					}
					if (found != null) {
						return found
								.toArray(new JobTreeElement[found.size()]);
					}
				}
			}
		}
		return null;
	}

	private void checkTasks(JobInfo info) {
		if (keep(info)) {
			TaskInfo tinfo = info.getTaskInfo();
			if (tinfo != null) {
				JobTreeElement[] toBeRemoved = null;
				boolean fire = false;
				JobTreeElement element = (JobTreeElement) tinfo.getParent();
				synchronized (keptjobinfos) {
					if (element == info && !keptjobinfos.contains(tinfo)) {
						toBeRemoved = findJobsToRemove(element);
						keptjobinfos.add(tinfo);
						finishedTime.put(tinfo, Long.valueOf(System
								.currentTimeMillis()));
					}
				}

				if (toBeRemoved != null) {
					for (JobTreeElement jobTreeElement : toBeRemoved) {
						remove(jobTreeElement);
					}
				}

				if (fire) {
					for (Object listener : getListeners()) {
						KeptJobsListener jv = (KeptJobsListener) listener;
						jv.finished(info);
					}
				}
			}
		}
	}

	/**
	 * Remove all kept jobs with result status {@link IStatus#ERROR error}.
	 */
	public void removeErrorJobs() {
		JobTreeElement[] infos = getKeptElements();
		for (JobTreeElement info : infos) {
			if (info.isJobInfo()) {
				JobInfo info1 = (JobInfo) info;
				Job job = info1.getJob();
				if (job != null) {
					IStatus status = job.getResult();
					if (status != null && status.getSeverity() == IStatus.ERROR) {
						JobTreeElement topElement = info1.getParent();
						if (topElement == null) {
							topElement = info1;
						}
						remove(topElement);
					}
				}
			}
		}
	}

	boolean remove(JobTreeElement jte) {
		boolean fire = false;
		boolean removed = false;

		synchronized (keptjobinfos) {
			if (keptjobinfos.remove(jte)) {
				removed = true;
				finishedTime.remove(jte);
				disposeAction(jte);

				// delete all elements that have jte as their direct or indirect
				// parent
				JobTreeElement jobTreeElements[] = getKeptElements();
				for (JobTreeElement jobTreeElement : jobTreeElements) {
					JobTreeElement parent = (JobTreeElement) jobTreeElement
							.getParent();
					if (parent != null) {
						if (parent == jte || parent.getParent() == jte) {
							if (keptjobinfos.remove(jobTreeElement)) {
								disposeAction(jobTreeElement);
							}
							finishedTime.remove(jobTreeElement);
						}
					}
				}
				fire = true;
			}
		}

		if (fire) {
			// notify listeners
			Object l[] = getListeners();
			for (Object element : l) {
				KeptJobsListener jv = (KeptJobsListener) element;
				jv.removed(jte);
			}
		}
		return removed;
	}

	/**
	 * Returns all kept elements.
	 */
	JobTreeElement[] getKeptElements() {
		synchronized (keptjobinfos) {
			if (keptjobinfos.isEmpty()) {
				return EMPTY_INFOS;
			}
			return keptjobinfos.toArray(new JobTreeElement[keptjobinfos.size()]);
		}
	}

	/**
	 * Get the date that indicates the finish time.
	 *
	 * @param jte job element to get finish date for
	 * @return finish date of the requested job element or <code>null</code> if
	 *         unknown element
	 */
	public Date getFinishDate(JobTreeElement jte) {
		Long value = finishedTime.get(jte);
		if (value != null) {
			return new Date(value.longValue());
		}
		return null;
	}

	/**
	 * Return whether or not the kept infos have the element.
	 *
	 * @param element the job element to check
	 * @return <code>true</code> if requested element is configured to be kept
	 */
	public boolean isKept(JobTreeElement element) {
		synchronized (keptjobinfos) {
			return keptjobinfos.contains(element);
		}
	}

	/**
	 * Clear all kept jobs.
	 */
	public void clearAll() {
		synchronized (keptjobinfos) {
			JobTreeElement[] all = getKeptElements();
			for (JobTreeElement element : all) {
				disposeAction(element);
			}
			keptjobinfos.clear();
			finishedTime.clear();
		}

		// notify listeners
		Object l[] = getListeners();
		for (Object element : l) {
			KeptJobsListener jv = (KeptJobsListener) element;
			jv.removed(null);
		}
	}
}
