/*
 * SOLTIX - Scalable automated framework for testing Solidity compilers.
 *
 * Author: Nils Weller <[email protected]>
 *
 * Copyright (C) 2018 Secure, Reliable, and Intelligent Systems Lab, ETH Zurich
 *
 * 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 soltix.interpretation.values;

import soltix.Configuration;
import soltix.ast.ASTElementaryTypeName;
import soltix.ast.ASTNode;
import soltix.ast.ASTVerbatimText;
import soltix.interpretation.Type;
import soltix.interpretation.TypeContainer;
import soltix.util.Hash;

import java.math.BigInteger;

/**
 * Class to reprsent bytes* values
 */
public class BytesValue extends Value implements IByteOperations {
    //private String value;
    private byte[] value;
    private BigInteger integerValue; // for relational operators
    private ASTElementaryTypeName type;
    private boolean isBytesType = false;

    // Constructor for "bytes<count>" types
    public BytesValue(ASTNode type, /*String*/ byte[] value) throws Exception {
        //super(type);
        this.type = (ASTElementaryTypeName)type;
        if (this.type.getElementaryType() != ASTElementaryTypeName.ElementaryType.ELEMENTARY_TYPE_BYTE) {
            throw new Exception("BytesValue constructed with incompatible type");
        }
        this.value = value;
        integerValue = new BigInteger(1, value); // for relational operators
        isBytesType = this.type.getBytes() == 0;
        if (!isBytesType && this.type.getBytes() != value.length) {
            throw new Exception("Inconsistent BytesValue type bytes " + this.type.getBytes() + " vs value bytes " + value.length);
        }
    }


    public int getBytesCount() throws Exception {
        return type.getBytes();
    }

    @Override
    public ASTNode getType() { return type; }

    @Override
    public ASTNode toASTNode(boolean forJavaScript) throws Exception {
        String valueString;

        // The byte[0-9]* types can be output as hexadecimal numbers. However, the "bytes" type seems to require
        // a hex string in Solidity (which also may not be cast to the result type bytes) - but not in JavaScript,
        // so it can apparently be treated like the other byte* types there!
        if (isBytesType && !forJavaScript) {
            // bytes type
            valueString = "hex\"";
            for (byte b : value) {
                valueString += String.format("%02X", b);
            }
            valueString += "\"";
        } else {
            /*valueString = "0x";
            for (byte b : value) {
                valueString += String.format("%02X", b);
            }*/
            valueString = toHexConstantString();

            if (value.length == 20 && Configuration.languageVersionMinor >= 5) {
                // Ensure that an address-like bytes constant also passes the checksum test, or else
                // a 0.5+ compiler will error out
                valueString = Hash.toChecksumAddress(valueString);
            }

            if (forJavaScript) {
                // Enclose value between quotes to prevent ABI errors caused by JavaScript hex constant limits
                valueString = "\"" + valueString + "\"";
            } else {
                valueString = type.toSolidityCode() + "(" + valueString + ")";
            }
        }
        return new ASTVerbatimText(0, valueString);
    }

    public String toHexConstantString() {
        String result = "0x";
        for (byte b : value) {
            result += String.format("%02X", b);
        }
        return result;
    }

    @Override
    public Object toJSONRepresentation() throws Exception {
        // Result: hex string with 0x prefix and 2 characters per digit
        StringBuilder result = new StringBuilder();
        result.append("0x");
        for (int i = 0; i < getBytesCount(); ++i) {
            result.append(String.format("%02x", value[i]));
        }
        return result.toString();
    }

    public BoolValue isEqualTo(BytesValue other) throws Exception {
        for (int i = 0; i < getBytesCount(); ++i) {
            if (i == other.getBytesCount()) {
                // End of other value reached - the remainder of this value must be 0 bytes
                for (int j = i; j < getBytesCount(); ++j) {
                    if (value[j] != 0) {
                        return ValueContainer.getBoolValue(false);
                    }
                }
                break;
            } else {
                // Both sides have comparable values
                if (value[i] != other.value[i]) {
                    return ValueContainer.getBoolValue(false);
                }
            }
        }
        // If the other value contains more bytes, those must be 0
        for (int i = getBytesCount(); i < other.getBytesCount(); ++i) {
            if (other.value[i] != 0) {
                return ValueContainer.getBoolValue(false);
            }
        }
        return ValueContainer.getBoolValue(true);
    }

