/*******************************************************************************
 * Copyright (c) 2001, 2013 IBM Corporation and others.
 * All rights reserved. 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
 *     David Carver (Intalio) - bug 300443 - some constants aren't static final
 *     
 *******************************************************************************/
package org.eclipse.wst.sse.core.internal.tasks;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.wst.sse.core.internal.Logger;
import org.eclipse.wst.sse.core.internal.SSECoreMessages;
import org.eclipse.wst.sse.core.internal.SSECorePlugin;
import org.eclipse.wst.sse.core.utils.StringUtils;
import org.osgi.framework.Bundle;

/**
 * Queueing Job for processing deltas and projects.
 */
class TaskScanningJob extends Job {
	static final int JOB_DELAY_DELTA = 1000;
	private static final int JOB_DELAY_PROJECT = 5000;
	static final String TASK_TAG_PROJECTS_ALREADY_SCANNED = "task-tag-projects-already-scanned"; //$NON-NLS-1$
	/** List of scanned projects.  Since it's easily too long for a persistent property, and can be exported if stored in general preferences, store as a regular file. */
	static final IPath TASK_TAG_PROJECTS_ALREADY_SCANNED_LOCATION = SSECorePlugin.getDefault().getStateLocation().append("task-tags.properties"); //$NON-NLS-1$
	static final QualifiedName QTASK_TAG_PROJECTS_ALREADY_SCANNED = new QualifiedName(SSECorePlugin.ID, TASK_TAG_PROJECTS_ALREADY_SCANNED);
	private List fQueue = null;

	/** symbolic name for OSGI framework */
	private static final String OSGI_FRAMEWORK_ID = "org.eclipse.osgi"; //$NON-NLS-1$

	TaskScanningJob() {
		super(SSECoreMessages.TaskScanner_0);
		fQueue = new ArrayList();
		setPriority(Job.DECORATE);
		setSystem(true);
		setUser(false);
	}

	synchronized void addProjectDelta(IResourceDelta delta) {
		IResource projectResource = delta.getResource();

		if (projectResource.getType() == IResource.PROJECT) {
			if (!projectResource.isAccessible()) {
				String[] projectsPreviouslyScanned = getScannedProjects();
				HashSet updatedProjects = new HashSet(Arrays.asList(projectsPreviouslyScanned));
				updatedProjects.remove(projectResource.getName());
				setScannedProjects((String[]) updatedProjects.toArray(new String[updatedProjects.size()]));
			}
			if (isEnabledOnProject((IProject) projectResource)) {
				if (delta.getFlags() == IResourceDelta.OPEN) {
					addProject((IProject) projectResource);
				}
				else {
					fQueue.add(delta);
				}
				if (Logger.DEBUG_TASKSJOB) {
					String kind = null;
					switch (delta.getKind()) {
						case IResourceDelta.ADDED :
							kind = " [IResourceDelta.ADDED]"; //$NON-NLS-1$
							break;
						case IResourceDelta.CHANGED :
							kind = " [IResourceDelta.CHANGED]"; //$NON-NLS-1$
							break;
						case IResourceDelta.REMOVED :
							kind = " [IResourceDelta.REMOVED]"; //$NON-NLS-1$
							break;
						case IResourceDelta.ADDED_PHANTOM :
							kind = " [IResourceDelta.ADDED_PHANTOM]"; //$NON-NLS-1$
							break;
						case IResourceDelta.REMOVED_PHANTOM :
							kind = " [IResourceDelta.REMOVED_PHANTOM]"; //$NON-NLS-1$
							break;
					}
					System.out.println("Adding delta " + delta.getFullPath() + kind); //$NON-NLS-1$
				}
				schedule(JOB_DELAY_DELTA);
			}
		}
	}

	synchronized void addProject(IProject project) {
		if (projectHasNotBeenFullyScanned(project)) {
			fQueue.add(project);
			if (Logger.DEBUG_TASKSJOB) {
				System.out.println("Adding project " + project.getName()); //$NON-NLS-1$
			}
			schedule(JOB_DELAY_PROJECT);
		}
	}

