/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.codehaus.groovy.transform;

import groovy.lang.GroovyClassLoader;
import groovy.transform.CompilationUnitAware;
import groovy.transform.NamedParam;
import groovy.transform.RecordBase;
import groovy.transform.RecordOptions;
import groovy.transform.RecordTypeMode;
import groovy.transform.options.PropertyHandler;
import org.apache.groovy.ast.tools.CopyWithUtils;
import org.apache.groovy.ast.tools.MethodNodeUtils;
import org.codehaus.groovy.ast.MethodNode;
import org.apache.groovy.lang.annotation.Incubating;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.GenericsType;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.RecordComponentNode;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.ast.stmt.SwitchStatement;
import org.codehaus.groovy.ast.tools.GenericsUtils;
import org.codehaus.groovy.classgen.asm.BytecodeHelper;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.SourceUnit;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import java.awt.Image;
import java.beans.BeanDescriptor;
import java.beans.EventSetDescriptor;
import java.beans.MethodDescriptor;
import java.beans.PropertyDescriptor;
import java.beans.SimpleBeanInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import static org.apache.groovy.ast.tools.ClassNodeUtils.addGeneratedMethod;
import static org.codehaus.groovy.ast.ClassHelper.LIST_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.MAP_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.OBJECT;
import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.TUPLE_CLASSES;
import static org.codehaus.groovy.ast.ClassHelper.boolean_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.getWrapper;
import static org.codehaus.groovy.ast.ClassHelper.int_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.long_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.make;
import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching;
import static org.codehaus.groovy.ast.tools.GeneralUtils.andX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
import static org.codehaus.groovy.ast.tools.GeneralUtils.arrayX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.assignS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.bytecodeX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.caseS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.eqX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.equalsNullX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstanceProperties;
import static org.codehaus.groovy.ast.tools.GeneralUtils.hasDeclaredMethod;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.indexX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.isTrueX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.listX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.mapEntryX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.mapX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.neX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.notX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.nullX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.orX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
import static org.codehaus.groovy.ast.tools.GeneralUtils.plusX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.propX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
import static org.codehaus.groovy.ast.tools.GeneralUtils.switchS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ternaryX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.thisPropX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.tryCatchS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
import static org.codehaus.groovy.runtime.StringGroovyMethods.isAtLeast;
import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.IRETURN;

/**
 * Handles generation of code for the @RecordType annotation.
 */
@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
public class RecordTypeASTTransformation extends AbstractASTTransformation implements CompilationUnitAware {

    private static final ClassNode ILLEGAL_ARGUMENT = makeWithoutCaching(IllegalArgumentException.class);
    private static final ClassNode NAMED_PARAM_TYPE = make(NamedParam.class);
    static final ClassNode RECORD_OPTIONS_TYPE = make(RecordOptions.class);
    private static final ClassNode SIMPLE_BEAN_INFO_TYPE = make(SimpleBeanInfo.class);
    private static final ClassNode BEAN_DESCRIPTOR_TYPE = make(BeanDescriptor.class);
    private static final ClassNode COLLECTIONS_TYPE = makeWithoutCaching(Collections.class, false);
    private static final ClassNode PROPERTY_DESCRIPTOR_TYPE = make(PropertyDescriptor.class);
    private static final ClassNode PROPERTY_DESCRIPTOR_ARRAY_TYPE = make(PropertyDescriptor[].class);
    private static final ClassNode EVENT_SET_DESCRIPTOR_TYPE = make(EventSetDescriptor.class);
    private static final ClassNode EVENT_SET_DESCRIPTOR_ARRAY_TYPE = make(EventSetDescriptor[].class);
    private static final ClassNode METHOD_DESCRIPTOR_TYPE = make(MethodDescriptor.class);
    private static final ClassNode METHOD_DESCRIPTOR_ARRAY_TYPE = make(MethodDescriptor[].class);
    private static final ClassNode IMAGE_TYPE = make(Image.class);

    static final String COMPONENTS = "components";
    static final String COPY_WITH = "copyWith";
    static final String GET_AT = "getAt";
    static final String NAMED_ARGS = "namedArgs";
    private static final String RECORD_CLASS_NAME = "java.lang.Record";
    static final String SIZE = "size";
    static final String TO_LIST = "toList";
    static final String TO_MAP = "toMap";

