package org.ds2os;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.ds2os.connector.Connector;
import org.ds2os.connector.IVirtualNodeHandler;
import org.ds2os.utils.FileWrite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.Level;

/**
 * All measurementClients and the measurementServer subscribe to
 * [measurementController]/timeOfNextRun. (Its change triggers a test run and logging activity on
 * all participating hosts.) Start in the following order: 1) ControlService 2) MeasurementService
 * 3) 1-n clients Trigger a measurement run with setr [mcs]/singleRunTrigger 1 setr
 * [mcs]/fullRunTrigger 1 It runs the according test and collects all logs.
 * @author pahl
 */
public class MeasurementControlService {
    private Connector c;
    private int testRuns;
    private int testRunsClient;
    private final String[] TESTMODES = {
            "get", "set", "vget", "vset"
    };
    private final String[] LOGNODES = {
            "delayLog", "throughputLog"
    };
    private int testMode;
    private int currentNumClients;
    // How many logs have to be collected before the next test is run?
    private int minimumLogsToCollectPerTest;
    // How many logs have been collected so far?
    private int logsCollectedSoFar;
    // delay until the measurement should start
    private Long delayPerClientUntilTestStarts;
    private int availableClientAmount, fullRunStepping, fullRunStartAmount;
    private Long timeOfNextRun;
    private Boolean singleRunTrigger, fullRunTrigger;
    private List<String> remainingClients;
    private List<String> logsToCollectFrom;
    private int retryClient;
    private static int roundsToWaitPerReturningClients = 1;
    private static final Logger LOGGER = LoggerFactory.getLogger(MeasurementControlService.class);

