/*******************************************************************************
 * Copyright (c) 2000, 2019 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
 *     Stephan Herrmann - Contribution for
 *							Bug 423504 - [1.8] Implement "18.5.3 Functional Interface Parameterization Inference"
 *							Bug 426676 - [1.8][compiler] Wrong generic method type inferred from lambda expression
 *							Bug 426542 - [1.8] Most specific method not picked when one method has intersection type as type parameter
 *							Bug 428019 - [1.8][compiler] Type inference failure with nested generic invocation.
 *     Andy Clement (GoPivotal, Inc) aclement@gopivotal.com - Contributions for
 *                          Bug 405104 - [1.8][compiler][codegen] Implement support for serializeable lambdas
 *     Jesper S Møller - Contributions for bug 381345 : [1.8] Take care of the Java 8 major version
 *                          Bug 527554 - [18.3] Compiler support for JEP 286 Local-Variable Type
 *******************************************************************************/

package org.eclipse.jdt.internal.compiler.lookup;

import java.util.Set;

import org.eclipse.jdt.internal.compiler.ast.Wildcard;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;

/**
 * Abstraction used for intersection casts in Java 8 + and inferred types:
 * <ul>
 * <li>type inference at 1.8+</li>
 * <li>lub at 1.8+</li>
 * <li>projections for 'var' at 10+</li>
 * </ul>
 */
public class IntersectionTypeBinding18 extends ReferenceBinding {

	private static final char[] INTERSECTION_PACKAGE_NAME = "<package intersection>".toCharArray(); //$NON-NLS-1$

	public ReferenceBinding [] intersectingTypes;
	private ReferenceBinding javaLangObject;
	int length;

	public IntersectionTypeBinding18(ReferenceBinding[] intersectingTypes, LookupEnvironment environment) {
		this.intersectingTypes = intersectingTypes;
		this.length = intersectingTypes.length;
		if (!intersectingTypes[0].isClass()) {
			this.javaLangObject = environment.getResolvedJavaBaseType(TypeConstants.JAVA_LANG_OBJECT, null);
			this.modifiers |= ClassFileConstants.AccInterface;
		}
	}

	private IntersectionTypeBinding18(IntersectionTypeBinding18 prototype) {
		this.intersectingTypes = prototype.intersectingTypes;
		this.length = prototype.length;
		if (!this.intersectingTypes[0].isClass()) {
			this.javaLangObject = prototype.javaLangObject;
			this.modifiers |= ClassFileConstants.AccInterface;
		}
	}

	@Override
	public TypeBinding clone(TypeBinding enclosingType) {
		return new IntersectionTypeBinding18(this);
	}

	@Override
	protected MethodBinding[] getInterfaceAbstractContracts(Scope scope, boolean replaceWildcards, boolean filterDefaultMethods) throws InvalidBindingException {
		int typesLength = this.intersectingTypes.length;
		MethodBinding[][] methods = new MethodBinding[typesLength][];
		int contractsLength = 0;
		for (int i = 0; i < typesLength; i++) {
			methods[i] = this.intersectingTypes[i].getInterfaceAbstractContracts(scope, replaceWildcards, true);
			contractsLength += methods[i].length;
		}
		MethodBinding[] contracts = new MethodBinding[contractsLength];
		int idx = 0;
		for (int i = 0; i < typesLength; i++) {
			int len = methods[i].length;
			System.arraycopy(methods[i], 0, contracts, idx, len);
			idx += len;
		}
		return contracts;
	}

	@Override
	public boolean hasTypeBit(int bit) { // Stephan ??
		for (int i = 0; i < this.length; i++) {
			if (this.intersectingTypes[i].hasTypeBit(bit))
				return true;
		}
		return false;
	}

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

	@Override
	public boolean canBeSeenBy(PackageBinding invocationPackage) {
		for (int i = 0; i < this.length; i++) {
			if (!this.intersectingTypes[i].canBeSeenBy(invocationPackage))
				return false;
		}
		return true;
	}

	@Override
	public boolean canBeSeenBy(Scope scope) {
		for (int i = 0; i < this.length; i++) {
			if (!this.intersectingTypes[i].canBeSeenBy(scope))
				return false;
		}
		return true;
	}

	@Override
	public boolean canBeSeenBy(ReferenceBinding receiverType, ReferenceBinding invocationType) {
		for (int i = 0; i < this.length; i++) {
			if (!this.intersectingTypes[i].canBeSeenBy(receiverType, invocationType))
				return false;
		}
		return true;
	}