	/**
	 * A check to see if the OSGI framework is shutting down.
	 * 
	 * @return true if the System Bundle is stopped (ie. the framework is
	 *         shutting down)
	 */
	boolean frameworkIsShuttingDown() {
		// in the Framework class there's a note:
		// set the state of the System Bundle to STOPPING.
		// this must be done first according to section 4.19.2 from the OSGi
		// R3 spec.
		boolean shuttingDown = !Platform.isRunning() || Platform.getBundle(OSGI_FRAMEWORK_ID).getState() == Bundle.STOPPING;
		if (Logger.DEBUG_TASKSJOB && shuttingDown) {
			System.out.println("TaskScanningJob: system is shutting down!"); //$NON-NLS-1$
		}
		return shuttingDown;
	}
	
	private boolean isEnabledOnProject(IProject p) {
		IPreferencesService preferencesService = Platform.getPreferencesService();
		IScopeContext[] lookupOrder = new IScopeContext[]{new ProjectScope(p), new InstanceScope(), new DefaultScope()};
		return preferencesService.getBoolean(TaskTagPreferenceKeys.TASK_TAG_NODE, TaskTagPreferenceKeys.TASK_TAG_ENABLE, false, lookupOrder);
	}

	private boolean projectHasNotBeenFullyScanned(IResource project) {
		String[] projectsScanned = getScannedProjects();

		boolean shouldScan = true;
		String name = project.getName();
		for (int j = 0; shouldScan && j < projectsScanned.length; j++) {
			if (projectsScanned[j].equals(name)) {
				if (Logger.DEBUG_TASKSJOB)
					System.out.println("Scanning Job skipping " + project.getName()); //$NON-NLS-1$
				shouldScan = false;
			}
		}
		return shouldScan;
	}

	synchronized List retrieveQueue() {
		List queue = fQueue;
		fQueue = new ArrayList();
		return queue;
	}

	protected IStatus run(IProgressMonitor monitor) {
		if (frameworkIsShuttingDown())
			return Status.CANCEL_STATUS;

		cleanupRememberedProjectList();

		IStatus status = null;
		List currentQueue = retrieveQueue();
		
		try {
			Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
		}
		catch (SecurityException e) {
			// not a critical problem
		}
		
		List errors = null;
		int ticks = currentQueue.size();
		String taskName = null;
		if (Logger.DEBUG_TASKSJOB) {
			taskName = SSECoreMessages.TaskScanningJob_0 + " (" + ticks + " work items)"; //$NON-NLS-1$ //$NON-NLS-2$
		}
		else {
			taskName = SSECoreMessages.TaskScanningJob_0;
		}
		monitor.beginTask(taskName, ticks);

		IProgressMonitor scanMonitor = null;
		while (!currentQueue.isEmpty()) {
			Object o = currentQueue.remove(0);
			if (frameworkIsShuttingDown() || monitor.isCanceled())
				return Status.CANCEL_STATUS;
			try {
				scanMonitor = new SubProgressMonitor(monitor, 1, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK);
				if (o instanceof IResourceDelta) {
					WorkspaceTaskScanner.getInstance().scan((IResourceDelta) o, scanMonitor);
				}
				else if (o instanceof IProject) {
					WorkspaceTaskScanner.getInstance().scan((IProject) o, scanMonitor);
					if (!scanMonitor.isCanceled()) {
						String[] projectsPreviouslyScanned = getScannedProjects();
						HashSet updatedProjects = new HashSet(Arrays.asList(projectsPreviouslyScanned));
						updatedProjects.add(((IProject) o).getName());
						setScannedProjects((String[]) updatedProjects.toArray(new String[updatedProjects.size()]));
					}
					if (!((IProject) o).isAccessible()) {
						String[] projectsPreviouslyScanned = getScannedProjects();
						HashSet updatedProjects = new HashSet(Arrays.asList(projectsPreviouslyScanned));
						updatedProjects.remove(((IProject) o).getName());
						setScannedProjects((String[]) updatedProjects.toArray(new String[updatedProjects.size()]));
					}
				}
			}
			catch (Exception e) {
				if (errors == null) {
					errors = new ArrayList();
				}
				errors.add(new Status(IStatus.ERROR, SSECorePlugin.ID, IStatus.ERROR, "", e)); //$NON-NLS-1$
			}
		}
		monitor.done();

		if (errors == null || errors.isEmpty()) {
			status = Status.OK_STATUS;
		}
		else {
			if (errors.size() == 1) {
				status = (IStatus) errors.get(0);
			}
			else {
				IStatus[] statii = (IStatus[]) errors.toArray(new IStatus[errors.size()]);
				status = new MultiStatus(SSECorePlugin.ID, IStatus.ERROR, statii, SSECoreMessages.TaskScanningJob_1, null);
			}
		}

		SSECorePlugin.getDefault().savePluginPreferences();
		return status;
	}

