package cz.fidentis.analyst.core;

import java.awt.Color;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.swing.AbstractAction;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.event.ChangeEvent;
import javax.swing.text.NumberFormatter;

/**
 * Builder for control panels. Layout is based on GridBagLayout. 
 * 
 * @author Radek Oslejsek
 */
public class ControlPanelBuilder {
    
    public static final Font CAPTION_FONT = new Font("Arial", 1, 18);
    public static final Insets CAPTION_PADDING = new Insets(20, 0, 20, 0);  // top, left, bottom, right
    public static final ImageIcon HELP_ICON = new ImageIcon(ControlPanelBuilder.class.getResource("/info.png"));
    public static final Font OPTION_TEXT_FONT = new Font("Arial", 1, 14);
    public static final Color OPTION_TEXT_COLOR = new Color(20, 114, 105);
    public static final int OPTION_TEXT_WIDTH = 20; // number of cells
    public static final int SLIDER_WIDTH = 20; // number of cells
    public static final int BUTTON_WIDTH = 12; // number of cells
    
    public static final int NUMBER_OF_FRACTION_DIGITS = 3;
    
    private static final double PERCENTAGE_VALUE_MINIMUM = 0.0;
    private static final double PERCENTAGE_VALUE_MAXIMUM = 1.0;
    
    public static final String TEXT_FIELD_BUTTON_PRESSED_PLUS = "plus-pressed";
    public static final String TEXT_FIELD_BUTTON_PRESSED_MINUS = "minus-pressed";
    
    private JPanel controlPanel;
    private int row = 0;
    private int col = 0;
    
    /**
     * Constructor.
     * 
     * @param controlPanel Control panel to which the GUI elements are inserted.
     */
    public ControlPanelBuilder(JPanel controlPanel) {
        if (controlPanel == null) {
            throw new IllegalArgumentException("controlPanel");
        }
        this.controlPanel = controlPanel;
        GridBagLayout layout = new GridBagLayout();
        this.controlPanel.setLayout(layout);
    }
    
    /**
     * Helper method that parses and returns integer from the input field taking
     * Local into consideration.
     * 
     * @param inputField The input field
     * @return Integer or 0
     */
    public static int parseLocaleInt(JTextField inputField) {
        NumberFormat format = NumberFormat.getInstance(Locale.getDefault());
        try {
            Number number = format.parse(inputField.getText());
            return number.intValue();
        } catch (ParseException ex) {
            return 0;
        }
    }
    
    /**
     * Helper method that parses and returns floating point number from the input field 
     * takingLocale into consideration Locale.
     * 
     * @param inputField The input field
     * @return Double or 0.0
     */
    public static double parseLocaleDouble(JTextField inputField) {
        NumberFormat format = NumberFormat.getInstance(Locale.getDefault());
        try {
            Number number = format.parse(inputField.getText());
            return number.doubleValue();
        } catch (ParseException ex) {
            return 0;
        }
    }
    
    /**
     * Converts number into the text taking into account Locale
     * @param value Number
     * @return Text representation of given number
     */
    public static String intToStringLocale(int value) {
        return ""+value;
    }
    
    /**
     * Converts number into the text taking into account Locale
     * @param value Number
     * @return Text representation of given number
     */
    public static String doubleToStringLocale(double value) {
        NumberFormat formatter = DecimalFormat.getInstance(Locale.getDefault());
        formatter.setMinimumFractionDigits(1);
        formatter.setMaximumFractionDigits(NUMBER_OF_FRACTION_DIGITS);
        formatter.setRoundingMode(RoundingMode.HALF_UP);
        return formatter.format(value);
    }
    
    /**
     * Adds a line with caption.
     * 
     * @param caption Caption text.
     * @return This new GUI object
     */
    public JLabel addCaptionLine(String caption) {
        GridBagConstraints c = new GridBagConstraints();
        c.insets = CAPTION_PADDING; 
        
        JLabel label = addLabelLineCustom(caption, c);
        label.setFont(CAPTION_FONT);
        
        //addLine();
        return label;
    }
    
