/*******************************************************************************
 * Copyright (c) 2000, 2015 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
 *     Code 9 Corporation - ongoing enhancements
 *     Bartosz Michalik <bartosz.michalik@gmail.com> - bug 109440
 *     Benjamin Cabe <benjamin.cabe@anyware-tech.com> - bug 248852, bug 247553
 *     Johannes Ahlers <Johannes.Ahlers@gmx.de> - bug 477677
 *******************************************************************************/
package org.eclipse.pde.internal.ui.wizards.plugin;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipFile;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
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.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.osgi.util.ManifestElement;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.core.build.IBuild;
import org.eclipse.pde.core.build.IBuildEntry;
import org.eclipse.pde.core.build.IBuildModelFactory;
import org.eclipse.pde.core.plugin.IPluginBase;
import org.eclipse.pde.core.plugin.IPluginLibrary;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.ISharedPluginModel;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.eclipse.pde.internal.core.ICoreConstants;
import org.eclipse.pde.internal.core.build.Build;
import org.eclipse.pde.internal.core.build.WorkspaceBuildModel;
import org.eclipse.pde.internal.core.bundle.BundlePluginBase;
import org.eclipse.pde.internal.core.ibundle.IBundle;
import org.eclipse.pde.internal.core.ibundle.IBundlePluginModelBase;
import org.eclipse.pde.internal.core.natures.PluginProject;
import org.eclipse.pde.internal.core.plugin.WorkspacePluginModelBase;
import org.eclipse.pde.internal.core.project.PDEProject;
import org.eclipse.pde.internal.ui.PDEPlugin;
import org.eclipse.pde.internal.ui.PDEUIMessages;
import org.eclipse.pde.internal.ui.search.dependencies.AddNewBinaryDependenciesOperation;
import org.eclipse.pde.internal.ui.wizards.IProjectProvider;
import org.eclipse.pde.ui.IFieldData;
import org.eclipse.pde.ui.IPluginContentWizard;
import org.eclipse.ui.dialogs.IOverwriteQuery;
import org.eclipse.ui.wizards.datatransfer.ImportOperation;
import org.eclipse.ui.wizards.datatransfer.ZipFileStructureProvider;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;

public class NewLibraryPluginCreationOperation extends NewProjectCreationOperation {

	private static final String SOURCE_PREFIX = "source."; //$NON-NLS-1$

	private final LibraryPluginFieldData fData;

	public NewLibraryPluginCreationOperation(LibraryPluginFieldData data, IProjectProvider provider, IPluginContentWizard contentWizard) {
		super(data, provider, contentWizard);
		fData = data;
	}

	private void addJar(File jarFile, IProject project, IProgressMonitor monitor) throws CoreException {
		String jarName = jarFile.getName();
		IFile file = project.getFile(jarName);
		monitor.subTask(NLS.bind(PDEUIMessages.NewProjectCreationOperation_copyingJar, jarName));
		try (InputStream in = new FileInputStream(jarFile)) {
			file.create(in, true, monitor);
		} catch (IOException fnfe) {
		}
	}

	private void adjustExportRoot(IProject project, IBundle bundle) throws CoreException {
		IResource[] resources = project.members(false);
		for (IResource resource : resources) {
			if (resource instanceof IFile) {
				if (".project".equals(resource.getName()) //$NON-NLS-1$
						|| ".classpath".equals(resource //$NON-NLS-1$
								.getName()) || ICoreConstants.PLUGIN_FILENAME_DESCRIPTOR.equals(resource.getName()) || ICoreConstants.BUILD_FILENAME_DESCRIPTOR.equals(resource.getName())) {
					continue;
				}
				// resource at the root, export root
				return;
			}
		}
		removeExportRoot(bundle);
	}

	@Override
	protected void adjustManifests(IProgressMonitor monitor, IProject project, IPluginBase base) throws CoreException {
		super.adjustManifests(monitor, project, base);
		int units = fData.doFindDependencies() ? 4 : 2;
		units += fData.isUpdateReferences() ? 1 : 0;
		SubMonitor subMonitor = SubMonitor.convert(monitor, units);
		IBundle bundle = (base instanceof BundlePluginBase) ? ((BundlePluginBase) base).getBundle() : null;
		if (bundle != null) {
			adjustExportRoot(project, bundle);
			subMonitor.worked(1);
			addExportedPackages(project, bundle);
			subMonitor.worked(1);
			if (fData.doFindDependencies()) {
				addDependencies(project, base.getModel(), subMonitor.split(2));
			}
			if (fData.isUpdateReferences()) {
				updateReferences(subMonitor.split(1), project);
			}
		}
	}

