/*
 *  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.classgen;

import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.GroovyCodeVisitor;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ExpressionTransformer;
import org.codehaus.groovy.classgen.asm.BytecodeHelper;
import org.objectweb.asm.MethodVisitor;

import static org.codehaus.groovy.ast.ClassHelper.int_TYPE;
import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.NEW;

/**
 * Initializes an enum constant that supplies no arguments of its own by calling the
 * {@code (String,int)} constructor of the enum directly.
 * <p>
 * The {@code $INIT} helper generated by {@link EnumVisitor} spreads an {@code Object[]}
 * over the constructors of the enum, which means the meta class selects the constructor
 * at run time by reflecting over {@code getDeclaredConstructors()}.  Where that reflection
 * is unavailable — a GraalVM native image for which the enum was not registered, or an
 * Android application whose code shrinker stripped the reflective-only constructor —
 * the static initializer of the enum fails.  When the arguments are known at compile time
 * to be exactly the name and the ordinal, the constructor can be selected there instead.
 * <p>
 * Only the bytecode generator sees the direct call: every other visitor is given the
 * {@code $INIT} call and every {@link ExpressionTransformer} rewrites it in place, so the
 * call that is emitted if the expected constructor turns out not to be present once all
 * transforms have run is the one that would have been emitted without this expression.
 * <p>
 * Two consequences of the direct call are deliberate.  A transform that rewrites the
 * wrapped call into something other than the creation of the constant only takes effect
 * when the {@code $INIT} call is emitted.  And where the {@code $INIT} call dispatches
 * through the meta class of the enum — which selects the constructor — the direct call
 * does not, so a meta class registered before the enum initializes no longer sees the
 * constants being created.  Declaring a constant with an explicit empty argument list,
 * e.g. {@code ONE()}, supplies the same name and ordinal through the {@code $INIT} path
 * and so restores both behaviours for that enum.
 */
final class EnumConstantInit extends BytecodeExpression {

    private static final Parameter[] NAME_AND_ORDINAL = params(param(ClassHelper.STRING_TYPE, "name"), param(int_TYPE, "ordinal"));

    private final ClassNode enumClass;
    private final String name;
    private final int ordinal;
    private final Expression initCall;

    EnumConstantInit(final ClassNode enumClass, final String name, final int ordinal, final Expression initCall) {
        super(enumClass.getPlainNodeReference());
        this.enumClass = enumClass;
        this.name = name;
        this.ordinal = ordinal;
        this.initCall = initCall;
    }

    @Override
    public String getText() {
        return initCall.getText();
    }

    @Override
    public void visit(final GroovyCodeVisitor visitor) {
        if (visitor instanceof AsmClassGenerator && enumClass.getDeclaredConstructor(NAME_AND_ORDINAL) != null) {
            super.visit(visitor); // i.e. visitBytecodeExpression(this)
        } else {
            initCall.visit(visitor);
        }
    }

    @Override
    public Expression transformExpression(final ExpressionTransformer transformer) {
        Expression result = new EnumConstantInit(enumClass, name, ordinal, transformer.transform(initCall));
        result.setSourcePosition(this);
        result.copyNodeMetaData(this);
        return result;
    }

    @Override
    public void visit(final MethodVisitor mv) {
        String owner = BytecodeHelper.getClassInternalName(enumClass);
        mv.visitTypeInsn(NEW, owner);
        mv.visitInsn(DUP);
        mv.visitLdcInsn(name);
        BytecodeHelper.pushConstant(mv, ordinal);
        mv.visitMethodInsn(INVOKESPECIAL, owner, "<init>", "(Ljava/lang/String;I)V", false);
    }
}
