/*******************************************************************************
 * Copyright (c) 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.jdt.jeview.properties;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;

import org.eclipse.core.runtime.jobs.ISchedulingRule;

import org.eclipse.ui.views.properties.IPropertyDescriptor;
import org.eclipse.ui.views.properties.IPropertySource;
import org.eclipse.ui.views.properties.PropertyDescriptor;

import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IImportDeclaration;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.jeview.JEViewPlugin;

public class JavaElementProperties implements IPropertySource {
	
	private static HashMap<String, Property<?>> fgIdToProperty= new HashMap<String, Property<?>>();
	private static LinkedHashMap<Class<?>, List<? extends Property>> fgTypeToProperty= new LinkedHashMap<Class<?>, List<? extends Property>>();
	
	abstract static class Property<E> {
		private final String fTypeName;
		private final String fName;
		private final PropertyDescriptor fDescriptor;
		
		public Property(Class<E> type, String name) {
			fTypeName= type.getSimpleName();
			fName= name;
			fDescriptor= new PropertyDescriptor(getId(), fName);
			fDescriptor.setAlwaysIncompatible(true);
			fDescriptor.setCategory(type.getSimpleName());
		}

		public abstract Object compute(E target) throws JavaModelException;

		
		public String getName() {
			return fName;
		}
		
		public String getTypeName() {
			return fTypeName;
		}
		
		public String getId() {
			return "org.eclipse.jdt.jeview." + fTypeName + "." + fName;
		}
		
		public PropertyDescriptor getDescriptor() {
			return fDescriptor;
		}
	}
	
	private abstract static class P1 extends Property<IJavaElement> {
		public static final Class<IJavaElement> CLASS= IJavaElement.class;

		private P1(String name) {
			super(CLASS, name);
		}

		static List<P1> createAll() {
			return Arrays.asList(
					new P1("elementName") {
						@Override public Object compute(IJavaElement element) {
							return element.getElementName();
						}
					},
					new P1("elementType") {
						@Override public Object compute(IJavaElement element) {
							return getElementTypeString(element.getElementType());
						}
					},
					new P1("exists") {
						@Override public Object compute(IJavaElement element) {
							return element.exists();
						}
					},
					new P1("isReadOnly") {
						@Override public Object compute(IJavaElement element) {
							return element.isReadOnly();
						}
					},
					new P1("isStructureKnown") {
						@Override public Object compute(IJavaElement element) throws JavaModelException {
							return element.isStructureKnown();
						}
					},
					new P1("handleIdentifier") {
						@Override public Object compute(IJavaElement element) {
							return element.getHandleIdentifier();
						}
					},
					new P1("path") {
						@Override public Object compute(IJavaElement element) {
							return element.getPath();
						}
					},
					new P1("schedulingRule") {
						@Override public Object compute(IJavaElement element) {
							return getSchedulingRuleString(element.getSchedulingRule());
						}
					});
		}
	}
	
	abstract static class IClassFileProperty extends Property<IClassFile> {
		public IClassFileProperty(String name) {
			super(IClassFile.class, name);
		}

		static void createAll() {
			addProperties(IClassFile.class, Arrays.asList(
					new IClassFileProperty("isClass") {
						@Override public Object compute(IClassFile element) throws JavaModelException {
							return element.isClass();
						}
					},
					new IClassFileProperty("isInterface") {
						@Override public Object compute(IClassFile element) throws JavaModelException {
							return element.isInterface();
						}
					}));
		}
	}
	
	abstract static class ICompilationUnitProperty extends Property<ICompilationUnit> {
		public ICompilationUnitProperty(String name) {
			super(ICompilationUnit.class, name);
		}

		static void createAll() {
			addProperties(ICompilationUnit.class, Arrays.asList(
					new ICompilationUnitProperty("hasResourceChanged") {
						@Override public Object compute(ICompilationUnit element) {
							return element.hasResourceChanged();
						}
					},
					new ICompilationUnitProperty("isWorkingCopy") {
						@Override public Object compute(ICompilationUnit element) {
							return element.isWorkingCopy();
						}
					}));
		}
	}
	
	abstract static class IImportDeclarationProperty extends Property<IImportDeclaration> {
		public IImportDeclarationProperty(String name) {
			super(IImportDeclaration.class, name);
		}

		static void createAll() {
			addProperties(IImportDeclaration.class, Arrays.asList(
					new IImportDeclarationProperty("flags") {
						@Override public Object compute(IImportDeclaration element) throws JavaModelException {
							return getFlagsString(element.getFlags());
						}
					},
					new IImportDeclarationProperty("isOnDemand") {
						@Override public Object compute(IImportDeclaration element) {
							return element.isOnDemand();
						}
					}));
		}
	}
	
	abstract static class IJavaProjectProperty extends Property<IJavaProject> {
		public IJavaProjectProperty(String name) {
			super(IJavaProject.class, name);
		}

		static void createAll() {
			addProperties(IJavaProject.class, Arrays.asList(
					new IJavaProjectProperty("hasBuildState") {
						@Override public Object compute(IJavaProject element) {
							return element.hasBuildState();
						}
					},
					new IJavaProjectProperty("getOutputLocation") {
						@Override public Object compute(IJavaProject element) throws JavaModelException {
							return element.getOutputLocation();
						}
					},
					new IJavaProjectProperty("readOutputLocation") {
						@Override public Object compute(IJavaProject element) {
							return element.readOutputLocation();
						}
					}));
		}
	}
	
	static {
		addProperties(P1.CLASS, P1.createAll());
		IClassFileProperty.createAll();
		ICompilationUnitProperty.createAll();
		IImportDeclarationProperty.createAll();
		IJavaProjectProperty.createAll();
		
/*
new Property\((\w+)\.class, "(\w+)"\) \{(\s+)@Override public Object compute\(IJavaElement element\)
new $1Property\("$2"\) \{$3@Override public Object compute\($1 element\)
*/
		
