/*******************************************************************************
 * Copyright (c) 2005, 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.jeview.views;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.Callable;

import org.eclipse.core.resources.IResource;

import org.eclipse.jdt.core.IAnnotatable;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJarEntryResource;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.ILocalVariable;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMemberValuePair;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IModularClassFile;
import org.eclipse.jdt.core.IOrdinaryClassFile;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IParent;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeParameter;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.ui.JavaElementLabels;


public class JavaElement extends JEAttribute {

	private static final long LABEL_OPTIONS= JavaElementLabels.F_APP_TYPE_SIGNATURE | JavaElementLabels.M_PARAMETER_TYPES | JavaElementLabels.M_APP_RETURNTYPE | JavaElementLabels.ALL_FULLY_QUALIFIED | JavaElementLabels.T_TYPE_PARAMETERS | JavaElementLabels.USE_RESOLVED;

	private final JEAttribute fParent; //can be null
	private final String fName; //can be null
	private final IJavaElement fJavaElement; //can be null

	public JavaElement(JEAttribute parent, String name, IJavaElement element) {
		fParent= parent;
		fName= name;
		fJavaElement= element;
	}

	public JavaElement(JEAttribute parent, IJavaElement element) {
		this(parent, null, element);
	}

	@Override
	public JEAttribute getParent() {
		return fParent;
	}

	public IJavaElement getJavaElement() {
		return fJavaElement;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null || !obj.getClass().equals(getClass())) {
			return false;
		}

		JavaElement other= (JavaElement) obj;
		if (!Objects.equals(fParent, other.fParent)) {
			return false;
		}

		if (!Objects.equals(fName, other.fName)) {
			return false;
		}

		if (!Objects.equals(fJavaElement, other.fJavaElement)) {
			return false;
		}

		return true;
	}

	@Override
	public int hashCode() {
		return (fParent != null ? fParent.hashCode() : 0)
				+ (fName != null ? fName.hashCode() : 0)
				+ (fJavaElement != null ? fJavaElement.hashCode() : 0);
	}

	@Override
	public Object getWrappedObject() {
		return fJavaElement;
	}

	@Override
	public String getLabel() {
		StringBuilder sb= new StringBuilder();
		if (fName != null)
			sb.append(fName).append(": ");

		if (fJavaElement == null) {
			sb.append("java element: null");
		} else {
			String classname= fJavaElement.getClass().getName();
			sb.append(classname.substring(classname.lastIndexOf('.') + 1)).append(": ");
			sb.append(JavaElementLabels.getElementLabel(fJavaElement, LABEL_OPTIONS));
			if (! fJavaElement.exists())
				sb.append(" (does not exist)");
		}
		return sb.toString();
	}

	@Override
	public JEAttribute[] getChildren() {
		if (fJavaElement == null)
			return EMPTY;

		ArrayList<JEAttribute> result= new ArrayList<>();

		if (fJavaElement instanceof IParent) {
			addParentChildren(result, (IParent) fJavaElement);
		}

		addJavaElementChildren(result, fJavaElement);

		if (fJavaElement instanceof IJavaModel)
			addJavaModelChildren(result, (IJavaModel) fJavaElement);
		if (fJavaElement instanceof IJavaProject)
			addJavaProjectChildren(result, (IJavaProject) fJavaElement);
		if (fJavaElement instanceof IPackageFragmentRoot)
			addPackageFragmentRootChildren(result, (IPackageFragmentRoot) fJavaElement);
		if (fJavaElement instanceof IPackageFragment)
			addPackageFragmentChildren(result, (IPackageFragment) fJavaElement);

		if (fJavaElement instanceof ITypeRoot)
			addTypeRootChildren(result, (ITypeRoot) fJavaElement);
		if (fJavaElement instanceof IOrdinaryClassFile)
			addClassFileChildren(result, (IOrdinaryClassFile) fJavaElement);
		if (fJavaElement instanceof IModularClassFile)
			addModularClassFileChildren(result, (IModularClassFile) fJavaElement);
		if (fJavaElement instanceof ICompilationUnit)
			addCompilationUnitChildren(result, (ICompilationUnit) fJavaElement);

		if (fJavaElement instanceof IType)
			addTypeChildren(result, (IType) fJavaElement);
		if (fJavaElement instanceof IMethod)
			addMethodChildren(result, (IMethod) fJavaElement);
		if (fJavaElement instanceof IMember)
			addMemberChildren(result, (IMember) fJavaElement);

		if (fJavaElement instanceof ITypeParameter)
			addTypeParameterChildren(result, (ITypeParameter) fJavaElement);
		if (fJavaElement instanceof ILocalVariable)
			addLocalVariableChildren(result, (ILocalVariable) fJavaElement);

		if (fJavaElement instanceof IAnnotation)
			addAnnotationChildren(result, (IAnnotation) fJavaElement);
		if (fJavaElement instanceof IAnnotatable)
			addAnnotatableChildren(result, (IAnnotatable) fJavaElement);

		return result.toArray(new JEAttribute[result.size()]);

	}

	private void addParentChildren(ArrayList<JEAttribute> result, final IParent parent) {
		result.add(new JavaElementChildrenProperty(this, "CHILDREN") {
			@Override
			public JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, parent.getChildren());
			}
		});
	}

	private void addJavaElementChildren(ArrayList<JEAttribute> result, final IJavaElement javaElement) {
		result.add(new JavaElement(this, "PARENT", javaElement.getParent()));
		result.add(new JavaElement(this, "PRIMARY ELEMENT", javaElement.getPrimaryElement()));
		result.add(new JavaElement(this, "JAVA MODEL", javaElement.getJavaModel()));
		result.add(new JavaElement(this, "JAVA PROJECT", javaElement.getJavaProject()));
		result.add(JEResource.create(this, "RESOURCE", javaElement.getResource()));
		result.add(JEResource.compute(this, "CORRESPONDING RESOURCE", () -> javaElement.getCorrespondingResource()));
		result.add(JEResource.compute(this, "UNDERLYING RESOURCE", () -> javaElement.getUnderlyingResource()));
	}

	private void addJavaModelChildren(ArrayList<JEAttribute> result, final IJavaModel javaModel) {
		result.add(new JavaElementChildrenProperty(this, "JAVA PROJECTS") {
			@Override
			public JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, javaModel.getJavaProjects());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "NON JAVA RESOURCES") {
			@Override
			public JEAttribute[] computeChildren() throws JavaModelException {
				return createResources(this, javaModel.getNonJavaResources());
			}
		});
	}

	private void addJavaProjectChildren(ArrayList<JEAttribute> result, final IJavaProject project) {
		result.add(new JavaElementChildrenProperty(this, "ALL PACKAGE FRAGMENT ROOTS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, project.getAllPackageFragmentRoots());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "PACKAGE FRAGMENT ROOTS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, project.getPackageFragmentRoots());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "PACKAGE FRAGMENTS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, project.getPackageFragments());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "NON JAVA RESOURCES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createResources(this, project.getNonJavaResources());
			}
		});
		result.add(JEResource.create(this, "PROJECT", project.getProject()));
		result.add(new JavaElementChildrenProperty(this, "REQUIRED PROJECT NAMES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createStrings(this, project.getRequiredProjectNames());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "OPTIONS(FALSE)") {
			@Override
			protected JEAttribute[] computeChildren() {
				return createOptions(this, project.getOptions(false));
			}
		});
		result.add(new JavaElementChildrenProperty(this, "OPTIONS(TRUE)") {
			@Override
			protected JEAttribute[] computeChildren() {
				return createOptions(this, project.getOptions(true));
			}
		});
		result.add(new JavaElementChildrenProperty(this, "RAW CLASSPATH") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createCPEntries(this, project.getRawClasspath());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "REFERENCED CLASSPATH ENTRIES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createCPEntries(this, project.getReferencedClasspathEntries());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "RESOLVED CLASSPATH") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createCPEntries(this, project.getResolvedClasspath(false));
			}
		});
	}

	private void addPackageFragmentRootChildren(ArrayList<JEAttribute> result, final IPackageFragmentRoot packageFragmentRoot) {
		result.add(new JavaElementChildrenProperty(this, "NON JAVA RESOURCES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createResources(this, packageFragmentRoot.getNonJavaResources());
			}
		});
		result.add(JEClasspathEntry.compute(this, "RAW CLASSPATH ENTRY", () -> packageFragmentRoot.getRawClasspathEntry()));
		result.add(JEClasspathEntry.compute(this, "RESOLVED CLASSPATH ENTRY", () -> packageFragmentRoot.getResolvedClasspathEntry()));
	}

	private void addPackageFragmentChildren(ArrayList<JEAttribute> result, final IPackageFragment packageFragment) {
		result.add(new JavaElementChildrenProperty(this, "COMPILATION UNITS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, packageFragment.getCompilationUnits());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "CLASS FILES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, packageFragment.getAllClassFiles());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "NON JAVA RESOURCES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createResources(this, packageFragment.getNonJavaResources());
			}
		});
	}

	private void addTypeRootChildren(ArrayList<JEAttribute> result, final ITypeRoot typeRoot) {
		result.add(JavaElement.compute(this, "FIND PRIMARY TYPE", () -> typeRoot.findPrimaryType()));
	}

	private void addClassFileChildren(ArrayList<JEAttribute> result, final IOrdinaryClassFile classFile) {
		result.add(JavaElement.compute(this, "TYPE", () -> classFile.getType()));
	}

	private void addModularClassFileChildren(ArrayList<JEAttribute> result, final IModularClassFile classFile) {
		result.add(JavaElement.compute(this, "MODULE", () -> classFile.getModule()));
	}

	private void addCompilationUnitChildren(ArrayList<JEAttribute> result, final ICompilationUnit compilationUnit) {
		//TODO: WorkingCopyOwner
		result.add(new JavaElement(this, "PRIMARY", compilationUnit.getPrimary()));
		result.add(new JavaElementChildrenProperty(this, "TYPES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, compilationUnit.getTypes());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "ALL TYPES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, compilationUnit.getAllTypes());
			}
		});
		result.add(new JavaElement(this, "IMPORT CONTAINER", compilationUnit.getImportContainer()));
		result.add(new JavaElementChildrenProperty(this, "IMPORTS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, compilationUnit.getImports());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "PACKAGE DECLARATIONS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, compilationUnit.getPackageDeclarations());
			}
		});
	}

	private void addMemberChildren(ArrayList<JEAttribute> result, final IMember member) {
		result.add(new JavaElement(this, "CLASS FILE", member.getClassFile()));
		result.add(new JavaElement(this, "COMPILATION UNIT", member.getCompilationUnit()));
		result.add(new JavaElement(this, "TYPE ROOT", member.getTypeRoot()));
		result.add(new JavaElement(this, "DECLARING TYPE", member.getDeclaringType()));
		result.add(new JavaElementChildrenProperty(this, "CATEGORIES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createStrings(this, member.getCategories());
			}
		});
	}

	private void addAnnotationChildren(ArrayList<JEAttribute> result, final IAnnotation annotation) {
		result.add(new JavaElementChildrenProperty(this, "MEMBER VALUE PAIRS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				IMemberValuePair[] memberValuePairs= annotation.getMemberValuePairs();
				return createMemberValuePairs(this, memberValuePairs);
			}
		});
	}

	private void addAnnotatableChildren(ArrayList<JEAttribute> result, final IAnnotatable annotatable) {
		result.add(new JavaElementChildrenProperty(this, "ANNOTATIONS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				IAnnotation[] annotations= annotatable.getAnnotations();
				return createJavaElements(this, annotations);
			}
		});
	}

	private void addTypeChildren(ArrayList<JEAttribute> result, final IType type) {
		result.add(new JavaElementProperty(this, "IS RESOLVED", type.isResolved()));
		result.add(new JavaElementProperty(this, "KEY", type.getKey()));
		result.add(new JavaElement(this, "PACKAGE FRAGMENT", type.getPackageFragment()));
		result.add(new JavaElementChildrenProperty(this, "TYPE PARAMETERS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, type.getTypeParameters());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "TYPE PARAMETER SIGNATURES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createStrings(this, type.getTypeParameterSignatures());
			}
		});

		result.add(new JavaElementProperty(this, "SUPERCLASS NAME") {
			@Override
			protected Object computeValue() throws Exception {
				return type.getSuperclassName();
			}
		});

		result.add(new JavaElementProperty(this, "SUPERCLASS TYPE SIGNATURE") {
			@Override
			protected Object computeValue() throws Exception {
				return type.getSuperclassTypeSignature();
			}
		});
		result.add(new JavaElementChildrenProperty(this, "SUPER INTERFACE NAMES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createStrings(this, type.getSuperInterfaceNames());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "SUPER INTERFACE TYPE SIGNATURES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createStrings(this, type.getSuperInterfaceTypeSignatures());
			}
		});

		result.add(new JavaElementChildrenProperty(this, "FIELDS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, type.getFields());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "INITIALIZERS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, type.getInitializers());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "METHODS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, type.getMethods());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "TYPES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, type.getTypes());
			}
		});
	}

	private void addMethodChildren(ArrayList<JEAttribute> result, final IMethod method) {
		result.add(new JavaElementChildrenProperty(this, "EXCEPTION TYPES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createStrings(this, method.getExceptionTypes());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "PARAMETERS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, method.getParameters());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "PARAMETER NAMES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createStrings(this, method.getParameterNames());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "PARAMETER TYPES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createStrings(this, method.getParameterTypes());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "TYPE PARAMETERS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createJavaElements(this, method.getTypeParameters());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "TYPE PARAMETER SIGNATURES") {
			@Override @Deprecated
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createStrings(this, method.getTypeParameterSignatures());
			}
		});
		try {
			result.add(new JEMemberValuePair(this, "DEFAULT_VALUE", method.getDefaultValue()));
		} catch (JavaModelException e) {
			result.add(new Error(this, "DEFAULT_VALUE", e));
		}
	}

	private void addTypeParameterChildren(ArrayList<JEAttribute> result, final ITypeParameter typeParameter) {
		result.add(new JavaElement(this, "TYPE ROOT", typeParameter.getTypeRoot()));
		result.add(new JavaElement(this, "DECLARING MEMBER", typeParameter.getDeclaringMember()));
		result.add(new JavaElementChildrenProperty(this, "BOUNDS") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createStrings(this, typeParameter.getBounds());
			}
		});
		result.add(new JavaElementChildrenProperty(this, "BOUNDS SIGNATURES") {
			@Override
			protected JEAttribute[] computeChildren() throws JavaModelException {
				return createStrings(this, typeParameter.getBoundsSignatures());
			}
		});
	}

	private void addLocalVariableChildren(ArrayList<JEAttribute> result, final ILocalVariable localVariable) {
		result.add(new JavaElement(this, "TYPE ROOT", localVariable.getTypeRoot()));
		result.add(new JavaElement(this, "DECLARING MEMBER", localVariable.getDeclaringMember()));
	}

	static JavaElement[] createJavaElements(JEAttribute parent, Object[] javaElements) {
		JavaElement[] jeChildren= new JavaElement[javaElements.length];
		for (int i= 0; i < javaElements.length; i++) {
			jeChildren[i]= new JavaElement(parent, (IJavaElement) javaElements[i]);
		}
		return jeChildren;
	}

	static JavaElement[] createJavaElements(JEAttribute parent, IJavaElement[] javaElements) {
		JavaElement[] jeChildren= new JavaElement[javaElements.length];
		for (int i= 0; i < javaElements.length; i++) {
			jeChildren[i]= new JavaElement(parent, javaElements[i]);
		}
		return jeChildren;
	}

	static JEAttribute[] createResources(JEAttribute parent, Object[] resources) {
		JEAttribute[] resourceChildren= new JEAttribute[resources.length];
		for (int i= 0; i < resources.length; i++) {
			Object resource= resources[i];
			if (resource instanceof IResource)
				resourceChildren[i]= new JEResource(parent, null, (IResource) resource);
			else if (resource instanceof IJarEntryResource)
				resourceChildren[i]= new JEJarEntryResource(parent, null, (IJarEntryResource) resource);
			else
				resourceChildren[i]= new JavaElementProperty(parent, null, resource);
		}
		return resourceChildren;
	}

	static JEAttribute[] createMemberValuePairs(JEAttribute parent, IMemberValuePair[] mvPairs) {
		JEAttribute[] mvPairChildren= new JEAttribute[mvPairs.length];
		for (int i= 0; i < mvPairs.length; i++) {
			IMemberValuePair mvPair= mvPairs[i];
			mvPairChildren[i]= new JEMemberValuePair(parent, mvPair);
		}
		return mvPairChildren;
	}

	static JEAttribute[] createCPEntries(JEAttribute parent, IClasspathEntry[] entries) {
		JEAttribute[] entryChildren= new JEAttribute[entries.length];
		for (int i= 0; i < entries.length; i++) {
			IClasspathEntry entry= entries[i];
			entryChildren[i]= new JEClasspathEntry(parent, null, entry);
		}
		return entryChildren;
	}

	static JEAttribute[] createOptions(JEAttribute parent, Map<String, String> options) {
		ArrayList<Entry<String, String>> entries= new ArrayList<>(options.entrySet());
		Collections.sort(entries, Comparator.comparing(Entry<String, String>::getKey));

		JEAttribute[] children= new JEAttribute[entries.size()];
		for (int i= 0; i < entries.size(); i++) {
			Entry<String, String> entry= entries.get(i);
			children[i]= new JavaElementProperty(parent, entry.getKey(), entry.getValue());
		}
		return children;
	}

	static JEAttribute[] createStrings(JEAttribute parent, String[] strings) {
		JEAttribute[] children= new JEAttribute[strings.length];
		for (int i= 0; i < strings.length; i++) {
			children[i]= new JavaElementProperty(parent, null, strings[i]);
		}
		return children;
	}

	public static JEAttribute compute(JEAttribute parent, String name, Callable<IJavaElement> computer) {
		try {
			IJavaElement javaElement= computer.call();
			return create(parent, name, javaElement);
		} catch (Exception e) {
			return new Error(parent, name, e);
		}
	}

	public static JEAttribute create(JEAttribute parent, String name, IJavaElement javaElement) {
		if (javaElement == null) {
			return new Null(parent, name);
		} else {
			return new JavaElement(parent, name, javaElement);
		}
	}

}