	protected void updateReferences(IProgressMonitor monitor, IProject project) throws JavaModelException {
		IJavaProject currentProject = JavaCore.create(project);
		IPluginModelBase[] pluginstoUpdate = fData.getPluginsToUpdate();
		SubMonitor subMonitor = SubMonitor.convert(monitor, pluginstoUpdate.length);
		for (IPluginModelBase element : pluginstoUpdate) {
			SubMonitor iterationMonitor = subMonitor.split(1).setWorkRemaining(2);
			IProject proj = element.getUnderlyingResource().getProject();
			if (currentProject.getProject().equals(proj))
				continue;
			IJavaProject javaProject = JavaCore.create(proj);
			IClasspathEntry[] cp = javaProject.getRawClasspath();
			IClasspathEntry[] updated = getUpdatedClasspath(cp, currentProject);
			if (updated != null) {
				javaProject.setRawClasspath(updated, iterationMonitor.split(1));
			}
			iterationMonitor.setWorkRemaining(1);
			try {
				updateRequiredPlugins(javaProject, iterationMonitor.split(1), element);
			} catch (CoreException e) {
				PDEPlugin.log(e);
			}
		}
	}

	private static void updateRequiredPlugins(IJavaProject javaProject, IProgressMonitor monitor, IPluginModelBase model) throws CoreException {
		IClasspathEntry[] entries = javaProject.getRawClasspath();
		List<IClasspathEntry> classpath = new ArrayList<>();
		List<IClasspathEntry> requiredProjects = new ArrayList<>();
		for (IClasspathEntry entry : entries) {
			if (isPluginProjectEntry(entry)) {
				requiredProjects.add(entry);
			} else {
				classpath.add(entry);
			}
		}
		if (requiredProjects.isEmpty())
			return;
		IFile file = PDEProject.getManifest(javaProject.getProject());
		try {
			// TODO format manifest
			Manifest manifest = new Manifest(file.getContents());
			String value = manifest.getMainAttributes().getValue(Constants.REQUIRE_BUNDLE);
			StringBuilder sb = value != null ? new StringBuilder(value) : new StringBuilder();
			if (sb.length() > 0)
				sb.append(","); //$NON-NLS-1$
			for (int i = 0; i < requiredProjects.size(); i++) {
				IClasspathEntry entry = requiredProjects.get(i);
				if (i > 0)
					sb.append(","); //$NON-NLS-1$
				sb.append(entry.getPath().segment(0));
				if (entry.isExported())
					sb.append(";visibility:=reexport"); // TODO is there a //$NON-NLS-1$
				// constant?
			}
			manifest.getMainAttributes().putValue(Constants.REQUIRE_BUNDLE, sb.toString());
			ByteArrayOutputStream content = new ByteArrayOutputStream();
			manifest.write(content);
			SubMonitor subMonitor = SubMonitor.convert(monitor, 2);
			file.setContents(new ByteArrayInputStream(content.toByteArray()), true, false, subMonitor.split(1));
			// now update .classpath
			javaProject.setRawClasspath(classpath.toArray(new IClasspathEntry[classpath.size()]),
					subMonitor.split(1));
//			ClasspathComputer.setClasspath(javaProject.getProject(), model);
		} catch (IOException | CoreException e) {
		}
	}

	private static boolean isPluginProjectEntry(IClasspathEntry entry) {
		if (IClasspathEntry.CPE_PROJECT != entry.getEntryKind())
			return false;
		IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
		IProject other = workspaceRoot.getProject(entry.getPath().segment(0));
		if (!PluginProject.isPluginProject(other))
			return false;
		if (PDEProject.getFragmentXml(other).exists())
			return false;

		try (InputStream is = PDEProject.getManifest(other).getContents()) {
			Manifest mf = new Manifest(is);
			if (mf.getMainAttributes().getValue(Constants.FRAGMENT_HOST) != null)
				return false;
		} catch (IOException | CoreException e) {
			// assume "not a fragment"
		}

		return true;
	}

