/* *******************************************************************
 * Copyright (c) 2002-2019 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
 * ******************************************************************/
package org.aspectj.weaver.patterns;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.ISourceLocation;
import org.aspectj.bridge.Message;
import org.aspectj.weaver.CompressingDataOutputStream;
import org.aspectj.weaver.ISourceContext;
import org.aspectj.weaver.ResolvedType;
import org.aspectj.weaver.UnresolvedType;
import org.aspectj.weaver.VersionedDataInputStream;
import org.aspectj.weaver.WeaverMessages;
import org.aspectj.weaver.World;

/**
 * @author Thomas Kiviaho
 * @author Andy Clement
 * @author PARC
 */
public class DeclareParents extends Declare {
	protected TypePattern child;
	protected TypePatternList parents;
	private boolean isWildChild = false;
	protected boolean isExtends = true;

	public DeclareParents(TypePattern child, List<TypePattern> parents, boolean isExtends) {
		this(child, new TypePatternList(parents), isExtends);
	}

	protected DeclareParents(TypePattern child, TypePatternList parents, boolean isExtends) {
		this.child = child;
		this.parents = parents;
		this.isExtends = isExtends;
		WildChildFinder wildChildFinder = new WildChildFinder();
		child.accept(wildChildFinder, null);
		isWildChild = wildChildFinder.containedWildChild();
	}

	public boolean match(ResolvedType typeX) {
		if (!child.matchesStatically(typeX)) {
			return false;
		}
		if (typeX.getWorld().getLint().typeNotExposedToWeaver.isEnabled() && !typeX.isExposedToWeaver()) {
			typeX.getWorld().getLint().typeNotExposedToWeaver.signal(typeX.getName(), getSourceLocation());
		}

		return true;
	}

	@Override
	public Object accept(PatternNodeVisitor visitor, Object data) {
		return visitor.visit(this, data);
	}

	public Object traverse(PatternNodeVisitor visitor, Object data) {
		Object ret = accept(visitor, data);
		if (this.child != null)
			this.child.traverse(visitor, ret);
		if (this.parents != null)
			this.parents.traverse(visitor, ret);
		return ret;
	}

