/* *******************************************************************
 * Copyright (c) 2003,2010 Contributors.
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *     Mik Kersten     initial implementation
 *     Andy Clement, IBM, SpringSource    Extensions for better IDE representation
 * ******************************************************************/

package org.aspectj.asm.internal;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.aspectj.asm.AsmManager;
import org.aspectj.asm.HierarchyWalker;
import org.aspectj.asm.IProgramElement;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.ISourceLocation;

/**
 * @author Mik Kersten
 * @author Andy Clement
 */
public class ProgramElement implements IProgramElement {

	public transient AsmManager asm; // which structure model is this node part of
	private static final long serialVersionUID = 171673495267384449L;
	public static boolean shortITDNames = true;

	private final static String UNDEFINED = "<undefined>";
	private final static int AccPublic = 0x0001;
	private final static int AccPrivate = 0x0002;
	private final static int AccProtected = 0x0004;
	private final static int AccPrivileged = 0x0006; // XXX is this right?
	private final static int AccStatic = 0x0008;
	private final static int AccFinal = 0x0010;
	private final static int AccSynchronized = 0x0020;
	private final static int AccVolatile = 0x0040;
	private final static int AccTransient = 0x0080;
	private final static int AccNative = 0x0100;
	// private final static int AccInterface = 0x0200;
	private final static int AccAbstract = 0x0400;
	// private final static int AccStrictfp = 0x0800;

	protected String name;
	private Kind kind;
	protected IProgramElement parent = null;
	protected List<IProgramElement> children = Collections.emptyList();
	public Map<String, Object> kvpairs = Collections.emptyMap();
	protected ISourceLocation sourceLocation = null;
	public int modifiers;
	private String handle = null;

	public AsmManager getModel() {
		return asm;
	}

	/** Used during deserialization */
	public ProgramElement() {
	}

	/** Use to create program element nodes that do not correspond to source locations */
	public ProgramElement(AsmManager asm, String name, Kind kind, List<IProgramElement> children) {
		this.asm = asm;
		if (asm == null && !name.equals("<build to view structure>")) {
			throw new RuntimeException();
		}
		this.name = name;
		this.kind = kind;
		if (children != null) {
			setChildren(children);
		}
	}

	public ProgramElement(AsmManager asm, String name, IProgramElement.Kind kind, ISourceLocation sourceLocation, int modifiers,
			String comment, List<IProgramElement> children) {
		this(asm, name, kind, children);
		this.sourceLocation = sourceLocation;
		setFormalComment(comment);
		// if (comment!=null && comment.length()>0) formalComment = comment;
		this.modifiers = modifiers;
	}

	public int getRawModifiers() {
		return this.modifiers;
	}

	public List<IProgramElement.Modifiers> getModifiers() {
		return genModifiers(this.modifiers);
	}

	public Accessibility getAccessibility() {
		return genAccessibility(this.modifiers);
	}

	public void setDeclaringType(String t) {
		if (t != null && t.length() > 0) {
			fixMap();
			kvpairs.put("declaringType", t);
		}
	}

	public String getDeclaringType() {
		String dt = (String) kvpairs.get("declaringType");
		if (dt == null) {
			return ""; // assumption that not having one means "" is at HtmlDecorator line 111
		}
		return dt;
	}

	public String getPackageName() {
		if (kind == Kind.PACKAGE) {
			return getName();
		}
		if (getParent() == null) {
			return "";
		}
		return getParent().getPackageName();
	}

	public Kind getKind() {
		return kind;
	}

	public boolean isCode() {
		return kind.equals(Kind.CODE);
	}

	public ISourceLocation getSourceLocation() {
		return sourceLocation;
	}

	// not really sure why we have this setter ... how can we be in the situation where we didn't
	// know the location when we built the node but we learned it later on?
	public void setSourceLocation(ISourceLocation sourceLocation) {
		// this.sourceLocation = sourceLocation;
	}

