/*
 * Copyright 2014 - 2020 Rafael Winterhalter
 *
 * Licensed 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 net.bytebuddy.implementation.bytecode.collection;

import net.bytebuddy.build.HashCodeAndEqualsPlugin;
import net.bytebuddy.description.type.TypeDefinition;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.bytecode.StackManipulation;
import net.bytebuddy.implementation.bytecode.StackSize;
import net.bytebuddy.implementation.bytecode.constant.IntegerConstant;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.util.List;

/**
 * A {@link net.bytebuddy.implementation.bytecode.collection.CollectionFactory} that is capable of
 * creating an array of a given type with any number of given values.
 */
@HashCodeAndEqualsPlugin.Enhance
public class ArrayFactory implements CollectionFactory {

    /**
     * The component type of the array this array factory is creating.
     */
    private final TypeDescription.Generic componentType;

    /**
     * The array creator delegate that supplies suitable opcodes for the creation of an array and the storage of
     * values inside it.
     */
    private final ArrayCreator arrayCreator;

    /**
     * The decrease of stack size after each value storage operation.
     */
    @HashCodeAndEqualsPlugin.ValueHandling(HashCodeAndEqualsPlugin.ValueHandling.Sort.IGNORE)
    private final StackManipulation.Size sizeDecrease;

    /**
     * Creates a new array factory with a given
     * {@link net.bytebuddy.implementation.bytecode.collection.ArrayFactory.ArrayCreator}
     * without inferring the type from the component type. Normally,
     * {@link net.bytebuddy.implementation.bytecode.collection.ArrayFactory#forType(net.bytebuddy.description.type.TypeDescription.Generic)}
     * should be used.
     *
     * @param componentType The component type of the array factory.
     * @param arrayCreator  The array creator responsible for providing the correct byte code instructions.
     */
    protected ArrayFactory(TypeDescription.Generic componentType, ArrayCreator arrayCreator) {
        this.componentType = componentType;
        this.arrayCreator = arrayCreator;
        // Size decreases by index and array reference (2) and array element (1, 2) after each element storage.
        sizeDecrease = StackSize.DOUBLE.toDecreasingSize().aggregate(componentType.getStackSize().toDecreasingSize());
    }

    /**
     * Creates a new array factory for a given component type.
     *
     * @param componentType The component type of the array that is to be build.
     * @return A new array factory for the given type.
     */
    public static ArrayFactory forType(TypeDescription.Generic componentType) {
        return new ArrayFactory(componentType, makeArrayCreatorFor(componentType));
    }

    /**
     * Creates a suitable array creator for the given component type.
     *
     * @param componentType The component type of the array to be created.
     * @return A suitable array creator.
     */
    private static ArrayCreator makeArrayCreatorFor(TypeDefinition componentType) {
        if (!componentType.isPrimitive()) {
            return new ArrayCreator.ForReferenceType(componentType.asErasure());
        } else if (componentType.represents(boolean.class)) {
            return ArrayCreator.ForPrimitiveType.BOOLEAN;
        } else if (componentType.represents(byte.class)) {
            return ArrayCreator.ForPrimitiveType.BYTE;
        } else if (componentType.represents(short.class)) {
            return ArrayCreator.ForPrimitiveType.SHORT;
        } else if (componentType.represents(char.class)) {
            return ArrayCreator.ForPrimitiveType.CHARACTER;
        } else if (componentType.represents(int.class)) {
            return ArrayCreator.ForPrimitiveType.INTEGER;
        } else if (componentType.represents(long.class)) {
            return ArrayCreator.ForPrimitiveType.LONG;
        } else if (componentType.represents(float.class)) {
            return ArrayCreator.ForPrimitiveType.FLOAT;
        } else if (componentType.represents(double.class)) {
            return ArrayCreator.ForPrimitiveType.DOUBLE;
        } else {
            throw new IllegalArgumentException("Cannot create array of type " + componentType);
        }
    }

    /**
     * {@inheritDoc}
     */
    public StackManipulation withValues(List<? extends StackManipulation> stackManipulations) {
        return new ArrayStackManipulation(stackManipulations);
    }

    /**
     * {@inheritDoc}
     */
    public TypeDescription.Generic getComponentType() {
        return componentType;
    }

    /**
     * An array creator is responsible for providing correct byte code instructions for creating an array
     * and for storing values into it.
     */
    protected interface ArrayCreator extends StackManipulation {

        /**
         * The creation of an array consumes one slot on the operand stack and adds a new value to it.
         * Therefore, the operand stack's size is not altered.
         */
        StackManipulation.Size ARRAY_CREATION_SIZE_CHANGE = StackSize.ZERO.toDecreasingSize();

        /**
         * The opcode instruction for storing a value of the component type inside an array.
         *
         * @return The correct storage opcode for the represented type.
         */
        int getStorageOpcode();

        /**
         * An array creator implementation for primitive types.
         */
        enum ForPrimitiveType implements ArrayCreator {

