/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.cryptolib.interoperation.js.commands;

import static java.text.MessageFormat.format;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import com.scytl.cryptolib.interoperation.js.constants.CommonConstants;

/**
 * Super class for all command classes.
 */
public abstract class Command {

    /**
     * Script engine.
     */
    protected final ScriptEngine _engine;

    /**
     * Map of exception data entries, to be used for debugging purposes.
     */
    protected final Map<String, String> _exceptionDataMap;

    /**
     * Constructor.
     * <p>
     * Internally creates a {@link ScriptEngine} instance and evaluates the
     * common scripts and explicitly specified scripts as well. The scripts are
     * loaded as classpath resources according to the contract of
     * {@link Class#getResourceAsStream(String)} method.
     *
     * @param resources
     *            the script resources
     */
    protected Command(final String... resources) {

        _engine = newScriptEngine();
        _exceptionDataMap = new LinkedHashMap<>();

        evaluateScript(CommonConstants.FORGE_PATH);
        evaluateScript(CommonConstants.SJCL_PATH);
        evaluateScript(CommonConstants.CRYPTOLIB_JS_PATH);
        evaluateScript(CommonConstants.JS_TESTS_INITIALIZE_PATH);
        evaluateScript(CommonConstants.CRYPTOLIB_DEFAULT_POLICY_JS_PATH);
        evaluateScript(CommonConstants.JS_TESTS_INITIALIZE_PATH);
        evaluateScript(CommonConstants.JS_TESTS_UTILITIES_PATH);
        for (String resource : resources) {
            evaluateScript(resource);
        }
    }

    private static ScriptEngine newScriptEngine() {

        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine =
            manager.getEngineByName(CommonConstants.SCRIPT_ENGINE_NAME);
        Bindings bindings = engine.getBindings(ScriptContext.GLOBAL_SCOPE);
        bindings.put("MATHEMATICAL_COLLECTION_SERIALIZATION_DELIMITER",
            CommonConstants.TEST_DATA_SERIALIZATION_DELIMITER);
        return engine;
    }

    /**
     * Executes the command.
     *
     * @throws CommandException
     *             if the command cannot be executed.
     */
    public abstract void execute() throws CommandException;

    /**
     * Evaluates the script represented by the specified classpath resource.
     *
     * @param resource
     *            the script resource.
     */
    protected final void evaluateScript(final String resource) {

        try (InputStream stream = getClass().getResourceAsStream(resource);
                Reader reader = new InputStreamReader(stream,
                    StandardCharsets.UTF_8)) {
            _engine.eval(reader);
        } catch (ScriptException | IOException e) {
            throw new IllegalStateException(
                format("Failed to evaluate script ''{0}''.", resource), e);
        }
    }

    /**
     * Returns {@link Invocable} instance provided by the {@link #_engine}.
     *
     * @return the {@link Invocable} instance.
     */
    protected final Invocable getInvocable() {

        return (Invocable) _engine;
    }

    /**
     * Adds a new entry to the exception data map.
     * 
     * @param key
     *            the key of the exception data entry to add.
     * @param value
     *            the value of the exception data entry, in String format.
     */
    protected final void putExceptionData(final String key,
            final String value) {

        _exceptionDataMap.put(key, value);
    }

    /**
     * Removes an entry from the exception data map.
     * 
     * @param key
     *            the key of the exception data entry to remove.
     */
    protected final void removeExceptionData(final String key) {

        _exceptionDataMap.remove(key);
    }

    /**
     * Clears the exception data map.
     */
    protected final void clearExceptionData() {

        _exceptionDataMap.clear();
    }

    /**
     * Builds a {@link CommandException} instance, using the contents of the
     * exception data map.
     * 
     * @param message
     *            the message for the {@link CommandException} instance.
     * @param cause
     *            the cause of the {@link CommandException} instance.
     * @return the {@link CommandException} instance.
     */
    protected final CommandException buildExceptionWithData(
            final String message, final Throwable cause) {

        CommandException.Builder builder = new CommandException.Builder()
            .addMessage(message).addCause(cause);
        _exceptionDataMap.forEach((k, v) -> {
            builder.addData(k, v);
        });

        return builder.build();
    }
}
