package blockchain;

import blockchain.genesis.GenCreator;
import utils.DiskUtils;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;

import static java.lang.String.format;

import utils.statistics.BCStat;
import utils.statistics.Statistics;
import proto.types.block.*;

public class Blockchain {

    private final static org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(Blockchain.class);
    private final int creatorID;
    private int swapSize = 0;
    private Path swapPath;
    private int maxCacheSize = 100;
    private boolean swapAble = false;
    private final ConcurrentHashMap<Integer, Block> blocks = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<Integer, BCStat> bsts = new ConcurrentHashMap<>();

    private Queue<Future> finishedTasks = new LinkedList<>();
    private Queue<Integer> tasksIdx = new LinkedList<>();

//    int size = 0;

    Blockchain(int creatorID, int maxCacheSize, GenCreator gcreator, Path swapPath) {
        this.creatorID = creatorID;
        if (maxCacheSize > 0) {
            this.maxCacheSize = maxCacheSize;
        }
        this.swapPath = swapPath;
        DiskUtils.createStorageDir(swapPath);
        this.swapAble = true;
        addBlock(gcreator.createGenesisBlock());
    }

    public Blockchain(int creatorID) {
        this.creatorID = creatorID;
    }

    public Blockchain(Blockchain orig, int start, int end) {
        this.creatorID = orig.creatorID;
        for (int i = start ; i < end ; i++) {
            addBlock(orig.getBlock(i));
        }
    }

    // We assume now that all the validated blocks are in memory
    public boolean validateCurrentLeader(int leader, int f) {
        int last =  lastIndex();
        for (int i = last ; i > Math.max(0, last - f) ; i--) {
            if (getBlock(i).getHeader().getBid().getPid() == leader) return false;
        }
        return true;
    }

    public void setBlocks(List<Block> Nblocks, int start)  {
        for (int i = start ; i < start + Nblocks.size() ; i++) {
            if (i > lastIndex()) {
                addBlock(Nblocks.get(i - start));
            } else {
                setBlock(i, Nblocks.get((i - start)));
            }

        }
    }

    public void setBlock(int index, Block b) {
        if (Statistics.isActive()) {
            BCStat st = new BCStat();
            st.bst = b.getBst();
            st.hst = b.getHeader().getHst();
            st.txCount = b.getDataCount();
            st.pid = b.getId().getPid();
            bsts.putIfAbsent(b.getHeader().getHeight(), st);
        }

        if (blocks.keySet().contains(index)) {
            blocks.replace(index, b);
            return;
        }
        try {
            DiskUtils.deleteBlockFile(index, swapPath);
            DiskUtils.cutBlock(b, swapPath);
        } catch (IOException e) {
            logger.error(e);
        }


    }

    public boolean validateBlockHash(Block b) {
        if (b.getHeader().getHeight() == 0) return true;
        int index = b.getHeader().getHeight() - 1;
        if (lastIndex() < index) return false;
        Block prev = getBlock(index);
        return Utils.validateBlockHash(prev, b);
    }

    public boolean isValid() {
        for (int i = 1 ; i < lastIndex() + 1 ; i++) {
            if (!validateBlockHash(getBlock(i - 1))) {
                logger.info(String.format("Invalid Blockchain!! [%d -> %d]", i-1, i));
                return false;
            }
        }
        return true;
    }

    public void addBlock(Block b) {
        blocks.putIfAbsent(b.getHeader().getHeight(), b);
    }

    public BCStat getBlockSts(int index) {
        BCStat st = bsts.get(index);
        bsts.remove(index);
        return st;
    }
    public Block getBlock(int index) {
        if (blocks.isEmpty()) return null;
        if (index > lastIndex()) return null;
        if (blocks.keySet().contains(index)) {
            return blocks.get(index);
        }
        try {
            return DiskUtils.getBlockFromFile(index, swapPath);
        } catch (IOException e) {
            logger.error(format("Unable to retrieve block from disk [index=%d ; height=%d]", index, lastIndex()), e);
            return null;
        }
    }

    public List<Block> getBlocks(int start, int end) {
        List<Block> ret = new ArrayList<>();
        for (int i = start ; i < end ; i++) {
            ret.add(getBlock(i));
        }
        return ret;
    }

    public int lastIndex() {
        return Collections.max(blocks.keySet(), Comparator.comparingInt(integer -> integer));
//        int li = 0;
//        for (Map.Entry<Integer, Block> e: blocks.entrySet()) {
//            li = Math.max(li, e.getKey());
//        }
//        return  li;
//        return max(blocks.keys());
    }


    // This should be used in its very specific usage only!!!
    public void removeBlock(int index) throws IOException {
        if (blocks.keySet().contains(index)) {
            blocks.remove(index);
            return;
        }
        DiskUtils.deleteBlockFile(index, swapPath);
    }
    public void writeNextToDisk() {
        if (!swapAble) return;
        if (blocks.size() < maxCacheSize) return;
        int currBlockIndex = swapSize;
        try {
            DiskUtils.cutBlock(blocks.get(currBlockIndex), swapPath);
        } catch (IOException e) {
            logger.error(e);
        }
        swapSize++;
        blocks.remove(currBlockIndex);
    }

    public void writeNextToDiskAsync() {
        if (!swapAble) return;
        if (blocks.size() < maxCacheSize) return;
        int currBlockIndex = swapSize;
        tasksIdx.add(currBlockIndex);
        finishedTasks.add(DiskUtils.cutBlockAsync(blocks.get(currBlockIndex), swapPath));
        swapSize++;
        assert finishedTasks.peek() != null;
        boolean task_done = finishedTasks.peek().isDone();
        while (task_done) {
            blocks.remove(tasksIdx.remove());
            finishedTasks.remove();
            if (finishedTasks.isEmpty()) break;
            task_done = finishedTasks.peek().isDone();
        }
    }

//    void gcPhase() {
//        if (blocks.size() > 10 * maxCacheSize) {
//            long start = System.currentTimeMillis();
//            while (finishedTasks.size() > 0) {
//                // TODO: Inspect this code...
//                finishedTasks.poll().get();
//
//            }
//            logger.info(format("Running synchronized writes to block (in order to prevent memory leaks." +
//                    " Took about [%d] ms", System.currentTimeMillis() - start));
//            System.out.println(format("Running synchronized writes to block (in order to prevent memory leaks." +
//                    " Took about [%d] ms", System.currentTimeMillis() - start));
//        }
//    }

    public boolean contains(int height) {
        return height <= lastIndex();
    }

}
