package kg.apc.jmeter.vizualizers;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import kg.apc.charting.AbstractGraphRow;
import kg.apc.jmeter.CMDLineArgumentsProcessor;
import kg.apc.jmeter.JMeterPluginsUtils;
import kg.apc.jmeter.graphs.AbstractOverTimeVisualizer;
import kg.apc.jmeter.gui.ButtonPanelAddCopyRemove;
import kg.apc.jmeter.vizualizers.CorrectedResultCollector;
import kg.apc.jmeter.vizualizers.JSettingsPanel;
import org.apache.jmeter.gui.util.PowerTableModel;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.NullProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;


public class PageDataExtractorOverTimeGui extends AbstractOverTimeVisualizer implements CMDLineArgumentsProcessor {

    private static final String REGEXPS_PROPERTY = "regexps";
    private static final Logger log = LoggerFactory.getLogger(PageDataExtractorOverTimeGui.class);
    private PowerTableModel tableModel;
    private JTable grid;
    public static final String[] columnIdentifiers = new String[]{
        "Chart label", "Regular expression value extractor", "Delta", "RegExp label"
    };
    public static final Class[] columnClasses = new Class[]{
        String.class, String.class, Boolean.class, Boolean.class
    };
    private static Object[] defaultValues = new Object[]{
        "", "", false, false
    };
    private CollectionProperty regExps = null;
    private HashMap<String, Pattern> patterns = new HashMap<>();
    private ArrayList<Object> cmdRegExps = null;
    private HashMap<String, Double> oldValues = new HashMap<>();

    public PageDataExtractorOverTimeGui() {
        setGranulation(1000);
        graphPanel.getGraphObject().setYAxisLabel("Metrics Extracted");
        graphPanel.getGraphObject().getChartSettings().setExpendRows(true);
    }

    @Override
    protected JSettingsPanel createSettingsPanel() {
        return new JSettingsPanel(this,
                JSettingsPanel.GRADIENT_OPTION
                | JSettingsPanel.MAXY_OPTION
                | JSettingsPanel.RELATIVE_TIME_OPTION
                | JSettingsPanel.AUTO_EXPAND_OPTION
                | JSettingsPanel.MARKERS_OPTION);
    }

    @Override
    public String getStaticLabel() {
        return JMeterPluginsUtils.prefixLabel("Page Data Extractor");
    }

    @Override
    public String getLabelResource() {
        return getClass().getSimpleName();
    }

    @Override
    public String getWikiPage() {
        return "PageDataExtractor";
    }

    private JTable createGrid() {
        grid = new JTable();
        createTableModel();
        grid.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        grid.setMinimumSize(new Dimension(200, 100));
        grid.getColumnModel().getColumn(0).setPreferredWidth(350);
        grid.getColumnModel().getColumn(1).setPreferredWidth(350);
        grid.getColumnModel().getColumn(2).setPreferredWidth(50);
        grid.getColumnModel().getColumn(3).setPreferredWidth(110);

        return grid;
    }

    private Component createRegExpPanel() {
        JPanel panel = new JPanel(new BorderLayout(5, 5));
        panel.setBorder(BorderFactory.createTitledBorder("Regular Expressions Data Extractor"));
        panel.setPreferredSize(new Dimension(150, 150));

        JScrollPane scroll = new JScrollPane(createGrid());
        scroll.setPreferredSize(scroll.getMinimumSize());
        panel.add(scroll, BorderLayout.CENTER);
        panel.add(new ButtonPanelAddCopyRemove(grid, tableModel, defaultValues), BorderLayout.SOUTH);

        return panel;
    }

    @Override
    protected JPanel getGraphPanelContainer() {
        JPanel panel = new JPanel(new BorderLayout());
        JPanel innerTopPanel = new JPanel(new BorderLayout());

        innerTopPanel.add(createRegExpPanel(), BorderLayout.CENTER);
        innerTopPanel.add(getFilePanel(), BorderLayout.SOUTH);

        panel.add(innerTopPanel, BorderLayout.NORTH);

        return panel;
    }