//		addProperty(new Property(ILocalVariable.class, "nameRange") {
//			@Override public Object compute(IJavaElement element) {
//				return getNameRangeString(((ILocalVariable) element).getNameRange());
//			}
//		});
//		addProperty(new Property(ILocalVariable.class, "typeSignature") {
//			@Override public Object compute(IJavaElement element) {
//				return ((ILocalVariable) element).getTypeSignature();
//			}
//		});
//		
//		addProperty(new Property(IMember.class, "nameRange") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return getNameRangeString(((IMember) element).getNameRange());
//			}
//		});
//		addProperty(new Property(IMember.class, "flags") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return getFlagsString(((IMember) element).getFlags());
//			}
//		});
//		addProperty(new Property(IMember.class, "isBinary") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IMember) element).isBinary();
//			}
//		});
//		
//		addProperty(new Property(IField.class, "isResolved") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IField) element).isResolved();
//			}
//		});
//		addProperty(new Property(IField.class, "key") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IField) element).getKey();
//			}
//		});
//		addProperty(new Property(IField.class, "typeSignature") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IField) element).getTypeSignature();
//			}
//		});
//		addProperty(new Property(IField.class, "constant") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IField) element).getConstant();
//			}
//		});
//		addProperty(new Property(IField.class, "isEnumConstant") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IField) element).isEnumConstant();
//			}
//		});
//		
//		addProperty(new Property(IMethod.class, "isResolved") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IMethod) element).isResolved();
//			}
//		});
//		addProperty(new Property(IMethod.class, "key") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IMethod) element).getKey();
//			}
//		});
//		addProperty(new Property(IMethod.class, "numberOfParameters") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IMethod) element).getNumberOfParameters();
//			}
//		});
//		addProperty(new Property(IMethod.class, "signature") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IMethod) element).getSignature();
//			}
//		});
//		addProperty(new Property(IMethod.class, "returnType") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IMethod) element).getReturnType();
//			}
//		});
//		addProperty(new Property(IMethod.class, "isConstructor") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IMethod) element).isConstructor();
//			}
//		});
//		addProperty(new Property(IMethod.class, "isMainMethod") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IMethod) element).isMainMethod();
//			}
//		});
//		
//		addProperty(new Property(IType.class, "isResolved") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IType) element).isResolved();
//			}
//		});
//		addProperty(new Property(IType.class, "key") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IType) element).getKey();
//			}
//		});
//		addProperty(new Property(IType.class, "fullyQualifiedName") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IType) element).getFullyQualifiedName();
//			}
//		});
//		addProperty(new Property(IType.class, "fullyQualifiedName('*')") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IType) element).getFullyQualifiedName('*');
//			}
//		});
//		addProperty(new Property(IType.class, "fullyQualifiedParameterizedName") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IType) element).getFullyQualifiedParameterizedName();
//			}
//		});
//		addProperty(new Property(IType.class, "typeQualifiedName") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IType) element).getTypeQualifiedName();
//			}
//		});
//		addProperty(new Property(IType.class, "typeQualifiedName('*')") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IType) element).getTypeQualifiedName('*');
//			}
//		});
//		addProperty(new Property(IType.class, "isAnnotation") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IType) element).isAnnotation();
//			}
//		});
//		addProperty(new Property(IType.class, "isClass") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IType) element).isClass();
//			}
//		});
//		addProperty(new Property(IType.class, "isEnum") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IType) element).isEnum();
//			}
//		});
//		addProperty(new Property(IType.class, "isInterface") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IType) element).isInterface();
//			}
//		});
//		addProperty(new Property(IType.class, "isAnonymous") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IType) element).isAnonymous();
//			}
//		});
//		addProperty(new Property(IType.class, "isLocal") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IType) element).isLocal();
//			}
//		});
//		addProperty(new Property(IType.class, "isMember") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IType) element).isMember();
//			}
//		});
//		
//		addProperty(new Property(IPackageFragment.class, "containsJavaResources") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IPackageFragment) element).containsJavaResources();
//			}
//		});
//		addProperty(new Property(IPackageFragment.class, "kind") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return getPFRKindString(((IPackageFragment) element).getKind());
//			}
//		});
//		addProperty(new Property(IPackageFragment.class, "hasSubpackages") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IPackageFragment) element).hasSubpackages();
//			}
//		});
//		addProperty(new Property(IPackageFragment.class, "isDefaultPackage") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IPackageFragment) element).isDefaultPackage();
//			}
//		});
//		
//		addProperty(new Property(IPackageFragmentRoot.class, "kind") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return getPFRKindString(((IPackageFragmentRoot) element).getKind());
//			}
//		});
//		addProperty(new Property(IPackageFragmentRoot.class, "sourceAttachmentPath") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IPackageFragmentRoot) element).getSourceAttachmentPath();
//			}
//		});
//		addProperty(new Property(IPackageFragmentRoot.class, "sourceAttachmentRootPath") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IPackageFragmentRoot) element).getSourceAttachmentRootPath();
//			}
//		});
//		addProperty(new Property(IPackageFragmentRoot.class, "isArchive") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IPackageFragmentRoot) element).isArchive();
//			}
//		});
//		addProperty(new Property(IPackageFragmentRoot.class, "isExternal") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IPackageFragmentRoot) element).isExternal();
//			}
//		});
//		
//		addProperty(new Property(ITypeParameter.class, "nameRange") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return getNameRangeString(((ITypeParameter) element).getNameRange());
//			}
//		});
//		
//		
//		addProperty(new Property(IParent.class, "hasChildren") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IParent) element).hasChildren();
//			}
//		});
//		
//		addProperty(new Property(ISourceReference.class, "sourceRange") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((ISourceReference) element).getSourceRange();
//			}
//		});
//		
//		addProperty(new Property(IOpenable.class, "hasUnsavedChanges") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IOpenable) element).hasUnsavedChanges();
//			}
//		});
//		addProperty(new Property(IOpenable.class, "isConsistent") {
//			@Override public Object compute(IJavaElement element) throws JavaModelException {
//				return ((IOpenable) element).isConsistent();
//			}
//		});
//		addProperty(new Property(IOpenable.class, "isOpen") {
//			@Override public Object compute(IJavaElement element) {
//				return ((IOpenable) element).isOpen();
//			}
//		});
	}

	static <T> void addProperties(Class<T> type, List<? extends Property<T>> properties) {
		for (Property<T> property : properties) {
			fgIdToProperty.put(property.getId(), property);
		}
		fgTypeToProperty.put(type, properties);
	}
	
	protected IJavaElement fJavaElement;

	public JavaElementProperties(IJavaElement javaElement) {
		fJavaElement= javaElement;
	}
	
	public IPropertyDescriptor[] getPropertyDescriptors() {
		List<IPropertyDescriptor> result= new ArrayList<IPropertyDescriptor>();
		for (Entry<Class<?>, List<? extends Property>> entry : fgTypeToProperty.entrySet()) {
			if (entry.getKey().isAssignableFrom(fJavaElement.getClass())) {
				for (Property property : entry.getValue()) {
					result.add(property.getDescriptor());
				}
			}
		}
		return result.toArray(new IPropertyDescriptor[result.size()]);
	}
	
	public Object getPropertyValue(Object id) {
		Property property= fgIdToProperty.get(id);
		if (property == null) {
			return null;
		} else {
			try {
				return property.compute(fJavaElement);
			} catch (JavaModelException e) {
				if (e.isDoesNotExist()) {
					return "JavaModelException: " + e.getLocalizedMessage();
				} else {
					JEViewPlugin.log("error calculating property '" + property.getTypeName() + '#' + property.getName() + '\'', e);
					return "Error: " + e.getLocalizedMessage();
				}
			}
		}
	}

	static String getElementTypeString(int elementType) {
		String name;
		switch (elementType) {
			case IJavaElement.JAVA_MODEL :
				name= "IJavaModel";
				break;
			case IJavaElement.JAVA_PROJECT :
				name= "IJavaProject";
				break;
			case IJavaElement.PACKAGE_FRAGMENT_ROOT :
				name= "IPackageFragmentRoot";
				break;
			case IJavaElement.PACKAGE_FRAGMENT :
				name= "IPackageFragment";
				break;
			case IJavaElement.COMPILATION_UNIT :
				name= "ICompilationUnit";
				break;
			case IJavaElement.CLASS_FILE :
				name= "IClassFile";
				break;
			case IJavaElement.TYPE :
				name= "IType";
				break;
			case IJavaElement.FIELD :
				name= "IField";
				break;
			case IJavaElement.METHOD :
				name= "IMethod";
				break;
			case IJavaElement.INITIALIZER :
				name= "IInitializer";
				break;
			case IJavaElement.PACKAGE_DECLARATION :
				name= "IPackageDeclaration";
				break;
			case IJavaElement.IMPORT_CONTAINER :
				name= "IImportContainer";
				break;
			case IJavaElement.IMPORT_DECLARATION :
				name= "IImportDeclaration";
				break;
			case IJavaElement.LOCAL_VARIABLE :
				name= "ILocalVariable";
				break;
			case IJavaElement.TYPE_PARAMETER :
				name= "ITypeParameter";
				break;
			default :
				name= "UNKNOWN";
				break;
		}
		return elementType + " (" + name + ")";
	}
	
	static String getNameRangeString(ISourceRange range) {
		return range.getOffset() + " + " + range.getLength();
	}

	static String getFlagsString(int flags) {
		StringBuffer sb = new StringBuffer().append("0x").append(Integer.toHexString(flags)).append(" (");
		int prologLen= sb.length();
		int rest= flags;
		
		rest&= ~ appendFlag(sb, flags, Flags.AccPublic, "public ");
		rest&= ~ appendFlag(sb, flags, Flags.AccPrivate, "private ");
		rest&= ~ appendFlag(sb, flags, Flags.AccProtected, "protected ");
		rest&= ~ appendFlag(sb, flags, Flags.AccStatic, "static ");
		rest&= ~ appendFlag(sb, flags, Flags.AccFinal, "final ");
		rest&= ~ appendFlag(sb, flags, Flags.AccSynchronized, "synchronized/super ");
		rest&= ~ appendFlag(sb, flags, Flags.AccVolatile, "volatile/bridge ");
		rest&= ~ appendFlag(sb, flags, Flags.AccTransient, "transient/varargs ");
		rest&= ~ appendFlag(sb, flags, Flags.AccNative, "native ");
		rest&= ~ appendFlag(sb, flags, Flags.AccInterface, "interface ");
		rest&= ~ appendFlag(sb, flags, Flags.AccAbstract, "abstract ");
		rest&= ~ appendFlag(sb, flags, Flags.AccStrictfp, "strictfp ");
		rest&= ~ appendFlag(sb, flags, Flags.AccSynthetic, "synthetic ");
		rest&= ~ appendFlag(sb, flags, Flags.AccAnnotation, "annotation ");
		rest&= ~ appendFlag(sb, flags, Flags.AccEnum, "enum ");
		rest&= ~ appendFlag(sb, flags, Flags.AccDeprecated, "deprecated ");
		
		if (rest != 0)
			sb.append("unknown:0x").append(Integer.toHexString(rest));
		int len = sb.length();
		if (len != prologLen)
			sb.setLength(len - 1);
		sb.append(")");
		return sb.toString();
	}
	
	private static int appendFlag(StringBuffer sb, int flags, int flag, String name) {
		if ((flags & flag) != 0) {
			sb.append(name);
			return flag;
		} else {
			return 0;
		}
	}

	static String getPFRKindString(int kind) {
		StringBuffer sb = new StringBuffer().append("0x").append(Integer.toHexString(kind)).append(" (");
		int prologLen= sb.length();
		int rest= kind;
		
		rest&= ~ appendFlag(sb, kind, IPackageFragmentRoot.K_BINARY, "binary ");
		rest&= ~ appendFlag(sb, kind, IPackageFragmentRoot.K_SOURCE, "source ");
		
		if (rest != 0)
			sb.append("unknown:0x").append(Integer.toHexString(rest));
		int len = sb.length();
		if (len != prologLen)
			sb.setLength(len - 1);
		sb.append(")");
		return sb.toString();
	}
	
	static String getSchedulingRuleString(ISchedulingRule schedulingRule) {
		if (schedulingRule == null)
			return null;
		else
			return schedulingRule.getClass().getSimpleName() + ": " + schedulingRule.toString();
	}
	
	public void setPropertyValue(Object name, Object value) {
		// do nothing
	}
	
	public Object getEditableValue() {
		return this;
	}
	
	public boolean isPropertySet(Object property) {
		return false;
	}
	
	public void resetPropertyValue(Object property) {
		// do nothing
	}
}
