/*******************************************************************************
 * Copyright (c) 2001, 2020 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
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     David Carver (Intalio) - bug 300443 - some constants aren't static final
 *     
 *******************************************************************************/
package org.eclipse.wst.sse.core.internal.tasks;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.content.IContentDescription;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.core.runtime.content.IContentTypeManager;
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.provisional.tasks.IFileTaskScanner;
import org.eclipse.wst.sse.core.internal.provisional.tasks.TaskTag;
import org.eclipse.wst.sse.core.utils.StringUtils;

/**
 * Dispatcher for scanning based on deltas and requested projects
 */
class WorkspaceTaskScanner {
	private static WorkspaceTaskScanner _instance = null;
	static final String SYNTHETIC_TASK = "org.eclipse.wst.sse.task-synthetic";
	static final String MODIFICATION_STAMP = "org.eclipse.wst.sse.modification-stamp";

	static synchronized WorkspaceTaskScanner getInstance() {
		if (_instance == null) {
			_instance = new WorkspaceTaskScanner();
		}
		return _instance;
	}

	static final String DEFAULT_MARKER_TYPE = IFileTaskScanner.TASK_MARKER_ID;
	private List<IFileTaskScanner> fActiveScanners = null;
	private IContentType[] fCurrentIgnoreContentTypes = null;
	private TaskTag[] fCurrentTaskTags = null;

	private FileTaskScannerRegistryReader registry = null;

	private long time0;

	/**
	 * 
	 */
	private WorkspaceTaskScanner() {
		super();
		registry = FileTaskScannerRegistryReader.getInstance();
		fActiveScanners = new ArrayList<>();
		fCurrentTaskTags = new TaskTag[0];
		fCurrentIgnoreContentTypes = new IContentType[0];
	}

	private IFileTaskScanner[] combined(IFileTaskScanner[] a, IFileTaskScanner[] b) {
		if (a.length == 0) {
			return b;
		}
		if (b.length == 0) {
			return a;
		}
		IFileTaskScanner[] both = new  IFileTaskScanner[a.length + b.length];
		System.arraycopy(a, 0, both, 0, a.length);
		System.arraycopy(b, 0, both, a.length, b.length);
		return both;
	}

	private IContentType[] detectContentTypes(IResource resource) {
		IContentType[] types = null;
		if (resource.getType() == IResource.FILE && resource.isAccessible()) {
			types = Platform.getContentTypeManager().findContentTypesFor(resource.getName());
			if (types.length == 0) {
				IContentDescription d = null;
				try {
					// optimized description lookup, might not succeed
					d = ((IFile) resource).getContentDescription();
					if (d != null) {
						types = new IContentType[]{d.getContentType()};
					}
				}
				catch (CoreException e) {
					/*
					 * should not be possible given the accessible and file
					 * type check above
					 */
				}
			}
			if (Logger.DEBUG_TASKSCONTENTTYPE) {
				if (types.length > 0) {
					if (types.length > 1) {
						System.out.println(resource.getFullPath() + ": " + "multiple based on name (probably hierarchical)"); //$NON-NLS-1$ //$NON-NLS-2$
					}
					for (int i = 0; i < types.length; i++) {
						System.out.println(resource.getFullPath() + " matched: " + types[i].getId()); //$NON-NLS-1$
					}
				}
			}
		}
		return types;
	}

	/**
	 * @param resource
	 * @return
	 */
	private IProject getProject(IResource resource) {
		IProject project = null;
		if (resource.getType() == IResource.PROJECT) {
			project = (IProject) resource;
		}
		else {
			project = resource.getProject();
		}
		return project;
	}