	@Override
	public char[] constantPoolName() {
		TypeBinding erasure = erasure();
		if (erasure != this) //$IDENTITY-COMPARISON$
			return erasure.constantPoolName();
		if (this.intersectingTypes[0].id == TypeIds.T_JavaLangObject && this.intersectingTypes.length > 1)
			return this.intersectingTypes[1].constantPoolName(); // improve stack map
		return this.intersectingTypes[0].constantPoolName();
	}

	@Override
	public PackageBinding getPackage() {
		throw new UnsupportedOperationException(); // cannot be referred to
	}

	@Override
	public ReferenceBinding[] getIntersectingTypes() {
		return this.intersectingTypes;
	}

	@Override
	public ReferenceBinding superclass() {
		return this.intersectingTypes[0].isClass() ? this.intersectingTypes[0] : this.javaLangObject;
	}

	@Override
	public ReferenceBinding [] superInterfaces() {
		if (this.intersectingTypes[0].isClass()) {
			ReferenceBinding [] superInterfaces = new ReferenceBinding[this.length - 1];
			System.arraycopy(this.intersectingTypes, 1, superInterfaces, 0, this.length - 1);
			return superInterfaces;
		}
		return this.intersectingTypes;
	}

	@Override
	public boolean isBoxedPrimitiveType() {
		return this.intersectingTypes[0].isBoxedPrimitiveType();
	}

	/* Answer true if the receiver type can be assigned to the argument type (right)
	 */
	@Override
	public boolean isCompatibleWith(TypeBinding right, Scope scope) {

		// easy way out?
		if (TypeBinding.equalsEquals(this, right))
			return true;

		// need to compare two intersection types?
		int rightKind = right.kind();
		TypeBinding[] rightIntersectingTypes = null;
		if (rightKind == INTERSECTION_TYPE && right.boundKind() == Wildcard.EXTENDS) {
			TypeBinding allRightBounds = ((WildcardBinding) right).allBounds();
			if (allRightBounds instanceof IntersectionTypeBinding18)
				rightIntersectingTypes = ((IntersectionTypeBinding18) allRightBounds).intersectingTypes;
		} else if (rightKind == INTERSECTION_TYPE18) {
			rightIntersectingTypes = ((IntersectionTypeBinding18) right).intersectingTypes;
		}
		if (rightIntersectingTypes != null) {
			nextRequired:
			for (TypeBinding required : rightIntersectingTypes) {
				for (TypeBinding provided : this.intersectingTypes) {
					if (provided.isCompatibleWith(required, scope))
						continue nextRequired;
				}
				return false;
			}
			return true;
		}

		// normal case:
		for (int i = 0; i < this.length; i++) {
			if (this.intersectingTypes[i].isCompatibleWith(right, scope))
				return true;
		}
		return false;
	}

	@Override
	public boolean isSubtypeOf(TypeBinding other, boolean simulatingBugJDK8026527) {
		if (TypeBinding.equalsEquals(this, other))
			return true;
		if (other instanceof ReferenceBinding) {
			TypeBinding[] rightIntersectingTypes = ((ReferenceBinding) other).getIntersectingTypes();
			if (rightIntersectingTypes != null && rightIntersectingTypes.length > 1) {
				int numRequired = rightIntersectingTypes.length;
				TypeBinding[] required = new TypeBinding[numRequired];
				System.arraycopy(rightIntersectingTypes, 0, required, 0, numRequired);
				for (int i = 0; i < this.length; i++) {
					TypeBinding provided = this.intersectingTypes[i];
					for (int j = 0; j < required.length; j++) {
						if (required[j] == null) continue;
						if (provided.isSubtypeOf(required[j], simulatingBugJDK8026527)) {
							required[j] = null;
							if (--numRequired == 0)
								return true;
						}
					}
				}
				return false;
			}
		}
		for (int i = 0; i < this.intersectingTypes.length; i++) {
			if (this.intersectingTypes[i].isSubtypeOf(other, false))
				return true;
		}
		return false;
	}

	@Override
	public TypeBinding erasure() {
		int classIdx = -1;
		for (int i = 0; i < this.intersectingTypes.length; i++) {
			if (this.intersectingTypes[i].isClass() && this.intersectingTypes[i].id != TypeIds.T_JavaLangObject) { // ignore j.l.Object to improve stack map
				if (classIdx == -1) {
					classIdx = i;
				} else {
					classIdx = Integer.MAX_VALUE;
					break;
				}
			}
		}
		if (classIdx > -1 && classIdx < Integer.MAX_VALUE)
			return this.intersectingTypes[classIdx].erasure();
		return this;
	}

	@Override
	public char[] qualifiedPackageName() {
		return INTERSECTION_PACKAGE_NAME;
	}