	public IMessage getMessage() {
		return (IMessage) kvpairs.get("message");
		// return message;
	}

	public void setMessage(IMessage message) {
		fixMap();
		kvpairs.put("message", message);
		// this.message = message;
	}

	public IProgramElement getParent() {
		return parent;
	}

	public void setParent(IProgramElement parent) {
		this.parent = parent;
	}

	public boolean isMemberKind() {
		return kind.isMember();
	}

	public void setRunnable(boolean value) {
		fixMap();
		if (value) {
			kvpairs.put("isRunnable", "true");
		} else {
			kvpairs.remove("isRunnable");
			// this.runnable = value;
		}
	}

	public boolean isRunnable() {
		return kvpairs.get("isRunnable") != null;
		// return runnable;
	}

	public boolean isImplementor() {
		return kvpairs.get("isImplementor") != null;
		// return implementor;
	}

	public void setImplementor(boolean value) {
		fixMap();
		if (value) {
			kvpairs.put("isImplementor", "true");
		} else {
			kvpairs.remove("isImplementor");
			// this.implementor = value;
		}
	}

	public boolean isOverrider() {
		return kvpairs.get("isOverrider") != null;
		// return overrider;
	}

	public void setOverrider(boolean value) {
		fixMap();
		if (value) {
			kvpairs.put("isOverrider", "true");
		} else {
			kvpairs.remove("isOverrider");
			// this.overrider = value;
		}
	}

	public String getFormalComment() {
		return (String) kvpairs.get("formalComment");
		// return formalComment;
	}

	public String toString() {
		return toLabelString();
	}

	private static List<IProgramElement.Modifiers> genModifiers(int modifiers) {
		List<IProgramElement.Modifiers> modifiersList = new ArrayList<>();
		if ((modifiers & AccStatic) != 0) {
			modifiersList.add(IProgramElement.Modifiers.STATIC);
		}
		if ((modifiers & AccFinal) != 0) {
			modifiersList.add(IProgramElement.Modifiers.FINAL);
		}
		if ((modifiers & AccSynchronized) != 0) {
			modifiersList.add(IProgramElement.Modifiers.SYNCHRONIZED);
		}
		if ((modifiers & AccVolatile) != 0) {
			modifiersList.add(IProgramElement.Modifiers.VOLATILE);
		}
		if ((modifiers & AccTransient) != 0) {
			modifiersList.add(IProgramElement.Modifiers.TRANSIENT);
		}
		if ((modifiers & AccNative) != 0) {
			modifiersList.add(IProgramElement.Modifiers.NATIVE);
		}
		if ((modifiers & AccAbstract) != 0) {
			modifiersList.add(IProgramElement.Modifiers.ABSTRACT);
		}
		return modifiersList;
	}

	public static IProgramElement.Accessibility genAccessibility(int modifiers) {
		if ((modifiers & AccPublic) != 0) {
			return IProgramElement.Accessibility.PUBLIC;
		}
		if ((modifiers & AccPrivate) != 0) {
			return IProgramElement.Accessibility.PRIVATE;
		}
		if ((modifiers & AccProtected) != 0) {
			return IProgramElement.Accessibility.PROTECTED;
		}
		if ((modifiers & AccPrivileged) != 0) {
			return IProgramElement.Accessibility.PRIVILEGED;
		} else {
			return IProgramElement.Accessibility.PACKAGE;
		}
	}

	public String getBytecodeName() {
		String s = (String) kvpairs.get("bytecodeName");
		if (s == null) {
			return UNDEFINED;
		}
		return s;
	}

	public void setBytecodeName(String s) {
		fixMap();
		kvpairs.put("bytecodeName", s);
	}