	private boolean init(IResource resource) {
		IProject project = getProject(resource);

		IPreferencesService preferencesService = Platform.getPreferencesService();
		IScopeContext[] lookupOrder = new IScopeContext[]{new ProjectScope(project), new InstanceScope(), new DefaultScope()};

		boolean proceed = preferencesService.getBoolean(TaskTagPreferenceKeys.TASK_TAG_NODE, TaskTagPreferenceKeys.TASK_TAG_ENABLE, false, lookupOrder);

		if (Logger.DEBUG_TASKSPREFS) {
			System.out.println(getClass().getName() + " scan of " + resource.getFullPath() + ":" + proceed); //$NON-NLS-1$ //$NON-NLS-2$
		}

		if (proceed) {
			String[] tags = StringUtils.unpack(preferencesService.getString(TaskTagPreferenceKeys.TASK_TAG_NODE, TaskTagPreferenceKeys.TASK_TAG_TAGS, null, lookupOrder));
			String[] priorities = StringUtils.unpack(preferencesService.getString(TaskTagPreferenceKeys.TASK_TAG_NODE, TaskTagPreferenceKeys.TASK_TAG_PRIORITIES, null, lookupOrder));
			String[] currentIgnoreContentTypeIDs = StringUtils.unpack(preferencesService.getString(TaskTagPreferenceKeys.TASK_TAG_NODE, TaskTagPreferenceKeys.TASK_TAG_CONTENTTYPES_IGNORED, null, lookupOrder));
			if (Logger.DEBUG_TASKSPREFS) {
				System.out.print(getClass().getName() + " tags: "); //$NON-NLS-1$
				for (int i = 0; i < tags.length; i++) {
					if (i > 0) {
						System.out.print(","); //$NON-NLS-1$
					}
					System.out.print(tags[i]);
				}
				System.out.println();
				System.out.print(getClass().getName() + " priorities: "); //$NON-NLS-1$
				for (int i = 0; i < priorities.length; i++) {
					if (i > 0) {
						System.out.print(","); //$NON-NLS-1$
					}
					System.out.print(priorities[i]);
				}
				System.out.println();
				System.out.print(getClass().getName() + " ignored content types: "); //$NON-NLS-1$
				for (int i = 0; i < currentIgnoreContentTypeIDs.length; i++) {
					if (i > 0) {
						System.out.print(","); //$NON-NLS-1$
					}
					System.out.print(currentIgnoreContentTypeIDs[i]);
				}
				System.out.println();
			}
			fCurrentIgnoreContentTypes = new IContentType[currentIgnoreContentTypeIDs.length];
			IContentTypeManager contentTypeManager = Platform.getContentTypeManager();
			for (int i = 0; i < currentIgnoreContentTypeIDs.length; i++) {
				fCurrentIgnoreContentTypes[i] = contentTypeManager.getContentType(currentIgnoreContentTypeIDs[i]);
			}
			int max = Math.min(tags.length, priorities.length);
			fCurrentTaskTags = new TaskTag[max];
			for (int i = 0; i < max; i++) {
				int priority = TaskTag.PRIORITY_NORMAL;
				try {
					priority = Integer.parseInt(priorities[i]);
				}
				catch (NumberFormatException e) {
					// default to normal priority
				}
				fCurrentTaskTags[i] = new TaskTag(tags[i], priority);
			}
		}
		return proceed;
	}

	void internalScan(final IProject project, final IResource resource, final IProgressMonitor scanMonitor) {
		if (scanMonitor.isCanceled())
			return;
		try {
			String name = resource.getName();
			if (resource.isAccessible() && !resource.isDerived() && !resource.isPhantom() && !resource.isTeamPrivateMember() && name.length() != 0 && name.charAt(0) != '.') {
				if ((resource.getType() & IResource.FOLDER) > 0 || (resource.getType() & IResource.PROJECT) > 0) {
					IResource[] children = ((IContainer) resource).members();
					scanMonitor.beginTask("", children.length); //$NON-NLS-1$
					for (int i = 0; i < children.length; i++) {
						internalScan(project, children[i], new SubProgressMonitor(scanMonitor, 1, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK));
					}
					scanMonitor.done();
				}
				else if ((resource.getType() & IResource.FILE) > 0) {
					scanFile(project, fCurrentTaskTags, (IFile) resource, scanMonitor);
				}
			}
		}
		catch (CoreException e) {
			Logger.logException(e);
		}
	}

	void internalScan(IResourceDelta delta, final IProgressMonitor monitor) {
		if (monitor.isCanceled())
			return;
		try {
			String name = delta.getFullPath().lastSegment();
			IResource resource = delta.getResource();
			if (!resource.isDerived() && !resource.isPhantom() && !resource.isTeamPrivateMember() && name.length() != 0 && name.charAt(0) != '.') {
				if ((resource.getType() & IResource.FOLDER) > 0 || (resource.getType() & IResource.PROJECT) > 0) {
					IResourceDelta[] children = delta.getAffectedChildren();
					monitor.beginTask("", children.length);
					if (name.length() != 0 && name.charAt(0) != '.' && children.length > 0) {
						for (int i = children.length - 1; i >= 0; i--) {
							internalScan(children[i], new SubProgressMonitor(monitor, 1, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK));
						}
					}
					monitor.done();
				}
				else if ((resource.getType() & IResource.FILE) > 0) {
					if ((delta.getKind() & IResourceDelta.ADDED) > 0 || ((delta.getKind() & IResourceDelta.CHANGED) > 0 && (delta.getFlags() & IResourceDelta.CONTENT) > 0)) {
						IFile file = (IFile) resource;
						scanFile(file.getProject(), fCurrentTaskTags, file, monitor);
					}
				}
			}
		}
		catch (Exception e) {
			Logger.logException(e);
		}
	}