	/**
	 * @return updated classpath or null if there were no changes
	 */
	private IClasspathEntry[] getUpdatedClasspath(IClasspathEntry[] cp, IJavaProject currentProject) {
		boolean exposed = false;
		int refIndex = -1;
		List<IClasspathEntry> result = new ArrayList<>();
		Set<Manifest> manifests = new HashSet<>();
		for (int i = 0; i < fData.getLibraryPaths().length; ++i) {
			try (JarFile jarFile = new JarFile(fData.getLibraryPaths()[i])) {
				manifests.add(jarFile.getManifest());
			} catch (IOException e) {
				PDEPlugin.log(e);
			}
		}
		IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
		for (IClasspathEntry cpe : cp) {
			switch (cpe.getEntryKind()) {
				case IClasspathEntry.CPE_LIBRARY :
					String path = null;
					IPath location = root.getFile(cpe.getPath()).getLocation();
					if (location != null) {
						path = location.toString();
					}
					//try maybe path is absolute
					if (path == null) {
						path = cpe.getPath().toString();
					}
				try (JarFile jarFile = new JarFile(path)) {

						if (manifests.contains(jarFile.getManifest())) {
							if (refIndex < 0) {
								// allocate slot
								refIndex = result.size();
								result.add(null);
							}
							exposed |= cpe.isExported();
						} else {
							result.add(cpe);
						}
					} catch (IOException e) {
						PDEPlugin.log(e);
					}
					break;
				default :
					result.add(cpe);
					break;
			}
		}
		if (refIndex >= 0) {
			result.set(refIndex, JavaCore.newProjectEntry(currentProject.getPath(), exposed));
			return result.toArray(new IClasspathEntry[result.size()]);
		}
		return null;
	}

	@Override
	protected void createContents(IProgressMonitor monitor, IProject project) throws CoreException, JavaModelException, InvocationTargetException, InterruptedException {
		// copy jars
		String[] paths = fData.getLibraryPaths();
		SubMonitor subMonitor = SubMonitor.convert(monitor, paths.length + 2);
		for (int i = paths.length - 1; i >= 0; i--) {
			File jarFile = new File(paths[i]);
			if (fData.isUnzipLibraries()) {
				importJar(jarFile, project, subMonitor.split(1));
			} else {
				addJar(jarFile, project, subMonitor);
			}
		}

		// delete manifest.mf imported from libraries
		IFile importedManifest = PDEProject.getManifest(project);
		if (importedManifest.exists()) {
			importedManifest.delete(true, false, subMonitor.split(1));
			subMonitor.setWorkRemaining(1);
			if (!fData.hasBundleStructure()) {
				IFolder meta_inf = project.getFolder("META-INF"); //$NON-NLS-1$
				if (meta_inf.members().length == 0) {
					meta_inf.delete(true, false, subMonitor.split(1));
				}
			}
		}
		subMonitor.setWorkRemaining(0);
	}

	@Override
	protected void fillBinIncludes(IProject project, IBuildEntry binEntry) throws CoreException {
		if (fData.hasBundleStructure())
			binEntry.addToken(ICoreConstants.MANIFEST_FOLDER_NAME);
		else
			binEntry.addToken(ICoreConstants.PLUGIN_FILENAME_DESCRIPTOR);

		if (fData.isUnzipLibraries()) {
			IResource[] resources = project.members(false);
			for (IResource resource : resources) {
				String resourceName = resource.getName();
				if (resource instanceof IFolder) {
					if (!".settings".equals(resourceName) && !binEntry.contains(resourceName + "/")) //$NON-NLS-1$ //$NON-NLS-2$
						binEntry.addToken(resourceName + "/"); //$NON-NLS-1$
				} else {
					if (".project".equals(resourceName) //$NON-NLS-1$
							|| ".classpath".equals(resourceName) //$NON-NLS-1$
							|| ICoreConstants.BUILD_FILENAME_DESCRIPTOR.equals(resourceName)) {
						continue;
					}
					if (!binEntry.contains(resourceName))
						binEntry.addToken(resourceName);
				}
			}
		} else {
			String[] libraryPaths = fData.getLibraryPaths();
			for (String libraryPath : libraryPaths) {
				File jarFile = new File(libraryPath);
				String name = jarFile.getName();
				if (!binEntry.contains(name))
					binEntry.addToken(name);
			}
		}
	}

