/*******************************************************************************
 * 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
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.browsing;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;

import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IResource;

import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.TreeViewer;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaElementDelta;
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.jdt.internal.ui.JavaPlugin;

/**
 * Tree content provider for the hierarchical layout in the packages view.
 * <p>
 * XXX: The standard Java browsing part content provider needs and calls
 * the browsing part/view. This class currently doesn't need to do so
 * but might be required to later.
 * </p>
 */
class PackagesViewHierarchicalContentProvider extends LogicalPackagesProvider implements ITreeContentProvider {

	public PackagesViewHierarchicalContentProvider(StructuredViewer viewer){
		super(viewer);
	}

	/*
	 * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(Object)
	 */
	@Override
	public Object[] getChildren(Object parentElement) {
		try {
			if (parentElement instanceof IJavaElement) {
				IJavaElement iJavaElement= (IJavaElement) parentElement;
				int type= iJavaElement.getElementType();

				switch (type) {
					case IJavaElement.JAVA_PROJECT :
						{

							//create new element mapping
							fMapToLogicalPackage.clear();
							fMapToPackageFragments.clear();
							IJavaProject project= (IJavaProject) parentElement;

							List<IPackageFragment> list= new ArrayList<>();
							for (IPackageFragment fragment : getTopLevelChildrenByElementName(project.getPackageFragments())) {
								IJavaElement el= fragment.getParent();
								if (el instanceof IPackageFragmentRoot) {
									IPackageFragmentRoot root= (IPackageFragmentRoot) el;
									if (!root.isArchive() || !root.isExternal())
										list.add(fragment);
								}
							}

							List<Object> folders= new ArrayList<>();
							for (IPackageFragmentRoot root : project.getPackageFragmentRoots()) {
								IResource resource= root.getUnderlyingResource();
								if (resource instanceof IFolder) {
									folders.addAll(getFolders(((IFolder)resource).members()));
								}
							}

							Object[] logicalPackages= combineSamePackagesIntoLogialPackages(list.toArray(new IPackageFragment[list.size()]));
							if (folders.size() > 0) {
								if (logicalPackages.length > 0)
									folders.addAll(Arrays.asList(logicalPackages));
								return folders.toArray();
							} else {
								return logicalPackages;
							}
						}

					case IJavaElement.PACKAGE_FRAGMENT_ROOT :
						{
							IPackageFragmentRoot root= (IPackageFragmentRoot) parentElement;

							//create new element mapping
							fMapToLogicalPackage.clear();
							fMapToPackageFragments.clear();
							IResource resource= root.getUnderlyingResource();
							if (root.isArchive() || root.isExternal()) {
								IJavaElement[] els= root.getChildren();
								IPackageFragment[] fragments= getTopLevelChildrenByElementName(els);
								addFragmentsToMap(fragments);
								return fragments;

							} else if (resource instanceof IFolder) {
								List<IAdaptable> children= getFoldersAndElements(((IFolder)resource).members());

								IPackageFragment defaultPackage= root.getPackageFragment(""); //$NON-NLS-1$
								if(defaultPackage.exists())
									children.add(defaultPackage);

								addFragmentsToMap(children);
								return children.toArray();
							} else {
								return NO_CHILDREN;
							}
						}

					case IJavaElement.PACKAGE_FRAGMENT :
						{
							IPackageFragment packageFragment= (IPackageFragment) parentElement;
							if (packageFragment.isDefaultPackage())
								return NO_CHILDREN;

							IPackageFragmentRoot parent= (IPackageFragmentRoot) packageFragment.getParent();
							IPackageFragment[] fragments= findNextLevelChildrenByElementName(parent, packageFragment);

							addFragmentsToMap(fragments);

							Object[] nonJavaResources= packageFragment.getNonJavaResources();
							if (nonJavaResources.length == 0) {
								return fragments;
							}
							ArrayList<Object> combined= new ArrayList<>();
							combined.addAll(Arrays.asList(fragments));
							for (Object curr : nonJavaResources) {
								if (curr instanceof IFolder) {
									combined.add(curr);
								}
							}
							return combined.toArray();
						}
				}

			//@Improve: rewrite using concatenate
			} else if (parentElement instanceof LogicalPackage) {

				List<IPackageFragment> children= new ArrayList<>();
				LogicalPackage logicalPackage= (LogicalPackage) parentElement;
				for (IPackageFragment fragment : logicalPackage.getFragments()) {
					IPackageFragment[] objects= findNextLevelChildrenByElementName((IPackageFragmentRoot) fragment.getParent(), fragment);
					children.addAll(Arrays.asList(objects));
				}
				return combineSamePackagesIntoLogialPackages(children.toArray(new IPackageFragment[children.size()]));
			} else if (parentElement instanceof IFolder) {
				IFolder folder= (IFolder)parentElement;
				IResource[] resources= folder.members();
				List<IAdaptable> children = getFoldersAndElements(resources);
				addFragmentsToMap(children);
				return children.toArray();
			}

		} catch (CoreException e) {
			return NO_CHILDREN;
		}
		return NO_CHILDREN;
	}

