/*******************************************************************************
 * Copyright (c) 2000, 2017 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 429958 - [1.8][null] evaluate new DefaultLocation attribute of @NonNullByDefault
 *								Bug 435570 - [1.8][null] @NonNullByDefault illegally tries to affect "throws E"
 *								Bug 466713 - Null Annotations: NullPointerException using <int @Nullable []> as Type Param
 *******************************************************************************/
package org.aspectj.org.eclipse.jdt.internal.compiler.ast;

import java.util.function.Consumer;

import org.aspectj.org.eclipse.jdt.core.compiler.CharOperation;
import org.aspectj.org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.AnnotationBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TagBits;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TypeIds;

public class ArrayTypeReference extends SingleTypeReference {
	public int dimensions;
	private Annotation[][] annotationsOnDimensions; // jsr308 style type annotations on dimensions.
	public int originalSourceEnd;
	public int extendedDimensions;
	public TypeBinding leafComponentTypeWithoutDefaultNullness;

	/**
	 * ArrayTypeReference constructor comment.
	 * @param source char[]
	 * @param dimensions int
	 * @param pos int
	 */
	public ArrayTypeReference(char[] source, int dimensions, long pos) {

		super(source, pos);
		this.originalSourceEnd = this.sourceEnd;
		this.dimensions = dimensions ;
		this.annotationsOnDimensions = null;
	}

	public ArrayTypeReference(char[] source, int dimensions, Annotation[][] annotationsOnDimensions, long pos) {
		this(source, dimensions, pos);
		if (annotationsOnDimensions != null) {
			this.bits |= ASTNode.HasTypeAnnotations;
		}
		this.annotationsOnDimensions = annotationsOnDimensions;
	}

	@Override
	public int dimensions() {

		return this.dimensions;
	}
	
	@Override
	public int extraDimensions() {
		return this.extendedDimensions;
	}

	/**
	 @see org.aspectj.org.eclipse.jdt.internal.compiler.ast.TypeReference#getAnnotationsOnDimensions(boolean)
	*/
	@Override
	public Annotation[][] getAnnotationsOnDimensions(boolean useSourceOrder) {
		if (useSourceOrder || this.annotationsOnDimensions == null || this.annotationsOnDimensions.length == 0 || this.extendedDimensions == 0 || this.extendedDimensions == this.dimensions)
			return this.annotationsOnDimensions;
		Annotation [][] externalAnnotations = new Annotation[this.dimensions][];
		final int baseDimensions = this.dimensions - this.extendedDimensions;
		System.arraycopy(this.annotationsOnDimensions, baseDimensions, externalAnnotations, 0, this.extendedDimensions);
		System.arraycopy(this.annotationsOnDimensions, 0, externalAnnotations, this.extendedDimensions, baseDimensions);
		return externalAnnotations;
	}
	
	@Override
	public void setAnnotationsOnDimensions(Annotation [][] annotationsOnDimensions) {
		this.annotationsOnDimensions = annotationsOnDimensions;
	}
	/**
	 * @return char[][]
	 */
	@Override
	public char [][] getParameterizedTypeName(){
		int dim = this.dimensions;
		char[] dimChars = new char[dim*2];
		for (int i = 0; i < dim; i++) {
			int index = i*2;
			dimChars[index] = '[';
			dimChars[index+1] = ']';
		}
		return new char[][]{ CharOperation.concat(this.token, dimChars) };
	}
	@Override
	protected TypeBinding getTypeBinding(Scope scope) {

		if (this.resolvedType != null) {
			return this.resolvedType;
		}
		if (this.dimensions > 255) {
			scope.problemReporter().tooManyDimensions(this);
		}
		TypeBinding leafComponentType = scope.getType(this.token);
		return scope.createArrayType(leafComponentType, this.dimensions);

	}

	@Override
	public StringBuffer printExpression(int indent, StringBuffer output){

		super.printExpression(indent, output);
		if ((this.bits & IsVarArgs) != 0) {
			for (int i= 0 ; i < this.dimensions - 1; i++) {
				if (this.annotationsOnDimensions != null && this.annotationsOnDimensions[i] != null) {
					output.append(' ');
					printAnnotations(this.annotationsOnDimensions[i], output);
					output.append(' ');
				}
				output.append("[]"); //$NON-NLS-1$
			}
			if (this.annotationsOnDimensions != null && this.annotationsOnDimensions[this.dimensions - 1] != null) {
				output.append(' ');
				printAnnotations(this.annotationsOnDimensions[this.dimensions - 1], output);
				output.append(' ');
			}
			output.append("..."); //$NON-NLS-1$
		} else {
			for (int i= 0 ; i < this.dimensions; i++) {
				if (this.annotationsOnDimensions != null && this.annotationsOnDimensions[i] != null) {
					output.append(" "); //$NON-NLS-1$
					printAnnotations(this.annotationsOnDimensions[i], output);
					output.append(" "); //$NON-NLS-1$
				}
				output.append("[]"); //$NON-NLS-1$
			}
		}
		return output;
	}

	@Override
	public void traverse(ASTVisitor visitor, BlockScope scope) {
		if (visitor.visit(this, scope)) {
			if (this.annotations != null) {
				Annotation [] typeAnnotations = this.annotations[0];
				for (int i = 0, length = typeAnnotations == null ? 0 : typeAnnotations.length; i < length; i++) {
					typeAnnotations[i].traverse(visitor, scope);
				}
			}
			if (this.annotationsOnDimensions != null) {
				for (int i = 0, max = this.annotationsOnDimensions.length; i < max; i++) {
					Annotation[] annotations2 = this.annotationsOnDimensions[i];
					if (annotations2 != null) {
						for (int j = 0, max2 = annotations2.length; j < max2; j++) {
							Annotation annotation = annotations2[j];
							annotation.traverse(visitor, scope);
						}
					}
				}
			}
		}
		visitor.endVisit(this, scope);
	}

