/*******************************************************************************
 * Copyright (c) 2008, 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
 *******************************************************************************/
package org.eclipse.pde.internal.ui.correction.java;

import java.text.MessageFormat;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IOpenable;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.ui.text.java.ClasspathFixProcessor.ClasspathFixProposal;
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.osgi.service.resolver.ExportPackageDescription;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.core.IBaseModel;
import org.eclipse.pde.core.plugin.IPluginImport;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.internal.core.ICoreConstants;
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.ibundle.IManifestHeader;
import org.eclipse.pde.internal.core.project.PDEProject;
import org.eclipse.pde.internal.core.text.bundle.ExportPackageHeader;
import org.eclipse.pde.internal.core.text.bundle.ExportPackageObject;
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.PDEPluginImages;
import org.eclipse.pde.internal.ui.PDEUIMessages;
import org.eclipse.pde.internal.ui.util.ModelModification;
import org.eclipse.pde.internal.ui.util.PDEModelUtility;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.text.edits.TextEdit;
import org.osgi.framework.Constants;

/**
 * A factory class used to create resolutions for JDT problem markers which involve modifying a project's MANIFEST.MF (or possibly plugin.xml)
 * @since 3.4
 */
public class JavaResolutionFactory {

	/**
	 * Type constant for a proposal of type IJavaCompletionProposal
	 */
	public static final int TYPE_JAVA_COMPLETION = 0x01;
	/**
	 * Type constant for a proposal of type ClasspathFixProposal
	 */
	public static final int TYPE_CLASSPATH_FIX = 0x02;

	/**
	 * This class represents a Change which will be applied to a Manifest file.  This change is meant to be
	 * used to create an IJavaCompletionProposal or ClasspathFixProposal.
	 */
	private static abstract class AbstractManifestChange extends Change {

		private final Object fChangeObject;
		private final IProject fProject;
		private CompilationUnit fCompilationUnit;
		private String fQualifiedTypeToImport;

		private AbstractManifestChange(IProject project, Object obj) {
			fProject = project;
			fChangeObject = obj;
		}

		public AbstractManifestChange(IProject project, Object changeObj, CompilationUnit cu,
				String qualifiedTypeToImport) {
			this(project, changeObj);
			fCompilationUnit = cu;
			fQualifiedTypeToImport = qualifiedTypeToImport;
		}

		protected Object getChangeObject() {
			return fChangeObject;
		}

		protected IProject getProject() {
			return fProject;
		}

		protected CompilationUnit getCompilationUnit() {
			return fCompilationUnit;
		}

		protected String getQualifiedTypeToImport() {
			return fQualifiedTypeToImport;
		}

		/*
		 * Provides an image for the Change
		 */
		public abstract Image getImage();

		/*
		 * Provides a description for the Change
		 */
		public abstract String getDescription();

		/*
		 * Added to allow creation of an "undo" change for each AbstractManifestChange
		 */
		protected boolean isUndo() {
			return false;
		}

		protected void insertImport(CompilationUnit compilationUnit, String qualifiedTypeToImport, IProgressMonitor pm)
				throws CoreException {
			if (compilationUnit == null || qualifiedTypeToImport == null) {
				return;
			}
			ImportRewrite rewrite = ImportRewrite.create(compilationUnit, true);
			if (rewrite == null) {
				return;
			}
			if (!isUndo()) {
				rewrite.addImport(qualifiedTypeToImport);
			} else {
				rewrite.removeImport(qualifiedTypeToImport);
			}
			TextEdit rewriteImports = rewrite.rewriteImports(pm);
			ICompilationUnit iCompilationUnit = (ICompilationUnit) compilationUnit.getJavaElement()
					.getAdapter(IOpenable.class);
			performTextEdit(rewriteImports, (IFile) iCompilationUnit.getResource(), pm);
		}

		private void performTextEdit(TextEdit textEdit, IFile file, IProgressMonitor pm)
				throws CoreException {
			TextFileChange textFileChange = new TextFileChange("Add import for " + fQualifiedTypeToImport, file); //$NON-NLS-1$
			textFileChange.setSaveMode(TextFileChange.KEEP_SAVE_STATE);
			textFileChange.setEdit(textEdit);
			textFileChange.perform(pm);
		}

