/*
 * BoundConstants.java
 *
 * Copyright (c) 2012 Mike Strobel
 *
 * This source code is based on the Dynamic Language Runtime from Microsoft,
 *   Copyright (c) Microsoft Corporation.
 *
 * This source code is subject to terms and conditions of the Apache License, Version 2.0.
 * A copy of the license can be found in the License.html file at the root of this distribution.
 * By using this source code in any fashion, you are agreeing to be bound by the terms of the
 * Apache License, Version 2.0.
 *
 * You must not remove this notice, or any other, from this software.
 */

package com.strobel.expressions;

import com.strobel.compilerservices.Closure;
import com.strobel.core.HashUtilities;
import com.strobel.core.MutableInteger;
import com.strobel.core.VerifyArgument;
import com.strobel.reflection.Type;
import com.strobel.reflection.Types;
import com.strobel.reflection.emit.CodeGenerator;
import com.strobel.reflection.emit.LocalBuilder;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;

/**
 * @author strobelm
 */
final class BoundConstants {

    /** The list of constants in the order they appear in the constant array */
    private final ArrayList<Object> _values = new ArrayList<>();

    /** The index of each constant in the constant array */
    private final IdentityHashMap<Object, MutableInteger> _indexes = new IdentityHashMap<>();

    /** Each constant referenced within this lambda, and how often it was referenced */
    private final HashMap<TypedConstant, MutableInteger> _references = new HashMap<>();

    /** Bytecode locals for storing frequently used constants */
    private final HashMap<TypedConstant, LocalBuilder> _cache = new HashMap<>();

    int count() {
        return _values.size();
    }
    
    Object[] toArray() {
        return _values.toArray();
    }

    void addReference(final Object value, final Type<?> type) {
        if (!_indexes.containsKey(value)) {
            _indexes.put(value, new MutableInteger(_values.size()));
            _values.add(value);
        }
        incrementCount(new TypedConstant(value, type), _references);
    }

    void emitConstant(final LambdaCompiler lc, final Object value, final Type<?> type) {
        assert !CodeGenerator.canEmitConstant(value, type)
            : "!CodeGenerator.canEmitConstant(value, type)";

        if (!lc.canEmitBoundConstants()) {
            throw Error.cannotCompileConstant(value);
        }

        final LocalBuilder local = _cache.get(new TypedConstant(value, type));
        
        if (local != null) {
            lc.generator.emitLoad(local);
            return;
        }
        
        emitConstantsArray(lc);
        emitConstantFromArray(lc, value, type);
    }

    private static void emitConstantsArray(final LambdaCompiler lc) {
        assert lc.canEmitBoundConstants()   // Should have been checked already.
            : "lc.canEmitBoundConstants()";

        lc.emitClosureArgument();
        lc.generator.getField(Type.of(Closure.class).getField("constants"));
    }

    private void emitConstantFromArray(final LambdaCompiler lc, final Object value, final Type type) {
        MutableInteger index = _indexes.get(value);
        
        if (index == null) {
            _indexes.put(value, (index = new MutableInteger(_values.size())));
            _values.add(value);
        }

        lc.generator.emitInteger(index.getValue());
        lc.generator.emitLoadElement(Types.Object);
        lc.generator.emitConversion(Types.Object, type);
    }

    void emitCacheConstants(final LambdaCompiler lc) {
        int count = 0;

        for (final TypedConstant reference : _references.keySet()) {
            if (!lc.canEmitBoundConstants()) {
                throw Error.cannotCompileConstant(reference.value);
            }

            final MutableInteger referenceCount = _references.get(reference);

            if (shouldCache(referenceCount.getValue())) {
                count++;
            }
        }

        if (count == 0) {
            return;
        }

        emitConstantsArray(lc);

        // The same lambda can be in multiple places in the tree, so we
        // need to clear any locals from last time.
        _cache.clear();

        for (final TypedConstant reference : _references.keySet()) {
            final MutableInteger referenceCount = _references.get(reference);

            if (shouldCache(referenceCount.getValue())) {
                if (--count > 0) {
                    // Dup array to keep it on the stack
                    lc.generator.dup();
                }

                final LocalBuilder local = lc.generator.declareLocal(reference.type);

                emitConstantFromArray(lc, reference.value, local.getLocalType());
                lc.generator.emitStore(local);

                _cache.put(reference, local);
            }
        }
    }

    private static boolean shouldCache(final int refCount) {
        // This caching is too aggressive in the face of conditionals and switch.
        // Also, it is too conservative for variables used inside of loops.
        return refCount > 2;
    }

    private void incrementCount(final TypedConstant typedConstant, final HashMap<TypedConstant, MutableInteger> references) {
        final MutableInteger count = references.get(typedConstant);
        if (count != null) {
            count.increment();
        }
        else {
            references.put(typedConstant, new MutableInteger(1));
        }
    }

    @SuppressWarnings("PackageVisibleField")
    private final static class TypedConstant {
        final Object value;
        final Type<?> type;

        TypedConstant(final Object value, final Type<?> type) {
            this.value = VerifyArgument.notNull(value, "value");
            this.type = VerifyArgument.notNull(type, "type");
        }

        @Override
        public int hashCode() {
            return HashUtilities.combineHashCodes(value, type);
        }

        @Override
        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
        public boolean equals(final Object obj) {
            if (obj == this) {
                return true;
            }

            if (obj == null) {
                return false;
            }

            final TypedConstant other = (TypedConstant)obj;

            return other.value == value &&
                   other.type.isEquivalentTo(type);
        }
    }
}
