/*******************************************************************************
 *  Copyright (c) 2005, 2021 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
 *     Johannes Ahlers <Johannes.Ahlers@gmx.de> - bug 477677
 *******************************************************************************/
package org.eclipse.pde.internal.ui.search.dependencies;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IOrdinaryClassFile;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.osgi.container.namespaces.EquinoxModuleDataNamespace;
import org.eclipse.pde.core.plugin.IPluginImport;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.eclipse.pde.internal.core.ClasspathUtilCore;
import org.eclipse.pde.internal.core.ibundle.IBundle;
import org.eclipse.pde.internal.core.ibundle.IBundlePluginModelBase;
import org.eclipse.pde.internal.core.ibundle.IManifestHeader;
import org.eclipse.pde.internal.core.plugin.PluginImport;
import org.eclipse.pde.internal.core.search.PluginJavaSearchUtil;
import org.eclipse.pde.internal.core.text.bundle.ExportPackageHeader;
import org.eclipse.pde.internal.core.text.bundle.ImportPackageHeader;
import org.eclipse.pde.internal.core.text.bundle.ImportPackageObject;
import org.eclipse.pde.internal.ui.PDEPlugin;
import org.eclipse.pde.internal.ui.PDEUIMessages;
import org.eclipse.pde.internal.ui.util.TextUtil;
import org.osgi.framework.Constants;

public class GatherUnusedDependenciesOperation implements IRunnableWithProgress {

	static class Requestor extends SearchRequestor {
		boolean fFound = false;

		@Override
		public void acceptSearchMatch(SearchMatch match) throws CoreException {
			fFound = true;
		}

		public boolean foundMatches() {
			return fFound;
		}
	}

	private final IPluginModelBase fModel;
	private ArrayList<Object> fList;

	public GatherUnusedDependenciesOperation(IPluginModelBase model) {
		fModel = model;
	}

	@Override
	public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {

		ImportPackageObject[] packages = null;
		Collection<String> exportedPackages = null;
		if (ClasspathUtilCore.hasBundleStructure(fModel)) {
			IBundle bundle = ((IBundlePluginModelBase) fModel).getBundleModel().getBundle();
			IManifestHeader header = bundle.getManifestHeader(Constants.IMPORT_PACKAGE);
			if (header instanceof ImportPackageHeader) {
				packages = ((ImportPackageHeader) header).getPackages();
			} else if (header != null && header.getValue() != null) {
				header = new ImportPackageHeader(Constants.IMPORT_PACKAGE, header.getValue(), bundle,
						TextUtil.getDefaultLineDelimiter());
				packages = ((ImportPackageHeader) header).getPackages();
			}

			header = bundle.getManifestHeader(Constants.EXPORT_PACKAGE);
			if (header instanceof ExportPackageHeader) {
				exportedPackages = ((ExportPackageHeader) header).getPackageNames();
			} else if (header != null && header.getValue() != null) {
				header = new ExportPackageHeader(Constants.EXPORT_PACKAGE, header.getValue(), bundle,
						TextUtil.getDefaultLineDelimiter());
				exportedPackages = ((ExportPackageHeader) header).getPackageNames();
			}
		}
		IPluginImport[] imports = fModel.getPluginBase().getImports();

		int totalWork = imports.length * 3 + (packages != null ? packages.length : 0) + 1;
		SubMonitor subMonitor = SubMonitor.convert(monitor, totalWork);

		HashMap<String, IPluginImport> usedPlugins = new HashMap<>();
		fList = new ArrayList<>();
		for (IPluginImport pluginImport : imports) {
			if (subMonitor.isCanceled())
				break;
			if (isUnused(pluginImport, subMonitor.split(3))) {
				fList.add(pluginImport);
			} else
				usedPlugins.put(pluginImport.getId(), pluginImport);
			updateMonitor(subMonitor, fList.size());
		}

		ArrayList<ImportPackageObject> usedPackages = new ArrayList<>();
		if (packages != null && !subMonitor.isCanceled()) {
			for (ImportPackageObject importPackage : packages) {
				if (subMonitor.isCanceled())
					break;
				if (isUnused(importPackage, exportedPackages, subMonitor.split(1))) {
					fList.add(importPackage);
					updateMonitor(subMonitor, fList.size());
				} else
					usedPackages.add(importPackage);
			}
		}
		if (!subMonitor.isCanceled()) {
			minimizeDependencies(usedPlugins, usedPackages, subMonitor);

		}
		if (ClasspathUtilCore.hasBundleStructure(fModel)) {
			IBundle bundle = ((IBundlePluginModelBase) fModel).getBundleModel().getBundle();
			IManifestHeader header = bundle.getManifestHeader(EquinoxModuleDataNamespace.REGISTERED_BUDDY_HEADER);
			if (header != null) {
				String values = header.getValue();
				String[] registerBud = values.split("\\s*,\\s*"); //$NON-NLS-1$
				List<Object> found = new ArrayList<>();
				for (String string : registerBud) {
					for (Object obj : fList) {
						if (obj instanceof PluginImport) {
							String id = ((PluginImport) obj).getId();
							if (string.equals(id))
								found.add(obj);
						}
					}
				}
				if (found.size() > 0)
					fList.removeAll(found);
			}

		}
	}