    public static final ClassNode MY_TYPE = makeWithoutCaching(RecordBase.class, false);
    private static final String MY_TYPE_NAME = MY_TYPE.getNameWithoutPackage();

    private CompilationUnit compilationUnit;

    @Override
    public String getAnnotationName() {
        return MY_TYPE_NAME;
    }

    @Override
    public void setCompilationUnit(final CompilationUnit unit) {
        this.compilationUnit = unit;
    }

    protected GroovyClassLoader getTransformLoader() {
        return compilationUnit != null ? compilationUnit.getTransformLoader() : sourceUnit.getClassLoader();
    }

    /**
     * Indicates that the given classnode is a native JVM record class.
     * For classes being compiled, this will only be valid after the
     * {@code RecordTypeASTTransformation} transform has been invoked.
     */
    @Incubating
    public static boolean recordNative(final ClassNode node) {
        return node.getUnresolvedSuperClass() != null && RECORD_CLASS_NAME.equals(node.getUnresolvedSuperClass().getName());
    }

    /**
     * Predicts whether {@code cNode} will be compiled as a native JVM record.
     * Mirrors the decision logic in {@link #doProcessRecordType} (presence of
     * {@code @RecordBase}, target bytecode {@code >= 17}, and
     * {@code @RecordOptions(mode != EMULATE)}) so callers running before the
     * transform itself fires can act on the same outcome.
     * <p>
     * The joint-compilation stub generator uses this at {@code Phases.CONVERSION}
     * to decide whether to emit {@code record Foo(...)} syntax. Unlike
     * {@link #recordNative(ClassNode)}, which inspects the post-transform
     * super class, this predicate inspects only the source-level annotations.
     *
     * @param cNode          the candidate class node
     * @param targetBytecode the target bytecode level
     *                       (see {@link CompilerConfiguration#getTargetBytecode()})
     *
     * @since 6.0.0
     */
    @Incubating
    public static boolean wouldBeNativeRecord(final ClassNode cNode, final String targetBytecode) {
        if (cNode == null || cNode.getAnnotations(MY_TYPE).isEmpty()) return false;
        if (targetBytecode == null || targetBytecode.trim().isEmpty()) return false;
        if (!isAtLeast(targetBytecode, CompilerConfiguration.JDK17)) return false;
        AnnotationNode options = getRecordOptions(cNode);
        return getMode(options, "mode") != RecordTypeMode.EMULATE;
    }

    /** Returns the {@code @RecordOptions} annotation on {@code cNode}, or {@code null}. */
    static AnnotationNode getRecordOptions(final ClassNode cNode) {
        List<AnnotationNode> opts = cNode.getAnnotations(RECORD_OPTIONS_TYPE);
        return opts.isEmpty() ? null : opts.get(0);
    }

    private static boolean memberIs(final AnnotationNode anno, final String name, final Object value) {
        if (anno == null) return false;
        Expression m = anno.getMember(name);
        return m instanceof ConstantExpression && value.equals(((ConstantExpression) m).getValue());
    }

    /** Whether the {@code getAt(int)} method should be added per {@code @RecordOptions}. */
    static boolean shouldAddGetAt(final ClassNode cNode, final AnnotationNode options) {
        return !memberIs(options, GET_AT, Boolean.FALSE) && !hasDeclaredMethod(cNode, GET_AT, 1);
    }

    /** Whether the {@code toList()} method should be added per {@code @RecordOptions}. */
    static boolean shouldAddToList(final ClassNode cNode, final AnnotationNode options) {
        return !memberIs(options, TO_LIST, Boolean.FALSE) && !hasDeclaredMethod(cNode, TO_LIST, 0);
    }

    /** Whether the {@code toMap()} method should be added per {@code @RecordOptions}. */
    static boolean shouldAddToMap(final ClassNode cNode, final AnnotationNode options) {
        return !memberIs(options, TO_MAP, Boolean.FALSE) && !hasDeclaredMethod(cNode, TO_MAP, 0);
    }

    /** Whether the {@code size()} method should be added per {@code @RecordOptions}. */
    static boolean shouldAddSize(final ClassNode cNode, final AnnotationNode options) {
        return !memberIs(options, SIZE, Boolean.FALSE) && !hasDeclaredMethod(cNode, SIZE, 0);
    }

