/*******************************************************************************
 * Copyright (c) 2009, 2017 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
 *******************************************************************************/
package org.eclipse.pde.api.tools.internal.builder;

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

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.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.core.builder.ReferenceCollection;
import org.eclipse.jdt.internal.core.builder.State;
import org.eclipse.jdt.internal.core.builder.StringSet;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.api.tools.internal.TypeAnnotations;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.Factory;
import org.eclipse.pde.api.tools.internal.provisional.IApiAnnotations;
import org.eclipse.pde.api.tools.internal.provisional.IApiMarkerConstants;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiBaseline;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent;
import org.eclipse.pde.api.tools.internal.util.Util;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.osgi.framework.Version;

/**
 * Used to incrementally build changed Java types
 *
 * @since 3.5
 */
public class IncrementalApiBuilder {

	private static final int UNKNOWN = 0;
	private static final int CLASS_FILE = 1;
	private static final int JAVA__FILE = 2;

	// bit mask for kinds of changes
	private static final int STRUCTURAL = 0x0001;
	private static final int DESCRIPTION = 0x0002;

	static class Change {
		int changeKind; // STUCTURAL | DESCRIPTION
		int fileKind; // JAVA | CLASS
		int deltaKind;
		IProject project;
		IFile resource;
		String typeName;

		/**
		 * Creates a change of the specified kinds.
		 *
		 * @param kind bit mask of STUCTURAL | DESCRIPTION
		 * @param deltaKind resource delta kind
		 * @param resource file that changed
		 * @param typeName associated qualified type name
		 * @param fileKind JAVA_FILE or CLASS_FILE
		 */
		Change(int kind, int deltaKind, IFile resource, String typeName, int fileKind) {
			this.changeKind = kind;
			this.deltaKind = deltaKind;
			this.resource = resource;
			this.project = resource.getProject();
			this.typeName = typeName;
			this.fileKind = fileKind;
		}

		/**
		 * IF the given {@link IProject} is the same as the project context, or
		 * is contained in the listing of interesting projects
		 *
		 * @return <code>true</code> if the project is the same or in the
		 *         interesting list <code>false</code> otherwise
		 */
		boolean isContained(IProject lproject, HashSet<IProject> others) {
			return this.project.equals(lproject) || (others != null && others.contains(this.project));
		}
	}

	/**
	 * Visits a resource delta to collect changes that need to be built
	 */
	class ResourceDeltaVisitor implements IResourceDeltaVisitor {
		List<Change> changes = new ArrayList<>();
		boolean buildpathChanged = false;

		/**
		 * Constructs a new visitor, noting whether the build path of the
		 * project has changed since the last build.
		 */
		ResourceDeltaVisitor(boolean pathChanged) {
			buildpathChanged = pathChanged;
		}

		@Override
		public boolean visit(IResourceDelta delta) throws CoreException {
			return switch (delta.getResource().getType())
				{
				case IResource.ROOT, IResource.PROJECT, IResource.FOLDER -> true;
				case IResource.FILE -> {
					IFile resource = (IFile) delta.getResource();
					String fileName = resource.getName();
					if (Util.isClassFile(fileName)) {
						if (delta.getKind() == IResourceDelta.REMOVED) {
							String typename = resolveTypeName(resource, CLASS_FILE);
							if (typename != null) {
								if (ApiPlugin.DEBUG_BUILDER) {
									System.out.println("ApiAnalysisBuilder: Found removed class file " + typename); //$NON-NLS-1$
								}
								changes.add(new Change(STRUCTURAL, delta.getKind(), resource, typename, CLASS_FILE));
							}
						} else if (buildpathChanged && delta.getKind() == IResourceDelta.CHANGED) {
							if ((delta.getFlags() & IResourceDelta.CONTENT) > 0) {
								// re-building due to build path (class path
								// changes, project add/remove, target platform
								// changes)
								// TODO: for now use structural change, but
								// really, we just need to re-analyze API use in
								// case of
								// required bundle API description changes
								String typename = resolveTypeName(resource, CLASS_FILE);
								if (typename != null) {
									changes.add(new Change(STRUCTURAL, delta.getKind(), resource, typename, CLASS_FILE));
								}
							}
						}

					} else if (Util.isJavaFileName(fileName)) {
						String type = resolveTypeName(resource, JAVA__FILE);
						if (type != null) {
							Change change = new Change(STRUCTURAL, delta.getKind(), resource, type, JAVA__FILE);
							changes.add(change);
							// check if description has changed
							IApiComponent component = workspaceBaseline.getApiComponent(resource.getProject());
							if (component != null) {
								try {
									IApiAnnotations annotations = component.getApiDescription().resolveAnnotations(Factory.typeDescriptor(type.replace('/', '.')));
									if (annotations instanceof TypeAnnotations ta) {
										if (ta.getBuildStamp() == BuildStamps.getBuildStamp(resource.getProject())) {
											// note description change in
											// addition to structure
											change.changeKind |= DESCRIPTION;
										}
									}
								} catch (CoreException e) {
									ApiPlugin.log(e);
								}
							}
						}
					}
					yield false;
				}
				default -> false;
				};
		}
	}

