/*******************************************************************************
 * Copyright (c) 2000, 2011 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.jdt.internal.corext.util;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.mapping.RemoteResourceMappingContext;
import org.eclipse.core.resources.mapping.ResourceMapping;
import org.eclipse.core.resources.mapping.ResourceMappingContext;
import org.eclipse.core.resources.mapping.ResourceTraversal;

import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.internal.core.manipulation.JavaManipulationPlugin;

import org.eclipse.jdt.internal.ui.browsing.LogicalPackage;
import org.eclipse.jdt.internal.ui.model.JavaModelProvider;

/**
 * An abstract super class to describe mappings from a Java element to a
 * set of resources. The class also provides factory methods to create
 * resource mappings.
 *
 * @since 3.1
 */
public abstract class JavaElementResourceMapping extends ResourceMapping {

	protected JavaElementResourceMapping() {
	}

	public IJavaElement getJavaElement() {
		Object o= getModelObject();
		if (o instanceof IJavaElement)
			return (IJavaElement)o;
		return null;
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof JavaElementResourceMapping))
			return false;
		return getJavaElement().equals(((JavaElementResourceMapping)obj).getJavaElement());
	}

	@Override
	public int hashCode() {
		IJavaElement javaElement= getJavaElement();
		if (javaElement == null)
			return super.hashCode();

		return javaElement.hashCode();
	}

	@Override
	public String getModelProviderId() {
		return JavaModelProvider.JAVA_MODEL_PROVIDER_ID;
	}

	@Override
	public boolean contains(ResourceMapping mapping) {
		if (mapping instanceof JavaElementResourceMapping) {
			JavaElementResourceMapping javaMapping = (JavaElementResourceMapping) mapping;
			IJavaElement element = getJavaElement();
			IJavaElement other = javaMapping.getJavaElement();
			if (other != null && element != null)
				return element.getPath().isPrefixOf(other.getPath());
		}
		return false;
	}

	//---- the factory code ---------------------------------------------------------------

	private static final class JavaModelResourceMapping extends JavaElementResourceMapping {
		private final IJavaModel fJavaModel;
		private JavaModelResourceMapping(IJavaModel model) {
			Assert.isNotNull(model);
			fJavaModel= model;
		}
		@Override
		public Object getModelObject() {
			return fJavaModel;
		}
		@Override
		public IProject[] getProjects() {
			IJavaProject[] projects= null;
			try {
				projects= fJavaModel.getJavaProjects();
			} catch (JavaModelException e) {
				JavaManipulationPlugin.log(e);
				return new IProject[0];
			}
			IProject[] result= new IProject[projects.length];
			for (int i= 0; i < projects.length; i++) {
				result[i]= projects[i].getProject();
			}
			return result;
		}
		@Override
		public ResourceTraversal[] getTraversals(ResourceMappingContext context, IProgressMonitor monitor) throws CoreException {
			IJavaProject[] projects= fJavaModel.getJavaProjects();
			ResourceTraversal[] result= new ResourceTraversal[projects.length];
			for (int i= 0; i < projects.length; i++) {
				result[i]= new ResourceTraversal(new IResource[] {projects[i].getProject()}, IResource.DEPTH_INFINITE, 0);
			}
			return result;
		}
	}

	private static final class JavaProjectResourceMapping extends JavaElementResourceMapping {
		private final IJavaProject fProject;
		private JavaProjectResourceMapping(IJavaProject project) {
			Assert.isNotNull(project);
			fProject= project;
		}
		@Override
		public Object getModelObject() {
			return fProject;
		}
		@Override
		public IProject[] getProjects() {
			return new IProject[] {fProject.getProject() };
		}
		@Override
		public ResourceTraversal[] getTraversals(ResourceMappingContext context, IProgressMonitor monitor) throws CoreException {
			return new ResourceTraversal[] {
				new ResourceTraversal(new IResource[] {fProject.getProject()}, IResource.DEPTH_INFINITE, 0)
			};
		}
	}

	private static final class PackageFragementRootResourceMapping extends JavaElementResourceMapping {
		private final IPackageFragmentRoot fRoot;
		private PackageFragementRootResourceMapping(IPackageFragmentRoot root) {
			Assert.isNotNull(root);
			fRoot= root;
		}
		@Override
		public Object getModelObject() {
			return fRoot;
		}
		@Override
		public IProject[] getProjects() {
			return new IProject[] {fRoot.getJavaProject().getProject() };
		}
		@Override
		public ResourceTraversal[] getTraversals(ResourceMappingContext context, IProgressMonitor monitor) throws CoreException {
			return new ResourceTraversal[] {
				new ResourceTraversal(new IResource[] {fRoot.getResource()}, IResource.DEPTH_INFINITE, 0)
			};
		}
	}

	private static final class PackageFragmentResourceMapping extends JavaElementResourceMapping {
		private final IPackageFragment fPack;
		private PackageFragmentResourceMapping(IPackageFragment pack) {
			Assert.isNotNull(pack);
			fPack= pack;
		}
		@Override
		public Object getModelObject() {
			return fPack;
		}
		@Override
		public IProject[] getProjects() {
			return new IProject[] { fPack.getJavaProject().getProject() };
		}
		@Override
		public ResourceTraversal[] getTraversals(ResourceMappingContext context, IProgressMonitor monitor) throws CoreException {
			if (context instanceof RemoteResourceMappingContext) {
				return getRemotePackageFragmentTraversals(fPack, (RemoteResourceMappingContext)context, monitor);
			} else {
				return getPackageFragmentTraversals(fPack);
			}
		}
	}

	private static ResourceTraversal[] getPackageFragmentTraversals(IPackageFragment pack) throws CoreException {
		ArrayList<ResourceTraversal> res= new ArrayList<>();
		IContainer container= (IContainer)pack.getResource();

		if (container != null) {
			res.add(new ResourceTraversal(new IResource[] { container }, IResource.DEPTH_ONE, 0));
			if (pack.exists()) { // folder may not exist any more, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=269167
				for (Object resource : pack.getNonJavaResources()) {
					if (resource instanceof IFolder) {
						res.add(new ResourceTraversal(new IResource[] { (IResource)resource }, IResource.DEPTH_INFINITE, 0));
					}
				}
			}
		}

		return res.toArray(new ResourceTraversal[res.size()]);
	}

	private static ResourceTraversal[] getRemotePackageFragmentTraversals(IPackageFragment pack, RemoteResourceMappingContext context, IProgressMonitor monitor) throws CoreException {
		ArrayList<ResourceTraversal> res= new ArrayList<>();
		IContainer container= (IContainer)pack.getResource();

		if (container != null) {
			res.add(new ResourceTraversal(new IResource[] {container}, IResource.DEPTH_ONE, 0));
			IResource[] remoteMembers= context.fetchRemoteMembers(container, monitor);
			if (remoteMembers == null) {
				remoteMembers= context.fetchMembers(container, monitor);
			}
			if (remoteMembers != null) {
				for (IResource member : remoteMembers) {
					if (member instanceof IFolder
						&& JavaConventionsUtil.validatePackageName(member.getName(), pack).getSeverity() == IStatus.ERROR) {
						res.add(new ResourceTraversal(new IResource[] { member }, IResource.DEPTH_INFINITE, 0));
					}
				}
			}
		}
		return res.toArray(new ResourceTraversal[res.size()]);
	}

	private static final class CompilationUnitResourceMapping extends JavaElementResourceMapping {
		private final ICompilationUnit fUnit;
		private CompilationUnitResourceMapping(ICompilationUnit unit) {
			Assert.isNotNull(unit);
			fUnit= unit;
		}
		@Override
		public Object getModelObject() {
			return fUnit;
		}
		@Override
		public IProject[] getProjects() {
			return new IProject[] {fUnit.getJavaProject().getProject() };
		}
		@Override
		public ResourceTraversal[] getTraversals(ResourceMappingContext context, IProgressMonitor monitor) throws CoreException {
			return new ResourceTraversal[] {
				new ResourceTraversal(new IResource[] {fUnit.getResource()}, IResource.DEPTH_ONE, 0)
			};
		}
	}

	private static final class ClassFileResourceMapping extends JavaElementResourceMapping {
		private final IClassFile fClassFile;
		private ClassFileResourceMapping(IClassFile classFile) {
			fClassFile= classFile;
		}
		@Override
		public Object getModelObject() {
			return fClassFile;
		}
		@Override
		public IProject[] getProjects() {
			return new IProject[] { fClassFile.getJavaProject().getProject() };
		}
		@Override
		public ResourceTraversal[] getTraversals(ResourceMappingContext context, IProgressMonitor monitor) throws CoreException {
			return new ResourceTraversal[] {
				new ResourceTraversal(new IResource[] {fClassFile.getResource()}, IResource.DEPTH_ONE, 0)
			};
		}
	}

	private static final class LogicalPackageResourceMapping extends ResourceMapping {
		private final IPackageFragment[] fFragments;
		private LogicalPackageResourceMapping(IPackageFragment[] fragments) {
			fFragments= fragments;
		}
		@Override
		public Object getModelObject() {
			return fFragments;
		}
		@Override
		public IProject[] getProjects() {
			Set<IProject> result= new HashSet<>();
			for (IPackageFragment fragment : fFragments) {
				result.add(fragment.getJavaProject().getProject());
			}
			return result.toArray(new IProject[result.size()]);
		}
		@Override
		public ResourceTraversal[] getTraversals(ResourceMappingContext context, IProgressMonitor monitor) throws CoreException {
			List<ResourceTraversal> result= new ArrayList<>();
			if (context instanceof RemoteResourceMappingContext) {
				for (IPackageFragment fragment : fFragments) {
					result.addAll(Arrays.asList(getRemotePackageFragmentTraversals(fragment, (RemoteResourceMappingContext)context, monitor)));
				}
			} else {
				for (IPackageFragment fragment : fFragments) {
					result.addAll(Arrays.asList(getPackageFragmentTraversals(fragment)));
				}
			}
			return result.toArray(new ResourceTraversal[result.size()]);
		}

		@Override
		public String getModelProviderId() {
			return JavaModelProvider.JAVA_MODEL_PROVIDER_ID;
		}
	}

	public static ResourceMapping create(IJavaElement element) {
		switch (element.getElementType()) {
			case IJavaElement.TYPE:
				return create((IType)element);
			case IJavaElement.COMPILATION_UNIT:
				return create((ICompilationUnit)element);
			case IJavaElement.CLASS_FILE:
				return create((IClassFile)element);
			case IJavaElement.PACKAGE_FRAGMENT:
				return create((IPackageFragment)element);
			case IJavaElement.PACKAGE_FRAGMENT_ROOT:
				return create((IPackageFragmentRoot)element);
			case IJavaElement.JAVA_PROJECT:
				return create((IJavaProject)element);
			case IJavaElement.JAVA_MODEL:
				return create((IJavaModel)element);
			default:
				return null;
		}

	}

	public static ResourceMapping create(final IJavaModel model) {
		return new JavaModelResourceMapping(model);
	}

	public static ResourceMapping create(final IJavaProject project) {
		return new JavaProjectResourceMapping(project);
	}

	public static ResourceMapping create(final IPackageFragmentRoot root) {
		if (root.isExternal())
			return null;
		return new PackageFragementRootResourceMapping(root);
	}

	public static ResourceMapping create(final IPackageFragment pack) {
		// test if in an archive
		IPackageFragmentRoot root= (IPackageFragmentRoot)pack.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
		if (!root.isArchive() && !root.isExternal()) {
			return new PackageFragmentResourceMapping(pack);
		}
		return null;
	}

	public static ResourceMapping create(ICompilationUnit unit) {
		if (unit == null)
			return null;
		return new CompilationUnitResourceMapping(unit.getPrimary());
	}

	public static ResourceMapping create(IClassFile classFile) {
		// test if in a archive
		IPackageFragmentRoot root= (IPackageFragmentRoot)classFile.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
		if (!root.isArchive() && !root.isExternal()) {
			return new ClassFileResourceMapping(classFile);
		}
		return null;
	}

	public static ResourceMapping create(IType type) {
		// top level types behave like the CU
		IJavaElement parent= type.getParent();
		if (parent instanceof ICompilationUnit) {
			return create((ICompilationUnit)parent);
		}
		return null;
	}

	public static ResourceMapping create(LogicalPackage logicalPackage) {
		IPackageFragment[] fragments= logicalPackage.getFragments();
		List<IPackageFragment> toProcess= new ArrayList<>(fragments.length);
		for (IPackageFragment fragment : fragments) {
			// only add if not part of an archive
			IPackageFragmentRoot root= (IPackageFragmentRoot) fragment.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
			if (!root.isArchive() && !root.isExternal()) {
				toProcess.add(fragment);
			}
		}
		if (toProcess.isEmpty())
			return null;
		return new LogicalPackageResourceMapping(toProcess.toArray(new IPackageFragment[toProcess.size()]));
	}
}