	private void addFragmentsToMap(List<IAdaptable> elements) {
		List<IPackageFragment> packageFragments= new ArrayList<>();
		for (IAdaptable iAdaptable : elements) {
			Object elem= iAdaptable;
			if (elem instanceof IPackageFragment)
				packageFragments.add((IPackageFragment) elem);
		}
		addFragmentsToMap(packageFragments.toArray(new IPackageFragment[packageFragments.size()]));
	}

	private List<IAdaptable> getFoldersAndElements(IResource[] resources) {
		List<IAdaptable> list= new ArrayList<>();
		for (IResource resource : resources) {
			if (resource instanceof IFolder) {
				IFolder folder= (IFolder) resource;
				IJavaElement element= JavaCore.create(folder);

				if (element instanceof IPackageFragment) {
					list.add(element);
				} else {
					list.add(folder);
				}
			}
		}
		return list;
	}

	private List<IFolder> getFolders(IResource[] resources) {
		List<IFolder> list= new ArrayList<>();
		for (IResource resource : resources) {
			if (resource instanceof IFolder) {
				IFolder folder= (IFolder) resource;
				IJavaElement element= JavaCore.create(folder);

				if (element == null) {
					list.add(folder);
				}
			}
		}
		return list;
	}

	private IPackageFragment[] findNextLevelChildrenByElementName(IPackageFragmentRoot parent, IPackageFragment fragment) {
		List<IPackageFragment> list= new ArrayList<>();
		try {

			String fragmentname= fragment.getElementName();
			for (IJavaElement element : parent.getChildren()) {
				if (element instanceof IPackageFragment) {
					IPackageFragment frag= (IPackageFragment) element;

					String name= element.getElementName();
					if (name.length() > fragmentname.length() && name.charAt(fragmentname.length()) == '.' && frag.exists() && !IPackageFragment.DEFAULT_PACKAGE_NAME.equals(fragmentname) && name.startsWith(fragmentname) && !name.equals(fragmentname)) {
						String tail= name.substring(fragmentname.length() + 1);
						if (!IPackageFragment.DEFAULT_PACKAGE_NAME.equals(tail) && tail.indexOf('.') == -1) {
							list.add(frag);
						}
					}
				}
			}

		} catch (JavaModelException e) {
			JavaPlugin.log(e);
		}
		return list.toArray(new IPackageFragment[list.size()]);
	}

	private IPackageFragment[] getTopLevelChildrenByElementName(IJavaElement[] elements){
		List<IJavaElement> topLevelElements= new ArrayList<>();
		for (IJavaElement iJavaElement : elements) {
			//if the name of the PackageFragment is the top level package it will contain no "." separators
			if (iJavaElement instanceof IPackageFragment && iJavaElement.getElementName().indexOf('.')==-1){
				topLevelElements.add(iJavaElement);
			}
		}
		return topLevelElements.toArray(new IPackageFragment[topLevelElements.size()]);
	}

	/*
	 * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(Object)
	 */
	@Override
	public Object getParent(Object element) {

		if (element instanceof IPackageFragment) {
			IPackageFragment fragment= (IPackageFragment) element;
			if(!fragment.exists())
				return null;
			Object parent= getHierarchicalParent(fragment);
			if(parent instanceof IPackageFragment) {
				IPackageFragment pkgFragment= (IPackageFragment)parent;
				LogicalPackage logicalPkg= findLogicalPackage(pkgFragment);
				if (logicalPkg != null)
					return logicalPkg;
				else {
					LogicalPackage lp= createLogicalPackage(pkgFragment);
					if(lp == null)
						return pkgFragment;
					else return lp;
				}
			}
			return parent;

		} else if(element instanceof LogicalPackage){
			LogicalPackage el= (LogicalPackage) element;
			IPackageFragment fragment= el.getFragments()[0];
			Object parent= getHierarchicalParent(fragment);

			if(parent instanceof IPackageFragment){
				IPackageFragment pkgFragment= (IPackageFragment) parent;
				LogicalPackage logicalPkg= findLogicalPackage(pkgFragment);
				if (logicalPkg != null)
					return logicalPkg;
				else {
					LogicalPackage lp= createLogicalPackage(pkgFragment);
					if(lp == null)
						return pkgFragment;
					else return lp;
				}
			} else
				return fragment.getJavaProject();
		} else if (element instanceof IFolder) {
			IFolder folder = (IFolder) element;
			IResource res = folder.getParent();

			IJavaElement el = JavaCore.create(res);
			if (el != null) {
				return el;
			} else {
				return res;
			}
		}
		return null;
	}