	ApiAnalysisBuilder builder = null;
	BuildContext context = null;
	IApiBaseline workspaceBaseline = null;

	/**
	 * Constructor
	 */
	public IncrementalApiBuilder(ApiAnalysisBuilder builder) {
		this.builder = builder;
	}

	/**
	 * Incrementally builds using the
	 * {@link org.eclipse.pde.api.tools.internal.provisional.builder.IApiAnalyzer}
	 * from the given {@link ApiAnalysisBuilder}
	 *
	 * @param baseline the baseline to compare with
	 * @param wbaseline the workspace baseline
	 * @param deltas the deltas to be built
	 * @param state the current JDT build state
	 * @param buildstate the current API tools build state
	 */
	public void build(IApiBaseline baseline, IApiBaseline wbaseline, IResourceDelta[] deltas, State state, BuildState buildstate, IProgressMonitor monitor) throws CoreException {
		IProject project = this.builder.getProject();
		this.workspaceBaseline = wbaseline;
		SubMonitor localmonitor = SubMonitor.convert(monitor, NLS.bind(BuilderMessages.IncrementalBuilder_builder_for_project, project.getName()), 1);
		this.context = new BuildContext();
		try {
			String[] projectNames = buildstate.getReexportedComponents();
			HashSet<IProject> depprojects = null;
			if (projectNames.length != 0) {
				depprojects = new HashSet<>();
				IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
				IProject pj = null;
				for (String projectName : projectNames) {
					pj = root.getProject(projectName);
					if (pj.isAccessible()) {
						// select only projects that don't exist in the
						// reference baseline
						if (baseline != null && baseline.getApiComponent(projectName) == null) {
							depprojects.add(pj);
						}
					}
				}
			}
			// check if the build path has changed
			long prev = buildstate.getBuildPathCRC();
			long curr = BuildState.computeBuildPathCRC(project);
			ResourceDeltaVisitor visitor = new ResourceDeltaVisitor(curr != prev);
			for (IResourceDelta delta : deltas) {
				delta.accept(visitor);
			}
			buildContext(project, state, visitor.changes, depprojects);
			build(project, baseline, wbaseline, state, buildstate, localmonitor.split(1));
		} catch (OperationCanceledException oce) {
			// do nothing, but don't forward it
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=304315
			if (ApiPlugin.DEBUG_BUILDER) {
				System.out.println("ApiAnalysisBuilder: Trapped OperationCanceledException"); //$NON-NLS-1$
			}
		} finally {
			this.context.dispose();
		}
	}