		@Override
		public RefactoringStatus isValid(IProgressMonitor pm) throws CoreException, OperationCanceledException {
			return RefactoringStatus.create(Status.OK_STATUS);
		}

		@Override
		public Object getModifiedElement() {
			return getProject();
		}

		@Override
		public void initializeValidationData(IProgressMonitor pm) {
		}
	}

	/*
	 * A Change which will add a Require-Bundle entry to resolve the given
	 * dependency or add multiple Require-Bundle entries to resolve the dependency
	 * based on description name
	 */
	private static class RequireBundleManifestChange extends AbstractManifestChange {

		private RequireBundleManifestChange(IProject project, ExportPackageDescription desc, CompilationUnit cu,
				String qualifiedTypeToImport) {
			super(project, desc, cu, qualifiedTypeToImport);
		}

		private RequireBundleManifestChange(IProject project, String desc, CompilationUnit cu,
				String qualifiedTypeToImport) {
			super(project, desc, cu, qualifiedTypeToImport);
		}

		@Override
		public Change perform(IProgressMonitor pm) throws CoreException {
			PDEModelUtility.modifyModel(new ModelModification(getProject()) {
				@Override
				protected void modifyModel(IBaseModel model, IProgressMonitor monitor) throws CoreException {
					if (!(model instanceof IPluginModelBase base))
						return;
					String[] pluginIdStrings = null;
					if ("JUnit 5 bundles".equals(getChangeObject())) { //$NON-NLS-1$
						pluginIdStrings = getJUnit5Bundles();
					}
					if (getChangeObject() instanceof ExportPackageDescription) {
						pluginIdStrings = new String[1];
						pluginIdStrings[0] = ((ExportPackageDescription) getChangeObject()).getSupplier()
								.getSymbolicName();
					}
					for (int i = 0; i < pluginIdStrings.length; i++) {
						String pluginId = pluginIdStrings[i];
						if (!isUndo()) {
							IPluginImport[] imports = base.getPluginBase().getImports();
							boolean duplicate = false;
							for (IPluginImport iPluginImport : imports) {
								if (iPluginImport.getId().equals(pluginId)) {
									duplicate = true;
									break;
								}
							}
							if (duplicate)
								continue;
							IPluginImport impt = base.getPluginFactory().createImport();
							impt.setId(pluginId);
							base.getPluginBase().add(impt);
						} else {
							IPluginImport[] imports = base.getPluginBase().getImports();
							for (IPluginImport pluginImport : imports) {
								if (pluginImport.getId().equals(pluginId)) {
									base.getPluginBase().remove(pluginImport);
								}
							}
						}
					}
				}

				private String[] getJUnit5Bundles() {
					return new String[] { "org.junit", "junit-jupiter-api" }; //$NON-NLS-1$ //$NON-NLS-2$
				}
			}, new NullProgressMonitor());

			insertImport(getCompilationUnit(), getQualifiedTypeToImport(), pm);

			if (!isUndo()) {
				if (getChangeObject() instanceof ExportPackageDescription) {
					return new RequireBundleManifestChange(getProject(), (ExportPackageDescription) getChangeObject(),
							getCompilationUnit(), getQualifiedTypeToImport()) {
						@Override
						public boolean isUndo() {
							return true;
						}
					};
				}
				if (getChangeObject() instanceof String) {
					return new RequireBundleManifestChange(getProject(),(String) getChangeObject(),
							getCompilationUnit(), getQualifiedTypeToImport()) {
						@Override
						public boolean isUndo() {
							return true;
						}
					};
				}
			}
			return null;
		}


		@Override
		public Image getImage() {
			return PDEPlugin.getDefault().getLabelProvider().get(PDEPluginImages.DESC_REQ_PLUGIN_OBJ);
		}

		@Override
		public String getDescription() {
			return PDEUIMessages.UnresolvedImportFixProcessor_2;
		}

		@Override
		public String getName() {
			if (!isUndo()) {
				if(getChangeObject() instanceof String) {
					return MessageFormat.format(PDEUIMessages.UnresolvedImportFixProcessor_0,
							(getChangeObject().toString()));
				}
				return MessageFormat.format(PDEUIMessages.UnresolvedImportFixProcessor_0, ((ExportPackageDescription) getChangeObject()).getExporter().getName());
			}
			return MessageFormat.format(PDEUIMessages.UnresolvedImportFixProcessor_1, ((ExportPackageDescription) getChangeObject()).getExporter().getName());
		}