	public void setBytecodeSignature(String s) {
		fixMap();
		// Different kinds of format here. The one worth compressing starts with a '(':
		// (La/b/c/D;Le/f/g/G;)Ljava/lang/String;
		// maybe want to avoid generics initially.
		// boolean worthCompressing = s.charAt(0) == '(' && s.indexOf('<') == -1 && s.indexOf('P') == -1; // starts parentheses and
		// no
		// // generics
		// if (worthCompressing) {
		// kvpairs.put("bytecodeSignatureCompressed", asm.compress(s));
		// } else {
		kvpairs.put("bytecodeSignature", s);
		// }
	}

	public String getBytecodeSignature() {
		String s = (String) kvpairs.get("bytecodeSignature");
		// if (s == null) {
		// List compressed = (List) kvpairs.get("bytecodeSignatureCompressed");
		// if (compressed != null) {
		// return asm.decompress(compressed, '/');
		// }
		// }
		// if (s==null) return UNDEFINED;
		return s;
	}

	public String getSourceSignature() {
		return (String) kvpairs.get("sourceSignature");
	}

	public void setSourceSignature(String string) {
		fixMap();
		// System.err.println(name+" SourceSig=>"+string);
		kvpairs.put("sourceSignature", string);
		// sourceSignature = string;
	}

	public void setKind(Kind kind) {
		this.kind = kind;
	}

	public void setCorrespondingType(String s) {
		fixMap();
		kvpairs.put("returnType", s);
		// this.returnType = s;
	}

	public void setParentTypes(List<String> ps) {
		fixMap();
		kvpairs.put("parentTypes", ps);
	}

	@SuppressWarnings("unchecked")
	public List<String> getParentTypes() {
		return (List<String>) (kvpairs == null ? null : kvpairs.get("parentTypes"));
	}

	/**
	 * {@inheritDoc}
	 */
	public void setAnnotationType(String fullyQualifiedAnnotationType) {
		fixMap();
		kvpairs.put("annotationType", fullyQualifiedAnnotationType);
	}

	public void setAnnotationRemover(boolean isRemover) {
		fixMap();
		kvpairs.put("annotationRemover", isRemover);
	}

	public String getAnnotationType() {
		if (isAnnotationRemover()) {
			return null;
		}
		return (String) (kvpairs == null ? null : kvpairs.get("annotationType"));
	}

	public boolean isAnnotationRemover() {
		if (kvpairs == null) {
			return false;
		}
		Boolean b = (Boolean) kvpairs.get("annotationRemover");
		if (b == null) {
			return false;
		}
		return b;
	}

	public String[] getRemovedAnnotationTypes() {
		if (!isAnnotationRemover()) {
			return null;
		}
		String annotype = (String) (kvpairs == null ? null : kvpairs.get("annotationType"));
		if (annotype == null) {
			return null;
		} else {
			return new String[] { annotype };
		}
	}

	public String getCorrespondingType() {
		return getCorrespondingType(false);
	}

	public String getCorrespondingTypeSignature() {
		String typename = (String) kvpairs.get("returnType");
		if (typename == null) {
			return null;
		}
		return nameToSignature(typename);
	}

