/*******************************************************************************
 * Copyright (c) 2001, 2019 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
 *     
 *******************************************************************************/
package org.eclipse.wst.sse.ui.internal.reconcile.validator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.core.runtime.content.IContentTypeManager;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.IReconcileResult;
import org.eclipse.jface.text.reconciler.IReconcileStep;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.wst.sse.core.utils.StringUtils;
import org.eclipse.wst.sse.ui.internal.IReleasable;
import org.eclipse.wst.sse.ui.internal.Logger;
import org.eclipse.wst.sse.ui.internal.reconcile.DocumentAdapter;
import org.eclipse.wst.sse.ui.internal.reconcile.ReconcileAnnotationKey;
import org.eclipse.wst.sse.ui.internal.reconcile.StructuredTextReconcilingStrategy;
import org.eclipse.wst.sse.ui.internal.reconcile.TemporaryAnnotation;
import org.eclipse.wst.validation.ValidationFramework;
import org.eclipse.wst.validation.Validator;
import org.eclipse.wst.validation.internal.provisional.core.IValidator;


/**
 * Special validator strategy. Runs validator steps contributed via the
 * <code>org.eclipse.wst.sse.ui.extensions.sourcevalidation</code> extension
 * point
 * 
 * @author pavery
 */
public class ValidatorStrategy extends StructuredTextReconcilingStrategy {

	private static final boolean DEBUG_VALIDATION_CAPABLE_BUT_DISABLED = Boolean.valueOf(Platform.getDebugOption("org.eclipse.wst.sse.ui/debug/reconcilerValidatorEnablement")).booleanValue();
	private static final boolean DEBUG_VALIDATION_UNSUPPORTED = Boolean.valueOf(Platform.getDebugOption("org.eclipse.wst.sse.ui/debug/reconcilerValidatorSupported")).booleanValue();
	private static final Object NO_FILE = new Object();

	private String[] fContentTypeIds = null;
	private List<ValidatorMetaData> fMetaData = null;
	/** validator id (as declared in ext point) -> ReconcileStepForValidator * */
	private Map<String, IReconcileStep> fVidToVStepMap = null;

	/*
	 * List of ValidatorMetaDatas of total scope validators that have been run
	 * since beginProcessing() was called.
	 */
	private List<ValidatorMetaData> fTotalScopeValidatorsAlreadyRun = new ArrayList<>();
	
	/*
	 * Whether the Validation Framework has indicated that validation is
	 * suspended for the current resource
	 */
	private boolean fValidatorsSuspended = false;
	private Object fFile;
	private boolean fIsCancelled = false;

	public ValidatorStrategy(ISourceViewer sourceViewer, String contentType) {
		super(sourceViewer);
		fMetaData = new ArrayList<>();
		fContentTypeIds = calculateParentContentTypeIds(contentType);
		fVidToVStepMap = new HashMap<>();
	}

	public void addValidatorMetaData(ValidatorMetaData vmd) {
		fMetaData.add(vmd);
	}

	public void beginProcessing() {
		fTotalScopeValidatorsAlreadyRun.clear();
	}

	/**
	 * The content type passed in should be the most specific one. TODO: This
	 * exact method is also in ValidatorMetaData. Should be in a common place.
	 * 
	 * @param contentType
	 * @return
	 */
	private String[] calculateParentContentTypeIds(String contentTypeId) {
		Set<String> parentTypes = new HashSet<>();

		IContentTypeManager ctManager = Platform.getContentTypeManager();
		IContentType ct = ctManager.getContentType(contentTypeId);
		String id = contentTypeId;

		while (ct != null && id != null) {
			parentTypes.add(id);
			ct = ctManager.getContentType(id);
			if (ct != null) {
				IContentType baseType = ct.getBaseType();
				id = (baseType != null) ? baseType.getId() : null;
			}
		}
		return parentTypes.toArray(new String[parentTypes.size()]);
	}

	protected boolean canHandlePartition(String partitionType) {
		ValidatorMetaData vmd = null;
		for (int i = 0; i < fMetaData.size(); i++) {
			vmd = fMetaData.get(i);
			if (vmd.canHandlePartitionType(getContentTypeIds(), partitionType))
				return true;
		}
		return false;
	}