	@Override
	public Declare parameterizeWith(Map<String,UnresolvedType> typeVariableBindingMap, World w) {
		DeclareParents ret = new DeclareParents(child.parameterizeWith(typeVariableBindingMap, w), parents.parameterizeWith(
				typeVariableBindingMap, w), isExtends);
		ret.copyLocationFrom(this);
		return ret;
	}

	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		buf.append("declare parents: ");
		buf.append(child);
		buf.append(isExtends ? " extends " : " implements "); // extends and implements are treated equivalently
		buf.append(parents);
		buf.append(";");
		return buf.toString();
	}

	@Override
	public boolean equals(Object other) {
		if (!(other instanceof DeclareParents)) {
			return false;
		}
		DeclareParents o = (DeclareParents) other;
		return o.child.equals(child) && o.parents.equals(parents);
	}

	// ??? cache this
	@Override
	public int hashCode() {
		int result = 23;
		result = 37 * result + child.hashCode();
		result = 37 * result + parents.hashCode();
		return result;
	}

	@Override
	public void write(CompressingDataOutputStream s) throws IOException {
		s.writeByte(Declare.PARENTS);
		child.write(s);
		parents.write(s);
		writeLocation(s);
	}

	public static Declare read(VersionedDataInputStream s, ISourceContext context) throws IOException {
		DeclareParents ret = new DeclareParents(TypePattern.read(s, context), TypePatternList.read(s, context), true);
		ret.readLocation(context, s);
		return ret;
	}

	public boolean parentsIncludeInterface(World w) {
		for (int i = 0; i < parents.size(); i++) {
			if (parents.get(i).getExactType().resolve(w).isInterface()) {
				return true;
			}
		}
		return false;
	}

	public boolean parentsIncludeClass(World w) {
		for (int i = 0; i < parents.size(); i++) {
			if (parents.get(i).getExactType().resolve(w).isClass()) {
				return true;
			}
		}
		return false;
	}

	@Override
	public void resolve(IScope scope) {
		TypePattern resolvedChild = child.resolveBindings(scope, Bindings.NONE, false, false);
	    if (!resolvedChild.equals(child)) {
	        WildChildFinder wildChildFinder = new WildChildFinder();
	        resolvedChild.accept(wildChildFinder, null);
	        isWildChild = wildChildFinder.containedWildChild();
	        this.child = resolvedChild;
	    }
		parents = parents.resolveBindings(scope, Bindings.NONE, false, true);
		// Could assert this ...
		// for (int i=0; i < parents.size(); i++) {
		// parents.get(i).assertExactType(scope.getMessageHandler());
		// }
	}

	public TypePatternList getParents() {
		return parents;
	}

	public TypePattern getChild() {
		return child;
	}

	// note - will always return true after deserialization, this doesn't affect weaver
	public boolean isExtends() {
		return this.isExtends;
	}

	@Override
	public boolean isAdviceLike() {
		return false;
	}

	private ResolvedType maybeGetNewParent(ResolvedType targetType, TypePattern typePattern, World world, boolean reportErrors) {
		if (typePattern == TypePattern.NO) {
			return null; // already had an error here
		}

		// isWildChild = (child instanceof WildTypePattern);
		UnresolvedType iType = typePattern.getExactType();
		ResolvedType parentType = iType.resolve(world);

		if (targetType.equals(world.getCoreType(UnresolvedType.OBJECT))) {
			world.showMessage(IMessage.ERROR, WeaverMessages.format(WeaverMessages.DECP_OBJECT), this.getSourceLocation(), null);
			return null;
		}

		// Ensure the target doesn't already have an
		// alternate parameterization of the generic type on it
		if (parentType.isParameterizedType() || parentType.isRawType()) {
			// Let's take a look at the parents we already have
			boolean isOK = verifyNoInheritedAlternateParameterization(targetType, parentType, world);
			if (!isOK) {
				return null;
			}
		}

		if (parentType.isAssignableFrom(targetType)) {
			return null; // already a parent
		}

		// Enum types that are targetted for decp through a wild type pattern get linted
		if (reportErrors && isWildChild && targetType.isEnum()) {
			world.getLint().enumAsTargetForDecpIgnored.signal(targetType.toString(), getSourceLocation());
		}

		// Annotation types that are targetted for decp through a wild type pattern get linted
		if (reportErrors && isWildChild && targetType.isAnnotation()) {
			world.getLint().annotationAsTargetForDecpIgnored.signal(targetType.toString(), getSourceLocation());
		}

		// 1. Can't use decp to make an enum/annotation type implement an interface
		if (targetType.isEnum() && parentType.isInterface()) {
			if (reportErrors && !isWildChild) {
				world.showMessage(IMessage.ERROR, WeaverMessages.format(WeaverMessages.CANT_DECP_ON_ENUM_TO_IMPL_INTERFACE,
						targetType), getSourceLocation(), null);
			}
			return null;
		}
		if (targetType.isAnnotation() && parentType.isInterface()) {
			if (reportErrors && !isWildChild) {
				world.showMessage(IMessage.ERROR, WeaverMessages.format(WeaverMessages.CANT_DECP_ON_ANNOTATION_TO_IMPL_INTERFACE,
						targetType), getSourceLocation(), null);
			}
			return null;
		}

		// 2. Can't use decp to change supertype of an enum/annotation
		if (targetType.isEnum() && parentType.isClass()) {
			if (reportErrors && !isWildChild) {
				world.showMessage(IMessage.ERROR, WeaverMessages.format(WeaverMessages.CANT_DECP_ON_ENUM_TO_EXTEND_CLASS,
						targetType), getSourceLocation(), null);
			}
			return null;
		}
		if (targetType.isAnnotation() && parentType.isClass()) {
			if (reportErrors && !isWildChild) {
				world.showMessage(IMessage.ERROR, WeaverMessages.format(WeaverMessages.CANT_DECP_ON_ANNOTATION_TO_EXTEND_CLASS,
						targetType), getSourceLocation(), null);
			}
			return null;
		}

		// 3. Can't use decp to declare java.lang.Enum/java.lang.annotation.Annotation as the parent of a type
		if (parentType.getSignature().equals(UnresolvedType.ENUM.getSignature())) {
			if (reportErrors && !isWildChild) {
				world.showMessage(IMessage.ERROR, WeaverMessages
						.format(WeaverMessages.CANT_DECP_TO_MAKE_ENUM_SUPERTYPE, targetType), getSourceLocation(), null);
			}
			return null;
		}
		if (parentType.getSignature().equals(UnresolvedType.ANNOTATION.getSignature())) {
			if (reportErrors && !isWildChild) {
				world.showMessage(IMessage.ERROR, WeaverMessages.format(WeaverMessages.CANT_DECP_TO_MAKE_ANNOTATION_SUPERTYPE,
						targetType), getSourceLocation(), null);
			}
			return null;
		}

		if (parentType.isAssignableFrom(targetType)) {
			return null; // already a parent
		}

		if (targetType.isAssignableFrom(parentType)) {
			world.showMessage(IMessage.ERROR, WeaverMessages.format(WeaverMessages.CANT_EXTEND_SELF, targetType.getName()), this
					.getSourceLocation(), null);
			return null;
		}

		if (parentType.isClass()) {
			if (targetType.isInterface()) {
				world.showMessage(IMessage.ERROR, WeaverMessages.format(WeaverMessages.INTERFACE_CANT_EXTEND_CLASS), this
						.getSourceLocation(), null);
				return null;
				// how to handle xcutting errors???
			}

			if (!targetType.getSuperclass().isAssignableFrom(parentType)) {
				world.showMessage(IMessage.ERROR, WeaverMessages.format(WeaverMessages.DECP_HIERARCHY_ERROR, iType.getName(),
						targetType.getSuperclass().getName()), this.getSourceLocation(), null);
				return null;
			} else {
				return parentType;
			}
		} else {
			return parentType;
		}
	}

	/**
	 * This method looks through the type hierarchy for some target type - it is attempting to find an existing parameterization
	 * that clashes with the new parent that the user wants to apply to the type. If it finds an existing parameterization that
	 * matches the new one, it silently completes, if it finds one that clashes (e.g. a type already has A<String> when the user
	 * wants to add A<Number>) then it will produce an error.
	 *
	 * It uses recursion and exits recursion on hitting 'jlObject'
	 *
	 * Related bugzilla entries: pr110788
	 */
	private boolean verifyNoInheritedAlternateParameterization(ResolvedType typeToVerify, ResolvedType newParent, World world) {

		if (typeToVerify.equals(ResolvedType.OBJECT)) {
			return true;
		}

		ResolvedType newParentGenericType = newParent.getGenericType();
		Iterator<ResolvedType> iter = typeToVerify.getDirectSupertypes();
		while (iter.hasNext()) {
			ResolvedType supertype = iter.next();
			if (((supertype.isRawType() && newParent.isParameterizedType()) || (supertype.isParameterizedType() && newParent
					.isRawType()))
					&& newParentGenericType.equals(supertype.getGenericType())) {
				// new parent is a parameterized type, but this is a raw type
				world.getMessageHandler().handleMessage(
						new Message(WeaverMessages.format(WeaverMessages.CANT_DECP_MULTIPLE_PARAMETERIZATIONS, newParent.getName(),
								typeToVerify.getName(), supertype.getName()), getSourceLocation(), true,
								new ISourceLocation[] { typeToVerify.getSourceLocation() }));
				return false;
			}
			if (supertype.isParameterizedType()) {
				ResolvedType generictype = supertype.getGenericType();

				// If the generic types are compatible but the parameterizations aren't then we have a problem
				if (generictype.isAssignableFrom(newParentGenericType) && !supertype.isAssignableFrom(newParent)) {
					world.getMessageHandler().handleMessage(
							new Message(WeaverMessages.format(WeaverMessages.CANT_DECP_MULTIPLE_PARAMETERIZATIONS, newParent
									.getName(), typeToVerify.getName(), supertype.getName()), getSourceLocation(), true,
									new ISourceLocation[] { typeToVerify.getSourceLocation() }));
					return false;
				}
			}
			if (!verifyNoInheritedAlternateParameterization(supertype, newParent, world)) {
				return false;
			}
		}
		return true;
	}

	public List<ResolvedType> findMatchingNewParents(ResolvedType onType, boolean reportErrors) {
		if (onType.isRawType()) {
			onType = onType.getGenericType();
		}
		if (!match(onType)) {
			return Collections.emptyList();
		}

		List<ResolvedType> ret = new ArrayList<>();
		for (int i = 0; i < parents.size(); i++) {
			ResolvedType t = maybeGetNewParent(onType, parents.get(i), onType.getWorld(), reportErrors);
			if (t != null) {
				ret.add(t);
			}
		}

		return ret;
	}

	@Override
	public String getNameSuffix() {
		return "parents";
	}

	public boolean isMixin() {
		return false;
	}
}