    /** Whether the {@code copyWith(Map)} method should be added (opt-in via {@code @RecordOptions}). */
    static boolean shouldAddCopyWith(final ClassNode cNode, final AnnotationNode options) {
        return memberIs(options, COPY_WITH, Boolean.TRUE) && !hasDeclaredMethod(cNode, COPY_WITH, 1);
    }

    /** Whether the {@code components()} method should be added (opt-in via {@code @RecordOptions}). */
    static boolean shouldAddComponents(final ClassNode cNode, final AnnotationNode options) {
        return memberIs(options, COMPONENTS, Boolean.TRUE) && !hasDeclaredMethod(cNode, COMPONENTS, 0);
    }

    @Override
    public void visit(final ASTNode[] nodes, final SourceUnit source) {
        init(nodes, source);
        AnnotatedNode parent = (AnnotatedNode) nodes[1];
        AnnotationNode anno = (AnnotationNode) nodes[0];
        if (parent instanceof ClassNode && MY_TYPE.equals(anno.getClassNode())) {
            PropertyHandler handler = PropertyHandler.createPropertyHandler(this, getTransformLoader(), (ClassNode) parent);
            if (handler != null && handler.validateAttributes(this, anno)) {
                doProcessRecordType((ClassNode) parent, handler);
            }
        }
    }

    //--------------------------------------------------------------------------