    /**
     * Adds a line with a plain text label.
     * 
     * @param text Text of the label
     * @return This new GUI object
     */
    public JLabel addLabelLine(String text) {
        return addLabelLineCustom(text, new GridBagConstraints());
    }
    
    /**
     * Adds a line with a plain text label whose appearance can be further
     * customized by {@code constraints}.
     * 
     * @param text Text of the label
     * @param constraints {@code GridBagConstraints} to customize the label
     * @return This new GUI object
     */
    private JLabel addLabelLineCustom(String text, GridBagConstraints constraints) {
        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = col;
        constraints.gridy = row;
        constraints.anchor = GridBagConstraints.LINE_START;
        constraints.fill = GridBagConstraints.NONE;
        
        JLabel label = new JLabel(text);
        controlPanel.add(label, constraints);
        
        return label;
    }
    
    /**
     * Adds a line with slider option.
     * 
     * @param helpAction Action listener invoked when the help icon is clicked. If {@code null}, then no help is shown.
     * @param text Option text.
     * @param sliderMax Max value of the slider (and the value field). If {@code -1}, then percentage slider is shown with 100 as the max. value.
     * @param inputAction Action listener invoked when the input field is changed
     * @return Creates slider
     */
    public JTextField addSliderOptionLine(ActionListener helpAction, String text, int sliderMax, ActionListener inputAction) {
        if (helpAction != null) {
            addOptionHelpIcon(helpAction);
        } else {
            col++;
        }
        addOptionText((text == null) ? "" : text);
        
        return addSliderWithVal(sliderMax, inputAction);
    }
    
    public JTextField addSliderButtonedOptionLine(ActionListener helpAction, String text, int sliderMax, double step, ActionListener inputAction) {
        if (helpAction != null) {
            addOptionHelpIcon(helpAction);
        } else {
            col++;
        }
        addOptionText((text == null) ? "" : text);
        
        return addSliderButtonedWithVal(sliderMax, step, inputAction);
    }
    
    /**
     * Adds a line with a checkbox option.
     * 
     * @param helpAction Action listener invoked when the help icon is clicked. If {@code null}, then no help is shown.
     * @param text Option text.
     * @param selected Initial state of the checkbox.
     * @param checkBoxAction Action listener invoked when the check box is clicked.
     * @return Created checkbox
     */
    public JCheckBox addCheckBoxOptionLine(ActionListener helpAction, String text, boolean selected, ActionListener checkBoxAction) {
        if (helpAction != null) {
            addOptionHelpIcon(helpAction);
        } else {
            col++;
        }
        addOptionText((text == null) ? "" : text);
        JCheckBox cb = addCheckBox(selected, checkBoxAction);
        //addLine();
        return cb;
    }

    /**
     * Moves the builder to the new line. Methods with "Line" suffix add new line automatically.
     * 
     * @return This builder
     */
    public ControlPanelBuilder addLine() {
        row++;
        col = 0; 
        return this;
    }
    
    /**
     * Adds a button section at the bottom of the control panel.
     * 
     * @param buttons Labels.
     * @param actions Action listener invoked when the corresponding button is clicked.
     * @return This builder
     * @throws IllegalArgumentException if the size of the two lists is different
     */
    public List<JButton> addButtons(List<String> buttons, List<ActionListener> actions) {
        if (buttons.size() != actions.size()) {
            throw new IllegalArgumentException();
        }
        
        GridBagConstraints c = new GridBagConstraints();
        c.insets = new Insets(2, 0, 40, 0); // small external space on top and bottom
        c.weighty = 1.0;   //request any extra vertical space
        c.anchor = GridBagConstraints.PAGE_END;
        c.gridwidth = BUTTON_WIDTH;
        
        List<JButton> retButtons = new ArrayList<>();
        for (int i = 0; i < buttons.size(); i++) {
            JButton button = addButtonCustom(buttons.get(i), actions.get(i), c);
            retButtons.add(button);
        }

        return retButtons;
    }
    
    /**
     * Adds a simple button.
     * 
     * @param caption Label
     * @param action Action listener invoked when the corresponding button is clicked
     * @return This new GUI object
     */
    public JButton addButton(String caption, ActionListener action) {
        final GridBagConstraints c = new GridBagConstraints();
        c.anchor = GridBagConstraints.LINE_START;
        c.gridwidth = BUTTON_WIDTH;
        
        return addButtonCustom(caption, action, c);
    }

