/*
 * 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.vm;

import org.ethereum.vm.program.Memory;
import org.junit.Test;
import org.spongycastle.util.encoders.Hex;

import java.util.Arrays;

import static java.lang.Math.ceil;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class MemoryTest {

    private static final int WORD_SIZE = 32;
    private static final int CHUNK_SIZE = 1024;

    @Test
    public void testExtend() {
        checkMemoryExtend(0);
        checkMemoryExtend(1);
        checkMemoryExtend(WORD_SIZE);
        checkMemoryExtend(WORD_SIZE * 2);
        checkMemoryExtend(CHUNK_SIZE - 1);
        checkMemoryExtend(CHUNK_SIZE);
        checkMemoryExtend(CHUNK_SIZE + 1);
        checkMemoryExtend(2000);
    }

    private static void checkMemoryExtend(int dataSize) {
        Memory memory = new Memory();
        memory.extend(0, dataSize);
        assertEquals(calcSize(dataSize, CHUNK_SIZE), memory.internalSize());
        assertEquals(calcSize(dataSize, WORD_SIZE), memory.size());
    }

    private static int calcSize(int dataSize, int chunkSize) {
        return (int) ceil((double) dataSize / chunkSize) * chunkSize;
    }

    @Test
    public void memorySave_1() {

        Memory memoryBuffer = new Memory();
        byte[] data = {1, 1, 1, 1};

        memoryBuffer.write(0, data, data.length, false);

        assertTrue(1 == memoryBuffer.getChunks().size());

        byte[] chunk = memoryBuffer.getChunks().get(0);
        assertTrue(chunk[0] == 1);
        assertTrue(chunk[1] == 1);
        assertTrue(chunk[2] == 1);
        assertTrue(chunk[3] == 1);
        assertTrue(chunk[4] == 0);

        assertTrue(memoryBuffer.size() == 32);
    }

    @Test
    public void memorySave_2() {

        Memory memoryBuffer = new Memory();
        byte[] data = Hex.decode("0101010101010101010101010101010101010101010101010101010101010101");

        memoryBuffer.write(0, data, data.length, false);

        assertTrue(1 == memoryBuffer.getChunks().size());

        byte[] chunk = memoryBuffer.getChunks().get(0);
        assertTrue(chunk[0] == 1);
        assertTrue(chunk[1] == 1);

        assertTrue(chunk[30] == 1);
        assertTrue(chunk[31] == 1);
        assertTrue(chunk[32] == 0);

        assertTrue(memoryBuffer.size() == 32);
    }

    @Test
    public void memorySave_3() {

        Memory memoryBuffer = new Memory();
        byte[] data = Hex.decode("010101010101010101010101010101010101010101010101010101010101010101");

        memoryBuffer.write(0, data, data.length, false);

        assertTrue(1 == memoryBuffer.getChunks().size());

        byte[] chunk = memoryBuffer.getChunks().get(0);
        assertTrue(chunk[0] == 1);
        assertTrue(chunk[1] == 1);

        assertTrue(chunk[30] == 1);
        assertTrue(chunk[31] == 1);
        assertTrue(chunk[32] == 1);
        assertTrue(chunk[33] == 0);

        assertTrue(memoryBuffer.size() == 64);
    }

    @Test
    public void memorySave_4() {

        Memory memoryBuffer = new Memory();
        byte[] data = new byte[1024];
        Arrays.fill(data, (byte) 1);

        memoryBuffer.write(0, data, data.length, false);

        assertTrue(1 == memoryBuffer.getChunks().size());

        byte[] chunk = memoryBuffer.getChunks().get(0);
        assertTrue(chunk[0] == 1);
        assertTrue(chunk[1] == 1);

        assertTrue(chunk[1022] == 1);
        assertTrue(chunk[1023] == 1);

        assertTrue(memoryBuffer.size() == 1024);
    }

    @Test
    public void memorySave_5() {

        Memory memoryBuffer = new Memory();

        byte[] data = new byte[1025];
        Arrays.fill(data, (byte) 1);

        memoryBuffer.write(0, data, data.length, false);

        assertTrue(2 == memoryBuffer.getChunks().size());

        byte[] chunk1 = memoryBuffer.getChunks().get(0);
        assertTrue(chunk1[0] == 1);
        assertTrue(chunk1[1] == 1);

        assertTrue(chunk1[1022] == 1);
        assertTrue(chunk1[1023] == 1);

        byte[] chunk2 = memoryBuffer.getChunks().get(1);
        assertTrue(chunk2[0] == 1);
        assertTrue(chunk2[1] == 0);

        assertTrue(memoryBuffer.size() == 1056);
    }

    @Test
    public void memorySave_6() {

        Memory memoryBuffer = new Memory();

        byte[] data1 = new byte[1024];
        Arrays.fill(data1, (byte) 1);

        byte[] data2 = new byte[1024];
        Arrays.fill(data2, (byte) 2);

        memoryBuffer.write(0, data1, data1.length, false);
        memoryBuffer.write(1024, data2, data2.length, false);

        assertTrue(2 == memoryBuffer.getChunks().size());

        byte[] chunk1 = memoryBuffer.getChunks().get(0);
        assertTrue(chunk1[0] == 1);
        assertTrue(chunk1[1] == 1);

        assertTrue(chunk1[1022] == 1);
        assertTrue(chunk1[1023] == 1);

        byte[] chunk2 = memoryBuffer.getChunks().get(1);
        assertTrue(chunk2[0] == 2);
        assertTrue(chunk2[1] == 2);

        assertTrue(chunk2[1022] == 2);
        assertTrue(chunk2[1023] == 2);

        assertTrue(memoryBuffer.size() == 2048);
    }

    @Test
    public void memorySave_7() {

        Memory memoryBuffer = new Memory();

        byte[] data1 = new byte[1024];
        Arrays.fill(data1, (byte) 1);

        byte[] data2 = new byte[1024];
        Arrays.fill(data2, (byte) 2);

        byte[] data3 = new byte[1];
        Arrays.fill(data3, (byte) 3);

        memoryBuffer.write(0, data1, data1.length, false);
        memoryBuffer.write(1024, data2, data2.length, false);
        memoryBuffer.write(2048, data3, data3.length, false);

        assertTrue(3 == memoryBuffer.getChunks().size());

        byte[] chunk1 = memoryBuffer.getChunks().get(0);
        assertTrue(chunk1[0] == 1);
        assertTrue(chunk1[1] == 1);

        assertTrue(chunk1[1022] == 1);
        assertTrue(chunk1[1023] == 1);

        byte[] chunk2 = memoryBuffer.getChunks().get(1);
        assertTrue(chunk2[0] == 2);
        assertTrue(chunk2[1] == 2);

        assertTrue(chunk2[1022] == 2);
        assertTrue(chunk2[1023] == 2);

        byte[] chunk3 = memoryBuffer.getChunks().get(2);
        assertTrue(chunk3[0] == 3);

        assertTrue(memoryBuffer.size() == 2080);
    }

    @Test
    public void memorySave_8() {

        Memory memoryBuffer = new Memory();

        byte[] data1 = new byte[128];
        Arrays.fill(data1, (byte) 1);

        memoryBuffer.extendAndWrite(0, 256, data1);

        int ones = 0; int zeroes = 0;
        for (int i = 0; i < memoryBuffer.size(); ++i){
            if (memoryBuffer.readByte(i) == 1) ++ones;
            if (memoryBuffer.readByte(i) == 0) ++zeroes;
        }

        assertTrue(ones == zeroes);
        assertTrue(256 == memoryBuffer.size());
    }



    @Test
    public void memoryLoad_1() {

        Memory memoryBuffer = new Memory();
        DataWord value = memoryBuffer.readWord(100);
        assertTrue(value.intValue() == 0);
        assertTrue(memoryBuffer.getChunks().size() == 1);
        assertTrue(memoryBuffer.size() == 32 * 5);
    }

    @Test
    public void memoryLoad_2() {

        Memory memoryBuffer = new Memory();
        DataWord value = memoryBuffer.readWord(2015);
        assertTrue(value.intValue() == 0);
        assertTrue(memoryBuffer.getChunks().size() == 2);
        assertTrue(memoryBuffer.size() == 2048);
    }

    @Test
    public void memoryLoad_3() {

        Memory memoryBuffer = new Memory();
        DataWord value = memoryBuffer.readWord(2016);
        assertTrue(value.intValue() == 0);
        assertTrue(memoryBuffer.getChunks().size() == 2);
        assertTrue(memoryBuffer.size() == 2048);
    }

    @Test
    public void memoryLoad_4() {

        Memory memoryBuffer = new Memory();
        DataWord value = memoryBuffer.readWord(2017);
        assertTrue(value.intValue() == 0);
        assertTrue(memoryBuffer.getChunks().size() == 3);
        assertTrue(memoryBuffer.size() == 2080);
    }

    @Test
    public void memoryLoad_5() {

        Memory memoryBuffer = new Memory();

        byte[] data1 = new byte[1024];
        Arrays.fill(data1, (byte) 1);

        byte[] data2 = new byte[1024];
        Arrays.fill(data2, (byte) 2);

        memoryBuffer.write(0, data1, data1.length, false);
        memoryBuffer.write(1024, data2, data2.length, false);

        assertTrue(memoryBuffer.getChunks().size() == 2);
        assertTrue(memoryBuffer.size() == 2048);

        DataWord val1 = memoryBuffer.readWord(0x3df);
        DataWord val2 = memoryBuffer.readWord(0x3e0);
        DataWord val3 = memoryBuffer.readWord(0x3e1);

        assertArrayEquals(
                Hex.decode("0101010101010101010101010101010101010101010101010101010101010101"),
                val1.getData());

        assertArrayEquals(
                Hex.decode("0101010101010101010101010101010101010101010101010101010101010101"),
                val2.getData());

        assertArrayEquals(
                Hex.decode("0101010101010101010101010101010101010101010101010101010101010102"),
                val3.getData());
        assertTrue(memoryBuffer.size() == 2048);
    }


    @Test
    public void memoryChunk_1(){
        Memory memoryBuffer = new Memory();

        byte[] data1 = new byte[32];
        Arrays.fill(data1, (byte) 1);

        byte[] data2 = new byte[32];
        Arrays.fill(data2, (byte) 2);

        memoryBuffer.write(0, data1, data1.length, false);
        memoryBuffer.write(32, data2, data2.length, false);

        byte[] data = memoryBuffer.read(0, 64);

        assertArrayEquals(
                Hex.decode("0101010101010101010101010101010101010101010101010101010101010101" +
                        "0202020202020202020202020202020202020202020202020202020202020202"),
                data
        );

        assertEquals(64, memoryBuffer.size());
    }


    @Test
    public void memoryChunk_2(){
        Memory memoryBuffer = new Memory();

        byte[] data1 = new byte[32];
        Arrays.fill(data1, (byte) 1);

        memoryBuffer.write(0, data1, data1.length, false);
        assertTrue(32 == memoryBuffer.size());

        byte[] data = memoryBuffer.read(0, 64);

        assertArrayEquals(
                Hex.decode("0101010101010101010101010101010101010101010101010101010101010101" +
                        "0000000000000000000000000000000000000000000000000000000000000000"),
                data
        );

        assertEquals(64, memoryBuffer.size());
    }

    @Test
    public void memoryChunk_3(){

        Memory memoryBuffer = new Memory();

        byte[] data1 = new byte[1024];
        Arrays.fill(data1, (byte) 1);

        byte[] data2 = new byte[1024];
        Arrays.fill(data2, (byte) 2);

        memoryBuffer.write(0, data1, data1.length, false);
        memoryBuffer.write(1024, data2, data2.length, false);

        byte[] data = memoryBuffer.read(0, 2048);

        int ones = 0; int twos = 0;
        for (int i = 0; i < data.length; ++i){
            if (data[i] == 1) ++ones;
            if (data[i] == 2) ++twos;
        }

        assertTrue(ones == twos);
        assertTrue(2048 == memoryBuffer.size());
    }

    @Test
    public void memoryChunk_4(){

        Memory memoryBuffer = new Memory();

        byte[] data1 = new byte[1024];
        Arrays.fill(data1, (byte) 1);

        byte[] data2 = new byte[1024];
        Arrays.fill(data2, (byte) 2);

        memoryBuffer.write(0, data1, data1.length, false);
        memoryBuffer.write(1024, data2, data2.length, false);

        byte[] data = memoryBuffer.read(0, 2049);

        int ones = 0; int twos = 0; int zero = 0;
        for (int i = 0; i < data.length; ++i){
            if (data[i] == 1) ++ones;
            if (data[i] == 2) ++twos;
            if (data[i] == 0) ++zero;
        }

        assertTrue(zero == 1);
        assertTrue(ones == twos);
        assertTrue(2080 == memoryBuffer.size());
    }


    @Test
    public void memoryWriteLimited_1(){

        Memory memoryBuffer = new Memory();
        memoryBuffer.extend(0, 3072);

        byte[] data1 = new byte[6272];
        Arrays.fill(data1, (byte) 1);

        memoryBuffer.write(2720, data1, data1.length, true);

        byte lastZero = memoryBuffer.readByte(2719);
        byte firstOne = memoryBuffer.readByte(2721);

        assertTrue(memoryBuffer.size() == 3072);
        assertTrue(lastZero == 0);
        assertTrue(firstOne == 1);

        byte[] data = memoryBuffer.read(2720, 352);

        int ones = 0; int zero = 0;
        for (int i = 0; i < data.length; ++i){
            if (data[i] == 1) ++ones;
            if (data[i] == 0) ++zero;
        }

        assertTrue(ones == data.length);
        assertTrue(zero == 0);
    }

    @Test
    public void memoryWriteLimited_2(){

        Memory memoryBuffer = new Memory();
        memoryBuffer.extend(0, 3072);

        byte[] data1 = new byte[6272];
        Arrays.fill(data1, (byte) 1);

        memoryBuffer.write(2720, data1, 300, true);

        byte lastZero = memoryBuffer.readByte(2719);
        byte firstOne = memoryBuffer.readByte(2721);

        assertTrue(memoryBuffer.size() == 3072);
        assertTrue(lastZero == 0);
        assertTrue(firstOne == 1);

        byte[] data = memoryBuffer.read(2720, 352);

        int ones = 0; int zero = 0;
        for (int i = 0; i < data.length; ++i){
            if (data[i] == 1) ++ones;
            if (data[i] == 0) ++zero;
        }

        assertTrue(ones == 300);
        assertTrue(zero == 52);
    }

    @Test
    public void memoryWriteLimited_3(){

        Memory memoryBuffer = new Memory();
        memoryBuffer.extend(0, 128);

        byte[] data1 = new byte[20];
        Arrays.fill(data1, (byte) 1);

        memoryBuffer.write(10, data1, 40, true);

        byte lastZero = memoryBuffer.readByte(9);
        byte firstOne = memoryBuffer.readByte(10);

        assertTrue(memoryBuffer.size() == 128);
        assertTrue(lastZero == 0);
        assertTrue(firstOne == 1);

        byte[] data = memoryBuffer.read(10, 30);

        int ones = 0; int zero = 0;
        for (int i = 0; i < data.length; ++i){
            if (data[i] == 1) ++ones;
            if (data[i] == 0) ++zero;
        }

        assertTrue(ones == 20);
        assertTrue(zero == 10);
    }



}