/*
 * Frame.java
 *
 * Copyright (c) 2013 Mike Strobel
 *
 * This source code is based on Mono.Cecil from Jb Evain, Copyright (c) Jb Evain;
 * and ILSpy/ICSharpCode from SharpDevelop, Copyright (c) AlphaSierraPapa.
 *
 * 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.assembler.ir;

import com.strobel.assembler.metadata.MetadataHelper;
import com.strobel.assembler.metadata.TypeReference;
import com.strobel.core.ArrayUtilities;
import com.strobel.core.CollectionUtilities;
import com.strobel.core.Comparer;
import com.strobel.core.HashUtilities;
import com.strobel.core.VerifyArgument;
import com.strobel.decompiler.DecompilerHelpers;
import com.strobel.decompiler.PlainTextOutput;
import com.strobel.util.ContractUtils;
import com.strobel.util.EmptyArrayCache;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * User: Mike Strobel Date: 1/6/13 Time: 4:09 PM
 */
public final class Frame {
    public final static FrameValue[] EMPTY_VALUES = EmptyArrayCache.fromElementType(FrameValue.class);

    public final static Frame NEW_EMPTY = new Frame(
        FrameType.New,
        EmptyArrayCache.fromElementType(FrameValue.class),
        EmptyArrayCache.fromElementType(FrameValue.class)
    );

    public final static Frame SAME = new Frame(
        FrameType.Same,
        EmptyArrayCache.fromElementType(FrameValue.class),
        EmptyArrayCache.fromElementType(FrameValue.class)
    );

    private final FrameType _frameType;
    private final List<FrameValue> _localValues;
    private final List<FrameValue> _stackValues;

    public Frame(final FrameType frameType, final FrameValue[] localValues, final FrameValue[] stackValues) {
        _frameType = VerifyArgument.notNull(frameType, "frameType");
        _localValues = ArrayUtilities.asUnmodifiableList(VerifyArgument.notNull(localValues, "localValues").clone());
        _stackValues = ArrayUtilities.asUnmodifiableList(VerifyArgument.notNull(stackValues, "stackValues").clone());
    }

    private Frame(final FrameType frameType, final List<FrameValue> localValues, final List<FrameValue> stackValues) {
        _frameType = frameType;
        _localValues = localValues;
        _stackValues = stackValues;
    }

    public final FrameType getFrameType() {
        return _frameType;
    }

    public final List<FrameValue> getLocalValues() {
        return _localValues;
    }

    public final List<FrameValue> getStackValues() {
        return _stackValues;
    }

    public final Frame withEmptyStack() {
        if (_frameType != FrameType.New) {
            throw new IllegalStateException("Can only call withEmptyStack() on New frames.");
        }

        return new Frame(_frameType, _localValues, Collections.<FrameValue>emptyList());
    }

    @Override
    public final boolean equals(final Object o) {
        if (this == o) {
            return true;
        }

        if (!(o instanceof Frame)) {
            return false;
        }

        final Frame frame = (Frame) o;

        return frame._frameType == _frameType &&
               CollectionUtilities.sequenceDeepEquals(frame._localValues, _localValues) &&
               CollectionUtilities.sequenceDeepEquals(frame._stackValues, _stackValues);
    }

    @Override
    public final int hashCode() {
        int result = _frameType.hashCode();

        for (int i = 0; i < _localValues.size(); i++) {
            result = HashUtilities.combineHashCodes(result, _localValues.get(i));
        }

        for (int i = 0; i < _stackValues.size(); i++) {
            result = HashUtilities.combineHashCodes(result, _stackValues.get(i));
        }

        return result;
    }

    @Override
    public final String toString() {
        final PlainTextOutput writer = new PlainTextOutput();
        DecompilerHelpers.writeFrame(writer, this);
        return writer.toString();
    }

    @SuppressWarnings("ConstantConditions")
    public static Frame computeDelta(final Frame previous, final Frame current) {
        VerifyArgument.notNull(previous, "previous");
        VerifyArgument.notNull(current, "current");

        final List<FrameValue> previousLocals = previous._localValues;
        final List<FrameValue> currentLocals = current._localValues;
        final List<FrameValue> currentStack = current._stackValues;

        final int previousLocalCount = previousLocals.size();
        final int currentLocalCount = currentLocals.size();
        final int currentStackSize = currentStack.size();

        int localCount = previousLocalCount;
        FrameType type = FrameType.Full;

        if (currentStackSize == 0) {
            switch (currentLocalCount - previousLocalCount) {
                case -3:
                case -2:
                case -1:
                    type = FrameType.Chop;
                    localCount = currentLocalCount;
                    break;

                case 0:
                    type = FrameType.Same;
                    break;

                case 1:
                case 2:
                case 3:
                    type = FrameType.Append;
                    break;
            }
        }
        else if (currentLocalCount == localCount && currentStackSize == 1) {
            type = FrameType.Same1;
        }

        if (type != FrameType.Full) {
            for (int i = 0; i < localCount; i++) {
                if (!currentLocals.get(i).equals(previousLocals.get(i))) {
                    type = FrameType.Full;
                    break;
                }
            }
        }

        switch (type) {
            case Append: {
                return new Frame(
                    type,
                    currentLocals.subList(previousLocalCount, currentLocalCount)
                                 .toArray(new FrameValue[currentLocalCount - previousLocalCount]),
                    EmptyArrayCache.fromElementType(FrameValue.class)
                );
            }

            case Chop: {
                return new Frame(
                    type,
                    previousLocals.subList(currentLocalCount, previousLocalCount)
                                  .toArray(new FrameValue[previousLocalCount - currentLocalCount]),
                    EmptyArrayCache.fromElementType(FrameValue.class)
                );
            }

            case Full: {
                return new Frame(
                    type,
                    currentLocals,
                    currentStack
                );
            }

            case Same: {
                return SAME;
            }

            case Same1: {
                return new Frame(
                    type,
                    EmptyArrayCache.fromElementType(FrameValue.class),
                    new FrameValue[] { currentStack.get(currentStackSize - 1) }
                );
            }

            default: {
                throw ContractUtils.unreachable();
            }
        }
    }

