package com.blazemeter.jmeter.debugger.gui;

import com.blazemeter.jmeter.debugger.elements.DebuggingThreadGroup;
import com.blazemeter.jmeter.debugger.elements.OriginalLink;
import com.blazemeter.jmeter.debugger.elements.ThreadGroupWrapper;
import com.blazemeter.jmeter.debugger.elements.Wrapper;
import com.blazemeter.jmeter.debugger.engine.Debugger;
import com.blazemeter.jmeter.debugger.engine.DebuggerFrontend;
import com.blazemeter.jmeter.debugger.engine.SearchClass;
import com.blazemeter.jmeter.debugger.engine.TestTreeProvider;
import org.apache.jmeter.JMeter;
import org.apache.jmeter.control.ReplaceableController;
import org.apache.jmeter.engine.TreeCloner;
import org.apache.jmeter.exceptions.IllegalUserActionException;
import org.apache.jmeter.gui.GuiPackage;
import org.apache.jmeter.gui.JMeterGUIComponent;
import org.apache.jmeter.gui.tree.JMeterTreeModel;
import org.apache.jmeter.gui.tree.JMeterTreeNode;
import org.apache.jmeter.reporters.ResultCollector;
import org.apache.jmeter.samplers.Clearable;
import org.apache.jmeter.samplers.SampleEvent;
import org.apache.jmeter.samplers.Sampler;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.threads.AbstractThreadGroup;
import org.apache.jmeter.threads.JMeterContext;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.collections.HashTree;
import org.apache.jorphan.collections.SearchByClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.*;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

public class DebuggerDialog extends DebuggerDialogBase implements DebuggerFrontend, TestTreeProvider {
    private static final Logger log = LoggerFactory.getLogger(DebuggerDialog.class);

    private boolean savedDirty = false;
    protected Debugger debugger = null;
    protected ResultCollector lastResultListener;

    public DebuggerDialog() {
        super();
        start.addActionListener(new StartDebugging());
        stop.addActionListener(new StopDebugging());
        step.addActionListener(new StepOver());
        pauseContinue.addActionListener(new PauseContinue());
        tgCombo.addItemListener(new ThreadGroupChoiceChanged());
        lastResultListener = (ResultCollector) lastSamplerResult.createTestElement();
    }

    @Override
    public void componentShown(ComponentEvent e) {
        log.debug("Showing dialog");
        if (GuiPackage.getInstance() != null) {
            savedDirty = GuiPackage.getInstance().isDirty();
        }
        this.debugger = new Debugger(this, this);
        tgCombo.removeAllItems();
        for (AbstractThreadGroup group : debugger.getThreadGroups()) {
            tgCombo.addItem(group);
        }

        AbstractThreadGroup selectedThreadGroup = debugger.getSelectedThreadGroup();
        if (selectedThreadGroup != null) {
            changeComboValue(selectedThreadGroup);
        }

        tgCombo.setEnabled(tgCombo.getItemCount() > 0);
        start.setEnabled(tgCombo.getItemCount() > 0);
        start.requestFocus();
        clearListeners();
    }

    private void changeComboValue(AbstractThreadGroup selectedThreadGroup) {
        ComboBoxModel<AbstractThreadGroup> model = this.tgCombo.getModel();
        for (int i = 0; i < model.getSize(); i++) {
            if (model.getElementAt(i).equals(selectedThreadGroup)) {
                tgCombo.setSelectedIndex(i);
            }
        }
    }

    @Override
    public void componentHidden(ComponentEvent e) {
        log.debug("Closing dialog");

        debugger.stop();
        if (GuiPackage.getInstance() != null) {
            GuiPackage.getInstance().setDirty(savedDirty);
        }
        clearListeners();
        clearStatusPane();
    }

    @Override
    public HashTree getTestTree() {
        GuiPackage gui = GuiPackage.getInstance();
        return gui.getTreeModel().getTestPlan();
    }

    private void clearListeners() {
        GuiPackage guiPackage = GuiPackage.getInstance();
        for (JMeterTreeNode node : guiPackage.getTreeModel().getNodesOfType(Clearable.class)) {
            JMeterGUIComponent guiComp = guiPackage.getGui(node.getTestElement());
            if (guiComp instanceof Clearable){
                Clearable item = (Clearable) guiComp;
                try {
                    item.clearData();
                } catch (Exception ex) {
                    log.error("Can't clear: {} {}", node, guiComp, ex);
                }
            }
        }
    }

