/*
 * Copyright 2006-2019 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * http://www.rcenvironment.de/
 */

package de.rcenvironment.core.component.wrapper;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import de.rcenvironment.core.component.datamanagement.stateful.StatefulComponentDataManagementService;
import de.rcenvironment.core.component.execution.api.ComponentContext;
import de.rcenvironment.core.toolkitbridge.transitional.TextStreamWatcherFactory;
import de.rcenvironment.core.utils.common.TempFileServiceAccess;
import de.rcenvironment.core.utils.common.textstream.TextOutputReceiver;
import de.rcenvironment.core.utils.common.textstream.TextStreamWatcher;
import de.rcenvironment.core.utils.common.textstream.receivers.AbstractTextOutputReceiver;
import de.rcenvironment.core.utils.common.textstream.receivers.LoggingTextOutReceiver;
import de.rcenvironment.core.utils.common.validation.ValidationFailureException;
import de.rcenvironment.core.utils.executor.CommandLineExecutor;

/**
 * An abstract base class for wrappers of command-line tools.
 * 
 * @author Robert Mischke
 * 
 * @param <C> the type of the configuration object passed to each tool invocation
 * @param <R> the type of the result returned from each tool invocation
 */
public abstract class WrapperBase<C, R> {

    /**
     * A simple {@link TextOutputReceiver} adapter that forwards all captured text lines to the
     * "stdout" monitoring channel.
     * 
     * @author Robert Mischke
     * 
     */
    protected final class StdoutMonitoringForwarder extends AbstractTextOutputReceiver {

        @Override
        public void addOutput(String line) {
            monitoringListener.appendStdout(line);
        }
    }

    /**
     * A simple {@link TextOutputReceiver} adapter that forwards all captured text lines to the
     * "stderr" monitoring channel.
     * 
     * @author Robert Mischke
     */
    protected final class StderrMonitoringForwarder extends AbstractTextOutputReceiver {

        @Override
        public void addOutput(String line) {
            monitoringListener.appendStderr(line);
        }
    }

    protected final MonitoringEventListener monitoringListener;

    protected final StdoutMonitoringForwarder stdoutMonitoringForwarder;

    protected final StderrMonitoringForwarder stderrMonitoringForwarder;

    protected final Log log = LogFactory.getLog(getClass());

    private final List<BasicWrapperHook> registeredHooks;

    private final StatefulComponentDataManagementService fileReferenceHandler;

    private ComponentContext componentContext;

    protected WrapperBase(StatefulComponentDataManagementService fileReferenceHandler, MonitoringEventListener listener,
        ComponentContext compInformation) {
        this.monitoringListener = listener;
        this.fileReferenceHandler = fileReferenceHandler;
        this.stdoutMonitoringForwarder = new StdoutMonitoringForwarder();
        this.stderrMonitoringForwarder = new StderrMonitoringForwarder();
        this.registeredHooks = new ArrayList<BasicWrapperHook>();
        this.componentContext = compInformation;
    }

    /**
     * Adds a new lifecycle hook/listener.
     * 
     * @param hook the hook to add
     */
    public void registerHook(BasicWrapperHook hook) {
        registeredHooks.add(hook);
    }

    /**
     * Removes a lifecycle hook/listener.
     * 
     * @param hook the hook to remove
     */
    public void unregisterHook(BasicWrapperHook hook) {
        registeredHooks.remove(hook);
    }

    /**
     * Performs initialization steps that are only required once for repeated execution runs, for
     * example network connections to the execution host.
     * 
     * @throws IOException on I/O errors
     * @throws ValidationFailureException on invalid configuration values
     */
    public void setupStaticEnvironment() throws IOException, ValidationFailureException {
        // empty by default; subclasses may (but don't have to) override
    }

    /**
     * The counterpart to {@link #setupStaticEnvironment()}; responsible for cleaning up everything
     * that is not meant to remain on the execution host.
     * 
     * @throws IOException on I/O errors
     */
    public void tearDownStaticEnvironment() throws IOException {
        // empty by default; subclasses may (but don't have to) override
    }