	private void updateMonitor(IProgressMonitor monitor, int size) {
		monitor.setTaskName(PDEUIMessages.UnusedDependencies_analyze + size + " " //$NON-NLS-1$
				+ PDEUIMessages.UnusedDependencies_unused + " " //$NON-NLS-1$
				+ (size == 1 ? PDEUIMessages.DependencyExtent_singular : PDEUIMessages.DependencyExtent_plural) + " " //$NON-NLS-1$
				+ PDEUIMessages.DependencyExtent_found);
	}

	private boolean isUnused(IPluginImport plugin, IProgressMonitor monitor) {
		IPluginModelBase[] models = PluginJavaSearchUtil.getPluginImports(plugin);
		return !provideJavaClasses(models, monitor);
	}

	private boolean isUnused(ImportPackageObject pkg, Collection<String> exportedPackages, IProgressMonitor monitor) {
		if (exportedPackages != null && exportedPackages.contains(pkg.getValue())) {
			return false;
		}
		return !provideJavaClasses(pkg, monitor);
	}

	private boolean provideJavaClasses(IPluginModelBase[] models, IProgressMonitor monitor) {
		try {
			IProject project = fModel.getUnderlyingResource().getProject();
			if (!project.hasNature(JavaCore.NATURE_ID))
				return false;

			IJavaProject jProject = JavaCore.create(project);
			IPackageFragment[] packageFragments = PluginJavaSearchUtil.collectPackageFragments(models, jProject, true);
			SearchEngine engine = new SearchEngine();
			IJavaSearchScope searchScope = PluginJavaSearchUtil.createSeachScope(jProject);

			SubMonitor subMonitor = SubMonitor.convert(monitor, packageFragments.length * 2);
			for (IPackageFragment pkgFragment : packageFragments) {
				SubMonitor iterationMonitor = subMonitor.split(2);
				if (pkgFragment.hasChildren()) {
					Requestor requestor = new Requestor();
					SearchPattern pattern = SearchPattern.createPattern(pkgFragment, IJavaSearchConstants.REFERENCES);
					if (pattern == null) {
						continue;
					}
					engine.search(pattern,
							new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }, searchScope,
							requestor, iterationMonitor.split(1));
					if (requestor.foundMatches()) {
						if (provideJavaClasses(pkgFragment, engine, searchScope,
								iterationMonitor.split(1))) {
							return true;
						}
					}
				}
			}
		} catch (CoreException e) {
			PDEPlugin.logException(e);
		}
		return false;
	}

	private boolean provideJavaClasses(IPackageFragment packageFragment, SearchEngine engine, IJavaSearchScope searchScope, IProgressMonitor monitor) throws JavaModelException, CoreException {
		Requestor requestor;
		IJavaElement[] children = packageFragment.getChildren();
		SubMonitor subMonitor = SubMonitor.convert(monitor, children.length);

		for (IJavaElement child : children) {
			IType[] types = null;
			if (child instanceof ICompilationUnit) {
				types = ((ICompilationUnit) child).getAllTypes();
			} else if (child instanceof IOrdinaryClassFile) {
				types = new IType[] { ((IOrdinaryClassFile) child).getType() };
			}
			if (types != null) {
				SubMonitor iterationMonitor = subMonitor.split(1).setWorkRemaining(types.length);
				for (IType type : types) {
					requestor = new Requestor();
					SearchPattern pattern = SearchPattern.createPattern(type, IJavaSearchConstants.REFERENCES);
					if (pattern == null) {
						continue;
					}
					engine.search(pattern,
							new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }, searchScope,
							requestor, iterationMonitor.split(1));
					if (requestor.foundMatches()) {
						return true;
					}
				}
			} else {
				subMonitor.worked(1);
			}
		}
		return false;
	}

	private boolean provideJavaClasses(ImportPackageObject pkg, IProgressMonitor monitor) {
		try {
			IProject project = fModel.getUnderlyingResource().getProject();

			if (!project.hasNature(JavaCore.NATURE_ID))
				return false;

			SubMonitor subMonitor = SubMonitor.convert(monitor, 1);
			IJavaProject jProject = JavaCore.create(project);
			SearchEngine engine = new SearchEngine();
			IJavaSearchScope searchScope = PluginJavaSearchUtil.createSeachScope(jProject);
			Requestor requestor = new Requestor();
			String packageName = pkg.getName();

			SearchPattern pattern = SearchPattern.createPattern(packageName, IJavaSearchConstants.PACKAGE,
					IJavaSearchConstants.REFERENCES, SearchPattern.R_EXACT_MATCH);
			if (pattern == null) {
				return false;
			}
			engine.search(
					pattern,
					new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }, searchScope, requestor,
					subMonitor.split(1));

			if (requestor.foundMatches())
				return true;
		} catch (CoreException e) {
			PDEPlugin.logException(e);
		}
		return false;
	}

	public ArrayList<Object> getList() {
		return fList;
	}

	public static void removeDependencies(IPluginModelBase model, Object[] elements) {
		ImportPackageHeader pkgHeader = null;
		for (Object element : elements) {
			if (element instanceof IPluginImport)
				try {
					model.getPluginBase().remove((IPluginImport) element);
				} catch (CoreException e) {
				}
			else if (element instanceof ImportPackageObject) {
				if (pkgHeader == null)
					pkgHeader = (ImportPackageHeader) ((ImportPackageObject) element).getHeader();
				pkgHeader.removePackage((ImportPackageObject) element);
			}
		}
	}

	private void minimizeDependencies(HashMap<String, IPluginImport> usedPlugins, ArrayList<ImportPackageObject> usedPackages, IProgressMonitor monitor) {
		ListIterator<ImportPackageObject> li = usedPackages.listIterator();
		while (li.hasNext()) {
			ImportPackageObject ipo = li.next();
			String bundle = ipo.getAttribute(Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE);
			if (usedPlugins.containsKey(bundle))
				fList.add(ipo);
		}

		Iterator<String> it = usedPlugins.keySet().iterator();
		ArrayDeque<String> plugins = new ArrayDeque<>();
		while (it.hasNext())
			plugins.push(it.next().toString());
		SubMonitor subMonitor = SubMonitor.convert(monitor);
		while (!(plugins.isEmpty())) {
			String pluginId = plugins.pop();
			IPluginModelBase base = PluginRegistry.findModel(pluginId);
			if (base == null)
				continue;
			IPluginImport[] imports = base.getPluginBase().getImports();
			SubMonitor iterationMonitor = subMonitor.setWorkRemaining(Math.max(plugins.size() + 1, 5)).split(1)
					.setWorkRemaining(imports.length);
			for (IPluginImport imp : imports) {
				if (imp.isReexported()) {
					String reExportedId = imp.getId();
					if (reExportedId != null && reExportedId.equals(pluginId)) {
						continue;
					}
					Object pluginImport = usedPlugins.remove(imp.getId());
					if (pluginImport != null) {
						fList.add(pluginImport);
						updateMonitor(iterationMonitor, fList.size());
					}
					plugins.push(reExportedId);
				}
				iterationMonitor.worked(1);
			}
		}
	}
}