	/*
	 * Check if the given IPackageFragment should be the member of a
	 * LogicalPackage and if so creates the LogicalPackage and adds it to the
	 * map.
	 */
	private LogicalPackage createLogicalPackage(IPackageFragment pkgFragment) {
		if(!fInputIsProject)
			return null;

		List<IPackageFragment> fragments= new ArrayList<>();
		try {
			for (IPackageFragmentRoot root : pkgFragment.getJavaProject().getPackageFragmentRoots()) {
				IPackageFragment fragment= root.getPackageFragment(pkgFragment.getElementName());
				if(fragment.exists() && !fragment.equals(pkgFragment))
					fragments.add(fragment);
			}
			if(!fragments.isEmpty()) {
				LogicalPackage logicalPackage= new LogicalPackage(pkgFragment);
				fMapToLogicalPackage.put(getKey(pkgFragment), logicalPackage);
				Iterator<IPackageFragment> iter= fragments.iterator();
				while(iter.hasNext()){
					IPackageFragment f= iter.next();
					if(logicalPackage.belongs(f)){
						logicalPackage.add(f);
						fMapToLogicalPackage.put(getKey(f), logicalPackage);
					}
				}

				return logicalPackage;
			}

		} catch (JavaModelException e) {
			JavaPlugin.log(e);
		}

		return null;
	}

	private Object getHierarchicalParent(IPackageFragment fragment) {
		IJavaElement parent= fragment.getParent();

		if ((parent instanceof IPackageFragmentRoot) && parent.exists()) {
			IPackageFragmentRoot root= (IPackageFragmentRoot) parent;
			if (root.isArchive() || root.isExternal() || !fragment.exists()) {
				return findNextLevelParentByElementName(fragment);
			} else {
				IResource resource= fragment.getResource();
				if ((resource != null) && (resource instanceof IFolder)) {
					IFolder folder= (IFolder) resource;
					IResource res= folder.getParent();

					IJavaElement el= JavaCore.create(res);
					if (el != null) {
						return el;
					} else {
						return res;
					}
				}
			}
		}
		return parent;
	}

	private Object findNextLevelParentByElementName(IPackageFragment child) {
		String name= child.getElementName();

		int index= name.lastIndexOf('.');
		if (index != -1) {
			String realParentName= name.substring(0, index);
			IPackageFragment element= ((IPackageFragmentRoot) child.getParent()).getPackageFragment(realParentName);
			if (element.exists()) {
				return element;
			}
		}
		return child.getParent();
	}


	/*
	 * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(Object)
	 */
	@Override
	public boolean hasChildren(Object element) {

		if (element instanceof IPackageFragment) {
			IPackageFragment fragment= (IPackageFragment) element;
			if(fragment.isDefaultPackage() || !fragment.exists())
				return false;
		}
		return getChildren(element).length > 0;
	}

	/*
	 * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(Object)
	 */
	@Override
	public Object[] getElements(Object inputElement) {
		return getChildren(inputElement);
	}

	@Override
	protected void processDelta(IJavaElementDelta delta) throws JavaModelException {

		int kind = delta.getKind();
		final IJavaElement element = delta.getElement();

		if (isClassPathChange(delta)) {
			Object input= fViewer.getInput();
			if (input != null) {
				if (fInputIsProject && input.equals(element.getJavaProject())) {
					postRefresh(input);
					return;
				} else if (!fInputIsProject && input.equals(element)) {
					if (element.exists())
						postRefresh(input);
					else
						postRemove(input);
					return;
				}
			}
		}

		if (kind == IJavaElementDelta.REMOVED) {
			Object input= fViewer.getInput();
			if (input != null && input.equals(element)) {
					postRemove(input);
					return;
				}
		}

		if (element instanceof IPackageFragment) {
			final IPackageFragment frag = (IPackageFragment) element;

			//if fragment was in LogicalPackage refresh,
			//otherwise just remove
			switch (kind) {
				case IJavaElementDelta.REMOVED:
					removeElement(frag);
					return;
				case IJavaElementDelta.ADDED:
					Object parent= getParent(frag);
					addElement(frag, parent);
					return;
				case IJavaElementDelta.CHANGED:
					//just refresh
					LogicalPackage logicalPkg= findLogicalPackage(frag);
					//in case changed object is filtered out
					if (logicalPkg != null)
						postRefresh(findElementToRefresh(logicalPkg));
					else
						postRefresh(findElementToRefresh(frag));
					return;
				default:
					break;
			}
		}

		processAffectedChildren(delta);
	}