    /**
     * Performs a single tool invocation.
     * 
     * @param runConfiguration the subclass-specific configuration object
     * @return the subclass-specific result object
     * @throws Exception on errors; TODO narrow down; does ComponentException fit here?
     */
    public abstract R execute(C runConfiguration) throws Exception;

    /**
     * Convenience variant of {@link #execute(Object)} that sets a hook only for the duration of
     * this single execution.
     * 
     * @param runConfiguration the subclass-specific configuration object
     * @param singleRunHook the hook to register only for this single execution
     * @return the subclass-specific result object
     * @throws Exception on errors; TODO narrow down; does ComponentException fit here?
     */
    public R execute(C runConfiguration, BasicWrapperHook singleRunHook) throws Exception {
        registerHook(singleRunHook);
        try {
            return execute(runConfiguration);
        } finally {
            unregisterHook(singleRunHook);
        }
    }

    /**
     * Uploads a set of files to the work directory. In the given map, the keys are the
     * workdir-relative filenames, and the values are the abstract content references.
     * 
     * @param fileMap the filename-to-reference map
     * @param executor the executor to use
     * @throws IOException on I/O errors
     */
    protected void uploadInputFiles(Map<String, String> fileMap, CommandLineExecutor executor) throws IOException {
        for (Entry<String, String> entry : fileMap.entrySet()) {
            String filename = entry.getKey();
            String reference = entry.getValue();
            uploadFileFromReference(filename, reference, executor);
        }
    }

    /**
     * @param filename the workdir-relative filename to create the remote file as
     * @param reference the abstract content reference
     * @param executor the command executor to use
     * @throws IOException on I/O errors
     */
    private void uploadFileFromReference(String filename, String reference, CommandLineExecutor executor) throws IOException {
        // TODO could be optimized with streaming pipe-through from DM to target
        File tempFile = TempFileServiceAccess.getInstance().createTempFileFromPattern("upload." + filename + "-*.tmp");
        tempFile.deleteOnExit(); // safeguard
        try {
            getDataManagementService().copyReferenceToLocalFile(reference, tempFile, componentContext.getStorageNetworkDestination());

            // monitoringListener.appendUserInformation("Uploading " + filename);
            executor.uploadFileToWorkdir(tempFile, filename);
        } finally {
            FileUtils.deleteQuietly(tempFile);
        }
    }

    /**
     * @param filename the workdir-relative filename to copy the remote file from
     * @param reference the abstract content reference
     * @param executor the command executor to use
     * @throws IOException on I/O errors
     */
    protected String downloadFileToReference(String filename, CommandLineExecutor executor) throws IOException {
        // TODO optimize by adding stream methods
        File targetFile = TempFileServiceAccess.getInstance().createTempFileFromPattern("download." + filename + "-*.tmp");
        targetFile.deleteOnExit(); // safeguard
        try {
            executor.downloadFileFromWorkdir(filename, targetFile);
            return getDataManagementService().createTaggedReferenceFromLocalFile(targetFile, filename);
        } finally {
            FileUtils.deleteQuietly(targetFile);
        }
    }

    protected void prepareAndTestEnvironment(CommandLineExecutor executor) throws InterruptedException, IOException {

        // FIXME this was originally a setup method; implement actual testing of expected state

        log.debug("Environment pre-test starting");

        executor.start("echo \"Effective Workdir: `pwd`\"", null);

        InputStream ansysStdout = executor.getStdout();
        InputStream ansysStderr = executor.getStderr();

        final TextStreamWatcher stdoutWatcher =
            TextStreamWatcherFactory.create(ansysStdout, new LoggingTextOutReceiver("Environment pre-test Stdout"));
        final TextStreamWatcher stderrWatcher =
            TextStreamWatcherFactory.create(ansysStderr, new LoggingTextOutReceiver("Environment pre-test Stderr"));

        stdoutWatcher.start();
        stderrWatcher.start();

        executor.waitForTermination();

        stdoutWatcher.waitForTermination();
        stderrWatcher.waitForTermination();

        log.debug("Environment pre-test complete");
    }

    protected StatefulComponentDataManagementService getDataManagementService() {
        return fileReferenceHandler;
    }

}