    /**
     * Adds a simple button whose appearance can be further customized by {@code constraints}.
     * 
     * @param caption Label
     * @param action Action listener invoked when the corresponding button is clicked
     * @param constraints {@code GridBagConstraints} to customize the button
     * @return This new GUI object
     */
    private JButton addButtonCustom(String caption, ActionListener action, GridBagConstraints constraints) {
        JButton button = new JButton();
        button.setText(caption);
        button.addActionListener(action);
        
        constraints.gridy = row;
        constraints.gridx = col;
        col += constraints.gridwidth;
        constraints.fill = GridBagConstraints.NONE;
        controlPanel.add(button, constraints);
        
        return button;
    }
    
    /**
     * Adds a combo box
     * @param items Items
     * @param action Action listener invoked when some item is selected
     * @return 
     */
    public JComboBox addComboBox(List<String> items, ActionListener action) {
        GridBagConstraints c = new GridBagConstraints();
        c.insets = new Insets(0, 25, 0, 0);
        c.gridwidth = OPTION_TEXT_WIDTH;
        c.gridy = row;
        c.gridx = col;
        col += OPTION_TEXT_WIDTH;
        c.anchor = GridBagConstraints.CENTER;
        c.fill = GridBagConstraints.NONE;
        
        JComboBox cbox = new JComboBox(items.toArray(String[]::new));
        cbox.addActionListener(action);
        controlPanel.add(cbox, c);
        
        return cbox;
    }
    
    /**
     * Adds a horizontal strut that moves upper lines to the top of the panel
     */
    public void addVerticalStrut() {
        GridBagConstraints c = new GridBagConstraints();
        c.weighty = 1.0;
        c.gridwidth = 1;
        c.gridy = row;
        c.gridx = col++;
        c.anchor = GridBagConstraints.CENTER;
        c.fill = GridBagConstraints.BOTH;
        controlPanel.add(Box.createHorizontalStrut(5), c);
    }
    
    /**
     * Adds a gap occupying given number of grid cells
     */
    public void addGap() {
        GridBagConstraints c = new GridBagConstraints();
        c.weightx = 0.4;
        c.gridwidth = 1;
        c.gridy = row;
        c.gridx = col++;
        c.anchor = GridBagConstraints.CENTER;
        c.fill = GridBagConstraints.HORIZONTAL;
        controlPanel.add(Box.createVerticalStrut(5), c);
    }
    
    /**
     * Adds a check box.
     * 
     * @param selected Initial state
     * @param action Action listener invoked when the checkbox is clicked.
     * @return This builder
     */
    public JCheckBox addCheckBox(boolean selected, ActionListener action) {
        GridBagConstraints c = new GridBagConstraints();
        c.insets = new Insets(0, 25, 0, 0);
        c.gridwidth = 1;
        c.gridx = col++;
        c.gridy = row;
        c.anchor = GridBagConstraints.CENTER;
        c.fill = GridBagConstraints.NONE;

        JCheckBox checkBox = new JCheckBox();
        checkBox.setSelected(selected);
        checkBox.addActionListener(action);
        controlPanel.add(checkBox, c);

        return checkBox;
    }
    