	/**
	 * Builds an API delta using the default baseline (from the workspace
	 * settings and the current
	 *
	 * @param baseline the baseline to compare to
	 * @param wbaseline the current workspace baseline
	 * @param state the current JDT build state
	 * @param buildstate the current API tools build state
	 */
	void build(final IProject project, final IApiBaseline baseline, final IApiBaseline wbaseline, final State state, BuildState buildstate, IProgressMonitor monitor) {
		SubMonitor localmonitor = SubMonitor.convert(monitor, BuilderMessages.api_analysis_on_0, 3);
		localmonitor.subTask(NLS.bind(BuilderMessages.ApiAnalysisBuilder_finding_affected_source_files, project.getName()));
		if (this.context.hasTypes()) {
			IPluginModelBase currentModel = this.builder.getCurrentModel();
			if (currentModel != null) {
				String id = currentModel.getBundleDescription().getSymbolicName();
				IApiComponent comp = wbaseline.getApiComponent(id);
				Set<IApiComponent> apiComponentMultiple = wbaseline.getAllApiComponents(id);
				if (!apiComponentMultiple.isEmpty()) {
					// add the exact match
					for (IApiComponent iApiComponent : apiComponentMultiple) {
						Version workspaceBaselineVersion = new Version(iApiComponent.getVersion());// removes
																									// qualifier
						Version currentProjectVersion = currentModel.getBundleDescription().getVersion();
						if (new Version(currentProjectVersion.getMajor(), currentProjectVersion.getMinor(), currentProjectVersion.getMicro()).compareTo(workspaceBaselineVersion) == 0) {
							comp = iApiComponent;
							break;
						}
					}
				}
				if (comp == null) {
					return;
				}
				extClean(project, buildstate, localmonitor.split(1));
				this.builder.getAnalyzer().analyzeComponent(buildstate, null, null, baseline, comp, this.context, localmonitor.split(1));
				localmonitor.split(1);
				this.builder.createMarkers();
			}
		}
	}

	/**
	 * Records the type name from the given IFile as a dependent type in the
	 * given build context
	 *
	 * @param kind mask of STRUCTURAL and/or DESCRIPTION
	 */
	private void addDependentTypeToContext(IFile file, int kind) {
		String type = resolveTypeName(file, JAVA__FILE);
		if (type == null) {
			return;
		}
		if ((STRUCTURAL & kind) > 0) {
			if (!this.context.containsStructuralChange(type)) {
				this.builder.cleanupCompatibilityMarkers(file);
			}
		}
		if ((DESCRIPTION & kind) > 0) {
			if (!this.context.containsDescriptionChange(type) && !this.context.containsDescriptionDependent(type)) {
				this.builder.cleanupUsageMarkers(file);
				this.builder.cleanUnusedFilterMarkers(file);
				this.context.recordDescriptionDependent(type);
			}
		}
		addInnerTypesToDependents(file, kind);
	}

	/**
	 * Collects the inner types from the compilation unit
	 *
	 * @param mask of STRUCTURAL and/or DESCRIPTION
	 */
	private void addInnerTypesToDependents(IFile file, int kind) {
		ICompilationUnit unit = (ICompilationUnit) JavaCore.create(file);
		IType[] types = null;
		try {
			types = unit.getAllTypes();
			String typename = null;
			for (IType type : types) {
				typename = type.getFullyQualifiedName('$');
				if ((DESCRIPTION & kind) > 0) {
					if (!this.context.containsDescriptionChange(typename) && !this.context.containsDescriptionDependent(typename)) {
						this.context.recordDescriptionDependent(typename);
					}
				}
			}
		} catch (JavaModelException jme) {
			// do nothing, just don't consider types
		}
	}

	/**
	 * Collects the inner types from the compilation unit
	 *
	 * @param mask of STRUCTURAL and/or DESCRIPTION
	 */
	private void addInnerTypes(IFile file, int kind) {
		ICompilationUnit unit = (ICompilationUnit) JavaCore.create(file);
		IType[] types = null;
		try {
			types = unit.getAllTypes();
			String typename = null;
			for (IType type : types) {
				typename = type.getFullyQualifiedName('$');
				if ((STRUCTURAL & kind) > 0) {
					if (!this.context.containsStructuralChange(typename)) {
						this.context.recordStructuralChange(typename);
					}
				}
				if ((DESCRIPTION & kind) > 0) {
					if (!this.context.containsDescriptionChange(typename)) {
						this.context.recordDescriptionChanged(typename);
					}
				}
			}
		} catch (JavaModelException jme) {
			// do nothing, just don't consider types
		}
	}