		@Override
		public Object getModifiedElement() {
			IFile[] files = new IFile[] {PDEProject.getManifest(getProject()), PDEProject.getPluginXml(getProject())};
			for (IFile file : files) {
				if (file.exists())
					return file;
			}
			return super.getModifiedElement();
		}

	}

	/*
	 * A Change which will add an Import-Package entry to resolve the given dependency
	 */
	private static class ImportPackageManifestChange extends AbstractManifestChange {

		private ImportPackageManifestChange(IProject project, ExportPackageDescription desc) {
			super(project, desc);
		}

		private ImportPackageManifestChange(IProject project, ExportPackageDescription desc, CompilationUnit cu,
				String qualifiedTypeToImport) {
			super(project, desc, cu, qualifiedTypeToImport);
		}

		@Override
		public Change perform(IProgressMonitor pm) throws CoreException {
			PDEModelUtility.modifyModel(new ModelModification(getProject()) {
				@Override
				protected void modifyModel(IBaseModel model, IProgressMonitor monitor) throws CoreException {
					if (!(model instanceof IBundlePluginModelBase base))
						return;
					IBundle bundle = base.getBundleModel().getBundle();
					ExportPackageDescription desc = (ExportPackageDescription) getChangeObject();
					String pkgId = desc.getName();
					IManifestHeader header = bundle.getManifestHeader(Constants.IMPORT_PACKAGE);
					if (header == null) {
						header = bundle.getModel().getFactory().createHeader(Constants.IMPORT_PACKAGE, pkgId);
					}
					if (header instanceof ImportPackageHeader ipHeader) {
						String versionAttr = (BundlePluginBase.getBundleManifestVersion(bundle) < 2)
								? ICoreConstants.PACKAGE_SPECIFICATION_VERSION
								: Constants.VERSION_ATTRIBUTE;
						ImportPackageObject impObject = new ImportPackageObject(ipHeader, desc, versionAttr);
						if (!isUndo()) {
							ipHeader.addPackage(impObject);
						} else {
							ipHeader.removePackage(impObject);
						}
					}
				}
			}, new NullProgressMonitor());

			insertImport(getCompilationUnit(), getQualifiedTypeToImport(), pm);

			if (!isUndo())
				return new ImportPackageManifestChange(getProject(), (ExportPackageDescription) getChangeObject()) {
					@Override
					public boolean isUndo() {
						return true;
					}
				};
			return null;
		}

		@Override
		public String getDescription() {
			return PDEUIMessages.UnresolvedImportFixProcessor_5;
		}

		@Override
		public Image getImage() {
			return PDEPlugin.getDefault().getLabelProvider().get(PDEPluginImages.DESC_BUNDLE_OBJ);
		}

		@Override
		public String getName() {
			if (!isUndo()) {
				return MessageFormat.format(PDEUIMessages.UnresolvedImportFixProcessor_3, ((ExportPackageDescription) getChangeObject()).getName());
			}
			return MessageFormat.format(PDEUIMessages.UnresolvedImportFixProcessor_4, ((ExportPackageDescription) getChangeObject()).getName());
		}

		@Override
		public Object getModifiedElement() {
			IFile file = PDEProject.getManifest(getProject());
			if (file.exists())
				return file;
			return super.getModifiedElement();
		}

	}

	private static class ExportPackageChange extends AbstractManifestChange {

		public ExportPackageChange(IProject project, IPackageFragment fragment) {
			super(project, fragment);
		}

