/*
 * Expression.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.decompiler.ast;

import com.strobel.annotations.NotNull;
import com.strobel.annotations.Nullable;
import com.strobel.assembler.metadata.FieldReference;
import com.strobel.assembler.metadata.MemberReference;
import com.strobel.assembler.metadata.MethodReference;
import com.strobel.assembler.metadata.TypeReference;
import com.strobel.collections.SmartList;
import com.strobel.componentmodel.Key;
import com.strobel.componentmodel.UserDataStore;
import com.strobel.componentmodel.UserDataStoreBase;
import com.strobel.core.ArrayUtilities;
import com.strobel.core.Comparer;
import com.strobel.core.StringUtilities;
import com.strobel.core.VerifyArgument;
import com.strobel.decompiler.DecompilerHelpers;
import com.strobel.decompiler.ITextOutput;
import com.strobel.decompiler.NameSyntax;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static com.strobel.decompiler.DecompilerHelpers.writeType;

public final class Expression extends Node implements Cloneable, UserDataStore {
    public static final Object ANY_OPERAND = new Object();
    
    /** a constant to indicate that no bytecode offset is known for an expression */
    public static final int MYSTERY_OFFSET = -34;

    private final SmartList<Expression> _arguments = new SmartList<>();

    private final SmartList<Range> _ranges = new SmartList<Range>() {
        @Override
        public boolean add(final Range range) {
            return !contains(range) && super.add(range);
        }

        @Override
        public void add(final int index, final Range element) {
            if (contains(element)) {
                return;
            }
            super.add(index, element);
        }
    };

    private AstCode _code;
    private Object _operand;
    
    /** the offset of 'this' Expression, as computed for its bytecode by the Java compiler */
    private int _offset;

    private TypeReference _expectedType;
    private TypeReference _inferredType;
    private UserDataStoreBase _userData;

    public Expression(final AstCode code, final Object operand, final int offset, final List<Expression> arguments) {
        _code = VerifyArgument.notNull(code, "code");
        _operand = VerifyArgument.notInstanceOf(Expression.class, operand, "operand");
        _offset = offset;
        
        if (arguments != null) {
            _arguments.addAll(arguments);
        }
    }

    public Expression(final AstCode code, final Object operand, final int offset, final Expression... arguments) {
        _code = VerifyArgument.notNull(code, "code");
        _operand = VerifyArgument.notInstanceOf(Expression.class, operand, "operand");
        _offset = offset;
        
        if (arguments != null) {
            Collections.addAll(_arguments, arguments);
        }
    }

    public final List<Expression> getArguments() {
        return _arguments;
    }

    public final AstCode getCode() {
        return _code;
    }

    public final void setCode(final AstCode code) {
        _code = code;
    }

    public final Object getOperand() {
        return _operand;
    }

    public final void setOperand(final Object operand) {
        _operand = operand;
    }

    /**
     * Returns the bytecode offset for 'this' expression, as computed by the Java compiler.
     */
    public final int getOffset() {
        return _offset;
    }
    
    public final TypeReference getExpectedType() {
        return _expectedType;
    }

    public final void setExpectedType(final TypeReference expectedType) {
        _expectedType = expectedType;
    }

    public final TypeReference getInferredType() {
        return _inferredType;
    }

    public final void setInferredType(final TypeReference inferredType) {
        _inferredType = inferredType;
    }

    public final boolean isBranch() {
        return _operand instanceof Label ||
               _operand instanceof Label[];
    }

    public final List<Label> getBranchTargets() {
        if (_operand instanceof Label) {
            return Collections.singletonList((Label) _operand);
        }

        if (_operand instanceof Label[]) {
            return ArrayUtilities.asUnmodifiableList((Label[]) _operand);
        }

        return Collections.emptyList();
    }

    public final List<Range> getRanges() {
        return _ranges;
    }

    @Override
    public final List<Node> getChildren() {
        final ArrayList<Node> childrenCopy = new ArrayList<>();

        childrenCopy.addAll(_arguments);

        if (_operand instanceof Lambda) {
            childrenCopy.add((Node) _operand);
        }

        return childrenCopy;
    }

    public final boolean containsReferenceTo(final Variable variable) {
        if (_operand == variable) {
            return true;
        }

        for (int i = 0; i < _arguments.size(); i++) {
            if (_arguments.get(i).containsReferenceTo(variable)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public final void writeTo(final ITextOutput output) {
        final AstCode code = _code;
        final Object operand = _operand;
        final TypeReference inferredType = _inferredType;
        final TypeReference expectedType = _expectedType;

        if (operand instanceof Variable /*&&
            ((Variable) operand).isGenerated()*/) {

            if (AstCodeHelpers.isLocalStore(code)) {
                output.write(((Variable) operand).getName());
                output.write(" = ");
                getArguments().get(0).writeTo(output);
                return;
            }

            if (AstCodeHelpers.isLocalLoad(code)) {
                output.write(((Variable) operand).getName());

                if (inferredType != null) {
                    output.write(':');
                    writeType(output, inferredType, NameSyntax.SHORT_TYPE_NAME);

                    if (expectedType != null &&
                        !Comparer.equals(expectedType.getInternalName(), inferredType.getInternalName())) {

                        output.write("[expected:");
                        writeType(output, expectedType, NameSyntax.SHORT_TYPE_NAME);
                        output.write(']');
                    }
                }

                return;
            }
        }

        output.writeReference(code.name().toLowerCase(), code);

        if (inferredType != null) {
            output.write(':');
            writeType(output, inferredType, NameSyntax.SHORT_TYPE_NAME);

            if (expectedType != null &&
                !Comparer.equals(expectedType.getInternalName(), inferredType.getInternalName())) {

                output.write("[expected:");
                writeType(output, expectedType, NameSyntax.SHORT_TYPE_NAME);
                output.write(']');
            }
        }
        else if (expectedType != null) {
            output.write("[expected:");
            writeType(output, expectedType, NameSyntax.SHORT_TYPE_NAME);
            output.write(']');
        }

        output.write('(');

        boolean first = true;

        if (operand != null) {
            if (operand instanceof Label) {
                output.writeReference(((Label) operand).getName(), operand);
            }
            else if (operand instanceof Label[]) {
                final Label[] labels = (Label[]) operand;

                for (int i = 0; i < (labels).length; i++) {
                    if (i != 0) {
                        output.write(", ");
                    }

                    output.writeReference(labels[i].getName(), labels[i]);
                }
            }
            else if (operand instanceof MethodReference ||
                     operand instanceof FieldReference) {

                final MemberReference member = (MemberReference) operand;
                final TypeReference declaringType = member.getDeclaringType();

                if (declaringType != null) {
                    writeType(output, declaringType, NameSyntax.SHORT_TYPE_NAME);
                    output.write("::");
                }

                output.writeReference(member.getName(), member);
            }
            else if (operand instanceof Node) {
                ((Node) operand).writeTo(output);
            }
            else {
                DecompilerHelpers.writeOperand(output, operand);
            }

            first = false;
        }

        for (final Expression argument : getArguments()) {
            if (!first) {
                output.write(", ");
            }

            argument.writeTo(output);
            first = false;
        }

        output.write(')');
    }

    @SuppressWarnings("CloneDoesntCallSuperClone")
    public final Expression clone() {
        final Expression clone = new Expression(_code, _operand, _offset);

        clone._code = _code;
        clone._expectedType = _expectedType;
        clone._inferredType = _inferredType;
        clone._operand = _operand;
        clone._userData = _userData != null ? _userData.clone() : null;
        clone._offset = _offset;

        for (final Expression argument : _arguments) {
            clone._arguments.add(argument.clone());
        }

        return clone;
    }

    public boolean isEquivalentTo(final Expression e) {
        if (e == null || _code != e._code) {
            return false;
        }

        if (_operand instanceof FieldReference) {
            if (!(e._operand instanceof FieldReference)) {
                return false;
            }

            final FieldReference f1 = (FieldReference) _operand;
            final FieldReference f2 = (FieldReference) e._operand;

            if (!StringUtilities.equals(f1.getFullName(), f2.getFullName())) {
                return false;
            }
        }
        else if (_operand instanceof MethodReference) {
            if (!(e._operand instanceof MethodReference)) {
                return false;
            }

            final MethodReference f1 = (MethodReference) _operand;
            final MethodReference f2 = (MethodReference) e._operand;

            if (!StringUtilities.equals(f1.getFullName(), f2.getFullName()) ||
                !StringUtilities.equals(f1.getErasedSignature(), f2.getErasedSignature())) {

                return false;
            }
        }
        else if (!Comparer.equals(e._operand, _operand)) {
            return false;
        }

        if (_arguments.size() != e._arguments.size()) {
            return false;
        }

        for (int i = 0, n = _arguments.size(); i < n; i++) {
            final Expression a1 = _arguments.get(i);
            final Expression a2 = e._arguments.get(i);

            if (!a1.isEquivalentTo(a2)) {
                return false;
            }
        }

        return true;
    }

    @Override
    public <T> T getUserData(@NotNull final Key<T> key) {
        if (_userData == null) {
            return null;
        }
        return _userData.getUserData(key);
    }

    @Override
    public <T> void putUserData(@NotNull final Key<T> key, @Nullable final T value) {
        if (_userData == null) {
            _userData = new UserDataStoreBase();
        }
        _userData.putUserData(key, value);
    }

    @Override
    public <T> T putUserDataIfAbsent(@NotNull final Key<T> key, @Nullable final T value) {
        if (_userData == null) {
            _userData = new UserDataStoreBase();
        }
        return _userData.putUserDataIfAbsent(key, value);
    }

    @Override
    public <T> boolean replace(@NotNull final Key<T> key, @Nullable final T oldValue, @Nullable final T newValue) {
        if (_userData == null) {
            _userData = new UserDataStoreBase();
        }
        return _userData.replace(key, oldValue, newValue);
    }
}
