/*
 * Copyright 2006-2022 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.core.communication.management.internal;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

import de.rcenvironment.core.command.common.CommandException;
import de.rcenvironment.core.command.spi.CommandContext;
import de.rcenvironment.core.command.spi.CommandDescription;
import de.rcenvironment.core.command.spi.CommandPlugin;
import de.rcenvironment.core.communication.api.CommunicationService;
import de.rcenvironment.core.communication.channel.MessageChannelService;
import de.rcenvironment.core.communication.common.InstanceNodeSessionId;
import de.rcenvironment.core.communication.management.BenchmarkService;
import de.rcenvironment.core.communication.management.BenchmarkSetup;
import de.rcenvironment.core.communication.nodeproperties.NodePropertiesService;
import de.rcenvironment.core.configuration.ConfigurationService;
import de.rcenvironment.core.configuration.ConfigurationService.ConfigurablePathId;
import de.rcenvironment.core.configuration.bootstrap.RuntimeDetection;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.incubator.GraphvizUtils;

/**
 * A {@link CommandPlugin} providing "net [...]" commands for querying the network and topology state, as well as performing benchmark
 * operations.
 * 
 * @author Robert Mischke
 * @author Jan Flink ("net components")
 */
public class NetCommandPlugin implements CommandPlugin {

    private static final String CMD_NET = "net";

    private CommunicationService communicationService;

    private BenchmarkService benchmarkService;

    private NodePropertiesService nodePropertiesService;

    private MessageChannelService messageChannelService;

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

    private File outputDir;

    @Override
    public Collection<CommandDescription> getCommandDescriptions() {
        final Collection<CommandDescription> contributions = new ArrayList<CommandDescription>();
        contributions.add(new CommandDescription(CMD_NET, "", false, "short version of \"net info\""));
        contributions.add(new CommandDescription("net info", "", false, "show a list of reachable RCE nodes"));
        contributions.add(new CommandDescription("net graph", "[<base name>]", true,
            "generates a Graphviz file of the current network topology"));
        contributions.add(new CommandDescription("net filter", "", false, "show IP filter status"));
        contributions.add(new CommandDescription("net filter reload", "", false, "reloads the IP filter configuration"));
        // developer commands
        contributions.add(new CommandDescription("net graph -a", "[<base name>]", true,
            "like \"net graph\", but include unreachable nodes"));
        contributions.add(new CommandDescription("net bench", "<taskdef>[;<taskDef>]*", true, "run communication benchmark",
            "<taskDef> = <targetNode|*>([<numMessages>],[<requestSize>],[<responseSize>],",
            "                           [<responseDelay(msec)>],[<threadsPerTarget>])"));
        contributions.add(new CommandDescription("net np", "", true, "show known RCE node properties"));
        return contributions;
    }

    @Override
    public void execute(CommandContext context) throws CommandException {
        context.consumeExpectedToken(CMD_NET);
        String subCmd = context.consumeNextToken();
        if (subCmd == null) {
            // "net" -> "net info"
            performNetInfo(context);
        } else {
            if ("add".equals(subCmd)) {
                // "net add <...>"
                context.println("Obsolete command; use \"cn add\" instead");
                return;
            } else if ("bench".equals(subCmd)) {
                // "net bench <...>"
                performNetBench(context);
            } else if ("filter".equals(subCmd)) {
                performNetFilter(context);
            } else if ("graph".equals(subCmd)) {
                performNetGraph(context);
            } else if ("info".equals(subCmd)) {
                // TODO review: add "extended" info output? (e.g. "-a" flag)
                performNetInfo(context);
            } else if ("np".equals(subCmd)) {
                performNetNp(context);
            } else {
                throw CommandException.unknownCommand(context);
            }
        }
    }

    /**
     * OSGi-DS bind method.
     * 
     * @param newInstance the new service instance
     */
    public void bindCommunicationService(CommunicationService newInstance) {
        this.communicationService = newInstance;
    }

    /**
     * OSGi-DS bind method.
     * 
     * @param newInstance the new service instance
     */
    public void bindBenchmarkService(BenchmarkService newInstance) {
        this.benchmarkService = newInstance;
    }

    /**
     * OSGi-DS bind method.
     * 
     * @param newInstance the new service instance
     */
    public void bindNodePropertiesService(NodePropertiesService newInstance) {
        this.nodePropertiesService = newInstance;
    }