	public static String nameToSignature(String name) {
		int len = name.length();
		if (len < 8) {
			if (name.equals("byte")) {
				return "B";
			}
			if (name.equals("char")) {
				return "C";
			}
			if (name.equals("double")) {
				return "D";
			}
			if (name.equals("float")) {
				return "F";
			}
			if (name.equals("int")) {
				return "I";
			}
			if (name.equals("long")) {
				return "J";
			}
			if (name.equals("short")) {
				return "S";
			}
			if (name.equals("boolean")) {
				return "Z";
			}
			if (name.equals("void")) {
				return "V";
			}
			if (name.equals("?")) {
				return name;
			}
		}
		if (name.endsWith("[]")) {
			return "[" + nameToSignature(name.substring(0, name.length() - 2));
		}
		if (len != 0) {
			// check if someone is calling us with something that is a signature already
			assert name.charAt(0) != '[';

			if (!name.contains("<")) {
				// not parameterized
				return new StringBuilder("L").append(name.replace('.', '/')).append(';').toString();
			} else {
				StringBuilder nameBuff = new StringBuilder();
				int nestLevel = 0;
				nameBuff.append("L");
				for (int i = 0; i < name.length(); i++) {
					char c = name.charAt(i);
					switch (c) {
					case '.':
						nameBuff.append('/');
						break;
					case '<':
						nameBuff.append("<");
						nestLevel++;
						StringBuilder innerBuff = new StringBuilder();
						while (nestLevel > 0) {
							c = name.charAt(++i);
							if (c == '<') {
								nestLevel++;
							}
							if (c == '>') {
								nestLevel--;
							}
							if (c == ',' && nestLevel == 1) {
								nameBuff.append(nameToSignature(innerBuff.toString()));
								innerBuff = new StringBuilder();
							} else {
								if (nestLevel > 0) {
									innerBuff.append(c);
								}
							}
						}
						nameBuff.append(nameToSignature(innerBuff.toString()));
						nameBuff.append('>');
						break;
					case '>':
						throw new IllegalStateException("Should by matched by <");
					case ',':
						throw new IllegalStateException("Should only happen inside <...>");
					default:
						nameBuff.append(c);
					}
				}
				nameBuff.append(";");
				return nameBuff.toString();
			}
		} else {
			throw new IllegalArgumentException("Bad type name: " + name);
		}
	}

	public String getCorrespondingType(boolean getFullyQualifiedType) {
		String returnType = (String) kvpairs.get("returnType");
		if (returnType == null) {
			returnType = "";
		}
		if (getFullyQualifiedType) {
			return returnType;
		}
		return trim(returnType);
	}

	/**
	 * Trim down fully qualified types to their short form (e.g., a.b.c.D&lt;e.f.G&gt; becomes D&lt;G&gt;)
	 */
	public static String trim(String fqname) {
		int i = fqname.indexOf("<");
		if (i == -1) {
			int lastdot = fqname.lastIndexOf('.');
			if (lastdot == -1) {
				return fqname;
			} else {
				return fqname.substring(lastdot + 1);
			}
		}
		char[] charArray = fqname.toCharArray();
		StringBuilder candidate = new StringBuilder(charArray.length);
		StringBuilder complete = new StringBuilder(charArray.length);
		for (char c : charArray) {
			switch (c) {
			case '.':
				candidate.setLength(0);
				break;
			case '<':
			case ',':
			case '>':
				complete.append(candidate).append(c);
				candidate.setLength(0);
				break;
			default:
				candidate.append(c);
			}
		}
		complete.append(candidate);
		return complete.toString();
	}

	public String getName() {
		return name;
	}

	public List<IProgramElement> getChildren() {
		return children;
	}

	public void setChildren(List<IProgramElement> children) {
		this.children = children;
		if (children == null) {
			return;
		}
		for (IProgramElement child : children) {
			child.setParent(this);
		}
	}

	public void addChild(IProgramElement child) {
		if (children == null || children == Collections.EMPTY_LIST) {
			children = new ArrayList<>();
		}
		children.add(child);
		child.setParent(this);
	}

	public void addChild(int position, IProgramElement child) {
		if (children == null || children == Collections.EMPTY_LIST) {
			children = new ArrayList<>();
		}
		children.add(position, child);
		child.setParent(this);
	}

	public boolean removeChild(IProgramElement child) {
		child.setParent(null);
		return children.remove(child);
	}

	public void setName(String string) {
		name = string;
	}

	public IProgramElement walk(HierarchyWalker walker) {
		if (children != null) {
			for (IProgramElement child : children) {
				walker.process(child);
			}
		}
		return this;
	}