    private void toggleControls(boolean state) {
        tgCombo.setEnabled(state);
        start.setEnabled(state);
        stop.setEnabled(!state);
        pauseContinue.setEnabled(!state);
        evaluatePanel.setEnabled(!state);
    }

    private void refreshVars(JMeterContext context) {
        varsTableModel.clearData();
        for (Map.Entry<String, Object> var : context.getVariables().entrySet()) {
            varsTableModel.addRow(new String[]{var.getKey(), var.getValue().toString()});
        }
        varsTableModel.fireTableDataChanged();
        if (context.getPreviousResult() != null) {
            lastResultListener.sampleOccurred(new SampleEvent(context.getPreviousResult(), context.getThreadGroup().getName()));
        }
    }

    private void refreshProperties() {
        propsTableModel.clearData();
        for (Map.Entry<Object, Object> var : JMeterUtils.getJMeterProperties().entrySet()) {
            propsTableModel.addRow(new String[]{var.getKey().toString(), var.getValue().toString()});
        }
        propsTableModel.fireTableDataChanged();
    }

    private void selectTargetInTree(Wrapper dbgElm) {
        TestElement wrpElm = (TestElement) dbgElm.getWrappedElement();
        TreePath treePath = getTreePathFor(wrpElm);
        if (treePath == null) {
            // case for wrapped controllers
            treePath = getTreePathFor(dbgElm);
        }

        if (treePath == null) {
            log.debug("Did not find tree path for element");
        } else {
            if (treePath.equals(tree.getSelectionPath())) {
                tree.setSelectionPath(treePath.getParentPath());
            }
            tree.setSelectionPath(treePath);
        }

        Sampler sampler = debugger.getCurrentSampler();
        if (sampler != null) {
            TreePath samplerPath = getTreePathFor(sampler);
            if (samplerPath != null) {
                log.debug("Expanding: " + samplerPath);
                tree.expandPath(samplerPath.getParentPath());
            }
        }
        
        tree.repaint();
    }

    private TreePath getTreePathFor(TestElement te) {
        List<Object> nodes = new ArrayList<>();
        JMeterTreeModel model = (JMeterTreeModel) tree.getModel();

        TreeNode treeNode = model.getNodeOf(te);
        if (treeNode != null) {
            nodes.add(treeNode);
            treeNode = treeNode.getParent();
            while (treeNode != null) {
                nodes.add(0, treeNode);
                treeNode = treeNode.getParent();
            }
        }

        return nodes.isEmpty() ? null : new TreePath(nodes.toArray());
    }

    private void selectThreadGroup(AbstractThreadGroup tg) {
        debugger.selectThreadGroup(tg);
        treeModel.clearTestPlan();
        HashTree origTree = debugger.getSelectedTree();
        TreeCloner cloner = new TreeCloner();
        origTree.traverse(cloner);
        HashTree selectedTree = cloner.getClonedTree();

        // Hack to resolve ModuleControllers from JMeter.java
        SearchClass<ReplaceableController> replaceableControllers = new SearchClass<>(ReplaceableController.class);
        selectedTree.traverse(replaceableControllers);
        Collection<ReplaceableController> replaceableControllersRes = replaceableControllers.getSearchResults();
        for (ReplaceableController replaceableController : replaceableControllersRes) {
            replaceableController.resolveReplacementSubTree((JMeterTreeNode) treeModel.getRoot());
        }

        JMeter.convertSubTree(selectedTree);
        try {
            treeModel.addSubTree(selectedTree, (JMeterTreeNode) treeModel.getRoot());
        } catch (IllegalUserActionException e) {
            throw new RuntimeException(e);
        }

        // select TG for visual convenience
        SearchByClass<DebuggingThreadGroup> tgs = new SearchByClass<>(DebuggingThreadGroup.class);
        selectedTree.traverse(tgs);
        for (DebuggingThreadGroup forSel : tgs.getSearchResults()) {
            Wrapper<AbstractThreadGroup> wtg = new ThreadGroupWrapper();
            wtg.setWrappedElement(forSel);
            selectTargetInTree(wtg);
        }
    }