    public BoolValue isNotEqualTo(BytesValue other) throws Exception {
        return ValueContainer.getBoolValue(!isEqualTo(other).getValue());
    }



    public BoolValue isSmallerThan(BytesValue value) throws Exception {
        return ValueContainer.getBoolValue(integerValue.compareTo(value.integerValue) < 0);
    }

    public BoolValue isSmallerThanOrEqualTo(BytesValue value) throws Exception {
        return ValueContainer.getBoolValue(integerValue.compareTo(value.integerValue) <= 0);
    }

    public BoolValue isGreaterThan(BytesValue value) throws Exception {
        return ValueContainer.getBoolValue(integerValue.compareTo(value.integerValue) > 0);
    }

    public BoolValue isGreaterThanOrEqualTo(BytesValue value) throws Exception {
        return ValueContainer.getBoolValue(integerValue.compareTo(value.integerValue) >= 0);
    }

    public BytesValue negateBitwise() throws Exception {
        byte[] newValue = new byte[type.getBytes()];
        for (int i = 0; i < newValue.length; ++i) {
            newValue[i] = (byte)~value[i];
        }
        return new BytesValue(type, newValue);
    }

    public BytesValue bitwiseOr(Value otherValue) throws Exception {
        BytesValue otherBytesValue = (BytesValue)otherValue;
        assert getBytesCount() == otherBytesValue.getBytesCount();

        byte[] newValue = new byte[type.getBytes()];
        for (int i = 0; i < getBytesCount(); ++i) {
            newValue[i] = (byte)(value[i] | otherBytesValue.value[i]);
        }
        return new BytesValue(type, newValue);
    }

    public BytesValue bitwiseAnd(Value otherValue) throws Exception {
        BytesValue otherBytesValue = (BytesValue)otherValue;
        assert getBytesCount() == otherBytesValue.getBytesCount();

        byte[] newValue = new byte[type.getBytes()];
        for (int i = 0; i < getBytesCount(); ++i) {
            newValue[i] = (byte)(value[i] & otherBytesValue.value[i]);
        }
        return new BytesValue(type, newValue);
    }

    public BytesValue bitwiseXor(Value otherValue) throws Exception {
        BytesValue otherBytesValue = (BytesValue)otherValue;
        assert getBytesCount() == otherBytesValue.getBytesCount();

        byte[] newValue = new byte[type.getBytes()];
        for (int i = 0; i < getBytesCount(); ++i) {
            newValue[i] = (byte)(value[i] ^ otherBytesValue.value[i]);
        }
        return new BytesValue(type, newValue);
    }

    private BigInteger ffMask = new BigInteger("ff", 16);
    protected void bigIntegerValueToBytesArray(byte[] array, BigInteger value) {
        for (int i = array.length - 1; i >= 0; --i) {
            array[i] = (byte)value.and(ffMask).intValue();
            value = value.shiftRight(8);
        }
    }

    public BytesValue bitwiseShiftLeft(Value value) throws Exception {
        IntegerValue shiftBits = (IntegerValue)value;
        byte[] result = new byte[type.getBytes()];
        if (shiftBits.isGreaterThanOrEqualTo(ValueContainer.getBigIntegerValue(shiftBits.getType(), BigInteger.valueOf(256))).getValue()) {
            // All bits are shifted out of range
            for (int i = 0; i < result.length; ++i) {
                result[i] = 0;
            }
        } else if (shiftBits.isSmallerThan(ValueContainer.getBigIntegerValue(shiftBits.getType(), BigInteger.ZERO)).getValue()) {
            throw new Exception("BytesValue.bitwiseShiftLeft with invalid negative count operand");
        } else {
            // For simplicity use BigInteger for now
            BigInteger shiftedValue = integerValue.shiftLeft(shiftBits.toInt());
            bigIntegerValueToBytesArray(result, shiftedValue);
        }

        return new BytesValue(type, result);
    }