	public String toLongString() {
		final StringBuffer buffer = new StringBuffer();
		HierarchyWalker walker = new HierarchyWalker() {
			private int depth = 0;

			public void preProcess(IProgramElement node) {
				for (int i = 0; i < depth; i++) {
					buffer.append(' ');
				}
				buffer.append(node.toString());
				buffer.append('\n');
				depth += 2;
			}

			public void postProcess(IProgramElement node) {
				depth -= 2;
			}
		};
		walker.process(this);
		return buffer.toString();
	}

	public void setModifiers(int i) {
		this.modifiers = i;
	}

	/**
	 * Convenience mechanism for setting new modifiers which do not require knowledge of the private internal representation
	 *
	 * @param newModifier
	 */
	public void addModifiers(IProgramElement.Modifiers newModifier) {
		modifiers |= newModifier.getBit();
	}

	public String toSignatureString() {
		return toSignatureString(true);
	}

	public String toSignatureString(boolean getFullyQualifiedArgTypes) {
		StringBuilder sb = new StringBuilder();
		sb.append(name);

		List<char[]> ptypes = getParameterTypes();
		if (ptypes != null && (!ptypes.isEmpty() || this.kind.equals(IProgramElement.Kind.METHOD))
				|| this.kind.equals(IProgramElement.Kind.CONSTRUCTOR) || this.kind.equals(IProgramElement.Kind.ADVICE)
				|| this.kind.equals(IProgramElement.Kind.POINTCUT) || this.kind.equals(IProgramElement.Kind.INTER_TYPE_METHOD)
				|| this.kind.equals(IProgramElement.Kind.INTER_TYPE_CONSTRUCTOR)) {
			sb.append('(');
			for (Iterator<char[]> it = ptypes.iterator(); it.hasNext();) {
				char[] arg = it.next();
				if (getFullyQualifiedArgTypes) {
					sb.append(arg);
				} else {
					int index = CharOperation.lastIndexOf('.', arg);
					if (index != -1) {
						sb.append(CharOperation.subarray(arg, index + 1, arg.length));
					} else {
						sb.append(arg);
					}
				}
				if (it.hasNext()) {
					sb.append(",");
				}
			}
			sb.append(')');
		}

		return sb.toString();
	}

	/**
	 * TODO: move the "parent != null"&rarr;injar heuristic to more explicit
	 */
	public String toLinkLabelString() {
		return toLinkLabelString(true);
	}

	public String toLinkLabelString(boolean getFullyQualifiedArgTypes) {
		String label;
		if (kind == Kind.CODE || kind == Kind.INITIALIZER) {
			label = parent.getParent().getName() + ": ";
		} else if (kind.isInterTypeMember()) {
			if (shortITDNames) {
				// if (name.indexOf('.')!=-1) return toLabelString().substring(name.indexOf('.')+1);
				label = "";
			} else {
				int dotIndex = name.indexOf('.');
				if (dotIndex != -1) {
					return parent.getName() + ": " + toLabelString().substring(dotIndex + 1);
				} else {
					label = parent.getName() + '.';
				}
			}
		} else if (kind == Kind.CLASS || kind == Kind.ASPECT || kind == Kind.INTERFACE) {
			label = "";
		} else if (kind.equals(Kind.DECLARE_PARENTS)) {
			label = "";
		} else {
			if (parent != null) {
				label = parent.getName() + '.';
			} else {
				label = "injar aspect: ";
			}
		}
		label += toLabelString(getFullyQualifiedArgTypes);
		return label;
	}

	public String toLabelString() {
		return toLabelString(true);
	}

	public String toLabelString(boolean getFullyQualifiedArgTypes) {
		String label = toSignatureString(getFullyQualifiedArgTypes);
		String details = getDetails();
		if (details != null) {
			label += ": " + details;
		}
		return label;
	}

	public String getHandleIdentifier() {
		return getHandleIdentifier(true);
	}