    public MeasurementControlService() {
        remainingClients = new ArrayList<String>();
        testRuns = 1;
        testRunsClient = 100;
        minimumLogsToCollectPerTest = 100;
        fullRunStartAmount = 100;
        fullRunStepping = 10;
        testMode = 0;
        timeOfNextRun = -1L;
        delayPerClientUntilTestStarts = 1L;
        singleRunTrigger = fullRunTrigger = false;
        retryClient = 2;
        logsToCollectFrom = new LinkedList<String>();
        currentNumClients = 1000;
        try {
            boolean registered = false;
            while (!registered) {
                try {
                    c = new Connector();
                    c.registerService("mcs", "/perfmeasure/measurementControl");
                    registered = true;
                } catch (Exception e) {
                    LOGGER.warn("Register: " + e.getLocalizedMessage());
                    Thread.sleep(2000);
                }
            }
            LOGGER.info("Registered mcs.");

            // Register handlers
            // Collect feedback from finished nodes and remove them from remainingClients list.
            c.registerVirtualNode(c.getKORSubtree() + "/remainingClientIds",
                    new IVirtualNodeHandler() {
                        // Remove me
                        @Override
                        public void set(String address, byte[] value, String writerID) {
                            String val = new String(value);
                            LOGGER.info("{} finished. Removing from {}.", val, remainingClients);
                            remainingClients.remove(val);
                            LOGGER.info("Remaining {}.", remainingClients);
                            logsToCollectFrom.add(val);
                        }

                        // Who remains?
                        @Override
                        public byte[] get(String address, String readerID) {
                            String clientList = "nobody";
                            for (String nextClient : remainingClients) {
                                clientList += "," + nextClient;
                            }
                            return clientList.getBytes();
                        }
                    });

            // amI participating?
            c.registerVirtualNode(c.getKORSubtree() + "/amIparticipating",
                    new IVirtualNodeHandler() {
                        // Remove me
                        @Override
                        public void set(String address, byte[] value, String writerID) {
                            // nothing
                        }

                        // Who remains?
                        @Override
                        public byte[] get(String address, String readerID) {
                            String IdToCheck = address.substring((c.getId() + "/amIparticipating")
                                    .length());
                            // System.out.println(IdToCheck + " check");
                            Boolean amIparticipating = remainingClients.contains(IdToCheck);
                            return (amIparticipating ? "1" : "0").getBytes();
                        }
                    });

            // How many test runs should be done in this service?
            c.registerVirtualNode(c.getKORSubtree() + "/testRuns", new IVirtualNodeHandler() {
                // Remove me
                @Override
                public void set(String address, byte[] value, String writerID) {
                    testRuns = Integer.parseInt(new String(value));
                }

                // Who remains?
                @Override
                public byte[] get(String address, String readerID) {
                    return Integer.toString(testRuns).getBytes();
                }
            });

            // How many logs must be collected?
            c.registerVirtualNode(c.getKORSubtree() + "/minimumLogsToCollectPerTest",
                    new IVirtualNodeHandler() {
                        // Remove me
                        @Override
                        public void set(String address, byte[] value, String writerID) {
                            minimumLogsToCollectPerTest = Integer.parseInt(new String(value));
                        }

                        // Who remains?
                        @Override
                        public byte[] get(String address, String readerID) {
                            return Integer.toString(minimumLogsToCollectPerTest).getBytes();
                        }
                    });

            // How long to wait (delayPerClientUntilTestStarts * currentNumClients) after the
            // collection of the participating nodes until the test
            // starts?
            c.registerVirtualNode(c.getKORSubtree() + "/waitUntilNextStart",
                    new IVirtualNodeHandler() {
                        // Remove me
                        @Override
                        public void set(String address, byte[] value, String writerID) {
                            delayPerClientUntilTestStarts = Long.parseLong(new String(value));
                        }

                        // Who remains?
                        @Override
                        public byte[] get(String address, String readerID) {
                            return Long.toString(delayPerClientUntilTestStarts).getBytes();
                        }
                    });

            // How many test runs should be done in the clients?
            c.registerVirtualNode(c.getKORSubtree() + "/testRunsClient",
                    new IVirtualNodeHandler() {
                        // Remove me
                        @Override
                        public void set(String address, byte[] value, String writerID) {
                            testRunsClient = Integer.parseInt(new String(value));
                        }

                        // Who remains?
                        @Override
                        public byte[] get(String address, String readerID) {
                            return Integer.toString(testRunsClient).getBytes();
                        }
                    });

            // Which is the test to be run next?
            c.registerVirtualNode(c.getKORSubtree() + "/testMode", new IVirtualNodeHandler() {
                @Override
                public void set(String address, byte[] value, String writerID) {
                    int val = Integer.parseInt(new String(value));
                    if (val >= 0 && TESTMODES.length >= val) {
                        testMode = val;
                    }
                }

                @Override
                public byte[] get(String address, String readerID) {
                    return Integer.toString(testMode).getBytes();
                }
            });

            // Which test modes exist?
            c.registerVirtualNode(c.getKORSubtree() + "/testModes", new IVirtualNodeHandler() {
                @Override
                public void set(String address, byte[] value, String writerID) {
                    // nothing
                }

                @Override
                public byte[] get(String address, String readerID) {
                    String allModes = "";
                    int counter = 0;
                    for (String nextMode : TESTMODES) {
                        allModes += "[" + (counter++) + ":" + nextMode + "]";
                    }
                    return allModes.getBytes();
                }
            });

            // Is/ Should a test run?
            c.registerVirtualNode(c.getKORSubtree() + "/singleRunTrigger",
                    new IVirtualNodeHandler() {
                        @Override
                        public void set(String address, byte[] value, String writerID) {
                            singleRunTrigger = (new String(value).equals("1"));
                            if (singleRunTrigger) {
                                System.out.println("Starting test " + TESTMODES[testMode]
                                        + " with " + testRuns + " runs.");
                                logsCollectedSoFar = 0;
                                while (logsCollectedSoFar < minimumLogsToCollectPerTest) {
                                    runTest();
                                    collectLogs(1, currentNumClients);
                                }
                            }
                        }

                        @Override
                        public byte[] get(String address, String readerID) {
                            return singleRunTrigger.toString().getBytes();
                        }
                    });

            // Is/ Should a test run?
            c.registerVirtualNode(c.getKORSubtree() + "/fullRunTrigger",
                    new IVirtualNodeHandler() {
                        @Override
                        public void set(String address, byte[] value, String writerID) {
                            fullRunTrigger = (new String(value).equals("1"));
                            if (fullRunTrigger) {
                                System.out.println("Starting test " + TESTMODES[testMode]
                                        + " with " + testRuns);
                                startFullTest();
                            }
                        }

                        @Override
                        public byte[] get(String address, String readerID) {
                            return fullRunTrigger.toString().getBytes();
                        }
                    });

            c.registerVirtualNode(c.getKORSubtree() + "/fullRunStartAmount",
                    new IVirtualNodeHandler() {
                        @Override
                        public void set(String address, byte[] value, String writerID) {
                            fullRunStartAmount = Integer.parseInt(new String(value));
                        }

                        @Override
                        public byte[] get(String address, String readerID) {
                            return Integer.toString(fullRunStartAmount).getBytes();
                        }
                    });

            c.registerVirtualNode(c.getKORSubtree() + "/fullRunStepping",
                    new IVirtualNodeHandler() {
                        @Override
                        public void set(String address, byte[] value, String writerID) {
                            fullRunStepping = Integer.parseInt(new String(value));
                        }

                        @Override
                        public byte[] get(String address, String readerID) {
                            return Integer.toString(fullRunStepping).getBytes();
                        }
                    });

            // make all nodes accessible
            c.addReaderToSubTree(c.getKORSubtree(), "*");
            c.addWriterToSubTree(c.getKORSubtree(), "*");
        } catch (Exception e) {
            LOGGER.warn(e.getMessage());
        }
    }

