package leigh.mecha.nodegraph;

import leigh.eo.EO;
import leigh.eo.EOLoop;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import leigh.mecha.nodegraph.javafx.NodeGraphJFX;

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

/**
 * Companion class to {@link NodeGraphJFX} which provides serialization functionality. This allows graph states to be
 * loaded/saved.
 *
 * @author Alex Leigh
 */
public class GraphSerializer {
    public static final String TYPE_NODE = "leigh.mecha.nodegraph.Node";
    public static final String TYPE_VALUE = "leigh.mecha.nodegraph.Value";
    public static final String TYPE_CONNECTION = "leigh.mecha.nodegraph.Connection";
    public static final String KEY_ID = "id";
    public static final String KEY_XPOS = "xPos";
    public static final String KEY_YPOS = "yPos";
    public static final String KEY_TYPE = "type";
    public static final String KEY_VALUES = "values";
    public static final String KEY_NAME = "name";
    public static final String KEY_VALUE = "value";
    public static final String KEY_SRCID = "srcId";
    public static final String KEY_SRCNAME = "srcName";
    public static final String KEY_DSTID = "dstId";
    public static final String KEY_DSTNAME = "dstName";
    public static final String KEY_CONNECTIONS = "connections";
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(GraphSerializer.class);

    /**
     * Convert the graph state from the {@link NodeGraphJFX} control into an {@link EOLoop} of node objects.
     */
    public EOLoop save(NodeGraphJFX graph) {
        logger.debug("save() called.");
        EOLoop nodes = new EOLoop();

        List<NodeObject> objects = graph.getObjects();

        for (NodeObject obj : objects) {
            EO nodeState = new EO(TYPE_NODE);
            nodeState.setValue(KEY_ID, obj.getId());
            nodeState.setValue(KEY_XPOS, obj.getPosX());
            nodeState.setValue(KEY_YPOS, obj.getPosY());
            nodeState.setValue(KEY_TYPE, obj.getNodeConfig().getName());
            EOLoop valueStates = nodeState.getValueLoop(KEY_VALUES);

            for (Map.Entry<String, String> value : obj.getValues().entrySet()) {
                EO valueState = new EO(TYPE_VALUE);
                valueState.setValue(KEY_NAME, value.getKey());
                valueState.setValue(KEY_VALUE, value.getValue());
                valueStates.add(valueState);
            }

            EOLoop conStates = nodeState.getValueLoop(KEY_CONNECTIONS);
            for (NodeConnection connection : obj.getConnections()) {
                // We allow this to fail for development purposes, to allow for changes between editions
                try {
                    EO conState = new EO(TYPE_CONNECTION);

                    conState.setValue(KEY_SRCID, connection.getSrcNode().getId());
                    conState.setValue(KEY_SRCNAME, connection.getSrcAttribute().getName());
                    conState.setValue(KEY_DSTID, connection.getDstNode().getId());
                    conState.setValue(KEY_DSTNAME, connection.getDstAttribute().getName());

                    conStates.add(conState);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            logger.debug("node: {}", obj);
            for (NodeConnection con : obj.getConnections()) {
                logger.debug("-> connection [remoteId: {}] [srcAttr: {}] [dstAttr: {}]",
                        con.getDstNode().getId(), con.getSrcAttribute(), con.getDstAttribute());
            }

            nodes.add(nodeState);
        }

        return nodes;
    }

    public NodeConfig getConfig(List<NodeConfig> configs, String name) {
        for (NodeConfig config : configs) {
            if (name.equals(config.getName())) return config;
        }
        return null;
    }

    public void load(NodeGraphJFX graph, EOLoop nodes, List<NodeConfig> configs) {
        HashMap<Integer, NodeObject> nodeObjects = new HashMap<>();

        // First we build the nodes
        for (EO node : nodes) {
            logger.debug("Deserializing node...");

            int id = node.getValueInteger(KEY_ID);
            NodeConfig cfg = getConfig(configs, node.getValueString(KEY_TYPE));
            NodeObject obj = new NodeObject(id, cfg);
            obj.setPosX(node.getValueDouble(KEY_XPOS));
            obj.setPosY(node.getValueDouble(KEY_YPOS));
            nodeObjects.put(obj.getId(), obj);

            EOLoop values = node.getValueLoop(KEY_VALUES);
            for (EO value : values) {
                logger.debug("Node value found: {}", value);
                obj.getValues().put(value.getValueString(KEY_NAME), value.getValueString(KEY_VALUE));
            }

            logger.debug("Regenerated node: {}", obj);
        }

        for (NodeObject x : nodeObjects.values()) {
            logger.debug("Loaded. [obj: {}]", x);
        }

        // Then we build the connections
        for (EO node : nodes) {
            EOLoop connections = node.getValueLoop(KEY_CONNECTIONS);
            for (EO connection : connections) {
                logger.debug("Building connection: {}", connection);
                if (connection.size() == 0) {
                    logger.error("Illegal State");
                    continue;
                }
                Integer srcId = connection.getValueInteger(KEY_SRCID);
                Integer dstId = connection.getValueInteger(KEY_DSTID);
                String srcName = connection.getValueString(KEY_SRCNAME);
                String dstName = connection.getValueString(KEY_DSTNAME);
                NodeObject srcObj = nodeObjects.get(srcId);
                NodeObject dstObj = nodeObjects.get(dstId);

                if (srcObj == null || dstObj == null) {
                    logger.error("State corrupted. [flow: {}]", connection);
                }

                NodeConfig srcNodeConfig = srcObj.getNodeConfig();
                NodeConfig dstNodeConfig = dstObj.getNodeConfig();

                // Can happen if we have a missing node. We tolerate this.
                if (srcNodeConfig == null || dstNodeConfig == null) continue;

                NodeAttribute srcAttr = srcNodeConfig.getAttr(srcName);
                NodeAttribute dstAttr = dstNodeConfig.getAttr(dstName);

                NodeConnection nodeConnection = new NodeConnection(nodeObjects.get(srcId), srcAttr,
                        nodeObjects.get(dstId), dstAttr);
                srcObj.getConnections().add(nodeConnection);
            }
        }
        graph.load(new ArrayList<>(nodeObjects.values()));
    }
}