		@Override
		public Change perform(IProgressMonitor pm) throws CoreException {
			ModelModification mod = new ModelModification(getProject()) {
				@Override
				protected void modifyModel(IBaseModel model, IProgressMonitor monitor) throws CoreException {
					if (model instanceof IBundlePluginModelBase) {
						IBundle bundle = ((IBundlePluginModelBase) model).getBundleModel().getBundle();

						ExportPackageHeader header = (ExportPackageHeader) bundle.getManifestHeader(Constants.EXPORT_PACKAGE);
						if (header == null) {
							bundle.setHeader(Constants.EXPORT_PACKAGE, ""); //$NON-NLS-1$
							header = (ExportPackageHeader) bundle.getManifestHeader(Constants.EXPORT_PACKAGE);
						}
						header.addPackage(new ExportPackageObject(header, (IPackageFragment) getChangeObject(), Constants.VERSION_ATTRIBUTE));
					}
				}
			};
			PDEModelUtility.modifyModel(mod, new NullProgressMonitor());
			// No plans to use as ClasspathFixProposal, therefore we don't have to worry about an undo
			return null;
		}

		@Override
		public String getName() {
			return NLS.bind(PDEUIMessages.ForbiddenAccessProposal_quickfixMessage, new String[] {((IPackageFragment) getChangeObject()).getElementName(), getProject().getName()});
		}

		@Override
		public Image getImage() {
			return PDEPluginImages.get(PDEPluginImages.OBJ_DESC_BUNDLE);
		}

		@Override
		public Object getModifiedElement() {
			IFile file = PDEProject.getManifest(getProject());
			if (file.exists())
				return file;
			return super.getModifiedElement();
		}

		@Override
		public String getDescription() {
			// No plans to use as ClasspathFixProposal, therefore we don't have to implement a description
			return null;
		}
	}

	/**
	 * Creates and returns a proposal which create a Require-Bundle entry in the
	 * MANIFEST.MF (or corresponding plugin.xml) for the supplier of desc. The
	 * object will be of the type specified by the type argument.
	 *
	 * @param project
	 *            the project to be updated
	 * @param desc
	 *            an ExportPackageDescription from the bundle that is to be
	 *            added as a Require-Bundle dependency
	 * @param type
	 *            the type of the proposal to be returned
	 * @param relevance
	 *            the relevance of the new proposal
	 * @param qualifiedTypeToImport
	 *            the qualified type name of the type that requires this
	 *            proposal. If this argument and cu are supplied the proposal
	 *            will add an import statement for this type to the source file
	 *            in which the proposal was invoked.
	 * @param cu
	 *            the AST root of the java source file in which this fix was
	 *            invoked
	 * @see JavaResolutionFactory#TYPE_JAVA_COMPLETION
	 * @see JavaResolutionFactory#TYPE_CLASSPATH_FIX
	 */
	public static final Object createRequireBundleProposal(IProject project, ExportPackageDescription desc, int type,
			int relevance, CompilationUnit cu, String qualifiedTypeToImport) {
		if (desc.getSupplier() == null)
			return null;
		AbstractManifestChange change = new RequireBundleManifestChange(project, desc, cu, qualifiedTypeToImport);
		return createWrapper(change, type, relevance);
	}

	/**
	 * Creates and returns a proposal which create multiple Require-Bundle entry in the
	 * MANIFEST.MF (or corresponding plugin.xml) for desc name.
	 *
	 * @param project
	 *            the project to be updated
	 * @param desc
	 *            multiple bundles that is to be  added as a Require-Bundle dependency
	 *            based on description name
	 * @param type
	 *            the type of the proposal to be returned
	 * @param relevance
	 *            the relevance of the new proposal
	 * @param qualifiedTypeToImport
	 *            the qualified type name of the type that requires this
	 *            proposal. If this argument and cu are supplied the proposal
	 *            will add an import statement for this type to the source file
	 *            in which the proposal was invoked.
	 * @param cu
	 *            the AST root of the java source file in which this fix was
	 *            invoked
	 * @see JavaResolutionFactory#TYPE_JAVA_COMPLETION
	 * @see JavaResolutionFactory#TYPE_CLASSPATH_FIX
	 */
	public static final Object createRequireBundleProposal(IProject project, String desc, int type, int relevance,
			CompilationUnit cu, String qualifiedTypeToImport) {
		if (desc == null)
			return null;
		AbstractManifestChange change = new RequireBundleManifestChange(project, desc, cu, qualifiedTypeToImport);
		return createWrapper(change, type, relevance);
	}