	@Override
	protected IClasspathEntry[] getInternalClassPathEntries(IJavaProject project, IFieldData data) {
		String[] libraryPaths;
		if (fData.isUnzipLibraries()) {
			libraryPaths = new String[] {""}; //$NON-NLS-1$
		} else {
			libraryPaths = fData.getLibraryPaths();
		}
		IClasspathEntry[] entries = new IClasspathEntry[libraryPaths.length];
		for (int j = 0; j < libraryPaths.length; j++) {
			File jarFile = new File(libraryPaths[j]);
			String jarName = jarFile.getName();
			IPath path = project.getProject().getFullPath().append(jarName);
			entries[j] = JavaCore.newLibraryEntry(path, null, null, true);
		}
		return entries;
	}

	@Override
	protected int getNumberOfWorkUnits() {
		int numUnits = super.getNumberOfWorkUnits();
		numUnits += fData.getLibraryPaths().length;
		return numUnits;
	}

	private void importJar(File jar, IResource destination, IProgressMonitor monitor) throws CoreException, InvocationTargetException, InterruptedException {
		try (ZipFile input = new ZipFile(jar);) {
			ZipFileStructureProvider provider = new ZipFileStructureProvider(input);
			ImportOperation op = new ImportOperation(destination.getFullPath(), provider.getRoot(), provider,
					pathString -> IOverwriteQuery.ALL);
			op.run(monitor);
		} catch (IOException e) {
			throw new CoreException(
					Status.error(NLS.bind(PDEUIMessages.NewProjectCreationOperation_errorImportingJar, jar), e));
		}
	}

	private void removeExportRoot(IBundle bundle) {
		String value = bundle.getHeader(Constants.BUNDLE_CLASSPATH);
		if (value == null)
			value = "."; //$NON-NLS-1$
		try {
			ManifestElement[] elems = ManifestElement.parseHeader(Constants.BUNDLE_CLASSPATH, value);
			StringBuilder buff = new StringBuilder(value.length());
			for (int i = 0; i < elems.length; i++) {
				if (!elems[i].getValue().equals(".")) //$NON-NLS-1$
					buff.append(elems[i].getValue());
			}
			bundle.setHeader(Constants.BUNDLE_CLASSPATH, buff.toString());
		} catch (BundleException e) {
		}
	}

	@Override
	protected void setPluginLibraries(WorkspacePluginModelBase model) throws CoreException {
		IPluginBase pluginBase = model.getPluginBase();
		if (fData.isUnzipLibraries()) {
			IPluginLibrary library = model.getPluginFactory().createLibrary();
			library.setName("."); //$NON-NLS-1$
			library.setExported(true);
			pluginBase.add(library);
		} else {
			String[] paths = fData.getLibraryPaths();
			for (String path : paths) {
				File jarFile = new File(path);
				IPluginLibrary library = model.getPluginFactory().createLibrary();
				library.setName(jarFile.getName());
				library.setExported(true);
				pluginBase.add(library);
			}
		}
	}

	@Override
	protected void createSourceOutputBuildEntries(WorkspaceBuildModel model, IBuildModelFactory factory) throws CoreException {
		if (fData.isUnzipLibraries()) {
			// SOURCE.<LIBRARY_NAME>
			IBuildEntry entry = factory.createEntry(IBuildEntry.JAR_PREFIX + "."); //$NON-NLS-1$
			entry.addToken("."); //$NON-NLS-1$
			model.getBuild().add(entry);

			// OUTPUT.<LIBRARY_NAME>
			entry = factory.createEntry(IBuildEntry.OUTPUT_PREFIX + "."); //$NON-NLS-1$
			entry.addToken("."); //$NON-NLS-1$
			model.getBuild().add(entry);
		}
	}

	private void addExportedPackages(IProject project, IBundle bundle) {
		String value = bundle.getHeader(Constants.BUNDLE_CLASSPATH);
		if (value == null)
			value = "."; //$NON-NLS-1$
		try {
			ManifestElement[] elems = ManifestElement.parseHeader(Constants.BUNDLE_CLASSPATH, value);
			Map<String, List<String>> map = new HashMap<>();
			for (ManifestElement elem : elems) {
				ArrayList<String> filter = new ArrayList<>();
				filter.add("*"); //$NON-NLS-1$
				map.put(elem.getValue(), filter);
			}
			Set<String> packages = getExports(project, map);
			String pkgValue = getCommaValuesFromPackagesSet(packages, fData.getVersion());
			bundle.setHeader(Constants.EXPORT_PACKAGE, pkgValue);
		} catch (BundleException e) {
		}
	}

