/*
 * Copyright (c) [2016] [ <ether.camp> ]
 * This file is part of the ethereumJ library.
 *
 * The ethereumJ library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * The ethereumJ library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
 */
package org.ethereum.jsontestsuite.suite;

import org.ethereum.util.ByteUtil;
import org.ethereum.vm.DataWord;

import org.json.simple.JSONObject;

import org.spongycastle.util.encoders.Hex;

import java.math.BigInteger;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author Roman Mandeleil
 * @since 28.06.2014
 */
public class AccountState {

    byte[] address;
    byte[] balance;
    byte[] code;
    byte[] nonce;

    Map<DataWord, DataWord> storage = new HashMap<>();


    public AccountState(byte[] address, JSONObject accountState) {

        this.address = address;
        String balance = accountState.get("balance").toString();
        String code = (String) accountState.get("code");
        String nonce = accountState.get("nonce").toString();

        JSONObject store = (JSONObject) accountState.get("storage");

        this.balance = TestCase.toBigInt(balance).toByteArray();

        if (code != null && code.length() > 2)
            this.code = Hex.decode(code.substring(2));
        else
            this.code = ByteUtil.EMPTY_BYTE_ARRAY;

        this.nonce = TestCase.toBigInt(nonce).toByteArray();

        int size = store.keySet().size();
        Object[] keys = store.keySet().toArray();
        for (int i = 0; i < size; ++i) {

            String keyS = keys[i].toString();
            String valS = store.get(keys[i]).toString();

            byte[] key = Utils.parseData(keyS);
            byte[] value = Utils.parseData(valS);
            storage.put(DataWord.of(key), DataWord.of(value));
        }
    }

    public byte[] getAddress() {
        return address;
    }

    public byte[] getBalance() {
        return balance;
    }

    public BigInteger getBigIntegerBalance() {
        return new BigInteger(balance);
    }


    public byte[] getCode() {
        return code;
    }

    public byte[] getNonce() {
        return nonce;
    }

    public long getNonceLong() {
        return new BigInteger(nonce).longValue();
    }


    public Map<DataWord, DataWord> getStorage() {
        return storage;
    }

    public List<String> compareToReal(org.ethereum.core.AccountState state, ContractDetailsImpl details) {

        List<String> results = new ArrayList<>();

        BigInteger expectedBalance = new BigInteger(1, this.getBalance());
        if (!state.getBalance().equals(expectedBalance)) {
            String formattedString = String.format("Account: %s: has unexpected balance, expected balance: %s found balance: %s",
                    Hex.toHexString(this.address), expectedBalance.toString(), state.getBalance().toString());
            results.add(formattedString);
        }

        BigInteger expectedNonce = new BigInteger(1, this.getNonce());
        if (!state.getNonce().equals(expectedNonce)) {
            state.getNonce();
            this.getNonce();
            String formattedString = String.format("Account: %s: has unexpected nonce, expected nonce: %s found nonce: %s",
                    Hex.toHexString(this.address), expectedNonce.toString(), state.getNonce().toString());
            results.add(formattedString);
        }

        if (!Arrays.equals(details.getCode(), this.getCode())) {
            String formattedString = String.format("Account: %s: has unexpected nonce, expected nonce: %s found nonce: %s",
                    Hex.toHexString(this.address), Hex.toHexString(this.getCode()), Hex.toHexString(details.getCode()));
            results.add(formattedString);
        }


        // compare storage
        Set<DataWord> keys = details.getStorage().keySet();
        Set<DataWord> expectedKeys = this.getStorage().keySet();
        Set<DataWord> checked = new HashSet<>();

        for (DataWord key : keys) {

            DataWord value = details.getStorage().get(key);
            DataWord expectedValue = this.getStorage().get(key);
            if (expectedValue == null) {

                String formattedString = String.format("Account: %s: has unexpected storage data: %s = %s",
                        Hex.toHexString(this.address),
                        key.toString(),
                        value.toString());

                results.add(formattedString);

                continue;
            }

            if (!expectedValue.equals(value)) {

                String formattedString = String.format("Account: %s: has unexpected value, for key: %s , expectedValue: %s real value: %s",
                        Hex.toHexString(this.address), key.toString(),
                        expectedValue.toString(), value.toString());
                results.add(formattedString);
                continue;
            }

            checked.add(key);
        }

        for (DataWord key : expectedKeys) {
            if (!checked.contains(key)) {
                String formattedString = String.format("Account: %s: doesn't exist expected storage key: %s",
                        Hex.toHexString(this.address), key.toString());
                results.add(formattedString);
            }
        }

        return results;
    }

    @Override
    public String toString() {
        return "AccountState{" +
                "address=" + Hex.toHexString(address) +
                ", balance=" + Hex.toHexString(balance) +
                ", code=" + Hex.toHexString(code) +
                ", nonce=" + Hex.toHexString(nonce) +
                ", storage=" + storage +
                '}';
    }
}