	private Object findElementToRefresh(Object object) {
		Object toBeRefreshed= object;
		if (fViewer.testFindItem(object) == null) {
			 Object parent= getParent(object);
			 if(parent instanceof IPackageFragmentRoot && fInputIsProject)
			 	parent= ((IPackageFragmentRoot)parent).getJavaProject();

			if(parent != null)
				toBeRefreshed= parent;
		}
		return toBeRefreshed;
	}

	private void processAffectedChildren(IJavaElementDelta delta) throws JavaModelException {
		for (IJavaElementDelta child : delta.getAffectedChildren()) {
			if (!(child instanceof ICompilationUnit)) {
				processDelta(child);
			}
		}
	}

	private void postAdd(final Object child, final Object parent) {
		postRunnable(() -> {
			Control ctrl = fViewer.getControl();
			if (ctrl != null && !ctrl.isDisposed()) {
				((TreeViewer)fViewer).add(parent, child);
			}
		});
	}

	private void postRemove(final Object object) {
		postRunnable(() -> {
			Control ctrl = fViewer.getControl();
			if (ctrl != null && !ctrl.isDisposed()) {
				((TreeViewer)fViewer).remove(object);
			}
		});
	}

	private void postRefresh(final Object object) {
		postRunnable(() -> {
			Control ctrl= fViewer.getControl();
			if (ctrl != null && !ctrl.isDisposed()) {
				((TreeViewer) fViewer).refresh(object);
			}
		});
	}

	private void postRunnable(final Runnable r) {
		Control ctrl= fViewer.getControl();
		if (ctrl != null && !ctrl.isDisposed()) {
		//	fBrowsingPart.setProcessSelectionEvents(false);
			try {
				Display currentDisplay= Display.getCurrent();
				if (currentDisplay != null && currentDisplay.equals(ctrl.getDisplay()))
					ctrl.getDisplay().syncExec(r);
				else
					ctrl.getDisplay().asyncExec(r);
			} finally {
		//		fBrowsingPart.setProcessSelectionEvents(true);
			}
		}
	}

	private void addElement(IPackageFragment frag, Object parent) {

		String key= getKey(frag);
		LogicalPackage lp= fMapToLogicalPackage.get(key);

		//if fragment must be added to an existing LogicalPackage
		if (lp != null && lp.belongs(frag)){
			lp.add(frag);
			return;
		}

		//if a new LogicalPackage must be created
		IPackageFragment iPackageFragment= fMapToPackageFragments.get(key);
		if (iPackageFragment!= null && !iPackageFragment.equals(frag)){
			lp= new LogicalPackage(iPackageFragment);
			lp.add(frag);
			//add new LogicalPackage to LogicalPackages map
			fMapToLogicalPackage.put(key, lp);

			//determine who to refresh
			if (parent instanceof IPackageFragmentRoot){
				IPackageFragmentRoot root= (IPackageFragmentRoot) parent;
				if (fInputIsProject){
					postRefresh(root.getJavaProject());
				} else {
					postRefresh(root);
				}
			} else {
				//@Improve: Should this be replaced by a refresh?
				postAdd(lp, parent);
				postRemove(iPackageFragment);
			}

		}
		//if this is a new Package Fragment
		else {
			fMapToPackageFragments.put(key, frag);

			//determine who to refresh
			if (parent instanceof IPackageFragmentRoot) {
				IPackageFragmentRoot root= (IPackageFragmentRoot) parent;
				if (fInputIsProject) {
					postAdd(frag, root.getJavaProject());
				} else
					postAdd(frag, root);
			} else {
				postAdd(frag, parent);
			}
		}
	}

	private void removeElement(IPackageFragment frag) {

		String key= getKey(frag);
		LogicalPackage lp= fMapToLogicalPackage.get(key);

		if(lp != null){
			lp.remove(frag);
			//if the LogicalPackage needs to revert back to a PackageFragment
			//remove it from the LogicalPackages map and add the PackageFragment
			//to the PackageFragment map
			if (lp.getFragments().length == 1) {
				IPackageFragment fragment= lp.getFragments()[0];
				fMapToPackageFragments.put(key, fragment);
				fMapToLogicalPackage.remove(key);

				//remove the LogicalPackage from viewer
				postRemove(lp);

				Object parent= getParent(fragment);
				if (parent instanceof IPackageFragmentRoot) {
					parent= ((IPackageFragmentRoot)parent).getJavaProject();
				}
				postAdd(fragment, parent);
			}

		} else {
			//remove the fragment from the fragment map and viewer
			IPackageFragment fragment= fMapToPackageFragments.get(key);
			if (fragment!= null && fragment.equals(frag)) {
				fMapToPackageFragments.remove(key);
				postRemove(frag);
			}
		}
	}
}