    private void doProcessRecordType(final ClassNode cNode, final PropertyHandler handler) {
        if (cNode.getNodeMetaData("_RECORD_HEADER") != null) {
            cNode.putNodeMetaData("_SKIPPABLE_ANNOTATIONS", Boolean.TRUE);
        }
        // GEP-21 Shape C: discard any stubber-emitted placeholder methods so
        // the existence checks below (hasDeclaredMethod) see only user-declared
        // methods and we add the real bodies. Use removeMethod() rather than
        // removeIf() since ClassNode keeps a parallel name->methods map.
        List<MethodNode> stubs = new ArrayList<>();
        for (MethodNode m : cNode.getMethods()) {
            if (StubberSupport.isStub(m)) stubs.add(m);
        }
        stubs.forEach(cNode::removeMethod);
        AnnotationNode options = getRecordOptions(cNode);
        String targetBytecode = (sourceUnit != null) ? sourceUnit.getConfiguration().getTargetBytecode() : null;
        List<PropertyNode> pList = Collections.unmodifiableList(getInstanceProperties(cNode));
        // At Groovy 6's JDK17 floor, wouldBeNativeRecord is true unless the
        // user explicitly opted out via @RecordOptions(mode=EMULATE), so the
        // "explicit NATIVE but JDK too old" arm is unreachable here.
        boolean isNative = wouldBeNativeRecord(cNode, targetBytecode);
        if (isNative) {
            String scName = cNode.getUnresolvedSuperClass().getName();
            // don't expect any parent to be set at this point but we only check at grammar
            // level when using the record keyword so do a few more sanity checks here
            if (!scName.equals(OBJECT) && !scName.equals(RECORD_CLASS_NAME)) {
                addError("Invalid superclass for native record found: " + scName, cNode);
            }
            cNode.setSuperClass(compilationUnit.getClassNodeResolver().resolveName(RECORD_CLASS_NAME, compilationUnit).getClassNode());
            cNode.setModifiers(cNode.getModifiers() | Opcodes.ACC_RECORD);
            if (!pList.isEmpty()) {
                cNode.setRecordComponents(new ArrayList<>());
            }
            for (PropertyNode pNode : pList) {
                ClassNode pType = pNode.getOriginType();
                ClassNode type = pType.getPlainNodeReference();
                type.setGenericsPlaceHolder(pType.isGenericsPlaceHolder());
                type.setGenericsTypes(pType.getGenericsTypes());
                RecordComponentNode rec = new RecordComponentNode(cNode, pNode.getName(), type, pNode.getAnnotations());
                rec.putNodeMetaData("_SKIPPABLE_ANNOTATIONS", Boolean.TRUE);
                cNode.getRecordComponents().add(rec);
            }
        } else {
            createBeanInfoClass(cNode);
        }

        String cName = cNode.getName();
        if (!checkNotInterface(cNode, MY_TYPE_NAME)) return;
        makeClassFinal(this, cNode);
        makeInnerRecordStatic(cNode);

        for (PropertyNode pNode : pList) {
            adjustPropertyForShallowImmutability(cNode, pNode, handler);
            pNode.setModifiers(pNode.getModifiers() | ACC_FINAL);
        }
        List<FieldNode> fList = cNode.getFields();
        for (FieldNode fNode : fList) {
            ensureNotPublic(this, cName, fNode);
        }

        // 0L serialVersionUID by default
        if (cNode.getDeclaredField("serialVersionUID") == null) {
            cNode.addField("serialVersionUID", ACC_PRIVATE | ACC_STATIC | ACC_FINAL, long_TYPE, constX(0L, true));
        }

        if (!hasAnnotation(cNode, ToStringASTTransformation.MY_TYPE)) {
            if (isNative) {
                createRecordToString(cNode);
            } else {
                ToStringASTTransformation.createToString(cNode, false, false, null,
                        null, true, false, false, false,
                        false, false, false, false, true,
                        new String[]{"[", "]", "=", ", "}, false);
            }
        }

        if (!hasAnnotation(cNode, EqualsAndHashCodeASTTransformation.MY_TYPE)) {
            if (isNative) {
                createRecordEquals(cNode);
                createRecordHashCode(cNode);
            } else {
                EqualsAndHashCodeASTTransformation.createEquals(cNode, false, false, false, null, null, false, false, true, false);
                EqualsAndHashCodeASTTransformation.createHashCode(cNode, true, false, false, null, null, false, false, true, false);
            }
        }

        if (hasAnnotation(cNode, TupleConstructorASTTransformation.MY_TYPE)) {
            AnnotationNode tupleConstructor = cNode.getAnnotations(TupleConstructorASTTransformation.MY_TYPE).get(0);
            if (pList.size() == 1 && pList.get(0).getType().equals(MAP_TYPE)
                    && memberHasValue(tupleConstructor, "namedVariant", Boolean.TRUE)) {
                tupleConstructor.setMember("namedVariant", ConstantExpression.PRIM_FALSE); // GROOVY-11644: conflicts
            }
            if (unsupportedTupleAttribute(tupleConstructor, "excludes")) return;
            if (unsupportedTupleAttribute(tupleConstructor, "includes")) return;
            if (unsupportedTupleAttribute(tupleConstructor, "includeProperties")) return;
            if (unsupportedTupleAttribute(tupleConstructor, "includeSuperFields")) return;
        }

        if (shouldAddCopyWith(cNode, options))   createCopyWith(cNode, pList);
        if (shouldAddGetAt(cNode, options))      createGetAt(cNode, pList);
        if (shouldAddToList(cNode, options))     createToList(cNode, pList);
        if (shouldAddToMap(cNode, options))      createToMap(cNode, pList);
        if (shouldAddComponents(cNode, options)) createComponents(cNode, pList);
        if (shouldAddSize(cNode, options)) {
            addGeneratedMethod(cNode, SIZE, ACC_PUBLIC | ACC_FINAL, int_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, returnS(constX(pList.size(), true)));
        }
    }

