package eu.gity.jmeter.ddos.stairsddosthreadgrp;

import eu.gity.jmeter.ddos.common.DDoSThreadGroup;
import eu.gity.jmeter.ddos.common.Trafgen;
import org.apache.jmeter.engine.StandardJMeterEngine;
import org.apache.jmeter.gui.GuiPackage;
import org.apache.jmeter.gui.action.ActionRouter;
import org.apache.jmeter.gui.action.EditCommand;
import org.apache.jmeter.gui.tree.JMeterTreeListener;
import org.apache.jmeter.gui.tree.JMeterTreeNode;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.threads.JMeterThread;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeNode;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Enumeration;

/**
 * DDoS thread group TestElement class which uses stair-type advanced load planning
 */
public class StairsDDoSThreadGroup extends AbstractDDoSThreadGroup implements DDoSThreadGroup {

    private static final long serialVersionUID = 240L;

    private static final Logger log = LoggerFactory.getLogger(StairsDDoSThreadGroup.class);
    private static final String THREAD_GROUP_DELAY = "Threads initial delay";
    private static final String INC_USER_PERIOD = "Start users period";
    private static final String INC_USER_COUNT = "Start users count";
    private static final String INC_USER_COUNT_BURST = "Start users count burst";
    private static final String DEC_USER_PERIOD = "Stop users period";
    private static final String DEC_USER_COUNT = "Stop users count";
    private static final String FLIGHT_TIME = "flighttime";