    private void runTest() {
        try {
            if (singleRunTrigger) {
                // collect availableClients
                List<String> clients = c.getNodesOfType("/", "/perfmeasure/measurementClient");
                availableClientAmount = clients.size();
                remainingClients = new ArrayList<String>();
                LOGGER.info("Running with {}/{} clients.", currentNumClients,
                        availableClientAmount);
                while (clients.size() > 0 && remainingClients.size() < currentNumClients) {
                    int nextClientNr = (int) (Math.random() * clients.size());
                    String nextClientId = clients.get(nextClientNr);
                    clients.remove(nextClientNr);
                    // Is it reachable?
                    LOGGER.info("Pinging client {}", nextClientId);
                    int counter = 0;
                    boolean success = false;
                    while (!success && counter++ < retryClient) {
                        try {
                            String result = c.get(nextClientId + "/ping");
                            if (result == null) {
                                LOGGER.warn("{} is not reachable via ping.", nextClientId);
                                success = true;
                            } else if (result.equals("pong")) {
                                LOGGER.info("Using client {}", nextClientId);
                                remainingClients.add(nextClientId);
                                success = true;
                            }
                        } catch (Exception e) {
                            LOGGER.warn("Ping {}: {}", nextClientId, e.getMessage());
                            Thread.sleep(1000);
                        }
                    }
                }

                if (remainingClients.size() < currentNumClients) {
                    throw new Exception("Not enough Clients! " + remainingClients.size() + "<"
                            + currentNumClients);
                }

                // So we have our client set in remainingClients
                // Set the timer...
                timeOfNextRun = System.currentTimeMillis() / 1000L
                        + (delayPerClientUntilTestStarts * currentNumClients);
                // this is with notification:
                // For the measurementServer
                c.set(c.getKORSubtree() + "/timeOfNextRun", Long.toString(timeOfNextRun));
                // this is via explicit set Virtual Node
                System.out.println("Explicitly triggering...");
                for (int i = 0; i < remainingClients.size(); i++) {
                    c.set(remainingClients.get(i) + "/timeOfNextRun", Long.toString(timeOfNextRun));
                    System.out.println(i + "\t" + remainingClients.get(i) + "/timeOfNextRun");
                }
                Long remainingTime = timeOfNextRun - System.currentTimeMillis() / 1000L;
                System.out.println("Test run " + TESTMODES[testMode] + " with "
                        + remainingClients.size() + "/" + currentNumClients + " will start in "
                        + remainingTime + "s at " + Long.toString(timeOfNextRun));

                Thread.sleep(remainingTime * 1000);
                LOGGER.info("Start collecting logs from {} clients.", remainingClients.size());

                // Wait until all clients finished
                int previousClientSize = remainingClients.size();
                int roundsWaited = roundsToWaitPerReturningClients * remainingClients.size();
                while (remainingClients.size() > 0) {
                    LOGGER.info("{} clients remaining... waiting {} more rounds...",
                            remainingClients.size(), roundsWaited);
                    if (previousClientSize == remainingClients.size() && roundsWaited-- <= 0) {
                        // // Add a grace periode...
                        // LOGGER.info("Waiting {}s...",
                        // (delayPerClientUntilTestStarts * currentNumClients));
                        // try {
                        // Thread.sleep((delayPerClientUntilTestStarts * currentNumClients) *
                        // 1000L);
                        // } catch (InterruptedException e1) {
                        // LOGGER.warn("Error while waiting?!");
                        // e1.printStackTrace();
                        // }
                        LOGGER.warn("Not waiting for {} clients anymore.", remainingClients.size());
                        remainingClients.clear();
                    } else {
                        // wait;
                        Thread.sleep(5000);
                    }
                    previousClientSize = remainingClients.size();
                }
                singleRunTrigger = false;
            }
        } catch (Exception e) {
            LOGGER.warn(e.getLocalizedMessage());
        }
    }