    private void createTableModel() {
        tableModel = new PowerTableModel(columnIdentifiers, columnClasses);
        grid.setModel(tableModel);
    }

    @Override
    public TestElement createTestElement() {
        TestElement te = super.createTestElement();
        modifyTestElement(te);
        te.setComment(JMeterPluginsUtils.getWikiLinkText(getWikiPage()));
        return te;
    }

    @Override
    public void modifyTestElement(TestElement te) {
        super.modifyTestElement(te);
        if (grid.isEditing()) {
            grid.getCellEditor().stopCellEditing();
        }

        if (te instanceof CorrectedResultCollector) {
            CorrectedResultCollector rc = (CorrectedResultCollector) te;
            CollectionProperty rows = JMeterPluginsUtils.tableModelRowsToCollectionProperty(tableModel, REGEXPS_PROPERTY);
            rc.setProperty(rows);
        }
    }

    @Override
    public void configure(TestElement te) {
        super.configure(te);
        CorrectedResultCollector rc = (CorrectedResultCollector) te;
        JMeterProperty regexpValues = rc.getProperty(REGEXPS_PROPERTY);
        if (!(regexpValues instanceof NullProperty)) {
            JMeterPluginsUtils.collectionPropertyToTableModelRows((CollectionProperty) regexpValues, tableModel, columnClasses);
            regExps = (CollectionProperty) regexpValues;
        } else {
            log.warn("Received null property instead of collection");
        }
    }

    private void processPageRegExpLabel(String pageBody, String regExpKey, String regExpValue, boolean isDelta, long time) {
        //handle multiple keys with same name found with the regexp
        ArrayList<String> labels = new ArrayList<>();
        Pattern patternKey = patterns.get(regExpKey);
        if (patternKey == null) {
            try {
                patternKey = Pattern.compile(regExpKey);
                patterns.put(regExpKey, patternKey);
            } catch (PatternSyntaxException ex) {
                log.error("Error compiling pattern: " + regExpKey);
            }
        }
        Pattern patternValue = patterns.get(regExpValue);
        if (patternValue == null) {
            try {
                patternValue = Pattern.compile(regExpValue);
                patterns.put(regExpValue, patternValue);
            } catch (PatternSyntaxException ex) {
                log.error("Error compiling pattern: " + regExpValue);
            }
        }

        if (patternKey != null && patternValue != null) {
            Matcher mKey = patternKey.matcher(pageBody);
            Matcher mValue = patternValue.matcher(pageBody);

            boolean found = false;

            while (mKey.find() && mValue.find()) {
                found = true;
                String key = mKey.group(1);

                if (labels.contains(key)) {
                    int i = 2;
                    while (labels.contains(key + "_" + i)) {
                        i++;
                    }
                    key = key + "_" + i;
                }

                labels.add(key);

                String sValue = mValue.group(1);

                try {
                    double value = Double.parseDouble(sValue);
                    if (isDelta) {
                        if (oldValues.containsKey(key)) {
                            double delta = value - oldValues.get(key);
                            addRecord(key, time, delta);
                        }
                        oldValues.put(key, value);
                    } else {
                        addRecord(key, time, value);
                    }
                } catch (NumberFormatException ex) {
                    log.error("Value extracted is not a number: " + sValue);
                }
            }
            if (!found) {
                log.warn("No data found for regExpKey: " + regExpKey + " and regExpValue: " + regExpValue);
            }
        }
    }