    private void createBeanInfoClass(ClassNode cNode) {
        ClassNode beanInfoClass = new ClassNode(cNode.getName() + "BeanInfo", ACC_PUBLIC, SIMPLE_BEAN_INFO_TYPE);
        beanInfoClass.addMethod("getBeanDescriptor", ACC_PUBLIC, BEAN_DESCRIPTOR_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, returnS(
            ctorX(BEAN_DESCRIPTOR_TYPE, args(classX(cNode)))
        ));
        final List<PropertyNode> pList = getInstanceProperties(cNode);
        BlockStatement block = new BlockStatement();
        VariableExpression p = varX("p", PROPERTY_DESCRIPTOR_ARRAY_TYPE);
        block.addStatement(
            declS(p, arrayX(PROPERTY_DESCRIPTOR_TYPE, Collections.emptyList(), List.of(constX(pList.size()))))
        );
        for (int i = 0; i < pList.size(); i++) {
            String name = pList.get(i).getName();
            block.addStatement(tryCatchS(
                assignS(indexX(p, constX(i)), ctorX(PROPERTY_DESCRIPTOR_TYPE, args(constX(name), classX(cNode), constX(name), nullX())))
            ));
        }
        block.addStatement(returnS(p));
        beanInfoClass.addMethod("getPropertyDescriptors", ACC_PUBLIC, PROPERTY_DESCRIPTOR_ARRAY_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, block);
        beanInfoClass.addMethod("getEventSetDescriptors", ACC_PUBLIC, EVENT_SET_DESCRIPTOR_ARRAY_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, returnS(
            arrayX(EVENT_SET_DESCRIPTOR_TYPE, Collections.emptyList(), List.of(constX(0)))
        ));
        beanInfoClass.addMethod("getMethodDescriptors", ACC_PUBLIC, METHOD_DESCRIPTOR_ARRAY_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, returnS(
            arrayX(METHOD_DESCRIPTOR_TYPE, Collections.emptyList(), List.of(constX(0)))
        ));
        beanInfoClass.addMethod("getDefaultPropertyIndex", ACC_PUBLIC, int_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, returnS(
            constX(-1)
        ));
        beanInfoClass.addMethod("getDefaultEventIndex", ACC_PUBLIC, int_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, returnS(
            constX(-1)
        ));
        beanInfoClass.addMethod("getIcon", ACC_PUBLIC, IMAGE_TYPE, params(param(int_TYPE, "iconKind")), ClassNode.EMPTY_ARRAY, returnS(
            nullX()
        ));
        cNode.getModule().addClass(beanInfoClass);
    }

    private void createComponents(ClassNode cNode, List<PropertyNode> pList) {
        if (pList.size() > 16) { // Groovy currently only goes to Tuple16
            addError("Record has too many components for a components() method", cNode);
        }
        ClassNode tuple = makeWithoutCaching(TUPLE_CLASSES[pList.size()], false);
        Statement body;
        if (pList.isEmpty()) {
            body = returnS(propX(classX(tuple), "INSTANCE"));
        } else {
            List<GenericsType> gtypes = new ArrayList<>();
            ArgumentListExpression args = new ArgumentListExpression();
            for (PropertyNode pNode : pList) {
                args.addExpression(callThisX(pNode.getName()));
                gtypes.add(new GenericsType(getWrapper(pNode.getType())));
            }
            tuple.setGenericsTypes(gtypes.toArray(GenericsType.EMPTY_ARRAY));
            body = returnS(ctorX(tuple, args));
        }
        addGeneratedMethod(cNode, COMPONENTS, ACC_PUBLIC | ACC_FINAL, tuple, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body);
    }

    private void createToList(ClassNode cNode, List<PropertyNode> pList) {
        List<Expression> args = new ArrayList<>();
        for (PropertyNode pNode : pList) {
            args.add(callThisX(pNode.getName()));
        }
        Statement body = returnS(callX(COLLECTIONS_TYPE, "unmodifiableList", listX(args)));
        addGeneratedMethod(cNode, TO_LIST, ACC_PUBLIC | ACC_FINAL, LIST_TYPE.getPlainNodeReference(), Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body);
    }

    private void createToMap(ClassNode cNode, List<PropertyNode> pList) {
        List<MapEntryExpression> entries = new ArrayList<>();
        for (PropertyNode pNode : pList) {
            String name = pNode.getName();
            entries.add(mapEntryX(name, callThisX(name)));
        }
        Statement body = returnS(callX(COLLECTIONS_TYPE, "unmodifiableMap", mapX(entries)));
        addGeneratedMethod(cNode, TO_MAP, ACC_PUBLIC | ACC_FINAL, MAP_TYPE.getPlainNodeReference(), Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body);
    }

    private void createGetAt(ClassNode cNode, List<PropertyNode> pList) {
        Expression i = varX("i");
        SwitchStatement body = switchS(i, throwS(ctorX(ILLEGAL_ARGUMENT, args(plusX(constX("No record component with index: "), i)))));
        for (int j = 0; j < pList.size(); j++) {
            body.addCase(caseS(constX(j), returnS(callThisX(pList.get(j).getName()))));
        }
        addGeneratedMethod(cNode, GET_AT, ACC_PUBLIC | ACC_FINAL, OBJECT_TYPE, params(param(int_TYPE, "i")), ClassNode.EMPTY_ARRAY, body);
    }