    @Override
    public void highlightNode(Component component, JMeterTreeNode node, TestElement mc) {
        component.setFont(component.getFont().deriveFont(~Font.BOLD).deriveFont(~Font.ITALIC));
        TestElement userObject = (TestElement) node.getUserObject();
        if (Debugger.isBreakpoint(userObject)) {
            component.setForeground(Color.RED);
        }

        if (debugger == null) {
            return;
        }

        Wrapper currentElement = debugger.getCurrentElement();
        if (currentElement == null) {
            return;
        }

        Font font = component.getFont();
        TestElement currentWrapped = (TestElement) currentElement.getWrappedElement();
        if (mc == currentElement || mc == currentWrapped) {
            setComponentStyle(component, font, Font.BOLD, Font.BOLD);
        }

        Sampler currentSampler = debugger.getCurrentSampler();
        if (mc == currentSampler) { // can this ever happen?
            setComponentStyle(component, font, Font.BOLD + Font.ITALIC, font.getStyle() | Font.ITALIC);
        } else if (currentSampler instanceof Wrapper && mc == ((Wrapper) currentSampler).getWrappedElement()) {
            setComponentStyle(component, font, Font.BOLD + Font.ITALIC, font.getStyle() | Font.ITALIC);
        }
    }

    private void setComponentStyle(Component component, Font font, int macStyle, int otherOsStyle) {
        if (isMac()) {
            component.setFont(new Font(font.getName(), macStyle, font.getSize()));
            component.setForeground(Color.BLACK);
        } else {
            component.setFont(font.deriveFont(otherOsStyle));
            component.setForeground(Color.BLUE);
        }
    }

    public static boolean isMac() {
        return (System.getProperty("os.name").toLowerCase().contains("mac"));
    }

    @Override
    public void valueChanged(TreeSelectionEvent treeSelectionEvent) {
        JMeterTreeNode node = (JMeterTreeNode) treeSelectionEvent.getPath().getLastPathComponent();
        TestElement wrpElm = node.getTestElement();
        if (wrpElm instanceof OriginalLink) {
            TestElement te = (TestElement) ((OriginalLink) wrpElm).getOriginal();
            if (!(te instanceof AbstractThreadGroup)) {
                wrpElm = te;
            }
        }

        displayElementGui(wrpElm);
    }

    private synchronized void displayElementGui(TestElement wrpElm) {
        GuiPackage gui = GuiPackage.getInstance();
        if (gui != null) {
            JMeterGUIComponent egui = gui.getGui(wrpElm);
            egui.configure(wrpElm);
            egui.modifyTestElement(wrpElm);

            elementContainer.removeAll();
            if (egui instanceof Component) {
//                egui.setEnabled(false);
                elementContainer.add((Component) egui, BorderLayout.CENTER);
            }
            elementContainer.updateUI();
        }
    }

    @Override
    public void started() {
        loggerPanel.clear();
        toggleControls(false);
    }

    @Override
    public void stopped() {
        toggleControls(true);
        elementContainer.removeAll();
    }

    @Override
    public void frozenAt(Wrapper wrapper) {
        pauseContinue.setText("Continue");
        pauseContinue.setIcon(DebuggerMenuItem.getContinueIcon());

        step.setEnabled(true);
        selectTargetInTree(wrapper);
    }

    @Override
    public void statusRefresh(JMeterContext context) {
        try {
            refreshVars(context);
            refreshProperties();
        } catch (Throwable e) {
            log.warn("Problem refreshing status pane", e);
        }
        evaluatePanel.refresh(context, debugger.isContinuing());
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                tree.repaint();
            }
        });
    }

    @Override
    public void continuing() {
        // to prevent buttons "jumping"
        pauseContinue.setMinimumSize(pauseContinue.getSize());
        pauseContinue.setPreferredSize(pauseContinue.getSize());
        pauseContinue.setSize(pauseContinue.getSize());

        pauseContinue.setText("Pause");
        pauseContinue.setIcon(DebuggerMenuItem.getPauseIcon());
        step.setEnabled(false);
    }

    private class ThreadGroupChoiceChanged implements ItemListener {
        @Override
        public void itemStateChanged(ItemEvent event) {
            if (event.getStateChange() == ItemEvent.SELECTED) {
                log.debug("Item choice changed: " + event.getItem());
                if (event.getItem() instanceof AbstractThreadGroup) {
                    selectThreadGroup((AbstractThreadGroup) event.getItem());
                }
            }
        }
    }

    private class StartDebugging implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            debugger.start();
        }
    }

    private class StepOver implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            synchronized (this) {
                debugger.proceed();
            }
        }
    }

    private class PauseContinue implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent actionEvent) {
            if (debugger.isContinuing()) {
                debugger.pause();
            } else {
                debugger.continueRun();
            }
        }
    }

    private class StopDebugging implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            debugger.stop();
        }
    }

}