	/**
	 * Constructs a build context based on the current JDT build state and known
	 * changes.
	 *
	 * @param project the current project being built
	 * @param state the current JDT build state
	 * @param list of changes
	 */
	void buildContext(final IProject project, State state, List<Change> changes, HashSet<IProject> depprojects) {
		StringSet structural = null;
		StringSet description = null;
		for (Change change : changes) {
			boolean contained = change.isContained(project, depprojects);
			if ((change.changeKind & STRUCTURAL) > 0) {
				// don't analyze dependents of removed types
				if (change.deltaKind != IResourceDelta.REMOVED) {
					if (structural == null) {
						structural = new StringSet(16);
					}
					structural.add(change.typeName);
				}
				// only add to structural types if contained in the project
				// being built
				if (contained) {
					context.recordStructuralChange(change.typeName);
					if (change.deltaKind == IResourceDelta.REMOVED) {
						context.recordRemovedType(change.typeName);
					}
				}
			}
			if ((change.changeKind & DESCRIPTION) > 0) {
				if (description == null) {
					description = new StringSet(16);
				}
				description.add(change.typeName);
				// only add to description changes if contained in the project
				// being built
				if (contained) {
					context.recordDescriptionChanged(change.typeName);
				}
			}
			if (contained) {
				if (change.fileKind == JAVA__FILE) {
					this.builder.cleanupMarkers(change.resource);
					addInnerTypes(change.resource, change.changeKind);
				} else {
					// look up the source file
					String path = state.typeLocators.get(change.typeName);
					if (path != null) {
						IResource member = this.builder.getProject().findMember(path);
						if (member != null && member.getType() == IResource.FILE) {
							IFile source = (IFile) member;
							this.builder.cleanupMarkers(source);
							addInnerTypes(source, change.changeKind);
						}
					}
				}
			}
		}
		// only resolve dependents once for case of 1 type changed and is both
		// structural and description
		if (changes.size() == 1 && structural != null && description != null) {
			String[] types = structural.values;
			if (types.length > 0) {
				addDependents(project, state, types, STRUCTURAL | DESCRIPTION);
			}
		} else {
			if (structural != null) {
				String[] types = structural.values;
				if (types.length > 0) {
					addDependents(project, state, types, STRUCTURAL);
				}
			}
			if (description != null) {
				String[] types = description.values;
				if (types.length > 0) {
					addDependents(project, state, types, DESCRIPTION);
				}
			}
		}
	}

	/**
	 * Adds the dependent files from the current build context based on the
	 * current JDT build state to either the structural or description
	 * dependents.
	 *
	 * @param project the current project being built
	 * @param state the current JDT build state
	 * @param types dot and $ qualified names of base types that changed
	 * @param kind mask of STRUCTURAL or DESCRIPTION
	 */
	private void addDependents(final IProject project, State state, String[] types, int kind) {
		StringSet packages = new StringSet(16);
		StringSet typenames = new StringSet(16);
		for (String type : types) {
			if (type != null) {
				splitName(type, packages, typenames);
			}
		}
		// the qualifiedStrings are of the form 'p1/p2' & the simpleStrings are
		// just 'X'
		char[][][] internedQualifiedNames = ReferenceCollection.internQualifiedNames(packages);
		// if a well known qualified name was found then we can skip over these
		if (internedQualifiedNames.length < packages.elementSize) {
			internedQualifiedNames = null;
		}
		char[][] internedSimpleNames = ReferenceCollection.internSimpleNames(typenames, true);
		// if a well known name was found then we can skip over these
		if (internedSimpleNames.length < typenames.elementSize) {
			internedSimpleNames = null;
		}
		Set<Entry<String, ReferenceCollection>> entrySet = state.getReferences().entrySet();
		for (Entry<String, ReferenceCollection> entry : entrySet) {
			String typeLocator = entry.getKey();
			ReferenceCollection refs = entry.getValue();
			if (refs.includes(internedQualifiedNames, internedSimpleNames, null)) {
				IFile file = project.getFile(typeLocator);
				if (file == null) {
					continue;
				}
				if (ApiPlugin.DEBUG_BUILDER) {
					System.out.println("ApiAnalysisBuilder:   adding affected source file " + file.getName()); //$NON-NLS-1$
				}
				addDependentTypeToContext(file, kind);
			}
		}
	}