	private void replaceTaskMarkers(final IFile file, final String[] markerTypes, final Map<String, Object> markerAttributeMaps[], IProgressMonitor monitor) {
		final IFile finalFile = file;
		if (file.isAccessible()) {
			try {
				IWorkspaceRunnable r = new IWorkspaceRunnable() {
					public void run(IProgressMonitor progressMonitor) throws CoreException {
						progressMonitor.beginTask("", 2);//$NON-NLS-1$
						try {
							/*
							 * Delete old Task markers (don't delete regular
							 * Tasks since that includes user-defined ones)
							 */
							for (int i = 0; i < markerTypes.length; i++) {
								if (IMarker.TASK.equals(markerTypes[i])) {
									// only remove if synthetic
									IMarker[] foundMarkers = file.findMarkers(markerTypes[i], true, IResource.DEPTH_ZERO);
									for (int j = 0; j < foundMarkers.length; j++) {
										if (foundMarkers[j].getAttribute(SYNTHETIC_TASK) != null) {
											foundMarkers[j].delete();
										}
									}
								}
								else {
									file.deleteMarkers(markerTypes[i], true, IResource.DEPTH_ZERO);
								}
							}
						}
						catch (CoreException e) {
							Logger.logException("exception deleting old tasks", e); //$NON-NLS-1$ 
						}
						finally {
							progressMonitor.worked(1);
						}
						if (markerAttributeMaps != null && markerAttributeMaps.length > 0) {
							if (Logger.DEBUG_TASKS) {
								System.out.println("" + markerAttributeMaps.length + " tasks for " + file.getFullPath()); //$NON-NLS-1$ //$NON-NLS-2$
							}
							for (int i = 0; i < markerAttributeMaps.length; i++) {
								String specifiedMarkerType = (String) markerAttributeMaps[i].get(IMarker.TASK);
								IMarker marker = finalFile.createMarker(specifiedMarkerType);
								markerAttributeMaps[i].put(IMarker.USER_EDITABLE, Boolean.FALSE);
								markerAttributeMaps[i].put(MODIFICATION_STAMP, Long.toString(file.getModificationStamp()));
								if (IMarker.TASK.equals(specifiedMarkerType)) {
									// set to synthetic and make user editable
									markerAttributeMaps[i].put(SYNTHETIC_TASK, true);
								}
								marker.setAttributes(markerAttributeMaps[i]);
							}
						}
						progressMonitor.worked(1);
						progressMonitor.done();
					}
				};
				if (file.isAccessible()) {
					finalFile.getWorkspace().run(r, ResourcesPlugin.getWorkspace().getRuleFactory().markerRule(file), IWorkspace.AVOID_UPDATE, monitor);
				}
			}
			catch (CoreException e1) {
				Logger.logException(e1);
			}
			catch(OperationCanceledException e) {
				// not an error condition
			}
		}
	}