	/**
	 * Creates and returns a proposal which create an Import-Package entry in
	 * the MANIFEST.MF for the package represented by desc. The object will be
	 * of the type specified by the type argument.
	 *
	 * @param project
	 *            the project to be updated
	 * @param desc
	 *            an ExportPackageDescription which represents the package to be
	 *            added
	 * @param type
	 *            the type of the proposal to be returned
	 * @param relevance
	 *            the relevance of the new proposal
	 * @param qualifiedTypeToImport
	 *            the qualified type name of the type that requires this
	 *            proposal. If this argument and cu are supplied the proposal
	 *            will add an import statement for this type to the source file
	 *            in which the proposal was invoked.
	 * @param cu
	 *            the AST root of the java source file in which this fix was
	 *            invoked
	 * @see JavaResolutionFactory#TYPE_JAVA_COMPLETION
	 * @see JavaResolutionFactory#TYPE_CLASSPATH_FIX
	 */
	public static final Object createImportPackageProposal(IProject project, ExportPackageDescription desc, int type,
			int relevance, CompilationUnit cu, String qualifiedTypeToImport) {
		AbstractManifestChange change = new ImportPackageManifestChange(project, desc, cu, qualifiedTypeToImport);
		return createWrapper(change, type, relevance);
	}

	public static final IJavaCompletionProposal createSearchRepositoriesProposal(String packageName) {
		return new SearchRepositoriesForIUProposal(packageName);
	}

	/**
	 * Creates and returns a proposal which create an Export-Package entry in the MANIFEST.MF for the package represented by
	 * pkg.  The object will be of the type specified by the type argument.
	 * @param project the project to be updated
	 * @param pkg an IPackageFragment which represents the package to be added
	 * @param type the type of the proposal to be returned
	 * @param relevance the relevance of the new proposal
	 * @see JavaResolutionFactory#TYPE_JAVA_COMPLETION
	 * @see JavaResolutionFactory#TYPE_CLASSPATH_FIX
	 */
	public static final Object createExportPackageProposal(IProject project, IPackageFragment pkg, int type, int relevance) {
		AbstractManifestChange change = new ExportPackageChange(project, pkg);
		return createWrapper(change, type, relevance);
	}

	private static final Object createWrapper(AbstractManifestChange change, int type, int relevance) {
		return switch (type) {
			case TYPE_JAVA_COMPLETION -> createJavaCompletionProposal(change, relevance);
			case TYPE_CLASSPATH_FIX -> createClasspathFixProposal(change, relevance);
			default -> null;
		};
	}

	// Methods to wrap a AbstractMethodChange into a consumable format

	/**
	 * Creates and returns a ClasspathFixProposal for the given AbstractManifestChange
	 * @param change the modification which should be performed by the proposal
	 * @since 3.4
	 * @see AbstractManifestChange
	 */
	public final static ClasspathFixProposal createClasspathFixProposal(final AbstractManifestChange change, final int relevance) {
		return new ClasspathFixProposal() {

			@Override
			public Change createChange(IProgressMonitor monitor) throws CoreException {
				return change;
			}

			@Override
			public String getAdditionalProposalInfo() {
				return change.getDescription();
			}

			@Override
			public String getDisplayString() {
				return change.getName();
			}

			@Override
			public Image getImage() {
				return change.getImage();
			}

			@Override
			public int getRelevance() {
				return relevance;
			}

		};
	}

	/**
	 * Creates and returns an IJavaCompletionProposal for the given AbstractManifestChange with the given relevance.
	 * @param change the modification which should be performed by the proposal
	 * @param relevance the relevance of the IJavaCompletionProposal
	 * @since 3.4
	 * @see AbstractManifestChange
	 */
	public final static IJavaCompletionProposal createJavaCompletionProposal(final AbstractManifestChange change, final int relevance) {
		return new IJavaCompletionProposal() {

			@Override
			public int getRelevance() {
				return relevance;
			}

			@Override
			public void apply(IDocument document) {
				try {
					change.perform(new NullProgressMonitor());
				} catch (CoreException e) {
				}
			}

			@Override
			public String getAdditionalProposalInfo() {
				return change.getDescription();
			}

			@Override
			public IContextInformation getContextInformation() {
				return null;
			}

			@Override
			public String getDisplayString() {
				return change.getName();
			}

			@Override
			public Image getImage() {
				return change.getImage();
			}

			@Override
			public Point getSelection(IDocument document) {
				return null;
			}
		};
	}

}
