package de.renew.gui.nin;

import java.awt.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.*;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;

import de.renew.draw.storables.ontology.Drawing;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.storables.ontology.FigureEnumeration;
import de.renew.gui.CPNApplication;
import de.renew.gui.CPNTextFigure;

/**
 * GUI which opens whenever a user selected the P/T NetInNet
 * formalism and runs the simulation.
 * It lets the user specify the settings with which
 * the {@link SystemNetBuilder} builds the system net.
 * These settings include which kinds of synchronization
 * to use and which channels should be used for these types.
 *
 * @author Lukas Voß
 */
public class ConstructionGUI extends JFrame {

    private static final String FRAME_NAME = "Settings for System Net Creation";
    private final JPanel container;
    private final SystemNetBuilder netBuilder;
    boolean[] options;
    private DefaultListModel<String> model;
    private Map<Integer, Set<String>> channelsForOption;
    private Map<JCheckBox, Object[]> netInstanceSelectionMap;

    /**
     * Constructor for the GUI.
     * It builds the main frame, adds all checkboxes and lists to it
     * and instantiates the {@link SystemNetBuilder} with the
     * user-selected options.
     *
     * @param app to read currently opened drawings and later open the system net
     */
    public ConstructionGUI(CPNApplication app) {
        super(FRAME_NAME);
        this.setLayout(new BoxLayout(this.getContentPane(), BoxLayout.X_AXIS));
        container = new JPanel();
        container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS));
        addCheckBoxesToContainer(app.drawings().asIterator());
        JScrollPane scrollPane = new JScrollPane(container);
        Dimension dimension = container.getPreferredSize();
        dimension.width = (dimension.width < 700) ? dimension.width + 50 : 700;
        dimension.height = (dimension.height < 600) ? dimension.height + 50 : 600;
        scrollPane.setPreferredSize(dimension);
        JOptionPane.showConfirmDialog(
            this, scrollPane, "Select System Net Options", JOptionPane.DEFAULT_OPTION);

        Map<Drawing, Integer> drawingsToConsider = new HashMap<>();
        for (JCheckBox box : netInstanceSelectionMap.keySet()) {
            if (box.isSelected()) {
                Object[] values = netInstanceSelectionMap.get(box);
                drawingsToConsider.put(
                    (Drawing) values[0], (Integer) ((JFormattedTextField) values[1]).getValue());
            }
        }
        netBuilder = new SystemNetBuilder(drawingsToConsider, options, channelsForOption, model);
    }

    /**
     * This method adds {@link JCheckBox JCheckBoxes} to the
     * overall GUI container of this {@link JFrame}.
     * The checkboxes let the user select which kinds of
     * synchronization should be allowed for each channel.
     * The hard-set selected booleans could be replaced by
     * the input of a config file, which would allow the user
     * to specify beforehand which options to select.
     * Currently, only communication between different net instances
     * is selected as default.
     *
     * @param drawings all currently opened {@link Drawing Drawings}
     */
    private void addCheckBoxesToContainer(Iterator<Drawing> drawings) {
        Set<Drawing> originalDrawings = new HashSet<>();
        drawings.forEachRemaining(originalDrawings::add);
        addNetSelectionPanel(originalDrawings);
        // Currently uses all channels of all opened drawings
        Set<String> channelNames = parseChannelNames(originalDrawings);

        options = new boolean[3];
        channelsForOption = new HashMap<>();
        addOptionPanel(
            channelNames, "Multiple Instances", "Synchronize two instances via selected channels",
            "Adds channels in the form of \"NetA:sync();NetB:sync()\"", 0, true);
        addCustomCompositionPanel(channelNames);
    }

    /**
     * Add a panel to the container that allows users to
     * input custom synchronization transitions
     * to the system net.
     *
     * @param channelNames Consider these channels
     */
    private void addCustomCompositionPanel(Set<String> channelNames) {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.setBorder(BorderFactory.createTitledBorder("Custom Compositions"));
        Dimension dimension = container.getPreferredSize();
        dimension.height = (dimension.width < 100) ? dimension.width + 50 : 150;
        panel.setPreferredSize(dimension);

        model = new DefaultListModel<>();
        JList<String> list = new JList<>(model);
        panel.add(new JScrollPane(list), BorderLayout.CENTER);

        JPanel buttonpanel = new JPanel();
        String title = "Add custom composition";
        JButton addNewButton = new JButton(title);
        addCustomCompositionBehaviorToButton(addNewButton, panel, title, channelNames, model);
        buttonpanel.setLayout(new BorderLayout());
        buttonpanel.add(addNewButton, BorderLayout.CENTER);
        JButton removeCustomCompButton = new JButton("-");
        removeCustomCompButton.addActionListener(e -> {
            if (!list.isSelectionEmpty()) {
                model.remove(list.getSelectedIndex());
            }
        });
        buttonpanel.add(removeCustomCompButton, BorderLayout.EAST);

        panel.add(buttonpanel, BorderLayout.SOUTH);
        container.add(panel);
    }

    /**
     * Add an action listener to the "Add new" button which
     * adds additional synchronization transitions to the
     * system net.
     * Whenever the button is pressed, a dialog panel
     * shall be presented to the user which lets them
     * add custom synchronizations.
     * These user-made synchronizations shall be put into the
     * model to later be passed to the {@link SystemNetBuilder}.
     *
     * @param addNewButton add an action listener to this button
     * @param panel add the dialog to this panel
     * @param title give the dialog this title
     * @param channelNames add these channel names to the dialog
     * @param model add the users composition to this model
     */
    private void addCustomCompositionBehaviorToButton(
        JButton addNewButton, JPanel panel, String title, Set<String> channelNames,
        DefaultListModel<String> model)
    {
        addNewButton.addActionListener(e -> {
            int netNumber = 0;
            for (JCheckBox box : netInstanceSelectionMap.keySet()) {
                if (box.isSelected()) {
                    Object[] values = netInstanceSelectionMap.get(box);
                    netNumber += (Integer) ((JFormattedTextField) values[1]).getValue();
                }
            }
            Map<String, Map<String, FormattedNumberTextField>> compSelectionMap =
                CustomCompositionPanel.showConfirmDialog(panel, title, channelNames, netNumber);
            StringBuilder newComposition = new StringBuilder();
            for (String net : compSelectionMap.keySet()) {
                Map<String, FormattedNumberTextField> channelMap = compSelectionMap.get(net);
                if (netHasNonZeroEntries(channelMap)) {
                    newComposition.append("this:getReference(").append(net).append(");\n");
                    for (String channel : channelMap.keySet()) {
                        Integer channelValue = (Integer) channelMap.get(channel).getValue();
                        if (channelValue > 0) {
                            for (int i = 0; i < channelValue; i++) {
                                newComposition.append(net).append(channel).append(";\n");
                            }
                        }
                    }
                }
            }
            if (!String.valueOf(newComposition).isBlank()) {
                model.addElement(String.valueOf(newComposition));
            }
        });
    }

    /**
     * Loops through a map of channels and
     * returns whether each channel shall not appear in a
     * composition.
     *
     * @param channelMap loop through this map
     * @return has this map only values zero?
     */
    private boolean netHasNonZeroEntries(Map<String, FormattedNumberTextField> channelMap) {
        for (String channel : channelMap.keySet()) {
            if ((Integer) channelMap.get(channel).getValue() != 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * This method adds a {@link JPanel} to the
     * overall GUI container of this {@link JFrame}.
     * The panel displays all {@link Drawing Drawings},
     * which are currently opened in the GUI to select from.
     * The selected nets than get considered in the creation
     * of the system net.
     *
     * @param originalDrawings all currently opened drawings.
     */
    private void addNetSelectionPanel(Set<Drawing> originalDrawings) {
        JPanel panel = new JPanel();
        panel.setLayout(new FlowLayout());
        panel.setBorder(BorderFactory.createTitledBorder("Select Nets"));

        netInstanceSelectionMap = new HashMap<>();
        int netCounter = 0;
        for (Drawing drawing : originalDrawings) {
            panel.add(netInstanceSelectionPanel(drawing, netCounter));
            netCounter += 1;
        }
        container.add(panel);
    }

    /**
     * Create the selection panel for one drawing.
     * Here, the user may select whether net instances
     * of the given drawing shall be created in the system net.
     *
     * @param drawing    shall instances of this drawing be considered?
     * @param netCounter how many panels have been created so far?
     * @return selection panel
     */
    JPanel netInstanceSelectionPanel(Drawing drawing, int netCounter) {
        JPanel container = new JPanel();
        String title = "Net " + netCounter;
        container.setBorder(new TitledBorder(title));
        container.setLayout(new GridBagLayout());

        FormattedNumberTextField numberOfInstances = new FormattedNumberTextField(1);

        JCheckBox box = new JCheckBox(drawing.getName());
        box.setSelected(true);
        Object[] values = new Object[] { drawing, numberOfInstances };
        netInstanceSelectionMap.put(box, values);

        container.add(box);
        container.add(numberOfInstances);
        return container;
    }

    /**
     * This method loops through the {@link Figure Figures} of
     * all given drawings.
     * Whenever such a figure is a {@link CPNTextFigure}, it
     * parses it to check if it is a single uplink.
     * If it is, the text of the figure is added to the set.
     * With that, this method returns all channels
     * in the form of uplinks contained in a set of drawings.
     *
     * @param drawings drawings to parse their channels
     * @return names of all channels in a given set of drawings
     */
    private Set<String> parseChannelNames(Set<Drawing> drawings) {
        Set<String> channelNames = new HashSet<>();
        Pattern pattern = Pattern.compile("(:.*\\()(.*)(\\))");
        for (Drawing drawing : drawings) {
            FigureEnumeration figureEnumerator = drawing.figures();
            while (figureEnumerator.hasMoreElements()) {
                Figure drawingFigure = figureEnumerator.nextFigure();
                // Currently only viable for uplinks without other inscriptions (like a downlink)
                if (drawingFigure instanceof CPNTextFigure textFigure) {
                    Matcher m = pattern.matcher(textFigure.getText());
                    if (m.find()) {
                        channelNames.add(m.group(1) + buildVariableText(m.group(2)) + m.group(3));
                    }
                }
            }
        }
        return channelNames;
    }

    /**
     * Change all integer parameters of an uplink inscription
     * to variables.
     *
     * @param parameterString contains the parameters of the inscription
     * @return a new string with variables instead of integers
     */
    private String buildVariableText(String parameterString) {
        if (parameterString.isBlank()) {
            return "";
        }
        String[] parameterArray = parameterString.strip().split(",");
        StringBuilder parameterText = new StringBuilder();
        for (int i = 0; i < parameterArray.length; i++) {
            parameterText.append("var").append(i);
            if (i < parameterArray.length - 1) {
                parameterText.append(", ");
            }
        }
        return String.valueOf(parameterText);
    }

    /**
     * This method adds a {@link JPanel} to the
     * overall GUI container of this {@link JFrame}.
     * This panel consists of a {@link JCheckBox} and
     * a {@link JList}.
     * The checkbox lets the user select if a given kind of
     * synchronization should be allowed in the system net.
     * The list contains all channels for which that
     * synchronization type should be added.
     *
     * @param channelNames names of all channels to consider
     * @param panelTitle   title of the overall panel
     * @param boxTitle     title of the checkbox
     * @param index        where in the options does this setting appear later
     * @param selected     whether the option is selected by default
     */
    private void addOptionPanel(
        Set<String> channelNames, String panelTitle, String boxTitle, String tooltip, int index,
        boolean selected)
    {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.setBorder(BorderFactory.createTitledBorder(panelTitle));

        JCheckBox box = buildCheckBoxOption(index, selected, boxTitle);
        box.setToolTipText(tooltip);

        panel.add(box, BorderLayout.WEST);
        JList<String> list = buildChannelSelectionList(channelNames, index, true);
        panel.add(list, BorderLayout.EAST);

        list.setEnabled(box.isSelected());
        box.addActionListener(e -> {
            if (box.isSelected()) {
                list.setEnabled(true);
                channelsForOption.put(index, new HashSet<>(list.getSelectedValuesList()));
            } else {
                list.setEnabled(false);
                channelsForOption.put(index, new HashSet<>());
            }
        });

        container.add(panel);
    }

    /**
     * This method builds a {@link JList} rendered by the
     * {@link CheckboxListCellRenderer}, containing all
     * channels in nets.
     * If a channel is selected or not changes whether it
     * appears in the later constructed system net.
     *
     * @param channelNames names of channels to consider
     * @param index        index of the synchronization option
     * @param selected     whether the option is selected by default
     * @return the rendered list for these settings
     */
    private JList<String> buildChannelSelectionList(
        Set<String> channelNames, int index, boolean selected)
    {
        JList<String> list = new JList<>(channelNames.toArray(String[]::new));
        list.setCellRenderer(new CheckboxListCellRenderer<>());
        TitledBorder title =
            BorderFactory.createTitledBorder(LineBorder.createBlackLineBorder(), "Select Channels");
        title.setTitleJustification(TitledBorder.CENTER);
        list.setBorder(title);
        list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        list.setSelectionModel(new DefaultListSelectionModel() {
            @Override
            public void setSelectionInterval(int index0, int index1) {
                if (super.isSelectedIndex(index0)) {
                    super.removeSelectionInterval(index0, index1);
                } else {
                    super.addSelectionInterval(index0, index1);
                }
            }
        });
        channelsForOption.put(index, channelNames);
        list.addListSelectionListener(
            e -> channelsForOption.put(index, new HashSet<>(list.getSelectedValuesList())));
        if (selected) {
            list.setSelectionInterval(0, channelNames.size() - 1);
        }
        return list;
    }

    /**
     * This method builds a {@link JCheckBox} in which the user
     * can select whether a certain type of synchronization
     * should be added to the system net.
     *
     * @param index    index of the option
     * @param selected whether the option is selected by default
     * @param title    title of the box
     * @return the constructed checkbox
     */
    private JCheckBox buildCheckBoxOption(int index, boolean selected, String title) {
        JCheckBox checkBox = new JCheckBox(title);
        checkBox.addItemListener(e -> options[index] = checkBox.isSelected());
        checkBox.setSelected(selected);
        return checkBox;
    }

    public SystemNetBuilder getNetBuilder() {
        return netBuilder;
    }
}