    private void collectLogs(int testRun, int participatingAmount) {
        String prefix = TESTMODES[testMode] + "-clients-"
                + String.format("%04d", participatingAmount) + "_nr-"
                + String.format("%04d", testRun) + "_" + Long.toString(timeOfNextRun) + "_";
        // measurementServer replies automatically and is collected therefore...
        while (!logsToCollectFrom.isEmpty()) {
            String nextClient = logsToCollectFrom.get(0);
            for (String nextLog : LOGNODES) {
                FileWrite f = new FileWrite(prefix + nextLog + "_" + nextClient.replace("/", "-")
                        + ".log");
                try {
                    LOGGER.info("Adding log {}/{}.", nextClient, nextLog);
                    String logData = c.get(nextClient + "/" + nextLog);
                    if (!logData.isEmpty() && logData != null) {
                        f.append(logData);
                        logsCollectedSoFar++;
                    }
                } catch (Exception e) {
                    LOGGER.error(prefix + nextLog + "_" + nextClient + ".log: "
                            + e.getLocalizedMessage());
                }
                f.closeFile();
                System.out.println("Wrote log " + prefix + nextLog + "_"
                        + nextClient.replace("/", "-") + ".log");
            }
            logsToCollectFrom.remove(0);
        }
        System.out.println("Collected " + logsCollectedSoFar + " of minimum required "
                + minimumLogsToCollectPerTest);
    }

    private void startFullTest() {
        // execution of all the test trigger and log collecting logic...
        int initialTestRuns = testRuns;
        logsCollectedSoFar = 0;
        try {
            availableClientAmount = c.getNodesOfType("/", "/perfmeasure/measurementClient").size();
        } catch (Exception e) {
            LOGGER.error("Could not find clients! " + e.getLocalizedMessage());
            e.printStackTrace();
        }
        currentNumClients = fullRunStartAmount;
        while (currentNumClients <= availableClientAmount && currentNumClients > 0) {
            do {
                while (testRuns-- > 0) {
                    singleRunTrigger = true;
                    runTest();
                    collectLogs(initialTestRuns - testRuns, currentNumClients);
                }
                testRuns = initialTestRuns;
            } while (// enough logs collected?
            logsCollectedSoFar < minimumLogsToCollectPerTest || (// yes -> reset counter:
                    (logsCollectedSoFar = 0) == 0 &&
                    // switch to next test mode:
                    ++testMode < TESTMODES.length));
            currentNumClients -= fullRunStepping;
            testMode = 0;
        }
        fullRunTrigger = false;
    }

    public static void main(String[] args) throws InterruptedException {
        ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory
                .getLogger(Logger.ROOT_LOGGER_NAME);
        root.setLevel(Level.INFO);
        MeasurementControlService mcs = new MeasurementControlService();
        // Trigger via vsl
        // Thread.sleep(2000);
        // System.out.println("Starting testrun routine...");
        // new Thread(mcn).start();
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String quitString;
        try {
            do {
                System.out.println("Type q to end this service.");
            } while ((quitString = in.readLine()) != null && !quitString.equals("q"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("Shutting down...");
        mcs.shutdown();
    }

    public final void shutdown() {
        // To end current loops:
        // Set to all collected
        logsCollectedSoFar = minimumLogsToCollectPerTest + 1;
        // Set to last test
        testMode = TESTMODES.length;
        if (c != null) {
            c.shutdown();
        }
    }
}
