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

import org.ethereum.vm.DataWord;
import org.ethereum.vm.program.listener.ProgramListener;
import org.ethereum.vm.program.listener.ProgramListenerAware;

import java.util.LinkedList;
import java.util.List;

import static java.lang.Math.ceil;
import static java.lang.Math.min;
import static java.lang.String.format;
import static org.ethereum.util.ByteUtil.EMPTY_BYTE_ARRAY;
import static org.ethereum.util.ByteUtil.oneByteToHexString;

public class Memory implements ProgramListenerAware {

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

    private List<byte[]> chunks = new LinkedList<>();
    private int softSize;
    private ProgramListener programListener;

    @Override
    public void setProgramListener(ProgramListener traceListener) {
        this.programListener = traceListener;
    }

    public byte[] read(int address, int size) {
        if (size <= 0) return EMPTY_BYTE_ARRAY;

        extend(address, size);
        byte[] data = new byte[size];

        int chunkIndex = address / CHUNK_SIZE;
        int chunkOffset = address % CHUNK_SIZE;

        int toGrab = data.length;
        int start = 0;

        while (toGrab > 0) {
            int copied = grabMax(chunkIndex, chunkOffset, toGrab, data, start);

            // read next chunk from the start
            ++chunkIndex;
            chunkOffset = 0;

            // mark remind
            toGrab -= copied;
            start += copied;
        }

        return data;
    }

    public void write(int address, byte[] data, int dataSize, boolean limited) {
        if (dataSize <= 0) return;

        if (data.length < dataSize)
            dataSize = data.length;

        if (!limited)
            extend(address, dataSize);

        int chunkIndex = address / CHUNK_SIZE;
        int chunkOffset = address % CHUNK_SIZE;

        int toCapture = 0;
        if (limited)
            toCapture = (address + dataSize > softSize) ? softSize - address : dataSize;
        else
            toCapture = dataSize;

        int start = 0;
        while (toCapture > 0) {
            int captured = captureMax(chunkIndex, chunkOffset, toCapture, data, start);

            // capture next chunk
            ++chunkIndex;
            chunkOffset = 0;

            // mark remind
            toCapture -= captured;
            start += captured;
        }

        if (programListener != null) programListener.onMemoryWrite(address, data, dataSize);
    }


    public void extendAndWrite(int address, int allocSize, byte[] data) {
        extend(address, allocSize);
        write(address, data, allocSize, false);
    }

    public void extend(int address, int size) {
        if (size <= 0) return;

        final int newSize = address + size;

        int toAllocate = newSize - internalSize();
        if (toAllocate > 0) {
            addChunks((int) ceil((double) toAllocate / CHUNK_SIZE));
        }

        toAllocate = newSize - softSize;
        if (toAllocate > 0) {
            toAllocate = (int) ceil((double) toAllocate / WORD_SIZE) * WORD_SIZE;
            softSize += toAllocate;

            if (programListener != null) programListener.onMemoryExtend(toAllocate);
        }
    }

    public DataWord readWord(int address) {
        return DataWord.of(read(address, 32));
    }

    // just access expecting all data valid
    public byte readByte(int address) {

        int chunkIndex = address / CHUNK_SIZE;
        int chunkOffset = address % CHUNK_SIZE;

        byte[] chunk = chunks.get(chunkIndex);

        return chunk[chunkOffset];
    }

    @Override
    public String toString() {

        StringBuilder memoryData = new StringBuilder();
        StringBuilder firstLine = new StringBuilder();
        StringBuilder secondLine = new StringBuilder();

        for (int i = 0; i < softSize; ++i) {

            byte value = readByte(i);

            // Check if value is ASCII
            String character = ((byte) 0x20 <= value && value <= (byte) 0x7e) ? new String(new byte[]{value}) : "?";
            firstLine.append(character).append("");
            secondLine.append(oneByteToHexString(value)).append(" ");

            if ((i + 1) % 8 == 0) {
                String tmp = format("%4s", Integer.toString(i - 7, 16)).replace(" ", "0");
                memoryData.append("").append(tmp).append(" ");
                memoryData.append(firstLine).append(" ");
                memoryData.append(secondLine);
                if (i + 1 < softSize) memoryData.append("\n");
                firstLine.setLength(0);
                secondLine.setLength(0);
            }
        }

        return memoryData.toString();
    }

    public int size() {
        return softSize;
    }

    public int internalSize() {
        return chunks.size() * CHUNK_SIZE;
    }

    public List<byte[]> getChunks() {
        return new LinkedList<>(chunks);
    }

    private int captureMax(int chunkIndex, int chunkOffset, int size, byte[] src, int srcPos) {

        byte[] chunk = chunks.get(chunkIndex);
        int toCapture = min(size, chunk.length - chunkOffset);

        System.arraycopy(src, srcPos, chunk, chunkOffset, toCapture);
        return toCapture;
    }

    private int grabMax(int chunkIndex, int chunkOffset, int size, byte[] dest, int destPos) {

        byte[] chunk = chunks.get(chunkIndex);
        int toGrab = min(size, chunk.length - chunkOffset);

        System.arraycopy(chunk, chunkOffset, dest, destPos, toGrab);

        return toGrab;
    }

    private void addChunks(int num) {
        for (int i = 0; i < num; ++i) {
            chunks.add(new byte[CHUNK_SIZE]);
        }
    }
}
