package com.sas.unravl.extractors;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.sas.unravl.ApiCall;
import com.sas.unravl.UnRAVL;
import com.sas.unravl.UnRAVLException;
import com.sas.unravl.generators.Text;
import com.sas.unravl.util.Json;

import java.io.IOException;
import java.util.Map;

import org.apache.log4j.Logger;

/**
 * A base class for script-based extractors, such as {@link GroovyExtractor} and
 * {@link JavaScriptExtractor}.
 * <p>
 * Usage:
 *
 * <pre>
 * "bind" : [
 *    { "lang" : { var-value-pairs } }
 *    ]
 * </pre>
 *
 * where "lang" is a supported script language name such as "groovy" or
 * "javascript".
 * <p>
 * The <var>var-value-pairs</var> are JSON object notation such as
 * <code>"varName" : "<em>expression</em>"</code>. Each
 * <code><em>expression</em></code> may be a string, a string in the form
 * "@file-or-URL", or an array of such source specifications, as defined in
 * {@link Text}. The resulting text is evaluated as a script in the target
 * language.
 * </p>
 * <p>
 * Expressions may use any variable binding that is currently in effect,
 * including preceding <var>var-value-pairs</var> in the current scriptlet.
 * Example (for Groovy):
 * </p>
 *
 * <pre>
 * "bind" : [
 *    { "groovy" : { "pi" : "Math.PI",
 * 	             "r" : "json.a[2].doubleValue()",
 * 	             "pirsquared" : "pi*r*r",
 * 	             "itemName" : "json.name.textValue()"
 * 	            }
 * 	  }
 * ]
 * </pre>
 *
 * <p>
 * The script value is subject to environment expansion before running. All
 * variables in the environment are available as local variables when the script
 * runs.
 * </p>
 *
 * @author David.Biesack@sas.com
 */
public class BaseScriptExtractor extends BaseUnRAVLExtractor {

    private static final Logger logger = Logger
            .getLogger(BaseUnRAVLExtractor.class);

    private final String language;

    public BaseScriptExtractor(String language) {
        this.language = language;
    }

    @Override
    public void extract(UnRAVL unravl, ObjectNode scriptlet, ApiCall call)
            throws UnRAVLException {
        super.extract(unravl, scriptlet, call);
        boolean unwrap = unwrapOption(scriptlet);
        ObjectNode bindings = Json.object(Json.firstFieldValue(scriptlet));
        for (Map.Entry<String, JsonNode> e : Json.fields(bindings)) {
            JsonNode sourceNode = null;

            sourceNode = e.getValue();
            try {
                String name = e.getKey();
                if (!(sourceNode.isTextual() || sourceNode.isArray())) {
                    throw new UnRAVLException(
                            key(scriptlet)
                                    + " extractor requires a string or array of strings, found "
                                    + sourceNode);
                }
                String expressionString = new Text(unravl, sourceNode).text();
                expressionString = call.getScript().expand(expressionString);
                Object value = evaluate(unravl, expressionString);
                unravl.bind(name, unwrap ? Json.unwrap(value) : value);

            } catch (RuntimeException rte) {
                logger.error("Script '" + sourceNode + "' (expansion '"
                        + sourceNode + "') threw a runtime exception "
                        + e.getClass().getName() + ", " + rte.getMessage());
                throw new UnRAVLException(rte.getMessage(), rte);
            } catch (IOException ioe) {
                logger.error("I/O exception " + ioe.getMessage()
                        + " reading script '" + sourceNode);
                throw new UnRAVLException(ioe.getMessage(), ioe);
            }
        }
    }

    /**
     * Evaluate an expression. Subclasses may override this. The default
     * implementation evaluates the expression using the Java script language
     * associated with this instance (Groovy, JavaScript, etc.)
     * 
     * @param unravl
     *            the current UnRAVL script
     * @param expressionString
     *            the expression to evaluate
     * @return the result of evaluating the expression
     * @throws UnRAVLException
     *             if the expression is invalid or results in an error during
     *             evaluation
     */
    protected Object evaluate(UnRAVL unravl, String expressionString)
            throws UnRAVLException {
        return unravl.evalWith(expressionString, language);
    }

}