    public StairsDDoSThreadGroup() {
        super();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void broadcastSamplerRateStrategy() {
        StairsDDoSThreadGroup tg = this;
        //Broadcast every time a DDoS node is added
        GuiPackage.getInstance().getTreeModel().addTreeModelListener(new TreeModelListener() {

            @Override
            public void treeNodesChanged(TreeModelEvent e) {
            }

            @Override
            public void treeNodesInserted(TreeModelEvent e) {
                Object[] children = e.getChildren();
                for (Object child : children) {
                    if (((JMeterTreeNode) child).getUserObject() instanceof Trafgen) {
                        //If parent of this DDoS node is this object...
                        if (((JMeterTreeNode) ((JMeterTreeNode) child).getParent()).getUserObject().equals(tg)) {
                            JMeterTreeNode tgNode = GuiPackage.getInstance().getNodeOf(tg);
                            if (tgNode != null) {
                                @SuppressWarnings("unchecked") Enumeration<TreeNode> kids = tgNode.children();
                                while (kids.hasMoreElements()) {
                                    JMeterTreeNode childTN = (JMeterTreeNode) kids.nextElement();
                                    TestElement childTE = childTN.getTestElement();
                                    if (childTE instanceof Trafgen) {
                                        ((Trafgen) childTE).setDynamicRate(false);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            @Override
            public void treeNodesRemoved(TreeModelEvent e) {
            }

            @Override
            public void treeStructureChanged(TreeModelEvent e) {
            }
        });

        //Broadcast every time on "EditCommand" event
        ActionRouter.getInstance().addPreActionListener(EditCommand.class, new ActionListener() {
            //TODO move to DDoSUtils??
            @Override
            public void actionPerformed(ActionEvent e) {
                if (e.getSource() instanceof JMeterTreeListener) {
                    JMeterTreeNode currentNode = ((JMeterTreeListener) e.getSource()).getCurrentNode();
                    JMeterTreeNode parentNode = ((JMeterTreeNode) currentNode.getParent());
                    if (parentNode != null) {
                        //If my own Trafgen child sampler is being edited
                        if (currentNode.getTestElement() instanceof Trafgen && parentNode.getTestElement().equals(tg)) {
                            @SuppressWarnings("unchecked") Enumeration<TreeNode> children = parentNode.children();
                            while (children.hasMoreElements()) {
                                JMeterTreeNode childTN = (JMeterTreeNode) children.nextElement();
                                TestElement childTE = childTN.getTestElement();
                                if (childTE instanceof Trafgen) {
                                    ((Trafgen) childTE).setDynamicRate(false);
                                }
                            }
                        }
                    }
                }
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected ArrayList<SettingChange> generateSettingChanges() {
        ArrayList<SettingChange> settingChanges = new ArrayList<SettingChange>();
        int maxPps = getNumThreads();
        long threadGroupDelay = 1000L * getThreadGroupDelayAsInt(); //Initial delay
        int incrementBurst = getInUserCountBurstAsInt();

        int incrementBy = getInUserCountAsInt(); //increment by
        if (incrementBy == 0 || incrementBy > maxPps) {
            incrementBy = maxPps;
        }
        long incrementPeriod = 1000L * getInUserPeriodAsInt();
        long flightTime = 1000L * getFlightTimeAsInt();

        int decrementBy = getOutUserCountAsInt(); //decrement by
        if (decrementBy == 0 || decrementBy > maxPps) {
            decrementBy = maxPps;
        }
        long decrementPeriod = 1000L * getOutUserPeriodAsInt();
        long startTime = threadGroupDelay;    //Or 0 if the delay is not set

        //Initial burst
        if (incrementBurst > 0) {
            settingChanges.add(new SettingChange(startTime, incrementBurst));
            startTime += incrementPeriod;
        }

        //Periodic incrementation
        int currentPps = incrementBurst; //Or 0 if burst is not set
        int i = 0;
        while (currentPps < maxPps) {
            settingChanges.add(new SettingChange(startTime + i * incrementPeriod, Math.min(incrementBy, maxPps - currentPps)));
            currentPps += incrementBy;
            i++;
        }
        currentPps = maxPps;
        long maxReachedTime = startTime + (i - 1) * incrementPeriod;

        //Periodic decrementation
        i = 1;
        while (currentPps > 0) {
            settingChanges.add(new SettingChange(maxReachedTime + flightTime + i * decrementPeriod, -Math.min(decrementBy, currentPps)));
            currentPps -= decrementBy;
            i++;
        }
        long endTime = maxReachedTime + flightTime + i * decrementPeriod;
        settingChanges.add(new SettingChange(endTime, 0)); //To instruct AbstractDDoSThreadGroup to terminate trafgen

        return settingChanges;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void scheduleThread(JMeterThread thread, long tgStartTime) {
        int inUserCount = getInUserCountAsInt();
        int outUserCount = getOutUserCountAsInt();

        if (inUserCount == 0) {
            inUserCount = getNumThreads();
        }
        if (outUserCount == 0) {
            outUserCount = getNumThreads();
        }

        int inUserCountBurst = Math.min(getInUserCountBurstAsInt(), getNumThreads());
        if (inUserCountBurst <= 0) {
            inUserCountBurst = inUserCount;
        }

        int rampUpBucket = thread.getThreadNum() < inUserCountBurst ? 0 : 1 + (thread.getThreadNum() - inUserCountBurst) / inUserCount;
        int rampUpBucketThreadCount = thread.getThreadNum() < inUserCountBurst ? inUserCountBurst : inUserCount;

        long threadGroupDelay = 1000L * getThreadGroupDelayAsInt();
        long ascentPoint = tgStartTime + threadGroupDelay;
        long inUserPeriod = 1000L * getInUserPeriodAsInt();

        long flightTime = 1000L * getFlightTimeAsInt();
        long outUserPeriod = 1000L * getOutUserPeriodAsInt();

        long iterationDuration = inUserPeriod;
        // number of complete iteration, ie full (in user time + rampup
        // duration) used
        int iterationCountTotal = getNumThreads() < inUserCountBurst ? 1 : (int) Math.ceil((double) (getNumThreads() - inUserCountBurst) / inUserCount);

        int lastIterationUserCount = (getNumThreads() - inUserCountBurst) % inUserCount;
        if (lastIterationUserCount == 0) {
            lastIterationUserCount = inUserCount;
        }
        long descentPoint = ascentPoint + iterationCountTotal * iterationDuration + flightTime;

        long rampUpBucketStartTime = ascentPoint + rampUpBucket * iterationDuration;
        int rampUpBucketThreadPosition = thread.getThreadNum() < inUserCountBurst ? thread.getThreadNum() : (thread.getThreadNum() - inUserCountBurst) % inUserCount;

        long startTime = rampUpBucketStartTime;
        long endTime = descentPoint + outUserPeriod * (int) Math.floor((double) thread.getThreadNum() / outUserCount);

        log.debug(String.format("threadNum=%d, rampUpBucket=%d, rampUpBucketThreadCount=%d, rampUpBucketStartTime=%d, rampUpBucketThreadPosition=%d, iterationDuration=%d, iterationCountTotal=%d, ascentPoint=%d, descentPoint=%d, startTime=%d, endTime=%d", thread.getThreadNum(), rampUpBucket, rampUpBucketThreadCount, rampUpBucketStartTime, rampUpBucketThreadPosition, iterationDuration, iterationCountTotal, ascentPoint, descentPoint, startTime, endTime));

        thread.setStartTime(startTime);
        thread.setEndTime(endTime);
        thread.setScheduled(true);
    }

    public String getThreadGroupDelay() {
        return getPropertyAsString(THREAD_GROUP_DELAY);
    }

    public void setThreadGroupDelay(String delay) {
        setProperty(THREAD_GROUP_DELAY, delay);
    }

    public String getInUserPeriod() {
        return getPropertyAsString(INC_USER_PERIOD);
    }

    public void setInUserPeriod(String value) {
        setProperty(INC_USER_PERIOD, value);
    }

    public String getInUserCount() {
        return getPropertyAsString(INC_USER_COUNT);
    }

    public void setInUserCount(String delay) {
        setProperty(INC_USER_COUNT, delay);
    }

    public String getInUserCountBurst() {
        return getPropertyAsString(INC_USER_COUNT_BURST);
    }

    public void setInUserCountBurst(String text) {
        setProperty(INC_USER_COUNT_BURST, text);
    }

    public String getFlightTime() {
        return getPropertyAsString(FLIGHT_TIME);
    }

    public void setFlightTime(String delay) {
        setProperty(FLIGHT_TIME, delay);
    }

    public String getOutUserPeriod() {
        return getPropertyAsString(DEC_USER_PERIOD);
    }

    public void setOutUserPeriod(String delay) {
        setProperty(DEC_USER_PERIOD, delay);
    }

    public String getOutUserCount() {
        return getPropertyAsString(DEC_USER_COUNT);
    }

    public void setOutUserCount(String delay) {
        setProperty(DEC_USER_COUNT, delay);
    }

    public int getThreadGroupDelayAsInt() {
        return getPropertyAsInt(THREAD_GROUP_DELAY);
    }

    public int getInUserPeriodAsInt() {
        return getPropertyAsInt(INC_USER_PERIOD);
    }

    public int getInUserCountAsInt() {
        return getPropertyAsInt(INC_USER_COUNT);
    }

    public int getInUserCountBurstAsInt() {
        return getPropertyAsInt(INC_USER_COUNT_BURST);
    }

    public int getFlightTimeAsInt() {
        return getPropertyAsInt(FLIGHT_TIME);
    }

    public int getOutUserPeriodAsInt() {
        return getPropertyAsInt(DEC_USER_PERIOD);
    }

    public int getOutUserCountAsInt() {
        return getPropertyAsInt(DEC_USER_COUNT);
    }

    public void setNumThreads(String execute) {
        setProperty(NUM_THREADS, execute);
    }

    public String getNumThreadsAsString() {
        return getPropertyAsString(NUM_THREADS);
    }

    @Override
    public JMeterThread addNewThread(int delay, StandardJMeterEngine engine) {
        return null;
    }
}