	protected boolean containsStep(IReconcileStep step) {
		return fVidToVStepMap.containsValue(step);
	}

	/**
	 * @see org.eclipse.wst.sse.ui.internal.provisional.reconcile.AbstractStructuredTextReconcilingStrategy#createReconcileSteps()
	 */
	public void createReconcileSteps() {
		// do nothing, steps are created
	}

	public void endProcessing() {
		fTotalScopeValidatorsAlreadyRun.clear();
	}

	/**
	 * All content types on which this ValidatorStrategy can run
	 * 
	 * @return
	 */
	public String[] getContentTypeIds() {
		return fContentTypeIds;
	}

	/**
	 * @param tr
	 *            Partition of the region to reconcile.
	 * @param dr
	 *            Dirty region representation of the typed region
	 */
	public void reconcile(ITypedRegion tr, DirtyRegion dr) {
		/*
		 * Abort if no workspace file is known (new validation framework does
		 * not support that scenario) or no validators have been specified
		 */
		if (isCanceled() || fMetaData.isEmpty() || fValidatorsSuspended)
			return;

		IDocument doc = getDocument();
		// for external files, this can be null
		if (doc == null)
			return;

		String partitionType = tr.getType();

		ValidatorMetaData vmd = null;
		List<IReconcileResult> annotationsToAdd = new ArrayList<>();
		List<ReconcileStepForValidator> stepsRanOnThisDirtyRegion = new ArrayList<>(1);
		
		/*
		 * Keep track of the disabled validators by source id for the V2
		 * validators.
		 */
		Set<String> disabledValsBySourceId = new HashSet<>(20);
		
		/*
		 * Keep track of the disabled validators by class id for the v1
		 * validators.
		 */
		Set<String> disabledValsByClass = new HashSet<>(20);
		IFile file = getFile();
		if (file != null) {
			if(!file.isAccessible())
				return;

			Collection<Validator> disabledValidators = null;
			try {
				/*
				 * Take extra care when calling this external code, as it
				 * can indirectly cause bundles to start
				 */
				disabledValidators = ValidationFramework.getDefault().getDisabledValidatorsFor(file);
			}
			catch (Exception e) {
				Logger.logException(e);
			}

			if (disabledValidators != null) {
				for (Iterator<Validator> it = disabledValidators.iterator(); it.hasNext();) {
					Validator v = it.next();
					Validator.V1 v1 = null;
					try {
						v1 = v.asV1Validator();
					}
					catch (Exception e) {
						Logger.logException(e);
					}
					if (v1 != null)
						disabledValsByClass.add(v1.getId());
					// not a V1 validator
					else if (v.getSourceId() != null) {
						//could be more then one sourceid per batch validator
						String[] sourceIDs = StringUtils.unpack(v.getSourceId());
						disabledValsBySourceId.addAll(Arrays.asList(sourceIDs));
					}
				}
			}
		}
				
		/*
		 * Loop through all of the relevant validator meta data to find
		 * supporting validators for this partition type. Don't check
		 * this.canHandlePartition() before-hand since it just loops through
		 * and calls vmd.canHandlePartitionType()...which we're already doing
		 * here anyway to find the right vmd.
		 */
		for (int i = 0; i < fMetaData.size() && !isCanceled(); i++) {
			vmd = fMetaData.get(i);
			if (vmd.canHandlePartitionType(getContentTypeIds(), partitionType)) {
				/*
				 * Check if validator is enabled according to validation
				 * preferences before attempting to create/use it
				 */
				if (!disabledValsBySourceId.contains(vmd.getValidatorId()) && !disabledValsByClass.contains(vmd.getValidatorClass())) {
					if (DEBUG_VALIDATION_UNSUPPORTED) {
						Logger.log(Logger.INFO, "Source validator "+vmd.getValidatorId()+" handling (content types:[" + StringUtils.pack(getContentTypeIds()) + "] partition type:" + partitionType);
					}
					int validatorScope = vmd.getValidatorScope();
					ReconcileStepForValidator validatorStep = null;
					// get step for partition type
					Object o = fVidToVStepMap.get(vmd.getValidatorId());
					if (o != null) {
						validatorStep = (ReconcileStepForValidator) o;
					}
					else {
						// if doesn't exist, create one
						IValidator validator = vmd.createValidator();

						validatorStep = new ReconcileStepForValidator(validator, validatorScope);
						validatorStep.setInputModel(new DocumentAdapter(doc));

						fVidToVStepMap.put(vmd.getValidatorId(), validatorStep);
					}

					if (!fTotalScopeValidatorsAlreadyRun.contains(vmd) && !fIsCancelled) {
						annotationsToAdd.addAll(Arrays.asList(validatorStep.reconcile(dr, dr)));
						stepsRanOnThisDirtyRegion.add(validatorStep);

						if (validatorScope == ReconcileAnnotationKey.TOTAL) {
							// mark this validator as "run"
							fTotalScopeValidatorsAlreadyRun.add(vmd);
						}
					}
				}
				else if (DEBUG_VALIDATION_CAPABLE_BUT_DISABLED) {
					String message = "Source validator able (id:" + vmd.getValidatorId() + " class:" + vmd.getValidatorClass() + " but skipped because it was reported as disabled";
					Logger.log(Logger.INFO, message);
				}
			}
			else if (DEBUG_VALIDATION_UNSUPPORTED) {
				Logger.log(Logger.INFO, "Source validator "+vmd.getValidatorId()+" can not handle (content types:[" + StringUtils.pack(getContentTypeIds()) + "] partition type:" + partitionType);
			}
		}

		TemporaryAnnotation[] annotationsToRemove = getAnnotationsToRemove(dr, stepsRanOnThisDirtyRegion);
		if (annotationsToRemove.length + annotationsToAdd.size() > 0 && !fIsCancelled)
			smartProcess(annotationsToRemove, annotationsToAdd.toArray(new IReconcileResult[annotationsToAdd.size()]));
	}

