/*******************************************************************************
 * Copyright (c) 2019, 2020 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
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.aspectj.org.eclipse.jdt.internal.compiler.ast;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.aspectj.org.eclipse.jdt.core.compiler.CharOperation;
import org.aspectj.org.eclipse.jdt.internal.compiler.ClassFile;
import org.aspectj.org.eclipse.jdt.internal.compiler.CompilationResult;
import org.aspectj.org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.ExtraCompilerModifiers;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.aspectj.org.eclipse.jdt.internal.compiler.parser.Parser;

public class RecordDeclaration extends TypeDeclaration {

	private Argument[] args;
	public int nRecordComponents;
	public boolean isLocalRecord;
	public static Set<String> disallowedComponentNames;
	static {
		disallowedComponentNames = new HashSet<>(6);
		disallowedComponentNames.add("clone"); //$NON-NLS-1$
		disallowedComponentNames.add("finalize"); //$NON-NLS-1$
		disallowedComponentNames.add("getClass"); //$NON-NLS-1$
		disallowedComponentNames.add("hashCode"); //$NON-NLS-1$
		disallowedComponentNames.add("notify");   //$NON-NLS-1$
		disallowedComponentNames.add("notifyAll");//$NON-NLS-1$
		disallowedComponentNames.add("toString"); //$NON-NLS-1$
		disallowedComponentNames.add("wait"); //$NON-NLS-1$
	}
	public RecordDeclaration(CompilationResult compilationResult) {
		super(compilationResult);
		this.modifiers |= ExtraCompilerModifiers.AccRecord;
	}
	public RecordDeclaration(TypeDeclaration t) {
		super(t.compilationResult);
		this.modifiers = t.modifiers | ExtraCompilerModifiers.AccRecord;
		this.modifiersSourceStart = t.modifiersSourceStart;
		this.annotations = t.annotations;
		this.name = t.name;
		this.superInterfaces = t.superInterfaces;
		this.fields = t.fields;
		this.methods = t.methods;
		this.memberTypes = t.memberTypes;
		this.binding = t.binding;
		this.scope = t.scope;
		this.initializerScope = t.initializerScope;
		this.staticInitializerScope = t.staticInitializerScope;
		this.ignoreFurtherInvestigation = t.ignoreFurtherInvestigation;
		this.maxFieldCount = t.maxFieldCount;
		this.declarationSourceStart = t.declarationSourceStart;
		this.declarationSourceEnd = t.declarationSourceEnd;
		this.bodyStart = t.bodyStart;
		this.bodyEnd = t.bodyEnd;
		this.missingAbstractMethods = t.missingAbstractMethods; // TODO: Investigate whether this is relevant.
		this.javadoc = t.javadoc;
		this.allocation = t.allocation;
		this.enclosingType = t.enclosingType;
		this.typeParameters = t.typeParameters;
		this.sourceStart = t.sourceStart;
		this.sourceEnd = t.sourceEnd;
		this.restrictedIdentifierStart = t.restrictedIdentifierStart;
	}
	public ConstructorDeclaration getConstructor(Parser parser) {
		if (this.methods != null) {
			for (int i = this.methods.length; --i >= 0;) {
				AbstractMethodDeclaration am;
				if ((am = this.methods[i]).isConstructor()) {
					if (!CharOperation.equals(am.selector, this.name)) {
						// the constructor was in fact a method with no return type
						// unless an explicit constructor call was supplied
						ConstructorDeclaration c = (ConstructorDeclaration) am;
						if (c.constructorCall == null || c.constructorCall.isImplicitSuper()) { //changed to a method
							MethodDeclaration m = parser.convertToMethodDeclaration(c, this.compilationResult);
							this.methods[i] = m;
						}
					} else {
						if (am instanceof CompactConstructorDeclaration) {
							CompactConstructorDeclaration ccd = (CompactConstructorDeclaration) am;
							ccd.recordDeclaration = this;
							if (ccd.arguments == null)
								ccd.arguments = this.args;
							return ccd;
						}
						// now we are looking at a "normal" constructor
						if (this.args == null && am.arguments == null)
							return (ConstructorDeclaration) am;
					}
				}
			}
		}
		/* At this point we can only say that there is high possibility that there is a constructor
		 * If it is a CCD, then definitely it is there (except for empty one); else we need to check
		 * the bindings to say that there is a canonical constructor. To take care at binding resolution time.
		 */
		return null;
	}

	/** Returns an implicit canonical constructor, if any.
	 */
	public static ConstructorDeclaration getImplicitCanonicalConstructor(AbstractMethodDeclaration[] methods) {
		if (methods == null)
			return null;
		for (AbstractMethodDeclaration am : methods) {
			if (am instanceof ConstructorDeclaration && (am.bits & (ASTNode.IsCanonicalConstructor | ASTNode.IsImplicit)) != 0)
				return (ConstructorDeclaration) am;
		}
		return null;
	}
	@Override
	public ConstructorDeclaration createDefaultConstructor(boolean needExplicitConstructorCall, boolean needToInsert) {
		//Add to method'set, the default constuctor that just recall the
		//super constructor with no arguments
		//The arguments' type will be positionned by the TC so just use
		//the default int instead of just null (consistency purpose)

		ConstructorDeclaration constructor = new ConstructorDeclaration(this.compilationResult);
		constructor.bits |= ASTNode.IsCanonicalConstructor | ASTNode.IsImplicit;
		constructor.selector = this.name;
//		constructor.modifiers = this.modifiers & ExtraCompilerModifiers.AccVisibilityMASK;
		constructor.modifiers = this.modifiers & ClassFileConstants.AccPublic;
		constructor.modifiers |= ClassFileConstants.AccPublic; // JLS 14 8.10.5
		constructor.arguments = this.args;

		constructor.declarationSourceStart = constructor.sourceStart =
				constructor.bodyStart = this.sourceStart;
		constructor.declarationSourceEnd =
			constructor.sourceEnd = constructor.bodyEnd =  this.sourceStart - 1;

		//the super call inside the constructor
		if (needExplicitConstructorCall) {
			constructor.constructorCall = SuperReference.implicitSuperConstructorCall();
			constructor.constructorCall.sourceStart = this.sourceStart;
			constructor.constructorCall.sourceEnd = this.sourceEnd;
		}
	/* The body of the implicitly declared canonical constructor initializes each field corresponding
		 * to a record component with the corresponding formal parameter in the order that they appear
		 * in the record component list.*/
		List<Statement> statements = new ArrayList<>();
		int l = this.args != null ? this.args.length : 0;
		if (l > 0 && this.fields != null) {
			List<String> fNames = Arrays.stream(this.fields)
					.filter(f -> f.isARecordComponent)
					.map(f ->new String(f.name))
					.collect(Collectors.toList());
			for (int i = 0; i < l; ++i) {
				Argument arg = this.args[i];
				if (!fNames.contains(new String(arg.name)))
					continue;
				FieldReference lhs = new FieldReference(arg.name, 0);
				lhs.receiver = ThisReference.implicitThis();
				statements.add(new Assignment(lhs, new SingleNameReference(arg.name, 0), 0));
			}
		}
		constructor.statements = statements.toArray(new Statement[0]);

		//adding the constructor in the methods list: rank is not critical since bindings will be sorted
		if (needToInsert) {
			if (this.methods == null) {
				this.methods = new AbstractMethodDeclaration[] { constructor };
			} else {
				AbstractMethodDeclaration[] newMethods;
				System.arraycopy(
					this.methods,
					0,
					newMethods = new AbstractMethodDeclaration[this.methods.length + 1],
					1,
					this.methods.length);
				newMethods[0] = constructor;
				this.methods = newMethods;
			}
		}
		return constructor;
	}

	@Override
	public void generateCode(ClassFile enclosingClassFile) {
		super.generateCode(enclosingClassFile);
	}
	@Override
	public boolean isRecord() {
		return true;
	}
	@Override
	public StringBuffer printHeader(int indent, StringBuffer output) {
		printModifiers(this.modifiers, output);
		if (this.annotations != null) {
			printAnnotations(this.annotations, output);
			output.append(' ');
		}

		output.append("record "); //$NON-NLS-1$
		output.append(this.name);
		output.append('(');
		if (this.nRecordComponents > 0 && this.fields != null) {
			for (int i = 0; i < this.nRecordComponents; i++) {
				if (i > 0) output.append(", "); //$NON-NLS-1$
				output.append(this.fields[i].type.getTypeName()[0]);
				output.append(' ');
				output.append(this.fields[i].name);
			}
		}
		output.append(')');
		if (this.typeParameters != null) {
			output.append("<");//$NON-NLS-1$
			for (int i = 0; i < this.typeParameters.length; i++) {
				if (i > 0) output.append( ", "); //$NON-NLS-1$
				this.typeParameters[i].print(0, output);
			}
			output.append(">");//$NON-NLS-1$
		}
		if (this.superInterfaces != null && this.superInterfaces.length > 0) {
			output.append(" implements "); //$NON-NLS-1$
			for (int i = 0; i < this.superInterfaces.length; i++) {
				if (i > 0) output.append( ", "); //$NON-NLS-1$
				this.superInterfaces[i].print(0, output);
			}
		}
		return output;
	}
	@Override
	public StringBuffer printBody(int indent, StringBuffer output) {
		output.append(" {"); //$NON-NLS-1$
		if (this.memberTypes != null) {
			for (int i = 0; i < this.memberTypes.length; i++) {
				if (this.memberTypes[i] != null) {
					output.append('\n');
					this.memberTypes[i].print(indent + 1, output);
				}
			}
		}
		if (this.fields != null) {
			for (int fieldI = 0; fieldI < this.fields.length; fieldI++) {
				if (this.fields[fieldI] != null) {
					output.append('\n');
					if (fieldI < this.nRecordComponents)
						output.append("/* Implicit */"); //$NON-NLS-1$ //TODO BETA_JAVA14: Move this to FD?
					this.fields[fieldI].print(indent + 1, output);
				}
			}
		}
		if (this.methods != null) {
			for (int i = 0; i < this.methods.length; i++) {
				if (this.methods[i] != null) {
					output.append('\n');
					AbstractMethodDeclaration amd = this.methods[i];
					if (amd instanceof MethodDeclaration && (amd.bits & ASTNode.IsImplicit) != 0)
						output.append("/* Implicit */\n"); //$NON-NLS-1$// TODO BETA_JAVA14: Move this to MD?
					amd.print(indent + 1, output);
				}
			}
		}
		output.append('\n');
		return printIndent(indent, output).append('}');
	}
	public Argument[] getArgs() {
		return this.args;
	}
	public void setArgs(Argument[] args) {
		this.args = args;
	}
	public static void checkAndFlagRecordNameErrors(char[] typeName, ASTNode node, Scope skope) {
		if (CharOperation.equals(typeName, TypeConstants.RECORD_RESTRICTED_IDENTIFIER)) {
			if (skope.compilerOptions().sourceLevel == ClassFileConstants.JDK14) {
					skope.problemReporter().recordIsAReservedTypeName(node);
			}
		}
	}
	AbstractMethodDeclaration[] getMethod(char[] name1) {
		if (name1 == null || name1.length == 0 || this.methods == null)
			return null;
		List<AbstractMethodDeclaration> amList = new ArrayList<>(0);
		for (AbstractMethodDeclaration amd : this.methods) {
			if (CharOperation.equals(name1, amd.selector))
				amList.add(amd);
		}
		return amList.toArray(new AbstractMethodDeclaration[0]);
	}
}