    /**
     * Adds a help icon.
     * 
     * @param action Action listener invoked when the icon is clicked.
     * @return This builder
     */
    public JButton addOptionHelpIcon(ActionListener action) {
        GridBagConstraints c = new GridBagConstraints();
        c.gridwidth = 1;
        c.gridx = col++;
        c.gridy = row;
        c.anchor = GridBagConstraints.CENTER;
        c.fill = GridBagConstraints.NONE;
        
        JButton button = new JButton();
        button.setBorderPainted(false);
        button.setFocusPainted(false);
        button.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR));
        button.addActionListener(action);
        button.setIcon(HELP_ICON);
        
        controlPanel.add(button, c);
        return button;
    }
    
    /**
     * Adds a text of the option.
     * 
     * @param text Text.
     * @return This builder
     */
    public JLabel addOptionText(String text) {
        GridBagConstraints c = new GridBagConstraints();
        c.gridwidth = OPTION_TEXT_WIDTH;
        c.gridx = col;
        col += OPTION_TEXT_WIDTH;
        c.gridy = row;
        c.anchor = GridBagConstraints.LINE_START;
        c.fill = GridBagConstraints.HORIZONTAL;
        
        JLabel label = new JLabel();
        label.setFont(OPTION_TEXT_FONT);
        label.setForeground(OPTION_TEXT_COLOR);
        label.setText(text);
        
        controlPanel.add(label, c);
        return label;
    }
    
    /**
     * Adds a slider and connects it with given input field
     * 
     * @param max Max value of the slider (and the value field). If {@code -1}, then percentage slider is shown with 100 as the max. value.
     * @param inputField Input field connected with the slider.
     * @return This builder
     */
    public JTextField addSliderWithVal(int max, ActionListener inputAction) {
        JSlider slider = addSlider(max);
        
        IntValueRange range = max == -1 ? null : new IntValueRange(0, max);
        JTextField inputField = addFormattedInputField(range, inputAction);
        
        connectSliderWithTextField(slider, max, inputField);
        
        return inputField;
    }
    
    public JTextField addSliderButtonedWithVal(int max, double step, ActionListener inputAction) {
        JSlider slider = addSlider(max);
        
        IntValueRange range = max == -1 ? null : new IntValueRange(0, max);
        JTextField inputField = addFormattedInputFieldButtoned(range, step, inputAction);
        
        connectSliderWithTextField(slider, max, inputField);
        
        return inputField;
    }
    
    private void connectSliderWithTextField(JSlider slider, int max, JTextField inputField) {
        slider.addChangeListener((ChangeEvent ce) -> {
            if (max == -1) {
                inputField.setText(doubleToStringLocale(slider.getValue() / 100.0));
            } else {
                inputField.setText(intToStringLocale(slider.getValue()));
            }
            inputField.postActionEvent(); // invoke textField action listener
        });
        slider.addMouseListener(
                new MouseAdapter() {
                    @Override
                    public void mouseExited(MouseEvent e) {
                        e.setSource(inputField);
                        inputField.dispatchEvent(e);
                    }

                    @Override
                    public void mouseEntered(MouseEvent e) {
                        e.setSource(inputField);
                        inputField.dispatchEvent(e);
                    }

                    @Override
                    public void mouseReleased(MouseEvent e) {
                        e.setSource(inputField);
                        inputField.dispatchEvent(e);
                    }

                    @Override
                    public void mousePressed(MouseEvent e) {
                        e.setSource(inputField);
                        inputField.dispatchEvent(e);
                    }

                    @Override
                    public void mouseClicked(MouseEvent e) {
                        e.setSource(inputField);
                        inputField.dispatchEvent(e);
                    }
                }
        );
        
        inputField.addActionListener((ActionEvent ae) -> {
            if (max == -1) { // percents in [0,1]
                slider.setValue((int) (parseLocaleDouble(inputField) * 100));
            } else { // integers
                slider.setValue(parseLocaleInt(inputField));
            }
        });
    }
    
    public JSlider addSlider(int max) {
        GridBagConstraints c = new GridBagConstraints();
        c.gridwidth = SLIDER_WIDTH;
        c.gridx = col;
        col += SLIDER_WIDTH;
        c.gridy = row;
        c.anchor = GridBagConstraints.CENTER;
        c.fill = GridBagConstraints.HORIZONTAL;

        JSlider slider = new JSlider();
        
        if (max == -1) { // percents
            slider.setMaximum(100);
        } else { // absolute values
            slider.setMaximum(max);
        }
        
        controlPanel.add(slider, c);
        
        return slider;
    }
    
    public JFormattedTextField addFormattedInputFieldButtoned(IntValueRange range, double stepSize, ActionListener inputAction) {
        GridBagConstraints c = new GridBagConstraints();
        c.gridwidth = 1;
        
        c.anchor = GridBagConstraints.LINE_END;
        JButton minusButton = addButtonCustom("-", null, c);
        JFormattedTextField inputField = addFormattedInputField(range, inputAction);
        c.anchor = GridBagConstraints.LINE_START;
        JButton plusButton = addButtonCustom("+", null, c);
        
        double step;
        if (range == null) { // Percents in [0, 1]
            if (stepSize < 0.0) {
                step = 0.01;
            } else {
                step = stepSize;
            }
        } else { // Range of integers between two given integers
            if (stepSize < 0.0) {
                step = 1.0;
            } else {
                step = Math.round(stepSize);
            }
        }

        minusButton.addActionListener(new AbstractAction() {
            double minimum = range == null ? PERCENTAGE_VALUE_MINIMUM : range.getMinimum();
            
            @Override
            public void actionPerformed(ActionEvent ae) {
                double newValue = parseLocaleDouble(inputField) - step;
                if (newValue < minimum) {
                    return;
                }
                
                inputField.setValue(newValue);
                for (ActionListener listener: inputField.getActionListeners()) {
                    listener.actionPerformed(new ActionEvent(
                            inputField,
                            ActionEvent.ACTION_PERFORMED,
                            TEXT_FIELD_BUTTON_PRESSED_MINUS));
                }
            }
        });
        plusButton.addActionListener(new AbstractAction() {
            double maximum = range == null ? PERCENTAGE_VALUE_MAXIMUM : range.getMaximum();
            
            @Override
            public void actionPerformed(ActionEvent ae) {
                double newValue = parseLocaleDouble(inputField) + step;
                if (newValue > maximum) {
                    return;
                }
                
                inputField.setValue(newValue);
                for (ActionListener listener: inputField.getActionListeners()) {
                    listener.actionPerformed(new ActionEvent(
                            inputField,
                            ActionEvent.ACTION_PERFORMED,
                            TEXT_FIELD_BUTTON_PRESSED_PLUS));
                }
            }
        });
        
        return inputField;
    }
    
    public JFormattedTextField addFormattedInputField(IntValueRange range, ActionListener inputAction) {
        GridBagConstraints c = new GridBagConstraints();
        c.gridwidth = 2;
        c.gridx = col;
        col += 2;
        c.gridy = row;
        c.anchor = GridBagConstraints.CENTER;
        c.fill = GridBagConstraints.BOTH;
        
        NumberFormatter formatter;
        if (range == null) { // percents in [0,1] => use two digit double
            NumberFormat format = DecimalFormat.getInstance(Locale.getDefault());
            format.setMinimumFractionDigits(1);
            format.setMaximumFractionDigits(NUMBER_OF_FRACTION_DIGITS);
            format.setRoundingMode(RoundingMode.HALF_UP);
            formatter = new NumberFormatter(format);
            formatter.setValueClass(Double.class);
            formatter.setMinimum(PERCENTAGE_VALUE_MINIMUM);
            formatter.setMaximum(PERCENTAGE_VALUE_MAXIMUM);
        } else {
            NumberFormat format = NumberFormat.getInstance();
            formatter = new NumberFormatter(format);
            formatter.setValueClass(Integer.class);
            formatter.setMinimum(range.getMinimum());
            formatter.setMaximum(range.getMaximum());
        }
        formatter.setAllowsInvalid(false);
        
        JFormattedTextField input = new JFormattedTextField(formatter);
        input.setText("0");
        input.addActionListener(inputAction);
        controlPanel.add(input, c);
        
        return input;
    }
    
    /**
     * Adds a scrollable pane that carries the given panel as its content.
     * 
     * @param content Panel to be made scrollable
     * @return This new GUI object
     */
    public JScrollPane addScrollPane(JPanel content) {
        JScrollPane scrollPane = new JScrollPane(content);
        
        GridBagConstraints c = new GridBagConstraints();
        c.weighty = 1.0;
        c.gridwidth = GridBagConstraints.REMAINDER;
        c.gridy = row;
        c.gridx = col;
        c.anchor = GridBagConstraints.CENTER;
        c.fill = GridBagConstraints.BOTH;
        controlPanel.add(scrollPane, c);
        
        return scrollPane;
    }
    
}