    private void createCopyWith(ClassNode cNode, List<PropertyNode> pList) {
        ArgumentListExpression args = new ArgumentListExpression();
        Parameter mapParam = param(GenericsUtils.nonGeneric(MAP_TYPE), NAMED_ARGS);
        Expression mapArg = varX(mapParam);
        BlockStatement body = new BlockStatement();
        body.addStatement(CopyWithUtils.nestedFlattenStmt(NAMED_ARGS));
        // Honour the documented identity contract: an empty/no-op map (after
        // flattening unknown and unchanged nested keys away) returns the
        // original instance, so an unchanged graph yields the original root.
        body.addStatement(ifS(
                orX(equalsNullX(mapArg), eqX(callX(mapArg, "size"), constX(0))),
                returnS(varX("this"))));
        body.addStatement(declS(localVarX("dirty", boolean_TYPE), ConstantExpression.PRIM_FALSE));
        for (PropertyNode pNode : pList) {
            String name = pNode.getName();
            args.addExpression(ternaryX(callX(mapArg, "containsKey", args(constX(name))), propX(mapArg, name), thisPropX(true, name)));
            body.addStatement(ifS(
                    andX(callX(mapArg, "containsKey", args(constX(name))),
                            neX(propX(mapArg, name), thisPropX(true, name))),
                    assignS(varX("dirty", boolean_TYPE), ConstantExpression.TRUE)));
            ClassNode pType = pNode.getType();
            ClassNode type = pType.getPlainNodeReference();
            type.setGenericsPlaceHolder(pType.isGenericsPlaceHolder());
            type.setGenericsTypes(pType.getGenericsTypes());
            AnnotationNode namedParam = new AnnotationNode(NAMED_PARAM_TYPE);
            namedParam.addMember("value", constX(name));
            namedParam.addMember("type", classX(type));
            namedParam.addMember("required", constX(false, true));
            mapParam.addAnnotation(namedParam);
        }
        // Return the original instance when nothing effectively changed;
        // otherwise keep the constructor call as the method's final return
        // expression, exactly as before (static compilation resolves the
        // constructor target in that position).
        body.addStatement(ifS(notX(isTrueX(varX("dirty", boolean_TYPE))),
                returnS(varX("this"))));
        body.addStatement(returnS(ctorX(cNode.getPlainNodeReference(), args)));
        addGeneratedMethod(cNode, COPY_WITH, ACC_PUBLIC | ACC_FINAL, cNode.getPlainNodeReference(), params(mapParam), ClassNode.EMPTY_ARRAY, body);

        CopyWithUtils.addCopyWithBlockMethod(cNode);
    }

    private void createRecordToString(ClassNode cNode) {
        String desc = BytecodeHelper.getMethodDescriptor(STRING_TYPE, new ClassNode[]{cNode});
        Statement body = stmt(bytecodeX(STRING_TYPE, mv -> {
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitInvokeDynamicInsn("toString", desc, createBootstrapMethod(), createBootstrapMethodArguments(cNode));
                    mv.visitInsn(ARETURN);
                    mv.visitMaxs(0, 0);
                    mv.visitEnd();
                })
        );
        addGeneratedMethod(cNode, "toString", ACC_PUBLIC | ACC_FINAL, STRING_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body);
    }

    private void createRecordEquals(ClassNode cNode) {
        String desc = BytecodeHelper.getMethodDescriptor(boolean_TYPE, new ClassNode[]{cNode, OBJECT_TYPE});
        Statement body = stmt(bytecodeX(boolean_TYPE, mv -> {
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitVarInsn(ALOAD, 1);
                    mv.visitInvokeDynamicInsn("equals", desc, createBootstrapMethod(), createBootstrapMethodArguments(cNode));
                    mv.visitInsn(IRETURN);
                    mv.visitMaxs(0, 0);
                    mv.visitEnd();
                })
        );
        addGeneratedMethod(cNode, "equals", ACC_PUBLIC | ACC_FINAL, boolean_TYPE, params(param(OBJECT_TYPE, "other")), ClassNode.EMPTY_ARRAY, body);
    }

    private void createRecordHashCode(ClassNode cNode) {
        String desc = BytecodeHelper.getMethodDescriptor(int_TYPE, new ClassNode[]{cNode});
        Statement body = stmt(bytecodeX(int_TYPE, mv -> {
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitInvokeDynamicInsn("hashCode", desc, createBootstrapMethod(), createBootstrapMethodArguments(cNode));
                    mv.visitInsn(IRETURN);
                    mv.visitMaxs(0, 0);
                    mv.visitEnd();
                })
        );
        addGeneratedMethod(cNode, "hashCode", ACC_PUBLIC | ACC_FINAL, int_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body);
    }