	private void cleanupRememberedProjectList() {
		String[] rememberedProjectNames = getScannedProjects();
		IResource[] workspaceProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
		String[] projectNames = new String[workspaceProjects.length];
		for (int i = 0; i < projectNames.length; i++) {
			projectNames[i] = workspaceProjects[i].getName();
		}

		List projectNamesToRemember = new ArrayList(rememberedProjectNames.length);
		for (int i = 0; i < rememberedProjectNames.length; i++) {
			boolean rememberedProjectExists = false;
			for (int j = 0; !rememberedProjectExists && j < projectNames.length; j++) {
				if (rememberedProjectNames[i].equals(projectNames[j])) {
					rememberedProjectExists = true;
				}
			}
			if (rememberedProjectExists) {
				projectNamesToRemember.add(rememberedProjectNames[i]);
			}
			else if (Logger.DEBUG_TASKSJOB) {
				System.out.println("Removing " + rememberedProjectNames[i] + " removed from " + TASK_TAG_PROJECTS_ALREADY_SCANNED); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}

		if (projectNamesToRemember.size() != rememberedProjectNames.length) {
			setScannedProjects((String[]) projectNamesToRemember.toArray(new String[projectNamesToRemember.size()]));
		}
	}
	
	static String[] getScannedProjects() {
		String rawValue = null;
		File file = TASK_TAG_PROJECTS_ALREADY_SCANNED_LOCATION.toFile();
		if (file.canRead()) {
			Properties props = new Properties();
			try {
				props.load(new FileInputStream(file));
				rawValue = props.getProperty(TASK_TAG_PROJECTS_ALREADY_SCANNED);
				props = null;
			}
			catch (IOException e) {
				Logger.logException(e);
			}
		}
		if (rawValue != null)
			return StringUtils.unpack(rawValue);
		return new String[0];
	}

	static void setScannedProjects(String[] projectNames) {
		if (Logger.DEBUG_TASKSJOB)
			System.out.println("Task scanned projects set to " + StringUtils.pack(projectNames)); //$NON-NLS-1$

		File file = TASK_TAG_PROJECTS_ALREADY_SCANNED_LOCATION.toFile();
		if (!file.exists() || file.canWrite()) {
			Properties props = new Properties();
			props.setProperty(TASK_TAG_PROJECTS_ALREADY_SCANNED, StringUtils.pack(projectNames));
			try {
				props.store(new FileOutputStream(file), ""); //$NON-NLS-1$
			}
			catch (IOException e) {
				Logger.logException(e);
			}
			props = null;
		}
		else {
			Logger.log(Logger.WARNING, "Could not write task tags scanned list to " + TASK_TAG_PROJECTS_ALREADY_SCANNED_LOCATION.toOSString()); //$NON-NLS-1$
		}
	}
}