	/**
	 * Adds a type to search for dependents of in considered projects for an
	 * incremental build
	 */
	void splitName(String typename, StringSet packages, StringSet simpleTypes) {
		// the qualifiedStrings are of the form 'p1/p2' & the simpleStrings are
		// just 'X'
		int idx = typename.lastIndexOf('/');
		String packageName = (idx < 0 ? Util.EMPTY_STRING : typename.substring(0, idx));
		String typeName = (idx < 0 ? typename : typename.substring(idx + 1, typename.length()));
		idx = typeName.indexOf('$');
		if (idx > 0) {
			typeName = typeName.substring(0, idx);
		}
		if (simpleTypes.add(typeName) && packages.add(packageName) && ApiPlugin.DEBUG_BUILDER) {
			System.out.println("ApiAnalysisBuilder:   will look for dependents of " + typeName + " in " + packageName); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	/**
	 * Returns an array of type names, and cleans up markers for the specified
	 * resource
	 *
	 * @param project the project being built
	 * @param state the current build state for the given project
	 */
	void extClean(final IProject project, BuildState state, IProgressMonitor monitor) {
		SubMonitor subMonitor = SubMonitor.convert(monitor, 3);
		// clean up the state -
		// https://bugs.eclipse.org/bugs/show_bug.cgi?id=271110
		String[] types = this.context.getRemovedTypes();
		for (String type : types) {
			state.cleanup(type);
		}
		subMonitor.split(1);
		IResource resource = project.findMember(ApiAnalysisBuilder.MANIFEST_PATH);
		if (resource != null) {
			try {
				// TODO we should find a way to cache markers to type names,
				// that way to get all
				// the manifest markers for a given type name is time of O(1)
				IMarker[] markers = resource.findMarkers(IApiMarkerConstants.COMPATIBILITY_PROBLEM_MARKER, false, IResource.DEPTH_ZERO);
				String tname = null;
				for (IMarker marker : markers) {
					tname = Util.getTypeNameFromMarker(marker);
					if (this.context.containsStructuralChange(tname)) {
						marker.delete();
					}
				}
				subMonitor.split(1);
				// TODO we should find a way to cache markers to type names,
				// that way to get all
				// the manifest markers for a given type name is time of O(1)
				markers = resource.findMarkers(IApiMarkerConstants.UNUSED_FILTER_PROBLEM_MARKER, false, IResource.DEPTH_ZERO);
				for (IMarker marker : markers) {
					tname = Util.getTypeNameFromMarker(marker);
					if (this.context.containsStructuralChange(tname)) {
						marker.delete();
					}
				}
				subMonitor.split(1);
			} catch (CoreException e) {
				ApiPlugin.log(e);
			}
		}
	}

	/**
	 * Resolves the java path from the given resource
	 *
	 * @param kind CLASS_FILE, JAVA_FILE, or UNKNOWN
	 * @return the resolved path or <code>null</code> if the resource is not
	 *         part of the java model
	 */
	String resolveTypeName(IResource resource, int kind) {
		IPath typepath = resource.getFullPath();
		int type = kind;
		if (kind == UNKNOWN) {
			if (Util.isClassFile(resource.getName())) {
				type = CLASS_FILE;
			} else if (Util.isJavaFileName(resource.getName())) {
				type = JAVA__FILE;
			}
		}
		HashSet<IPath> paths = switch (type)
			{
			case JAVA__FILE -> this.builder.src_locs.get(resource.getProject());
			case CLASS_FILE -> this.builder.output_locs.get(resource.getProject());
			default -> null;
			};
		if (paths != null) {
			for (IPath path : paths) {
				if (path.isPrefixOf(typepath)) {
					typepath = typepath.removeFirstSegments(path.segmentCount()).removeFileExtension();
					return typepath.toString();
				}
			}
		}
		return null;
	}
}