    public BytesValue bitwiseShiftRight(Value value) throws Exception {
        IntegerValue shiftBits = (IntegerValue)value;
        byte[] result = new byte[type.getBytes()];
        if (shiftBits.isGreaterThanOrEqualTo(ValueContainer.getBigIntegerValue(shiftBits.getType(), BigInteger.valueOf(256))).getValue()) {
            // All bits are shifted out of range
            for (int i = 0; i < result.length; ++i) {
                result[i] = 0;
            }
        } else if (shiftBits.isSmallerThan(ValueContainer.getBigIntegerValue(shiftBits.getType(), BigInteger.ZERO)).getValue()) {
            throw new Exception("BytesValue.bitwiseShiftLeft with invalid negative count operand");
        } else {
            // For simplicity use BigInteger for now
            BigInteger shiftedValue = integerValue.shiftRight(shiftBits.toInt());
            bigIntegerValueToBytesArray(result, shiftedValue);
        }

        return new BytesValue(type, result);
    }

    public BytesValue indexAccess(Value index) throws Exception {
        IntegerValue integerIndex = (IntegerValue)index;
        IntegerValue valueLength = length();
        if (integerIndex.isGreaterThanOrEqualTo(valueLength).getValue()
                || integerIndex.isSmallerThan(ValueContainer.getBigIntegerValue(index.getType(), BigInteger.ZERO)).getValue()) {
            throw new Exception("Invalid bytes index access out of bounds for type " + type.toSolidityCode() + ": " + index.toString());
        }

        byte selectedValue = value[integerIndex.toInt()];
        return new BytesValue(TypeContainer.getByteType(1), new byte[]{selectedValue});
    }

    public IntegerValue length() throws Exception {
        // Experiments indicate that the length is of type uint8 for fixed-size arrays, and uint256 for the
        // variable-length "bytes" type
        int resultBits;
        if (type.getBytes() == 0) {
            resultBits = 256;
        } else {
            resultBits = 8;
        }
        BigInteger valueLength = BigInteger.valueOf(value.length);
        return ValueContainer.getBigIntegerValue(TypeContainer.getIntegerType(false, resultBits), valueLength);
    }

    public BytesValue convertToBytesType(ASTElementaryTypeName newType) throws Exception {
        byte[] result = new byte[newType.getBytes()];

        if (newType.getBytes() == 0) {
            // Converting to variable-length "bytes" does not appear to be allowed
            throw new Exception("BytesValue.convertToBytesType called for variable-length bytes type");
        }

        if (newType.getBytes() == type.getBytes()) {
            // No-op
            return this;
        } else if (newType.getBytes() < type.getBytes()) {
            // Converting to a smaller type takes the most significant bytes (= at lowest indices):
            //
            // bytes4 v = 0xaabbccdd;
            // ==> bytes2(v) = 0xaabb = v[0],v[1]
            for (int i = 0; i < newType.getBytes(); ++i) {
                result[i] = value[i];
            }
        } else if (newType.getBytes() > type.getBytes()) {
            // Converting to a larger type takes the old value as most significant bytes (= at lowest indices) and
            // pads with least significant zero bytes up to the desired size:
            //
            // bytes2 v = 0xaabb;
            // ==> bytes4(v) = 0xaabb0000;
            for (int i = 0; i < type.getBytes(); ++i) {
                result[i] = value[i];
            }
            for (int i = type.getBytes(); i < newType.getBytes(); ++i) {
                result[i] = 0;
            }
        }
        return new BytesValue(newType, result);
    }

    public IntegerValue convertToIntegerType(ASTElementaryTypeName newType) throws Exception {
        // First construct unsigned integer type of same size and with the value
        String s = "";
        for (int i = 0; i < value.length; ++i) {
            s += String.format("%02x", value[i]);
        }
        BigInteger unsgnedInteger = new BigInteger(s, 16);

        ASTElementaryTypeName unsignedType = TypeContainer.getIntegerType(false, type.getBytes() * 8);
        IntegerValue unsignedIntegerValue = ValueContainer.getBigIntegerValue(unsignedType, unsgnedInteger);

        if (Type.isSameType(null, unsignedType, newType)) {
            return unsignedIntegerValue;
        } else {
            return unsignedIntegerValue.convertToIntegerType(newType);
        }
    }
}