            /**
             * An array creator for creating {@code boolean[]} arrays.
             */
            BOOLEAN(Opcodes.T_BOOLEAN, Opcodes.BASTORE),

            /**
             * An array creator for creating {@code byte[]} arrays.
             */
            BYTE(Opcodes.T_BYTE, Opcodes.BASTORE),

            /**
             * An array creator for creating {@code short[]} arrays.
             */
            SHORT(Opcodes.T_SHORT, Opcodes.SASTORE),

            /**
             * An array creator for creating {@code char[]} arrays.
             */
            CHARACTER(Opcodes.T_CHAR, Opcodes.CASTORE),

            /**
             * An array creator for creating {@code int[]} arrays.
             */
            INTEGER(Opcodes.T_INT, Opcodes.IASTORE),

            /**
             * An array creator for creating {@code long[]} arrays.
             */
            LONG(Opcodes.T_LONG, Opcodes.LASTORE),

            /**
             * An array creator for creating {@code float[]} arrays.
             */
            FLOAT(Opcodes.T_FLOAT, Opcodes.FASTORE),

            /**
             * An array creator for creating {@code double[]} arrays.
             */
            DOUBLE(Opcodes.T_DOUBLE, Opcodes.DASTORE);

            /**
             * The opcode for creating an array of this type.
             */
            private final int creationOpcode;

            /**
             * The opcode for storing a value in an array of this type.
             */
            private final int storageOpcode;

            /**
             * Creates a new primitive array creator.
             *
             * @param creationOpcode The opcode for creating an array of this type.
             * @param storageOpcode  The opcode for storing a value in an array of this type.
             */
            ForPrimitiveType(int creationOpcode, int storageOpcode) {
                this.creationOpcode = creationOpcode;
                this.storageOpcode = storageOpcode;
            }

            /**
             * {@inheritDoc}
             */
            public boolean isValid() {
                return true;
            }

            /**
             * {@inheritDoc}
             */
            public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext) {
                methodVisitor.visitIntInsn(Opcodes.NEWARRAY, creationOpcode);
                return ARRAY_CREATION_SIZE_CHANGE;
            }

            /**
             * {@inheritDoc}
             */
            public int getStorageOpcode() {
                return storageOpcode;
            }
        }

        /**
         * An array creator implementation for reference types.
         */
        @HashCodeAndEqualsPlugin.Enhance
        class ForReferenceType implements ArrayCreator {

            /**
             * The internal name of this array's non-primitive component type.
             */
            private final String internalTypeName;

            /**
             * Creates a new array creator for a reference type.
             *
             * @param referenceType The internal name of this array's non-primitive component type.
             */
            protected ForReferenceType(TypeDescription referenceType) {
                this.internalTypeName = referenceType.getInternalName();
            }

            /**
             * {@inheritDoc}
             */
            public boolean isValid() {
                return true;
            }

            /**
             * {@inheritDoc}
             */
            public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext) {
                methodVisitor.visitTypeInsn(Opcodes.ANEWARRAY, internalTypeName);
                return ARRAY_CREATION_SIZE_CHANGE;
            }

            /**
             * {@inheritDoc}
             */
            public int getStorageOpcode() {
                return Opcodes.AASTORE;
            }
        }
    }

    /**
     * A stack manipulation for creating an array as defined by the enclosing array factory.
     */
    @HashCodeAndEqualsPlugin.Enhance(includeSyntheticFields = true)
    protected class ArrayStackManipulation implements StackManipulation {

        /**
         * A list of value load instructions that are to be stored inside the created array.
         */
        private final List<? extends StackManipulation> stackManipulations;

        /**
         * Creates a new array loading instruction.
         *
         * @param stackManipulations A list of value load instructions that are to be stored inside the created array.
         */
        protected ArrayStackManipulation(List<? extends StackManipulation> stackManipulations) {
            this.stackManipulations = stackManipulations;
        }

        /**
         * {@inheritDoc}
         */
        public boolean isValid() {
            for (StackManipulation stackManipulation : stackManipulations) {
                if (!stackManipulation.isValid()) {
                    return false;
                }
            }
            return arrayCreator.isValid();
        }

        /**
         * {@inheritDoc}
         */
        public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext) {
            Size size = IntegerConstant.forValue(stackManipulations.size()).apply(methodVisitor, implementationContext);
            // The array's construction does not alter the stack's size.
            size = size.aggregate(arrayCreator.apply(methodVisitor, implementationContext));
            int index = 0;
            for (StackManipulation stackManipulation : stackManipulations) {
                methodVisitor.visitInsn(Opcodes.DUP);
                size = size.aggregate(StackSize.SINGLE.toIncreasingSize());
                size = size.aggregate(IntegerConstant.forValue(index++).apply(methodVisitor, implementationContext));
                size = size.aggregate(stackManipulation.apply(methodVisitor, implementationContext));
                methodVisitor.visitInsn(arrayCreator.getStorageOpcode());
                size = size.aggregate(sizeDecrease);
            }
            return size;
        }
    }
}