    private Object[] createBootstrapMethodArguments(ClassNode cNode) {
        String internalName = cNode.getName().replace('.', '/');
        String names = cNode.getRecordComponents().stream()
                                                    .map(RecordComponentNode::getName)
                                                    .collect(Collectors.joining(";"));
        List<Object> args = new LinkedList<>();
        args.add(Type.getType(BytecodeHelper.getTypeDescription(cNode)));
        args.add(names);
        cNode.getRecordComponents().forEach(rcn -> args.add(createFieldHandle(rcn, internalName)));
        return args.toArray();
    }

    private Object createFieldHandle(RecordComponentNode rcn, String cName) {
        return new Handle(
                Opcodes.H_GETFIELD,
                cName,
                rcn.getName(),
                BytecodeHelper.getTypeDescription(rcn.getType()),
                false
        );
    }

    private Handle createBootstrapMethod() {
        return new Handle(
                Opcodes.H_INVOKESTATIC,
                "java/lang/runtime/ObjectMethods",
                "bootstrap",
                "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;",
                false
        );
    }

    private static RecordTypeMode getMode(AnnotationNode node, String name) {
        if (node != null) {
            final Expression member = node.getMember(name);
            if (member instanceof PropertyExpression prop) {
                Expression oe = prop.getObjectExpression();
                if (oe instanceof ClassExpression ce) {
                    if ("groovy.transform.RecordTypeMode".equals(ce.getType().getName())) {
                        return RecordTypeMode.valueOf(prop.getPropertyAsString());
                    }
                }
            }
        }
        return null;
    }

    private boolean unsupportedTupleAttribute(AnnotationNode anno, String memberName) {
        if (getMemberValue(anno, memberName) != null) {
            String tname = TupleConstructorASTTransformation.MY_TYPE_NAME;
            addError("Error during " + MY_TYPE_NAME + " processing: Annotation attribute '" + memberName +
                    "' not supported for " + tname + " when used with " + MY_TYPE_NAME, anno);
            return true;
        }
        return false;
    }

    private static void makeInnerRecordStatic(ClassNode cNode) {
        if (cNode instanceof InnerClassNode) {
            cNode.setModifiers(cNode.getModifiers() | ACC_STATIC);
        }
    }

    private static void makeClassFinal(AbstractASTTransformation xform, ClassNode cNode) {
        int modifiers = cNode.getModifiers();
        if ((modifiers & ACC_FINAL) == 0) {
            if ((modifiers & (ACC_ABSTRACT | ACC_SYNTHETIC)) == (ACC_ABSTRACT | ACC_SYNTHETIC)) {
                xform.addError("Error during " + MY_TYPE_NAME + " processing: annotation found on inappropriate class " + cNode.getName(), cNode);
                return;
            }
            cNode.setModifiers(modifiers | ACC_FINAL);
        }
    }

    private static void ensureNotPublic(AbstractASTTransformation xform, String cNode, FieldNode fNode) {
        String fName = fNode.getName();
        if (fNode.isPublic() && !fName.contains("$") && !(fNode.isStatic() && fNode.isFinal())) {
            xform.addError("Public field '" + fName + "' not allowed for " + MY_TYPE_NAME + " class '" + cNode + "'.", fNode);
        }
    }

    // guarantee shallow immutability but property handler may do defensive copying
    private static void adjustPropertyForShallowImmutability(ClassNode cNode, PropertyNode pNode, PropertyHandler handler) {
        final FieldNode fNode = pNode.getField();
        fNode.setModifiers((pNode.getModifiers() & (~ACC_PUBLIC)) | ACC_FINAL | ACC_PRIVATE);
        boolean isGetterDefined = cNode.getDeclaredMethods(pNode.getName()).stream()
                .anyMatch(MethodNodeUtils::isGetterCandidate);
        pNode.setGetterName(pNode.getName());
        if (!isGetterDefined) {
            Statement getter = handler.createPropGetter(pNode);
            if (getter != null) {
                pNode.setGetterBlock(getter);
            }
        }
    }
}