    @SuppressWarnings("ConstantConditions")
    public static Frame merge(final Frame input, final Frame output, final Frame next, final Map<Instruction, TypeReference> initializations) {
        VerifyArgument.notNull(input, "input");
        VerifyArgument.notNull(output, "output");
        VerifyArgument.notNull(next, "next");

        final List<FrameValue> inputLocals = input._localValues;
        final List<FrameValue> outputLocals = output._localValues;

        final int inputLocalCount = inputLocals.size();
        final int outputLocalCount = outputLocals.size();
        final int nextLocalCount = next._localValues.size();
        final int tempLocalCount = Math.max(nextLocalCount, inputLocalCount);

        final FrameValue[] nextLocals = next._localValues.toArray(new FrameValue[tempLocalCount]);

        FrameValue t;
        boolean changed = false;

        for (int i = 0; i < inputLocalCount; i++) {
            if (i < outputLocalCount) {
                t = outputLocals.get(i);
            }
            else {
                t = inputLocals.get(i);
            }

            if (initializations != null) {
                t = initialize(initializations, t);
            }

            changed |= merge(t, nextLocals, i);
        }

        final List<FrameValue> inputStack = input._stackValues;
        final List<FrameValue> outputStack = output._stackValues;

        final int inputStackSize = inputStack.size();
        final int outputStackSize = outputStack.size();
        final int nextStackSize = next._stackValues.size();

        final FrameValue[] nextStack = next._stackValues.toArray(new FrameValue[nextStackSize]);

        for (int i = 0, max = Math.min(nextStackSize, inputStackSize); i < max; i++) {
            t = inputStack.get(i);

            if (initializations != null) {
                t = initialize(initializations, t);
            }

            changed |= merge(t, nextStack, i);
        }

        for (int i = inputStackSize, max = Math.min(nextStackSize, outputStackSize); i < max; i++) {
            t = outputStack.get(i);

            if (initializations != null) {
                t = initialize(initializations, t);
            }

            changed |= merge(t, nextStack, i);
        }

        if (!changed) {
            return next;
        }

        int newLocalCount = nextLocalCount;
/*
        int newStackSize;

        //noinspection StatementWithEmptyBody
        for (newLocalCount = nextLocalCount;
             newLocalCount < inputLocalCount && nextLocals[newLocalCount] != null;
             newLocalCount++) {
        }
*/

/*
        //noinspection StatementWithEmptyBody
        for (newStackSize = nextStackSize;
             newStackSize < inputStackSize && nextStack[newStackSize] != null;
             newStackSize++) {
        }
*/

        return new Frame(
            FrameType.New,
            nextLocals.length == newLocalCount ? nextLocals
                                               : Arrays.copyOf(nextLocals, newLocalCount),
            nextStack
        );
    }

    private static FrameValue initialize(final Map<Instruction, TypeReference> initializations, FrameValue t) {
        if (t == null) {
            return t;
        }

        final Object parameter = t.getParameter();

        if (parameter instanceof Instruction) {
            final TypeReference initializedType = initializations.get(parameter);

            if (initializedType != null) {
                t = FrameValue.makeReference(initializedType);
            }
        }

        return t;
    }

    private static boolean merge(final FrameValue t, final FrameValue[] values, final int index) {
        final FrameValue u = values[index];

        if (Comparer.equals(t, u)) {
            return false;
        }

        if (t == FrameValue.EMPTY) {
            return false;
        }

        if (t == FrameValue.NULL) {
            if (u == FrameValue.NULL) {
                return false;
            }
        }

        if (u == FrameValue.EMPTY) {
            values[index] = t;
            return true;
        }

        final FrameValueType tType = t.getType();
        final FrameValueType uType = u.getType();

        final FrameValue v;

        if (uType == FrameValueType.Reference) {
            if (t == FrameValue.NULL) {
                return false;
            }

            if (tType == FrameValueType.Reference) {
                v = FrameValue.makeReference(
                    MetadataHelper.findCommonSuperType(
                        (TypeReference) t.getParameter(),
                        (TypeReference) u.getParameter()
                    )
                );
            }
            else {
                v = FrameValue.TOP;
            }
        }
        else if (u == FrameValue.NULL && tType == FrameValueType.Reference) {
            v = t;
        }
        else {
            v = FrameValue.TOP;
        }

        if (!u.equals(v)) {
            values[index] = v;
            return true;
        }

        return false;
    }
}