    private void processPageLabel(String pageBody, String label, String regExpValue, boolean isDelta, long time) {
        Pattern patternValue = patterns.get(regExpValue);
        if (patternValue == null) {
            try {
                patternValue = Pattern.compile(regExpValue);
                patterns.put(regExpValue, patternValue);
            } catch (PatternSyntaxException ex) {
                log.error("Error compiling pattern: " + regExpValue);
            }
        }

        if (patternValue != null) {
            Matcher mValue = patternValue.matcher(pageBody);

            int counter = 0;

            while (mValue.find()) {
                counter++;
                String key;
                if (counter > 1) {
                    key = label + " (" + counter + ")";
                } else {
                    key = label;
                }

                String sValue = mValue.group(1);

                try {
                    double value = Double.parseDouble(sValue);
                    if (isDelta) {
                        if (oldValues.containsKey(key)) {
                            double delta = value - oldValues.get(key);
                            addRecord(key, time, delta);
                        }
                        oldValues.put(key, value);
                    } else {
                        addRecord(key, time, value);
                    }
                } catch (NumberFormatException ex) {
                    log.error("Value extracted is not a number: " + sValue);
                }
            }
            if (counter == 0) {
                log.warn("No data found for regExpValue: " + regExpValue);
            }
        }
    }

    private void addRecord(String label, long time, double value) {

        if (!isSampleIncluded(label)) {
            return;
        }

        AbstractGraphRow row = model.get(label);

        if (row == null) {
            row = getNewRow(model, AbstractGraphRow.ROW_AVERAGES, label, AbstractGraphRow.MARKER_SIZE_SMALL, false, false, false, true, true);
        }
        row.add(time, value);
    }

    @Override
    public void add(SampleResult res) {
        super.add(res);

        if (regExps == null && cmdRegExps == null) {
            return;
        }

        String pageBody = res.getResponseDataAsString();
        long time = normalizeTime(res.getEndTime());

        if (cmdRegExps == null) {
            PropertyIterator iter = regExps.iterator();
            while (iter.hasNext()) {
                CollectionProperty props = (CollectionProperty) iter.next();
                String label = props.get(0).getStringValue();
                String regExpValue = props.get(1).getStringValue();
                Boolean isDelta = props.get(2).getBooleanValue();
                Boolean isLabelRegExp = props.get(3).getBooleanValue();

                if (isLabelRegExp) {
                    processPageRegExpLabel(pageBody, label, regExpValue, isDelta, time);
                } else {
                    processPageLabel(pageBody, label, regExpValue, isDelta, time);
                }
            }
        } else {
            Iterator<Object> regExpIter = cmdRegExps.iterator();
            while (regExpIter.hasNext()) {
                String label = (String) regExpIter.next();
                String regExpValue = (String) regExpIter.next();
                boolean isDelta = (Boolean) regExpIter.next();
                boolean isLabelRegExp = (Boolean) regExpIter.next();

                if (isLabelRegExp) {
                    processPageRegExpLabel(pageBody, label, regExpValue, isDelta, time);
                } else {
                    processPageLabel(pageBody, label, regExpValue, isDelta, time);
                }
            }
        }

        updateGui(null);
    }

    @Override
    public void clearData() {
        oldValues.clear();
        super.clearData();
    }

    private void storeRegExps(String regExps) {
        String[] regStrings = regExps.split("\\{;\\}");

        if (regStrings.length % 4 != 0) {
            throw new IllegalArgumentException("Regular expressions must be succession of key/value/isDelta(true or false)/isRegExpLabel(true or false) separated by {;}");
        }

        ArrayList<Object> data = new ArrayList<>();
        for (int i = 0; i < regStrings.length; i = i + 4) {
            data.add(regStrings[i]);
            data.add(regStrings[i + 1]);
            data.add("true".equalsIgnoreCase(regStrings[i + 2]));
            data.add("true".equalsIgnoreCase(regStrings[i + 3]));
        }

        this.cmdRegExps = data;
    }

    public void processCMDOption(String nextArg, ListIterator args) {
        if (nextArg.equalsIgnoreCase("--extractor-regexps")) {
            // 
            if (!args.hasNext()) {
                throw new IllegalArgumentException("Missing regular expressions");
            }

            storeRegExps((String) args.next());
        } else {
            throw new UnsupportedOperationException("Unknown option: " + nextArg);
        }
    }
}