	@Override
	public char[] qualifiedSourceName() {
		StringBuilder qualifiedSourceName = new StringBuilder(16);
		for (int i = 0; i < this.length; i++) {
				qualifiedSourceName.append(this.intersectingTypes[i].qualifiedSourceName());
				if (i != this.length - 1)
					qualifiedSourceName.append(" & "); //$NON-NLS-1$
		}
		return qualifiedSourceName.toString().toCharArray();
	}

	@Override
	public char[] sourceName() {
		StringBuilder srcName = new StringBuilder(16);
		for (int i = 0; i < this.length; i++) {
				srcName.append(this.intersectingTypes[i].sourceName());
				if (i != this.length - 1)
					srcName.append(" & "); //$NON-NLS-1$
		}
		return srcName.toString().toCharArray();
	}

	@Override
	public char[] readableName() {
		StringBuilder readableName = new StringBuilder(16);
		for (int i = 0; i < this.length; i++) {
				readableName.append(this.intersectingTypes[i].readableName());
				if (i != this.length - 1)
					readableName.append(" & "); //$NON-NLS-1$
		}
		return readableName.toString().toCharArray();
	}
	@Override
	public char[] shortReadableName() {
		StringBuilder shortReadableName = new StringBuilder(16);
		for (int i = 0; i < this.length; i++) {
				shortReadableName.append(this.intersectingTypes[i].shortReadableName());
				if (i != this.length - 1)
					shortReadableName.append(" & "); //$NON-NLS-1$
		}
		return shortReadableName.toString().toCharArray();
	}
	@Override
	public boolean isIntersectionType18() {
		return true;
	}
	@Override
	public int kind() {
		return Binding.INTERSECTION_TYPE18;
	}
	@Override
	public String debugName() {
		StringBuilder debugName = new StringBuilder(16);
		for (int i = 0; i < this.length; i++) {
				debugName.append(this.intersectingTypes[i].debugName());
				if (i != this.length - 1)
					debugName.append(" & "); //$NON-NLS-1$
		}
		return debugName.toString();
	}
	@Override
	public String toString() {
	    return debugName();
	}

	public TypeBinding getSAMType(Scope scope) {
		for (int i = 0, max = this.intersectingTypes.length; i < max; i++) {
			TypeBinding typeBinding = this.intersectingTypes[i];
			MethodBinding methodBinding = typeBinding.getSingleAbstractMethod(scope, true);
			// Why doesn't getSingleAbstractMethod do as the javadoc says, and return null
			// when it is not a SAM type
			if (methodBinding != null && methodBinding.problemId() != ProblemReasons.NoSuchSingleAbstractMethod) {
				return typeBinding; // answer the first SAM we find
			}
		}
		return null;
	}

	@Override
	void collectInferenceVariables(Set<InferenceVariable> variables) {
		for (int i = 0; i < this.intersectingTypes.length; i++)
			this.intersectingTypes[i].collectInferenceVariables(variables);
	}

	@Override
	public ReferenceBinding upwardsProjection(Scope scope, TypeBinding[] mentionedTypeVariables) {
		ReferenceBinding[] projectedTypes = new ReferenceBinding[this.intersectingTypes.length];
		for (int i = 0; i < this.intersectingTypes.length; ++i) {
			projectedTypes[i] =  this.intersectingTypes[i].upwardsProjection(scope, mentionedTypeVariables);
		}
		return (ReferenceBinding) scope.environment().createIntersectionType18(projectedTypes);
	}

	@Override
	public ReferenceBinding downwardsProjection(Scope scope, TypeBinding[] mentionedTypeVariables) {
		ReferenceBinding[] projectedTypes = new ReferenceBinding[this.intersectingTypes.length];
		for (int i = 0; i < this.intersectingTypes.length; ++i) {
			projectedTypes[i] = this.intersectingTypes[i].downwardsProjection(scope, mentionedTypeVariables);
		}
		return (ReferenceBinding) scope.environment().createIntersectionType18(projectedTypes);
	}

	@Override
	public boolean mentionsAny(TypeBinding[] parameters, int idx) {
		if (super.mentionsAny(parameters, idx))
			return true;
		for (int i = 0; i < this.intersectingTypes.length; i++) {
			if (this.intersectingTypes[i].mentionsAny(parameters, -1))
				return true;
		}
		return false;
	}
	@Override
	public long updateTagBits() {
		for (TypeBinding intersectingType : this.intersectingTypes)
			this.tagBits |= intersectingType.updateTagBits();
		return super.updateTagBits();
	}

	@Override
	public boolean isNonDenotable() {
		return true;
	}
}
