/*******************************************************************************
 * Copyright (c) 2022 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
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.ast;

import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.codegen.BranchLabel;
import org.eclipse.jdt.internal.compiler.codegen.CodeStream;
import org.eclipse.jdt.internal.compiler.codegen.Opcodes;
import org.eclipse.jdt.internal.compiler.flow.FlowContext;
import org.eclipse.jdt.internal.compiler.flow.FlowInfo;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.LocalVariableBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.RecordComponentBinding;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;

public class RecordPattern extends TypePattern {

	public Pattern[] patterns;
	public TypeReference type;
	int thenInitStateIndex1 = -1;
	int thenInitStateIndex2 = -1;

	public RecordPattern(LocalDeclaration local) {
		super(local);
		this.type = local.type;
		this.sourceStart = local.sourceStart;
		this.sourceEnd = local.sourceEnd;
	}
	public RecordPattern(TypeReference type, int sourceStart, int sourceEnd) {
		super();
		this.type = type;
		this.sourceStart = sourceStart;
		this.sourceEnd = sourceEnd;
	}
	@Override
	public TypeReference getType() {
		return this.type;
	}
 	@Override
	public void collectPatternVariablesToScope(LocalVariableBinding[] variables, BlockScope scope) {
		if (this.resolvedType == null) {
			this.resolveType(scope);
		}
		this.addPatternVariablesWhenTrue(variables);
		super.collectPatternVariablesToScope(variables, scope);
		for (Pattern p : this.patterns) {
			p.collectPatternVariablesToScope(variables, scope);
			this.addPatternVariablesWhenTrue(p.patternVarsWhenTrue);
		}
	}
	@Override
	public boolean checkUnsafeCast(Scope scope, TypeBinding castType, TypeBinding expressionType, TypeBinding match, boolean isNarrowing) {
		if (!castType.isReifiable())
			return CastExpression.checkUnsafeCast(this, scope, castType, expressionType, match, isNarrowing);
		else
			return super.checkUnsafeCast(scope, castType, expressionType, match, isNarrowing);
	}

	@Override
	public LocalDeclaration getPatternVariable() {
		return super.getPatternVariable();
	}
	@Override
	public FlowInfo analyseCode(BlockScope currentScope, FlowContext flowContext, FlowInfo flowInfo) {
		this.thenInitStateIndex1 = currentScope.methodScope().recordInitializationStates(flowInfo);
		flowInfo = super.analyseCode(currentScope, flowContext, flowInfo);
		for (Pattern p : this.patterns) {
			 p.analyseCode(currentScope, flowContext, flowInfo);
		}
		flowInfo = flowInfo.safeInitsWhenTrue();
		this.thenInitStateIndex2 = currentScope.methodScope().recordInitializationStates(flowInfo);
		return flowInfo;
	}
	@Override
	public boolean isTotalForType(TypeBinding t) {
		return false;
	}
	@Override
	public void resolveWithExpression(BlockScope scope, Expression exp) {
		this.expression = exp;
	}
	@Override
	public TypeBinding resolveAtType(BlockScope scope, TypeBinding u) {
		for (Pattern p : this.patterns) {
			p.resolveAtType(scope, u);
		}
		if (this.local != null) {
			this.resolvedType = super.resolveAtType(scope, u);
		}
		return this.resolvedType;
	}
	@Override
	public TypeBinding resolveType(BlockScope scope, boolean isPatternVariable) {
		if (this.resolvedType != null)
			return this.resolvedType;
		super.resolveType(scope, isPatternVariable);

		if (this.local != null) {
			this.resolvedType = super.resolveType(scope);
		} else {
			this.resolvedType = this.type.resolveType(scope);
		}
		if (!this.resolvedType.isValidBinding())
			return this.resolvedType;

		initSecretPatternVariable(scope);

		// check whether the give type reference is a record
		// check whether a raw type is being used in pattern types
		// check whether the pattern signature matches that of the record declaration
		if (!this.resolvedType.isRecord()) {
			scope.problemReporter().unexpectedTypeinRecordPattern(this.resolvedType, this.type);
			return this.resolvedType;
		} else if (this.resolvedType.isRawType()) {
			scope.problemReporter().rawTypeInRecordPattern(this.resolvedType, this.type);
		}
		this.isTotalTypeNode = isTotalForType(this.resolvedType);
		RecordComponentBinding[] components = this.resolvedType.components();
		if (components.length != this.patterns.length) {
			scope.problemReporter().recordPatternSignatureMismatch(this.resolvedType, this);
		} else {
			for (int i = 0; i < components.length; i++) {
				Pattern p = this.patterns[i];
				if (!(p instanceof TypePattern))
					continue;
				TypePattern tp = (TypePattern) p;
				RecordComponentBinding componentBinding = components[i];
				if (p.getType().isTypeNameVar(scope)) {
					infuseInferredType(tp, componentBinding);
				}
				p.resolveType(scope, true);
				TypeBinding expressionType = componentBinding.type;
				if (p.isPatternTypeCompatible(expressionType, scope)) {
					p.isTotalTypeNode = p.isTotalForType(componentBinding.type);
					MethodBinding[] methods = this.resolvedType.getMethods(componentBinding.name);
					if (methods != null && methods.length > 0) {
						p.accessorMethod = methods[0];
					}
				}
			}
		}
		return this.resolvedType;
	}
	private void infuseInferredType(TypePattern tp, RecordComponentBinding componentBinding) {
		SingleTypeReference ref = new SingleTypeReference(tp.local.type.getTypeName()[0],
				tp.local.type.sourceStart,
				tp.local.type.sourceEnd) {
			@Override
			public TypeBinding resolveType(BlockScope scope, boolean checkBounds) {
				return componentBinding.type;
			}
		};
		tp.local.type = ref;
	}
	@Override
	public boolean isAlwaysTrue() {
		return false;
	}
	@Override
	public boolean dominates(Pattern p) {
		if (!this.resolvedType.isValidBinding())
			return false;
		if (!super.isTotalForType(p.resolvedType)) {
			return false;
		}
		if (p instanceof RecordPattern) {
			RecordPattern rp = (RecordPattern) p;
			if (this.patterns.length != rp.patterns.length)
				return false;
			for(int i = 0; i < this.patterns.length; i++) {
				if (!this.patterns[i].dominates(rp.patterns[i])) {
					return false;
				}
			}
		}
		return true;
	}
	@Override
	public void generateOptimizedBoolean(BlockScope currentScope, CodeStream codeStream, BranchLabel trueLabel, BranchLabel falseLabel) {
		codeStream.checkcast(this.resolvedType);
		initializePatternVariables(currentScope, codeStream);
		generatePatternVariable(currentScope, codeStream, trueLabel, falseLabel);
		wrapupGeneration(codeStream);
		if (this.thenInitStateIndex2 != -1) {
			codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.thenInitStateIndex2);
			codeStream.addDefinitelyAssignedVariables(currentScope, this.thenInitStateIndex2);
		}
	}
	@Override
	protected void generatePatternVariable(BlockScope currentScope, CodeStream codeStream, BranchLabel trueLabel, BranchLabel falseLabel) {
		if (!this.isTotalTypeNode) {
			codeStream.load(this.secretPatternVariable);
			codeStream.instance_of(this.resolvedType);
			BranchLabel target = falseLabel != null ? falseLabel : new BranchLabel(codeStream);
			codeStream.ifeq(target);
		}
		for (Pattern p : this.patterns) {
			if (p.accessorMethod != null) {
				codeStream.load(this.secretPatternVariable);
				if (!this.isTotalTypeNode)
					codeStream.checkcast(this.resolvedType);
				generateArguments(p.accessorMethod, null, currentScope, codeStream);
				codeStream.invoke(Opcodes.OPC_invokevirtual, p.accessorMethod, this.resolvedType, null);
				if (!p.isTotalTypeNode) {
					if (p instanceof TypePattern) {
						((TypePattern)p).initializePatternVariables(currentScope, codeStream);
						codeStream.load(p.secretPatternVariable);
						codeStream.instance_of(p.resolvedType);
						BranchLabel target = falseLabel != null ? falseLabel : new BranchLabel(codeStream);
						codeStream.ifeq(target);
						codeStream.load(p.secretPatternVariable);
					}
				}
				p.generateOptimizedBoolean(currentScope, codeStream, trueLabel, falseLabel);
			}
		}
		super.generatePatternVariable(currentScope, codeStream, trueLabel, falseLabel);
	}
	@Override
	public void wrapupGeneration(CodeStream codeStream) {
		for (Pattern p : this.patterns) {
			p.wrapupGeneration(codeStream);
		}
		super.wrapupGeneration(codeStream);
	}
	@Override
	public void suspendVariables(CodeStream codeStream, BlockScope scope) {
		codeStream.removeNotDefinitelyAssignedVariables(scope, this.thenInitStateIndex1);
	}
	@Override
	public void resumeVariables(CodeStream codeStream, BlockScope scope) {
		codeStream.addDefinitelyAssignedVariables(scope, this.thenInitStateIndex2);
	}
	@Override
	public void traverse(ASTVisitor visitor, BlockScope scope) {
		for (Pattern p : this.patterns) {
			visitor.visit(p, scope);
		}
		if (visitor.visit(this, scope)) {
			if (this.local != null)
				this.local.traverse(visitor, scope);
			else if (this.type != null) {
				this.type.traverse(visitor, scope);
			}
			for (Pattern p : this.patterns) {
				p.traverse(visitor, scope);
			}
		}
		visitor.endVisit(this, scope);
	}

	@Override
	public StringBuffer printExpression(int indent, StringBuffer output) {
		output.append(this.type).append('(');
		if (this.patterns != null) {
			for (int i = 0; i < this.patterns.length; i++) {
				if (i > 0) output.append(", "); //$NON-NLS-1$
				this.patterns[i].print(0, output);
			}
		}
		output.append(')');
		if (this.local != null)
			output.append(' ').append(this.local.name);
		return output;
	}
}
