/**
 * Copyright (c) André Bargull
 * Alle Rechte vorbehalten / All Rights Reserved.  Use is subject to license terms.
 *
 * <https://github.com/anba/es6draft>
 */
package com.github.anba.es6draft.compiler.assembler;

import java.lang.reflect.Modifier;

import com.github.anba.es6draft.compiler.assembler.Code.ClassCode;
import com.github.anba.es6draft.compiler.assembler.Code.MethodCode;

/**
 * Concrete {@link ConstantPool} implementation which handles all constant values as extern constants, i.e. the constant
 * value is retrieved from an external class.
 */
final class ExternConstantPool extends ConstantPool {
    private static final int EXTERN_CONSTANTS_LIMIT = 0x6000;
    private static final int METHOD_LIMIT = 0x400;

    private static final class TypeSpec<T> {
        final String methodName;
        final MethodTypeDescriptor methodDescriptor;
        final T defaultValue;

        private TypeSpec(String methodName, Type type, T defaultValue) {
            this.methodName = methodName;
            this.methodDescriptor = MethodTypeDescriptor.methodType(type, Type.INT_TYPE);
            this.defaultValue = defaultValue;
        }

        String methodName(int index) {
            if (index < 0) {
                return methodName;
            }
            return methodName + "_" + index;
        }

        static final TypeSpec<Integer> INT = new TypeSpec<>("getInt", Type.INT_TYPE, 0);
        static final TypeSpec<Long> LONG = new TypeSpec<>("getLong", Type.LONG_TYPE, 0L);
        static final TypeSpec<Float> FLOAT = new TypeSpec<>("getFloat", Type.FLOAT_TYPE, 0f);
        static final TypeSpec<Double> DOUBLE = new TypeSpec<>("getDouble", Type.DOUBLE_TYPE, 0d);
        static final TypeSpec<String> STRING = new TypeSpec<>("getString", Types.String, "");
    }

    private static final class ConstantClassAssembler extends InstructionAssembler {
        public ConstantClassAssembler(MethodCode method) {
            super(method, new EmptyStack());
        }
    }

    private final ClassCode classCode;
    private boolean closed = false;

    ExternConstantPool(Code code) {
        super(code, EXTERN_CONSTANTS_LIMIT);
        classCode = code.newClass(this);
    }

    @Override
    protected ConstantPool newConstantPool() {
        return new ExternConstantPool(code);
    }

    @Override
    protected void close() {
        assert !closed && (closed = true) : "constant pool closed";
        generate(TypeSpec.INT, getIntegers());
        generate(TypeSpec.LONG, getLongs());
        generate(TypeSpec.FLOAT, getFloats());
        generate(TypeSpec.DOUBLE, getDoubles());
        generate(TypeSpec.STRING, getStrings());
    }

    private <T> void generate(TypeSpec<T> spec, T[] values) {
        if (values.length <= METHOD_LIMIT) {
            generate(spec, -1, values, 0, values.length);
        } else {
            generateSplit(spec, values);
        }
    }

    private <T> void generateSplit(TypeSpec<T> spec, T[] values) {
        // split into multiple methods
        for (int i = 0, index = 0; i < values.length; i += METHOD_LIMIT, ++index) {
            int from = i, to = Math.min(from + METHOD_LIMIT, values.length);
            generate(spec, index, values, from, to);
        }
        // generate entry method
        InstructionAssembler asm = newMethod(spec.methodName, spec);
        asm.begin();
        for (int i = 0, index = 0; i < values.length; i += METHOD_LIMIT, ++index) {
            int from = i, to = Math.min(from + METHOD_LIMIT, values.length);
            Jump jump = new Jump();
            asm.loadParameter(0, Type.INT_TYPE);
            asm.sipush(to);
            asm.ificmpge(jump);
            asm.loadParameter(0, Type.INT_TYPE);
            asm.invokestatic(classCode.classType, spec.methodName(index), spec.methodDescriptor, false);
            asm._return();
            asm.mark(jump);
        }
        asm.ldc(spec.defaultValue);
        asm._return();
        asm.end();
    }

    private <T> void generate(TypeSpec<T> spec, int index, T[] values, int from, int to) {
        int length = to - from;
        if (length == 0) {
            return;
        }
        Jump dflt = new Jump();
        Jump[] jumps = new Jump[length];
        for (int caseIndex = 0; caseIndex < length; ++caseIndex) {
            jumps[caseIndex] = new Jump();
        }
        InstructionAssembler asm = newMethod(spec.methodName(index), spec);
        asm.begin();
        asm.loadParameter(0, Type.INT_TYPE);
        asm.tableswitch(from, to - 1, dflt, jumps);
        for (int caseIndex = 0; caseIndex < length; ++caseIndex) {
            asm.mark(jumps[caseIndex]);
            asm.ldc(values[from + caseIndex]);
            asm._return();
        }
        asm.mark(dflt);
        asm.ldc(spec.defaultValue);
        asm._return();
        asm.end();
    }

    private <T> ConstantClassAssembler newMethod(String methodName, TypeSpec<T> spec) {
        int modifier = Modifier.PUBLIC | Modifier.STATIC;
        MethodCode method = classCode.newMethod(modifier, methodName, spec.methodDescriptor, null, null);
        return new ConstantClassAssembler(method);
    }

    private void load(InstructionAssembler assembler, int index, TypeSpec<?> spec) {
        assert 0 <= index && index <= EXTERN_CONSTANTS_LIMIT;
        assembler.iconst(index);
        assembler.invokestatic(classCode.classType, spec.methodName, spec.methodDescriptor, false);
    }

    @Override
    protected void iconst(InstructionAssembler assembler, Integer cst, int index) {
        load(assembler, index, TypeSpec.INT);
    }

    @Override
    protected void lconst(InstructionAssembler assembler, Long cst, int index) {
        load(assembler, index, TypeSpec.LONG);
    }

    @Override
    protected void fconst(InstructionAssembler assembler, Float cst, int index) {
        load(assembler, index, TypeSpec.FLOAT);
    }

    @Override
    protected void dconst(InstructionAssembler assembler, Double cst, int index) {
        load(assembler, index, TypeSpec.DOUBLE);
    }

    @Override
    protected void aconst(InstructionAssembler assembler, String cst, int index) {
        load(assembler, index, TypeSpec.STRING);
    }
}