	public Set<String> getExports(IProject proj, Map<String, List<String>> libs) {
		IFile buildProperties = PDEProject.getBuildProperties(proj);
		IBuild build = null;
		if (buildProperties != null) {
			WorkspaceBuildModel buildModel = new WorkspaceBuildModel(buildProperties);
			build = buildModel.getBuild();
		} else
			build = new Build();
		return findPackages(proj, libs, build);
	}

	private Set<String> findPackages(IProject proj, Map<String, List<String>> libs, IBuild build) {
		TreeSet<String> result = new TreeSet<>();
		IJavaProject jp = JavaCore.create(proj);
		Iterator<Map.Entry<String, List<String>>> it = libs.entrySet().iterator();
		while (it.hasNext()) {
			Map.Entry<String, List<String>> entry = it.next();
			String libName = entry.getKey();
			List<String> filter = entry.getValue();
			IBuildEntry libEntry = build.getEntry(SOURCE_PREFIX + libName);
			if (libEntry != null) {
				String[] tokens = libEntry.getTokens();
				for (String token : tokens) {
					IResource folder = null;
					if (token.equals(".")) //$NON-NLS-1$
						folder = proj;
					else
						folder = proj.getFolder(token);
					if (folder != null)
						addPackagesFromFragRoot(jp.getPackageFragmentRoot(folder), result, filter);
				}
			} else {
				IResource res = proj.findMember(libName);
				if (res != null)
					addPackagesFromFragRoot(jp.getPackageFragmentRoot(res), result, filter);
			}
		}
		return result;
	}

	private void addPackagesFromFragRoot(IPackageFragmentRoot root, Collection<String> result, List<?> filter) {
		if (root == null)
			return;
		try {
			if (filter != null && !filter.contains("*")) { //$NON-NLS-1$
				ListIterator<?> li = filter.listIterator();
				while (li.hasNext()) {
					String pkgName = li.next().toString();
					if (pkgName.endsWith(".*")) //$NON-NLS-1$
						pkgName = pkgName.substring(0, pkgName.length() - 2);

					IPackageFragment frag = root.getPackageFragment(pkgName);
					if (frag != null)
						result.add(pkgName);
				}
				return;
			}
			IJavaElement[] children = root.getChildren();
			for (IJavaElement child : children) {
				IPackageFragment fragment = (IPackageFragment) child;
				String name = fragment.getElementName();
				if (fragment.hasChildren() && !result.contains(name)) {
					result.add(name);
				}
			}
		} catch (JavaModelException e) {
		}
	}

	private void addDependencies(IProject project, ISharedPluginModel model, IProgressMonitor monitor) {
		if (!(model instanceof IBundlePluginModelBase)) {
			return;
		}
		final boolean unzip = fData.isUnzipLibraries();
		try {
			new AddNewBinaryDependenciesOperation(project, (IBundlePluginModelBase) model) {
				// Need to override this function to include every bundle in the
				// target platform as a possible dependency
				@Override
				protected String[] findSecondaryBundles(IBundle bundle, Set<String> ignorePkgs) {
					IPluginModelBase[] bases = PluginRegistry.getActiveModels();
					String[] ids = new String[bases.length];
					for (int i = 0; i < bases.length; i++) {
						BundleDescription desc = bases[i].getBundleDescription();
						if (desc == null)
							ids[i] = bases[i].getPluginBase().getId();
						else
							ids[i] = desc.getSymbolicName();
					}
					return ids;
				}

				// Need to override this function because when jar is unzipped,
				// build.properties does not contain entry for '.'.
				// Therefore, the super.addProjectPackages will not find the
				// project packages(it includes only things in bin.includes)
				@Override
				protected void addProjectPackages(IBundle bundle, Set<String> ignorePkgs) {
					if (!unzip)
						super.addProjectPackages(bundle, ignorePkgs);
					ArrayDeque<IResource> stack = new ArrayDeque<>();
					stack.push(fProject);
					try {
						while (!stack.isEmpty()) {
							IContainer folder = (IContainer) stack.pop();
							IResource[] children = folder.members();
							for (IResource child : children) {
								if (child instanceof IContainer)
									stack.push(child);
								else if ("class".equals(child.getFileExtension())) { //$NON-NLS-1$
									String path = folder.getProjectRelativePath().toString();
									ignorePkgs.add(path.replace('/', '.'));
								}
							}
						}
					} catch (CoreException e) {
					}

				}
			}.run(monitor);
		} catch (InvocationTargetException | InterruptedException e) {
		}
	}

}
