package leigh.conflux.nodegraph;

import leigh.eo.EO;
import leigh.eo.EOLoop;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import leigh.mecha.nodegraph.Attribute;
import leigh.mecha.nodegraph.Node;
import leigh.mecha.nodegraph.ProcessorState;
import leigh.mecha.nodegraph.UselessAttribute;
import org.apache.commons.lang.StringUtils;

import java.util.ArrayList;

/**
 * This node loops through an input set, applying transformations to each object in that set, outputting a new set
 * of computed values.
 * <p>
 * The Loop operates on the input object set <code>KEY_INPUT_OBJS</code>. For every object in that set, it will read the
 * values from every node connected to <code>KEY_INPUT_PROC</code>. So for example, if there are 256 objects in
 * the input set, it will read the process nodes 256 times. When the process nodes are read,
 * <code>KEY_OUTPUT_CURSOR</code> contains all the objects from the attribute of the current
 * object in <code>KEY_INPUT_OBJS</code>. For example, if the attribute is "nums", the 1st time process is read,
 * <code>KEY_OUTPUT_CURSOR</code> will contain the contents of the 1st object's "nums" attribute.
 * <p>
 * The values produced by the process nodes are gathered together into a new synthetic object, and that synthetic
 * object is added to a synthetic record set which is made available at <code>KEY_OUTPUT_OBJS</code>.
 * When the loop has finished processing, the number of objects in the synthetic output set will be equal to the
 * number of objects in the input set, and the order will be the same. Therefore, the 5th synthetic object contains
 * the product of the 5th input object, etc.
 *
 * @author Alex Leigh
 */
public class LoopNode extends Node {
    public static final String NODE_NAME = "leigh.conflux.v1.Loop";
    public static final String KEY_INPUT_OBJS = "objs";
    public static final String KEY_IN_PATH = "path";
    public static final String KEY_INPUT_ATTR = "objAttr";
    public static final String KEY_OUTPUT_OBJS = "objs";
    public static final String KEY_OUTPUT_CURSOR = "cursor";
    public static final String KEY_INPUT_PROC = "process";
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(LoopNode.class);
    private EOLoop outputSet;
    private EOLoop currentRows;
    private EOLoop filteredSet;

    public LoopNode(String id) {
        super(id);
        addOutput(KEY_OUTPUT_OBJS, new UselessAttribute() {
            public EOLoop getObjectsValue(ProcessorState state) throws Exception {
                logger.debug("KEY_OUTPUT_OBJS read.");
                process(state);
                return outputSet;
            }
        });

        addOutput(KEY_OUTPUT_CURSOR, new UselessAttribute() {
            public EOLoop getObjectsValue(ProcessorState state) {
                logger.debug("KEY_OUTPUT_CURSOR attribute read. Returning {}", currentRows);
                return currentRows;
            }

            public EO getObjectValue(ProcessorState state) {
                if (currentRows != null && currentRows.size() > 0) return currentRows.get(0);
                return null;
            }
        });
    }


    /**
     * Attempt to filter the input set, if a filter has been set. If this is the case, then the output set will
     * be synthetic. If no filter has been set, then the output set will be the same as the input set.
     */
    private void filter(ProcessorState state) throws Exception {
        filteredSet = getInputObjects(KEY_INPUT_OBJS, state);
    }

    public void process(ProcessorState state) throws Exception {
        filter(state);

        String childKey = getInputStaticString(KEY_INPUT_ATTR, state);
        String path = getInputStaticString(KEY_IN_PATH, state);

        if (StringUtils.isBlank(path)) {
            logger.debug("Direct case.");
            outputSet = filteredSet;
        } else {
            outputSet = new EOLoop();
            for (EO obj : filteredSet) {
                logger.debug("Recursive case.");
                obj.recurse(outputSet, path);
            }
        }

        logger.debug("Loop processing. [objSz: {}] [procSz: {}] [key: {}] [me: {}]",
                filteredSet.size(), outputSet.size(), childKey, this.hashCode());

        if (!StringUtils.isBlank(childKey)) {
            processChildren(childKey, state);
            return;
        }

        ArrayList<Attribute> inputs = getInputs(KEY_INPUT_PROC);

        // Not children, so we are going to process "parents".
        for (EO inputRow : outputSet) {
            currentRows = new EOLoop().add(inputRow);

            logger.debug("Firing loop. [sz: {}] [k: {}] [obj: {}] {}", currentRows.size(), childKey,
                    inputRow, currentRows);

            // Read all the input nodes, and copy their produced value into the data object

            if (inputs != null) {
                for (Attribute input : inputs) {
                    EO ieo = input.getObjectValue(state);
                    logger.debug("Process input: [ieo: {} {}] [input: {}]", ieo, ieo.hashCode(), input);
                    if (ieo != null) {
                        inputRow.merge(ieo);
                    }
                }
            }
        }
    }

    private void processChildren(String childKey, ProcessorState state) throws Exception {
        for (EO inputRow : outputSet) {
            // Set the current row output to this row.
            logger.debug("Input row: {}", inputRow);

            if (!inputRow.containsKey(childKey)) {
                logger.warn("Input object does not contain child key. Results will be null.");
            }
            currentRows = inputRow.getValueLoop(childKey);

            logger.debug("Current rows. [sz: {}] [k: {}] [obj: {}] {}", currentRows.size(), childKey,
                    inputRow, currentRows);

            // Read all the input graphs
            ArrayList<Attribute> inputs = getInputs(KEY_INPUT_PROC);
            if (inputs != null) {
                for (Attribute input : inputs) {
                    logger.debug("Process input: {}", input);
                    EO ieo = input.getObjectValue(state);
                    if (ieo != null)
                        inputRow.setAll(ieo);
                }
            }
        }
    }
}