	void scan(final IProject project, final IProgressMonitor scanMonitor) {
		if (scanMonitor.isCanceled())
			return;
		if (Logger.DEBUG_TASKS) {
			System.out.println(getClass().getName() + " scanning project " + project.getName()); //$NON-NLS-1$
		}
		if (!project.isAccessible()) {
			if (Logger.DEBUG_TASKS) {
				System.out.println(getClass().getName() + " skipping inaccessible project " + project.getName()); //$NON-NLS-1$
			}
			return;
		}

		if (Logger.DEBUG_TASKSOVERALLPERF) {
			time0 = System.currentTimeMillis();
		}
		if (init(project)) {
			internalScan(project, project, scanMonitor);
			shutdownDelegates(project);
		}
		
		if (Logger.DEBUG_TASKSOVERALLPERF) {
			System.out.println("" + (System.currentTimeMillis() - time0) + "ms for " + project.getFullPath()); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}


	void scan(IResourceDelta delta, final IProgressMonitor monitor) {
		if (monitor.isCanceled())
			return;
		if (Logger.DEBUG_TASKSOVERALLPERF) {
			time0 = System.currentTimeMillis();
		}
		if (init(delta.getResource())) {
			internalScan(delta, monitor);
			shutdownDelegates(delta.getResource().getProject());
		}
		if (Logger.DEBUG_TASKSOVERALLPERF) {
			System.out.println("" + (System.currentTimeMillis() - time0) + "ms for " + delta.getFullPath()); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	void scanFile(IProject project, TaskTag[] taskTags, IFile file, IProgressMonitor monitor) {
		if (monitor.isCanceled())
			return;

		// 3 "stages"
		monitor.beginTask("", 8);//$NON-NLS-1$
		monitor.subTask(file.getFullPath().toString().substring(1));

		final List<Map<String, Object>> markerAttributes = new ArrayList<>();
		IContentType[] types = detectContentTypes(file);
		Set<String> markerTypes = new HashSet<>(3);
		// Always included for safety and migration
		markerTypes.add(DEFAULT_MARKER_TYPE);
		monitor.worked(1);

		IFileTaskScanner[] fileScanners = null;
		IFileTaskScanner[] ignoredFileScanners = null;
		if (types != null) {
			if (fCurrentIgnoreContentTypes.length == 0) {
				fileScanners = registry.getFileTaskScanners(types);
			}
			else {
				List<IContentType> validTypes = new ArrayList<>();
				// obtain a filtered list of delegates
				for (int i = 0; i < types.length; i++) {
					boolean ignoreContentType = false;
					for (int j = 0; j < fCurrentIgnoreContentTypes.length; j++) {
						ignoreContentType = ignoreContentType || types[i].isKindOf(fCurrentIgnoreContentTypes[j]);
					}
					if (!ignoreContentType) {
						validTypes.add(types[i]);
					}
					else {
						ignoredFileScanners = registry.getFileTaskScanners(new IContentType[] {types[i]});
					}
				}
				fileScanners = registry.getFileTaskScanners(validTypes.toArray(new IContentType[validTypes.size()]));
			}
			FileTaskScannerRegistryReader.EnabledDisabledScanners filenameExtensionScanners = registry.getFileTaskScanners(file.getFullPath(), fCurrentIgnoreContentTypes);
			fileScanners = combined(fileScanners, filenameExtensionScanners.enabled);
			monitor.worked(1);
			if (ignoredFileScanners != null && ignoredFileScanners.length > 0) {
				for (int i = 0; i < ignoredFileScanners.length; i++) {
					markerTypes.add(ignoredFileScanners[i].getMarkerType());
				}
			}
			for (int i = 0; i < filenameExtensionScanners.disabled.length; i++) {
				markerTypes.add(filenameExtensionScanners.disabled[i].getMarkerType());
			}

			if (fileScanners.length > 0) {
				IProgressMonitor scannerMonitor = new SubProgressMonitor(monitor, 3, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK);
				scannerMonitor.beginTask("", fileScanners.length); //$NON-NLS-1$
				for (int j = 0; fileScanners != null && j < fileScanners.length; j++) {
					if (monitor.isCanceled())
						continue;
					try {
						if (!fActiveScanners.contains(fileScanners[j]) && !monitor.isCanceled()) {
							fileScanners[j].startup(file.getProject());
							fActiveScanners.add(fileScanners[j]);
						}
						markerTypes.add(fileScanners[j].getMarkerType());
						Map<String, Object>[] taskMarkerAttributes = fileScanners[j].scan(file, taskTags, new SubProgressMonitor(scannerMonitor, 1, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK));
						/*
						 * TODO: pool the marker results so there's only one
						 * operation creating them
						 */
						for (int i = 0; i < taskMarkerAttributes.length; i++) {
							if (!taskMarkerAttributes[i].containsKey(IMarker.TASK)) {
								taskMarkerAttributes[i].put(IMarker.TASK, fileScanners[j].getMarkerType());
							}
							taskMarkerAttributes[i].put(IMarker.SOURCE_ID, fileScanners[j].getClass().getName());
							markerAttributes.add(taskMarkerAttributes[i]);
						}
					}
					catch (Exception e) {
						Logger.logException(file.getFullPath().toString(), e);
					}
				}
				scannerMonitor.done();
			}
		}
		else {
			monitor.worked(4);
		}

		if (monitor.isCanceled())
			return;
		// only update markers if we ran a scanner on this file
		if (fileScanners != null && fileScanners.length > 0 ||
				ignoredFileScanners != null && ignoredFileScanners.length > 0 ||
				markerTypes.size() > 0) {
			IProgressMonitor markerUpdateMonitor = new SubProgressMonitor(monitor, 3, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK);
			if (markerAttributes != null) {
				replaceTaskMarkers(file, markerTypes.toArray(new String[markerTypes.size()]), markerAttributes.toArray(new Map[markerAttributes.size()]), markerUpdateMonitor);
			}
		}
		else {
			monitor.worked(3);
		}
		monitor.done();
	}

	private void shutdownDelegates(IProject project) {
		for (int j = 0; j < fActiveScanners.size(); j++) {
			try {
				fActiveScanners.get(j).shutdown(project);
			}
			catch (Exception e) {
				Logger.logException(project.getFullPath().toString(), e);
			}
		}
		fActiveScanners = new ArrayList<>(1);
	}
}
