package org.neo4j.nlp.impl.manager;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Transaction;
import org.neo4j.helpers.collection.IteratorUtil;
import org.neo4j.nlp.abstractions.Manager;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

/**
 * This class manages a caching layer for reads and writes to
 * the attached data storage.
 */
public class NodeManager {

    // Initialize static global node cache
    public static final Cache<Long, HashMap<String, Object>>
            globalNodeCache = CacheBuilder.newBuilder()
            .maximumSize(20000000)
            .build();

    public NodeManager() {
    }

    public static Map<String, Object> getNodeFromGlobalCache(Long id)
    {
        return globalNodeCache.getIfPresent(id);
    }

    public Node getOrCreateNode(Manager manager, String key, GraphDatabaseService db)
    {
        // Federates requests to a graph node data management object
        return manager.getOrCreateNode(key, db);
    }

    public static Map<String, Object> getNodeAsMap(Long id, GraphDatabaseService graphDb)
    {
        boolean success;

        // Update the node's property in cache
        Map<String, Object> node = globalNodeCache.getIfPresent(id);

        if(node == null)
        {
            // The node isn't available in the cache, go to the database and retrieve it
            success = addNodeToCache((gdb, cache) -> getNodeHashMap(id, gdb, cache), graphDb);
            if(success) node = globalNodeCache.getIfPresent(id);
        }

        return node;
    }

    public Object getNodeProperty(Long id, String key, GraphDatabaseService graphDb)
    {
        boolean success;

        // Update the node's property in cache
        Map<String, Object> node = globalNodeCache.getIfPresent(id);

        if(node == null)
        {
            // The node isn't available in the cache, go to the database and retrieve it
            success = addNodeToCache((gdb, cache) -> getNodeHashMap(id, gdb, cache), graphDb);
            if(success) node = globalNodeCache.getIfPresent(id);
        }

        return node != null ? node.get(key) : null;
    }

    public boolean setNodeProperty(Long id, String key, Object value, GraphDatabaseService graphDb)
    {
        boolean success = true;

        // Update the node's property in cache
        Map<String, Object> node = globalNodeCache.getIfPresent(id);

        if(node == null)
        {
            // The node isn't available in the cache, go to the database and retrieve it
            success = addNodeToCache((gdb, cache) -> getNodeHashMap(id, gdb, cache), graphDb);
            if(success) node = globalNodeCache.getIfPresent(id);
        }

        // Set the node property
        if (node != null) {
            node.put(key, value);
        }

        // TODO: Remove this in favor of a distributed messaging bus architecture
        Transaction tx = graphDb.beginTx();
        graphDb.getNodeById(id).setProperty(key, value);
        tx.success();
        tx.close();

        return success;
    }

    private static void getNodeHashMap(Long id, GraphDatabaseService gdb, Cache<Long, HashMap<String, Object>> cache) {
        Node thisNode = gdb.getNodeById(id);
        List<String> keys = new ArrayList<>();
        HashMap<String, Object> nodeMap = new HashMap<>();
        IteratorUtil.addToCollection(thisNode.getPropertyKeys(), keys)
                .stream()
                .forEach(n -> nodeMap.put(n, thisNode.getProperty(n)));
        nodeMap.put("id", id);
        cache.put(id, nodeMap);
    }

    private static boolean addNodeToCache(BiConsumer<GraphDatabaseService, Cache<Long, HashMap<String, Object>>> operation, GraphDatabaseService db)
    {

        try (Transaction tx = db.beginTx()) {
            operation.accept(db, globalNodeCache);
            tx.success();
        } catch (Exception e) {
            return false;
        }

        return true;
    }
}