package org.omnetpp.scave.editors;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Assert;
import org.omnetpp.common.util.StringUtils;
import org.omnetpp.scave.engine.ByteVector;
import org.omnetpp.scave.engine.ResultFileManager;

/**
 * Memoization cache for methods of ResultProvider.
 *
 * @author andras
 */
public class MemoizationCache {
    private ResultFileManager rfm;
    private int currentSerial = 0;
    private long memoryLimit;
    private long memoryUsed = 0;

    public static class Key {
        private String method;
        private Object[] args;

        public Key(String method, Object... args) {
            this.method = method;
            this.args = args;
        }

        public String getMethodName() {
            return method;
        }

        @Override
        public int hashCode() {
            return method.hashCode() + 31 * Arrays.deepHashCode(args);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null || getClass() != obj.getClass())
                return false;
            Key other = (Key) obj;
            return method.equals(other.method) && Arrays.deepEquals(args, other.args);
        }

        @Override
        public String toString() {
            return "PickleKey(" + method + ": " + StringUtils.join(args, ",") + ")";
        }
    }

    private Map<Key,List<ByteVector>> cache = new LinkedHashMap<>(); // for FIFO order

    public MemoizationCache(ResultFileManager rfm) {
        this(rfm, Long.MAX_VALUE);
    }

    public MemoizationCache(ResultFileManager rfm, long memoryLimit) {
        this.rfm = rfm;
        this.currentSerial = rfm.getSerial();
        this.memoryLimit = memoryLimit;
    }

    private void checkSerial() {
        if (currentSerial != rfm.getSerial()) {
            cache.clear();
            memoryUsed = 0;
            currentSerial = rfm.getSerial();
        }
    }

    public boolean contains(Key key) {
        checkSerial();
        return cache.containsKey(key);
    }

    public List<ByteVector> get(Key key) {
        checkSerial();
        return cache.get(key);
    }

    public boolean isCacheable(Key key, long replySize) {
        return replySize <= memoryLimit;
    }

    public void put(Key key, ByteVector reply) {
        // note: using this API, memory consumption can temporarily exceed memoryLimit
        checkSerial();
        Assert.isTrue(reply.size() <= memoryLimit);
        memoryUsed += reply.size();
        cache.put(key, List.of(reply));
        while (memoryUsed > memoryLimit)
            discardFirst();
    }

    public void put(Key key, List<ByteVector> reply) {
        // note: using this API, memory consumption can temporarily exceed memoryLimit
        checkSerial();
        long totalSize = 0;
        for (ByteVector v : reply)
            totalSize += v.size();
        memoryUsed += totalSize;
        Assert.isTrue(totalSize <= memoryLimit);
        cache.put(key, reply);
        while (memoryUsed > memoryLimit)
            discardFirst();
    }

    private void discardFirst() {
        Map.Entry<Key,List<ByteVector>> entry = cache.entrySet().iterator().next();
        memoryUsed -= entry.getValue().size();
        cache.remove(entry.getKey());
    }

    public void clear() {
        cache.clear();
        memoryUsed = 0;
    }

}