    /**
     * OSGi-DS bind method.
     * 
     * @param newInstance the new service instance
     */
    public void bindConfigurationService(ConfigurationService newInstance) {
        if (RuntimeDetection.isImplicitServiceActivationDenied()) {
            // skip implicit bind actions if is was spawned as part of a default test environment;
            // if this causes errors in mocked service tests, invoke RuntimeDetection.allowSimulatedServiceActivation()
            return;
        }
        this.outputDir = newInstance.getConfigurablePath(ConfigurablePathId.PROFILE_OUTPUT);
    }

    /**
     * OSGi-DS bind method.
     * 
     * @param newInstance the new service instance
     */
    public void bindMessageChannelService(MessageChannelService newInstance) {
        messageChannelService = newInstance;
    }

    private void performNetGraph(CommandContext context) throws CommandException {
        // sanity check
        if (outputDir == null || !outputDir.isDirectory()) {
            throw new IllegalStateException("Invalid output dir: " + outputDir);
        }
        String formatName = "graphviz";
        if (context.consumeNextTokenIfEquals("-a")) {
            formatName = "graphviz-all";
            context.setDeveloperCommandSetEnabled(true);
        }
        List<String> parameters = context.consumeRemainingTokens();
        if (parameters.size() > 1) {
            throw CommandException.wrongNumberOfParameters(context);
        }
        String baseName = "rce_network"; // default
        if (parameters.size() == 1) {
            baseName = parameters.get(0);
        }

        String graphvizData = communicationService.getFormattedNetworkInformation(formatName);

        File gvFile = new File(outputDir, baseName + ".gv");
        File pngFile = new File(outputDir, baseName + ".png");
        try {
            FileUtils.writeStringToFile(gvFile, graphvizData);
            context.println("Graphviz file written to " + gvFile.getAbsolutePath());
        } catch (IOException e) {
            log.error("Error writing script file " + gvFile.getAbsolutePath(), e);
            return;
        }
        if (GraphvizUtils.renderDotFileToPng(gvFile, pngFile, context.getOutputReceiver())) {
            context.println("PNG file written to " + pngFile.getAbsolutePath());
        } else {
            context.println("Error running graphviz - PNG file " + pngFile.getAbsolutePath() + " was probably not generated");
        }
    }

    private void performNetInfo(CommandContext context) {
        context.println(communicationService.getFormattedNetworkInformation("info"));
    }

    private void performNetBench(CommandContext context) throws CommandException {
        context.setDeveloperCommandSetEnabled(true);
        List<String> parameters = context.consumeRemainingTokens();
        if (parameters.size() != 1) {
            throw CommandException.wrongNumberOfParameters(context);
        }

        BenchmarkSetup setup;
        try {
            String benchmarkDescription = parameters.get(0);
            setup = benchmarkService.parseBenchmarkDescription(benchmarkDescription);
        } catch (IllegalArgumentException e) {
            throw CommandException.syntaxError("Error parsing benchmark setup: " + e.toString(), context);
        }

        context.println("Benchmark starting");
        benchmarkService.executeBenchmark(setup, context.getOutputReceiver());
        context.println("Benchmark complete");
    }

    private void performNetFilter(CommandContext context) throws CommandException {
        String nextToken = context.consumeNextToken();
        if (nextToken == null) {
            // show status
            messageChannelService.printIPFilterInformation(context.getOutputReceiver());
        } else if ("reload".equals(nextToken)) {
            messageChannelService.loadAndApplyIPFilterConfiguration();
            messageChannelService.printIPFilterInformation(context.getOutputReceiver());
        } else {
            throw CommandException.unknownCommand(context);
        }
    }

    private void performNetNp(CommandContext context) {
        Set<InstanceNodeSessionId> nodes = communicationService.getReachableInstanceNodes();
        Map<InstanceNodeSessionId, Map<String, String>> allMetadata = nodePropertiesService.getAllNodeProperties(nodes);
        context.println("Known node properties:");
        for (Map.Entry<InstanceNodeSessionId, Map<String, String>> entry1 : allMetadata.entrySet()) {
            InstanceNodeSessionId nodeId = entry1.getKey();
            context.println(nodeId.toString());
            Map<String, String> map = entry1.getValue();
            for (Map.Entry<String, String> entry2 : map.entrySet()) {
                context.println(StringUtils.format("  %s = %s", entry2.getKey(), entry2.getValue()));
            }
        }

    }

}