	@Override
	public void traverse(ASTVisitor visitor, ClassScope scope) {
		if (visitor.visit(this, scope)) {
			if (this.annotations != null) {
				Annotation [] typeAnnotations = this.annotations[0];
				for (int i = 0, length = typeAnnotations == null ? 0 : typeAnnotations.length; i < length; i++) {
					typeAnnotations[i].traverse(visitor, scope);
				}
			}
			if (this.annotationsOnDimensions != null) {
				for (int i = 0, max = this.annotationsOnDimensions.length; i < max; i++) {
					Annotation[] annotations2 = this.annotationsOnDimensions[i];
					if (annotations2 != null) {
						for (int j = 0, max2 = annotations2.length; j < max2; j++) {
							Annotation annotation = annotations2[j];
							annotation.traverse(visitor, scope);
						}
					}
				}
			}
		}
		visitor.endVisit(this, scope);
	}

	@Override
	protected TypeBinding internalResolveType(Scope scope, int location) {
		TypeBinding internalResolveType = super.internalResolveType(scope, location);
		internalResolveType = maybeMarkArrayContentsNonNull(scope, internalResolveType, this.sourceStart, this.dimensions,
									leafType -> this.leafComponentTypeWithoutDefaultNullness = leafType);

		return internalResolveType;
	}

	static TypeBinding maybeMarkArrayContentsNonNull(Scope scope, TypeBinding typeBinding, int sourceStart, int dimensions, Consumer<TypeBinding> leafConsumer) {
		LookupEnvironment environment = scope.environment();
		if (environment.usesNullTypeAnnotations()
				&& scope.hasDefaultNullnessFor(Binding.DefaultLocationArrayContents, sourceStart)) {
			AnnotationBinding nonNullAnnotation = environment.getNonNullAnnotation();
			typeBinding = addNonNullToDimensions(scope, typeBinding, nonNullAnnotation, dimensions);

			TypeBinding leafComponentType = typeBinding.leafComponentType();
			if ((leafComponentType.tagBits & TagBits.AnnotationNullMASK) == 0 && leafComponentType.acceptsNonNullDefault()) {
				if (leafConsumer != null)
					leafConsumer.accept(leafComponentType);
				TypeBinding nonNullLeafComponentType = scope.environment().createAnnotatedType(leafComponentType,
						new AnnotationBinding[] { nonNullAnnotation });
				typeBinding = scope.createArrayType(nonNullLeafComponentType, typeBinding.dimensions(),
						typeBinding.getTypeAnnotations());
			}
		}
		return typeBinding;
	}

	static TypeBinding addNonNullToDimensions(Scope scope, TypeBinding typeBinding,
			AnnotationBinding nonNullAnnotation, int dimensions2) {
		AnnotationBinding[][] newAnnots = new AnnotationBinding[dimensions2][];
		AnnotationBinding[] oldAnnots = typeBinding.getTypeAnnotations();
		if (oldAnnots == null) {
			for (int i = 1; i < dimensions2; i++) {
				newAnnots[i] = new AnnotationBinding[] { nonNullAnnotation };
			}
		} else {
			int j = 0;
			for (int i = 0; i < dimensions2; i++) {
				if (j >= oldAnnots.length || oldAnnots[j] == null) {
					if (i != 0) {
						newAnnots[i] = new AnnotationBinding[] { nonNullAnnotation };
					}
					j++;
				} else {
					int k = j;
					boolean seen = false;
					while (oldAnnots[k] != null) {
						seen |= oldAnnots[k].getAnnotationType()
								.hasNullBit(TypeIds.BitNonNullAnnotation | TypeIds.BitNullableAnnotation);
						k++;
					}
					if (seen || i == 0) {
						if (k > j) {
							AnnotationBinding[] annotationsForDimension = new AnnotationBinding[k - j];
							System.arraycopy(oldAnnots, j, annotationsForDimension, 0, k - j);
							newAnnots[i] = annotationsForDimension;
						}
					} else {
						AnnotationBinding[] annotationsForDimension = new AnnotationBinding[k - j + 1];
						annotationsForDimension[0] = nonNullAnnotation;
						System.arraycopy(oldAnnots, j, annotationsForDimension, 1, k - j);
						newAnnots[i] = annotationsForDimension;
					}
					j = k + 1;
				}
			}
		}
		return scope.environment().createAnnotatedType(typeBinding, newAnnots);
	}
	
	@Override
	public boolean hasNullTypeAnnotation(AnnotationPosition position) {
		switch (position) {
			case LEAF_TYPE:
				// ignore annotationsOnDimensions:
				return super.hasNullTypeAnnotation(position);
			case MAIN_TYPE:
				// outermost dimension only:
				if (this.annotationsOnDimensions != null && this.annotationsOnDimensions.length > 0) {
					Annotation[] innerAnnotations = this.annotationsOnDimensions[0];
					return containsNullAnnotation(innerAnnotations);
				}
				// e.g. subclass ParameterizedSingleTypeReference is not only used for arrays
				return super.hasNullTypeAnnotation(position);
			case ANY:
				if (super.hasNullTypeAnnotation(position))
					return true;
				if (this.resolvedType != null && !this.resolvedType.hasNullTypeAnnotations())
					return false; // shortcut
				if (this.annotationsOnDimensions != null) {
					for (int i = 0; i < this.annotationsOnDimensions.length; i++) {
						Annotation[] innerAnnotations = this.annotationsOnDimensions[i];
						if (containsNullAnnotation(innerAnnotations))
							return true;
					}
				}
		}
    	return false;
	}
}