	public String getHandleIdentifier(boolean create) {
		String h = handle;
		if (null == handle && create) {
			if (asm == null && name.equals("<build to view structure>")) {
				h = "<build to view structure>";
			} else {
				try {
					h = asm.getHandleProvider().createHandleIdentifier(this);
				} catch (ArrayIndexOutOfBoundsException aioobe) {
					throw new RuntimeException("AIOOBE whilst building handle for " + this, aioobe);
				}
			}
		}
		setHandleIdentifier(h);
		return h;
	}

	public void setHandleIdentifier(String handle) {
		this.handle = handle;
	}

	@SuppressWarnings("unchecked")
	public List<String> getParameterNames() {
		List<String> parameterNames = (List<String>) kvpairs.get("parameterNames");
		return parameterNames;
	}

	public void setParameterNames(List<String> list) {
		if (list == null || list.size() == 0) {
			return;
		}
		fixMap();
		kvpairs.put("parameterNames", list);
		// parameterNames = list;
	}

	public List<char[]> getParameterTypes() {
		List<char[]> l = getParameterSignatures();
		if (l == null || l.isEmpty()) {
			return Collections.emptyList();
		}
		List<char[]> params = new ArrayList<>();
		for (char[] param : l) {
			params.add(NameConvertor.convertFromSignature(param));
		}
		return params;
	}

	@SuppressWarnings("unchecked")
	public List<char[]> getParameterSignatures() {
		List<char[]> parameters = (List<char[]>) kvpairs.get("parameterSigs");
		return parameters;
	}

	@SuppressWarnings("unchecked")
	public List<String> getParameterSignaturesSourceRefs() {
		List<String> parameters = (List<String>) kvpairs.get("parameterSigsSourceRefs");
		return parameters;
	}

	/**
	 * Set the parameter signatures for this method/constructor. The bit flags tell us if any were not singletypereferences in the
	 * the source. A singletypereference would be 'String' - whilst a qualifiedtypereference would be 'java.lang.String' - this has
	 * an effect on the handles.
	 */
	public void setParameterSignatures(List<char[]> list, List<String> sourceRefs) {
		fixMap();
		if (list == null || list.size() == 0) {
			kvpairs.put("parameterSigs", Collections.EMPTY_LIST);
		} else {
			kvpairs.put("parameterSigs", list);
		}
		if (sourceRefs != null && sourceRefs.size() != 0) {
			kvpairs.put("parameterSigsSourceRefs", sourceRefs);
		}
	}

	public String getDetails() {
		String details = (String) kvpairs.get("details");
		return details;
	}

	public void setDetails(String string) {
		fixMap();
		kvpairs.put("details", string);
	}

	public void setFormalComment(String txt) {
		if (txt != null && txt.length() > 0) {
			fixMap();
			kvpairs.put("formalComment", txt);
		}
	}

	private void fixMap() {
		if (kvpairs == Collections.EMPTY_MAP) {
			kvpairs = new HashMap<>();
		}
	}

	public void setExtraInfo(ExtraInformation info) {
		fixMap();
		kvpairs.put("ExtraInformation", info);
	}

	public ExtraInformation getExtraInfo() {
		return (ExtraInformation) kvpairs.get("ExtraInformation");
	}

	public boolean isAnnotationStyleDeclaration() {
		return kvpairs.get("annotationStyleDeclaration") != null;
	}

	public void setAnnotationStyleDeclaration(boolean b) {
		if (b) {
			fixMap();
			kvpairs.put("annotationStyleDeclaration", "true");
		}
	}

	@SuppressWarnings("unchecked")
	public Map<String, List<String>> getDeclareParentsMap() {
		Map<String, List<String>> s = (Map<String, List<String>>) kvpairs.get("declareparentsmap");
		return s;
	}

	public void setDeclareParentsMap(Map<String, List<String>> newmap) {
		fixMap();
		kvpairs.put("declareparentsmap", newmap);
	}

	public void addFullyQualifiedName(String fqname) {
		fixMap();
		kvpairs.put("itdfqname", fqname);
	}

	public String getFullyQualifiedName() {
		return (String) kvpairs.get("itdfqname");
	}
}