	public void release() {
		super.release();
		fIsCancelled = true;
		Iterator<IReconcileStep> it = fVidToVStepMap.values().iterator();
		IReconcileStep step = null;
		while (it.hasNext()) {
			step = it.next();
			if (step instanceof IReleasable) {
				((IReleasable) step).release();
			}
		}
		fFile = null;
	}

	/**
	 * @see org.eclipse.wst.sse.ui.internal.reconcile.AbstractStructuredTextReconcilingStrategy#setDocument(org.eclipse.jface.text.IDocument)
	 */
	public void setDocument(IDocument document) {
		super.setDocument(document);

		fFile = null;

		try {
			fValidatorsSuspended = false;
			if (document != null) {
				IFile file = getFile();
				if (file != null) {
					// Validation is suspended for this resource, do nothing
					fValidatorsSuspended = !file.isAccessible() || ValidationFramework.getDefault().isSuspended(file.getProject()) || ValidationFramework.getDefault().getProjectSettings(file.getProject()).getSuspend();
				}
			}
		}
		catch (Exception e) {
			fValidatorsSuspended = true;
			Logger.logException(e);
		}
		
		// validator steps are in "fVIdToVStepMap" (as opposed to fFirstStep >
		// next step etc...)
		Iterator<IReconcileStep> it = fVidToVStepMap.values().iterator();
		IReconcileStep step = null;
		while (it.hasNext()) {
			step = it.next();
			step.setInputModel(new DocumentAdapter(document));
		}
	}

	/**
	 * Gets IFile from current document
	 * 
	 * @return IFile the IFile, null if no such file exists
	 */
	private IFile getFile() {
		if (fFile == null) {
			fFile = NO_FILE;
			ITextFileBuffer buffer = FileBuffers.getTextFileBufferManager().getTextFileBuffer(getDocument());
			if (buffer != null && buffer.getLocation() != null) {
				IPath path = buffer.getLocation();
				if (path.segmentCount() > 1) {
					IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(path);
					if (file.isAccessible()) {
						fFile = file;
					}
				}
			}
		}

		if (fFile != NO_FILE)
			return (IFile) fFile;
		return null;
	}
}
