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.util.Snavig;
import org.apache.commons.lang.StringUtils;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public abstract class Node {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(Node.class);
    private final HashMap<String, ArrayList<Attribute>> inputs = new HashMap<>();
    private final HashMap<String, Attribute> outputs = new HashMap<>();
    private final String id;

    protected Node(String id) {
        this.id = id;
    }

    /**
     * Return the ID associated with this node. The ID is guaranteed to be unique within the model. If the Model
     * is re-authored (re-saved), the ID is not guaranteed to be stable.
     */
    public String getId() {
        return id;
    }

    /**
     * Return a specific processor node attribute for the given key. This implementation requires that there
     * only be a single node connected to this attribute, otherwise, an exception will be thrown. This can commonly
     * happened if an end-user created a {@link StaticAttribute} and then made it blank.
     */
    @Deprecated
    public Attribute getInput(String key) {
        ArrayList<Attribute> a = inputs.get(key);
        if (a == null || a.size() == 0) return null;

        logger.debug("Found attributes for key. [key: {}] [attr: {}]", key, a);

        if (a.size() != 1) {
            logger.debug("Connections: {}", a);
            throw new IllegalStateException("getAttr() called on a node attribute with multiple connections: " +
                    this.toString());
        }

        return a.get(0);
    }

    /**
     * Add the given input to the node.
     */
    public void addInput(String key, Attribute attr) {
        ArrayList<Attribute> a = inputs.computeIfAbsent(key, k -> new ArrayList<>());

        a.add(attr);
    }

    public void addOutput(String key, Attribute output) {
        outputs.put(key, output);
    }

    public Attribute getOutput(String key) {
        return outputs.get(key);
    }

    public ArrayList<Attribute> getInputs(String key) {
        return inputs.get(key);
    }

    public EO getOutputObject(String key, ProcessorState state) throws Exception {
        Attribute out = getOutput(key);
        if (out == null) return null;
        return out.getObjectValue(state);
    }

    public EOLoop getOutputObjects(String key, ProcessorState state) throws Exception {
        Attribute out = getOutput(key);
        if (out == null) return null;
        return out.getObjectsValue(state);
    }

    public Serializable getOutputSingle(String key, ProcessorState state) throws Exception {
        Attribute out = getOutput(key);
        if (out == null) return null;
        return out.getSingleValue(state);
    }

    /**
     * Sort all the nodes attributes into the lists <code>local</code> and <code>foreign</code> based on whether
     * they are {@link StaticAttribute} or other.
     */
    private void sortAttributes(ArrayList<Attribute> local, ArrayList<Attribute> foreign, String key) {
        List<Attribute> attrs = getInputs(key);

        if (attrs == null) return;

        for (Attribute a : attrs) {
            if (a instanceof StaticAttribute) {
                // If the thing is blank, we don't count it.
                Serializable ser = ((StaticAttribute) a).getValue();
                if (ser != null) {
                    String str = String.valueOf(ser);
                    if (!StringUtils.isBlank(str)) {
                        local.add(a);
                    }
                }
            } else {
                foreign.add(a);
            }
        }
    }

    /**
     * Return the string value contained within a {@link StaticAttribute}, or <code>null</code>. If there are
     * {@link RemoteAttribute} attributes, they are ignored.
     */
    public String getInputStaticString(String key, ProcessorState state) throws Exception {
        List<Attribute> atts = getInputs(key);

        if (atts != null) {
            for (Attribute a : atts) {
                if (a instanceof StaticAttribute) {
                    return Snavig.convertToString(a.getSingleValue(state));
                }
            }
        }

        return null;
    }

    /**
     * Return a {@link EOLoop} of objects stored in an attribute of this node.
     * <p>
     * If there is only a {@link RemoteAttribute}, then <code>getObjectsValue()</code> is called on it.
     * <p>
     * If there is only a {@link StaticAttribute}, then <code>null</code> is returned since static values cannot
     * contain multiple objects.
     * <p>
     * If there is both a {@link StaticAttribute} and a {@link RemoteAttribute} for this key, then special
     * behavior takes place. <code>getObjectValue()</code> is called on the remote attribute returning a single
     * object. Then, <code>getValueLoop()</code> is called on that object using for a key the string stored
     * in the {@link StaticAttribute}.
     * <p>
     * If <code>getObjectValue()</code> fails to produce a result, then as a fallback, <code>getObjectsValue()</code>
     * is called on the {@link RemoteAttribute} and the key stored in the {@link StaticAttribute} is ignored.
     */
    public EOLoop getInputObjects(String key, ProcessorState state) throws Exception {
        ArrayList<Attribute> foreign = new ArrayList<>();
        ArrayList<Attribute> local = new ArrayList<>();

        sortAttributes(local, foreign, key);

        // If we have no remote attribute, we can't possibly have objects.
        if (foreign.size() == 0) return null;

        if (local.size() == 0) {
            Attribute far = foreign.get(0);
            assert (far != null);
            return far.getObjectsValue(state);
        }

        logger.debug("getInputObjects: [key: {}] [foreignCnt: {}] [localCnt: {}]", key, foreign.size(), local.size());

        for (Attribute near : local) {
            logger.debug("Local attribute: {}", near);
        }

        // This is by definition going to be a StaticAttribute
        Attribute far = foreign.get(0);
        EO obj = far.getObjectValue(state);
        if (obj != null) {
            // It may be that we get a bare value from this, instead of a loop. If so, we meant to call
            // getObjectsValue instead.
            String k = getInputStaticString(key, state);
            Serializable sv = obj.get(k);
            if (sv instanceof EOLoop) return (EOLoop) sv;
            if (sv == null) {
                EOLoop loop = new EOLoop();
                obj.setValue(k, loop);
                return loop;
            }
        }

        return far.getObjectsValue(state);
    }

    public EO getInputObject(String key, ProcessorState state) throws Exception {
        ArrayList<Attribute> inputs = getInputs(key);
        if (inputs == null) return null;

        for (Attribute a : inputs) {
            if (a instanceof RemoteAttribute) {
                return a.getObjectValue(state);
            }
        }

        return null;
    }

    public Serializable getInputSingle(String key, ProcessorState state) throws Exception {
        ArrayList<Attribute> foreign = new ArrayList<>();
        ArrayList<Attribute> local = new ArrayList<>();

        sortAttributes(local, foreign, key);

        if (foreign.size() == 0) {
            // We expect only a local attribute.

            if (local.size() == 0) return null;

            Attribute near = local.get(0);
            assert (near != null);
            return near.getSingleValue(state);
        }

        String nearValue = null;
        if (local.size() > 0) {
            Attribute near = local.get(0);
            assert (near != null);
            nearValue = Snavig.convertToString(near.getSingleValue(state));
        }

        if (StringUtils.isNotEmpty(nearValue)) {
            // We expect an object, and want to read a value from it.
            Attribute far = foreign.get(0);
            assert (far != null);

            EO obj = far.getObjectValue(state);
            logger.debug("getInputSingle(): Reading from far object. [key: {}] [attr: {}] [obj: {}]",
                    nearValue, far, obj);
            if (obj == null) return null;

            // TODO: Could return an EOLoop, are we OK with that?
            return obj.getValue(nearValue);
        } else {
            // We expect a value.
            Attribute far = foreign.get(0);
            assert (far != null);
            // TODO: Could return an EOLoop, are we OK with that?
            return far.getSingleValue(state);
        }
    }

    public String getInputSingleString(String key, ProcessorState state) throws Exception {
        return Snavig.convertToString(getInputSingle(key, state));
    }

    public List<Node> getInputNodesSafe(String key) {
        List<Node> nodes = getInputNodes(key);
        if (nodes == null) return new ArrayList<>();
        return nodes;
    }

    /**
     * Returns a list of nodes which are remotely connected to the given input. This method does not mutate any state
     * or use any context-specific data, so a {@link ProcessorState} is not required. This method assumes
     * that the schema is legal, specifically, that {@link RemoteAttribute} actually contain a remote
     * {@link Node}.
     */
    public List<Node> getInputNodes(String key) {
        ArrayList<Attribute> attrs = inputs.get(key);
        ArrayList<Node> nodes = new ArrayList<>();
        if (attrs == null) return nodes;

        for (Attribute attr : attrs) {
            if (attr instanceof RemoteAttribute) {
                logger.debug("Located remote attribute: {}", attr);

                Node remoteNode = ((RemoteAttribute) attr).getRemoteNode();
                assert (remoteNode != null);
                nodes.add(remoteNode);
            }
        }
        return nodes;
    }

    public Attribute getFirstRemoteNode(String key) {
        ArrayList<Attribute> attrs = inputs.get(key);
        for (Attribute attr : attrs) {
            if (attr instanceof RemoteAttribute) {
                return attr;
            }
        }
        return null;
    }
}
