package de.renew.propertymanagementgui.gui;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.border.EmptyBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

import de.renew.propertymanagement.prop.ConfigurableHotkeyProperty;
import de.renew.propertymanagement.prop.ConfigurableProperty;
import de.renew.propertymanagement.prop.ConfigurablePropertyManager;
import de.renew.propertymanagementgui.controller.SettingsController;
import de.renew.propertymanagementgui.reactivecomponents.Component;


/**
 * The page of the settings window. Shows the settings of the selected plugin.
 */
public class PageComponent extends Component {

    private static final Font START_TEXT_FONT = new Font("Arial", Font.BOLD, 24);
    private static final String START_TEXT = "<html>Click on a plugin to see its settings.</html>";
    private static final EmptyBorder PAGE_BORDER = new EmptyBorder(10, 10, 10, 10);
    private static final Insets PROPERTY_LABEL_INSETS = new Insets(10, 0, 10, 0);
    private static final Insets PROPERTY_COMPONENT_INSETS = new Insets(0, 0, 0, 0);

    private final List<ConfigurableProperty> _properties;
    private final ConfigurablePropertyManager _propertyManager =
        ConfigurablePropertyManager.getInstance();
    private GridBagConstraints _constraints;
    private JComponent _page;
    private final int toolTipIconScale = 32;
    private JComponent _propertiesPage;
    private JComponent _commandHotkeysPage;
    private JComponent _toolHotkeysPage;
    private final Map<ConfigurableProperty, JComponent> _buttonMap;
    private final Map<ConfigurableProperty, JButton> _defaultButtonMap;
    private final Map<ConfigurableProperty, JButton> _resetButtonMap;
    private JTabbedPane _tabbedPane;
    private final int _selectedIndex;

    /**
     * Creates a new PageComponent.
     *
     * @param properties The properties of the selected plugin.
     */
    public PageComponent(List<ConfigurableProperty> properties) {
        this(properties, 0);
    }

    public PageComponent(List<ConfigurableProperty> properties, int selectedIndex) {
        _properties = properties;
        _selectedIndex = selectedIndex;
        _buttonMap = new HashMap<>();
        _defaultButtonMap = new HashMap<>();
        _resetButtonMap = new HashMap<>();
    }

    @Override
    protected JComponent render() {
        _tabbedPane = new JTabbedPane();
        _propertiesPage = new JPanel();
        _propertiesPage.setBorder(PAGE_BORDER);
        _propertiesPage.setLayout(new GridBagLayout());
        _commandHotkeysPage = new JPanel();
        _commandHotkeysPage.setBorder(PAGE_BORDER);
        _commandHotkeysPage.setLayout(new GridBagLayout());
        _toolHotkeysPage = new JPanel();
        _toolHotkeysPage.setBorder(PAGE_BORDER);
        _toolHotkeysPage.setLayout(new GridBagLayout());
        _constraints = new GridBagConstraints();
        _constraints.fill = GridBagConstraints.HORIZONTAL;
        _constraints.weightx = 1.0;
        _constraints.gridx = 0;

        if (_properties != null) {
            createPropertiesGui();
        } else {
            JComponent startText = new JLabel(START_TEXT);
            startText.setFont(START_TEXT_FONT);
            _propertiesPage.add(startText);
            _commandHotkeysPage.add(startText);
            _toolHotkeysPage.add(startText);
        }

        JScrollPane propertiesScrollPane = new JScrollPane(_propertiesPage);
        propertiesScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        propertiesScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        propertiesScrollPane.getVerticalScrollBar().setUnitIncrement(10);
        propertiesScrollPane.getVerticalScrollBar().setBlockIncrement(50);
        JScrollPane commandHotkeysScrollPane = new JScrollPane(_commandHotkeysPage);
        commandHotkeysScrollPane
            .setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        commandHotkeysScrollPane
            .setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        commandHotkeysScrollPane.getVerticalScrollBar().setUnitIncrement(10);
        commandHotkeysScrollPane.getVerticalScrollBar().setBlockIncrement(50);
        JScrollPane toolHotkeyScrollPane = new JScrollPane(_toolHotkeysPage);
        toolHotkeyScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        toolHotkeyScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        toolHotkeyScrollPane.getVerticalScrollBar().setUnitIncrement(10);
        toolHotkeyScrollPane.getVerticalScrollBar().setBlockIncrement(50);
        _tabbedPane.addTab("Properties", propertiesScrollPane);
        _tabbedPane.addTab("Command Hotkeys", commandHotkeysScrollPane);
        _tabbedPane.addTab("Tool Hotkeys", toolHotkeyScrollPane);
        _tabbedPane.setSelectedIndex(_selectedIndex);
        return _tabbedPane;
    }

    /**
     * Iterates over the properties of the selected plugin and adds them to the page.
     */
    private void createPropertiesGui() {
        for (int i = 0; i < _properties.size(); i++) {
            ConfigurableProperty property = _properties.get(i);

            JLabel label = createPropertyLabel(property);

            _constraints.weightx = 10.0;
            _constraints.insets = PROPERTY_LABEL_INSETS;
            _constraints.gridy = i;
            _constraints.gridx = 0;

            JComponent targetFrame = _propertiesPage;

            JComponent field;
            if ("true".equalsIgnoreCase(property.getDefaultValue())
                || "false".equalsIgnoreCase(property.getDefaultValue())) {
                // Add checkbox if the property is a boolean
                field = createCheckBox(property);
            } else if (property instanceof ConfigurableHotkeyProperty hotkeyProperty) {
                // Add configurable hotkey field if property can be interpreted as a hotkey
                field = new HotkeyField(hotkeyProperty);
                switch (hotkeyProperty.getHotkeyType()) {
                    case TOOL_HOTKEY -> targetFrame = _toolHotkeysPage;
                    case COMMAND_HOTKEY -> targetFrame = _commandHotkeysPage;
                }
            } else if (property.getSelectionChoices() == null) {
                // Add text field for non-boolean values if no selection choices are available
                field = createTextField(property);
            } else {
                // Add combobox for non-boolean values if selection choices are available
                field = createComboBox(property);
            }
            field.setMaximumSize(new Dimension(Integer.MAX_VALUE, field.getPreferredSize().height));
            targetFrame.add(label, _constraints);
            _constraints.insets = PROPERTY_COMPONENT_INSETS;
            _constraints.gridx = 1;
            targetFrame.add(field, _constraints);
            if (property.isSystemProperty()) {
                _constraints.gridx = GridBagConstraints.RELATIVE;
                _constraints.weightx = 0.1;
                ImageIcon icon =
                    new ImageIcon(Objects.requireNonNull(getClass().getResource("/warning.png")));
                Image image = icon.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH);
                icon.setImage(image);
                JLabel warningLabel = new JLabel(icon);
                warningLabel.setToolTipText("Changes only apply on restart");
                targetFrame.add(warningLabel, _constraints);
            }
            // Add a tooltip to any property which has a description which is not empty
            insertToolTipIcon(property, targetFrame);

            JButton defaultButton = createDefaultButton(field, property);
            JButton resetButton = createResetButton(field, property);
            _defaultButtonMap.put(property, defaultButton);
            _resetButtonMap.put(property, resetButton);
            _constraints.weightx = 1.0;
            _constraints.gridx = GridBagConstraints.RELATIVE;
            targetFrame.add(defaultButton, _constraints);
            _constraints.gridx = GridBagConstraints.RELATIVE;
            targetFrame.add(resetButton, _constraints);
            _buttonMap.put(property, field);
        }
        _constraints.gridx = 0;
        _constraints.gridy = _properties.size();
        _constraints.weighty = 1.0;
        _constraints.fill = GridBagConstraints.BOTH;
        _propertiesPage.add(Box.createVerticalGlue(), _constraints);
        _commandHotkeysPage.add(Box.createVerticalGlue(), _constraints);
        _toolHotkeysPage.add(Box.createVerticalGlue(), _constraints);
    }

    /**
     * Inserts a tool tip icon next to a property which when hovered over gives a description about what the specific property is used for.
     *
     * @param property the property for which the tooltip will be displayed
     * @param targetFrame the frame in which the property is displayed
     */
    private void insertToolTipIcon(ConfigurableProperty property, JComponent targetFrame) {
        ImageIcon icon = new ImageIcon(getClass().getResource("/toolTipIcon.png"));
        Image img = icon.getImage();
        Image resizedImg = img.getScaledInstance(
            icon.getIconWidth() / toolTipIconScale, icon.getIconHeight() / toolTipIconScale,
            Image.SCALE_SMOOTH);
        ImageIcon halfSizeToolTipIcon = new ImageIcon(resizedImg);
        JLabel toolTipLabel = new JLabel(halfSizeToolTipIcon);
        if (property.getDescription() != null && !property.getDescription().isEmpty()) {
            toolTipLabel.setToolTipText(property.getDescription());
            if (property.isSystemProperty()) {
                _constraints.gridx = 3;
            } else {
                _constraints.gridx = 2;
            }
            _constraints.weightx = 0.2;
            targetFrame.add(toolTipLabel, _constraints);
        }
    }

    /**
     * Create a label for a given property.
     *
     * @param property The property to add a label for.
     * @return The created label.
     */
    private JLabel createPropertyLabel(ConfigurableProperty property) {
        JLabel label;
        if (!(property.getDisplayName() == null || property.getDisplayName().isEmpty())) {
            label = new JLabel(property.getDisplayName());
        } else {
            label = new JLabel(property.getKey());
        }
        return label;
    }

    /**
     * Creates a checkbox for a given property and adds an item listener to it.
     *
     * @param property The property to create a checkbox for.
     * @return The created checkbox.
     */
    private JCheckBox createCheckBox(ConfigurableProperty property) {
        JCheckBox checkBox = new JCheckBox();
        checkBox.setSelected(Boolean.parseBoolean(getPropertyValue(property)));
        checkBox.addItemListener(e -> {
            ConfigurablePropertyManager.getInstance()
                .stageChange(property, String.valueOf(checkBox.isSelected()));
            SettingsController.notifySettingsChanged();
        });
        return checkBox;
    }

    /**
     * Creates a text field for a given property and adds a document listener to it.
     *
     * @param property The property to create a text field for.
     * @return The created text field.
     */
    private JTextField createTextField(ConfigurableProperty property) {
        JTextField textField;
        textField = new JTextField(getPropertyValue(property));

        textField.getDocument().addDocumentListener(new DocumentListener() {

            @Override
            public void insertUpdate(DocumentEvent e) {
                ConfigurablePropertyManager.getInstance()
                    .stageChange(property, textField.getText());
                SettingsController.notifySettingsChanged();
            }

            @Override
            public void removeUpdate(DocumentEvent e) {
                ConfigurablePropertyManager.getInstance()
                    .stageChange(property, textField.getText());
                SettingsController.notifySettingsChanged();
            }

            @Override
            public void changedUpdate(DocumentEvent e) {
                ConfigurablePropertyManager.getInstance()
                    .stageChange(property, textField.getText());
                SettingsController.notifySettingsChanged();
            }
        });
        return textField;
    }

    /**
     * Creates a combobox for a given property and adds an action listener to it.
     *
     * @param property The property to create a combobox for.
     * @return The created combobox.
     */
    private JComboBox<String> createComboBox(ConfigurableProperty property) {
        HashSet<String> optionsList = new HashSet<>();
        optionsList.add(property.getCurrentValue());
        optionsList.add(property.getDefaultValue());
        optionsList.addAll(List.of(property.getSelectionChoices()));

        JComboBox<String> comboBox = new JComboBox<>(optionsList.toArray(new String[0]));
        comboBox.setSelectedItem(getPropertyValue(property));

        comboBox.addActionListener(e -> {
            ConfigurablePropertyManager.getInstance()
                .stageChange(property, comboBox.getSelectedItem());
            SettingsController.notifySettingsChanged();
        });
        return comboBox;
    }

    /**
     * Returns the best value for the current value of a ConfigurableProperty, first tries to look for a staged change, then a current value and then the default.
     * @param property the property to check, if {@link ConfigurableHotkeyProperty} returns null
     * @return the best value as a {@code String}
     */
    private String getPropertyValue(ConfigurableProperty property) {
        if (property instanceof ConfigurableHotkeyProperty) {
            return null;
        }
        if (_propertyManager.getStagedChange(property) != null) {
            return (String) _propertyManager.getStagedChange(property);
        } else if (property.getCurrentValue() != null) {
            return property.getCurrentValue();
        } else {
            return property.getDefaultValue();
        }
    }

    /**
     * Gets the currently selected tab.
     *
     * @return the index of the current tab.
     */
    public int getSelectedIndex() {
        return _tabbedPane.getSelectedIndex();
    }

    /**
     * Returns the best value for the current value of a ConfigurableHotkeyProperty, first tries to look for a staged change, then a current value and then the default.
     *
     * @param property the property to check, if property is null returns null
     * @return the best value as a {@code KeyStroke}
     */
    private KeyStroke getHotkeyPropertyValue(ConfigurableHotkeyProperty property) {
        if (property == null) {
            return null;
        }
        if (_propertyManager.getStagedChange(property) != null) {
            return (KeyStroke) _propertyManager.getStagedChange(property);
        } else if (property.getKeyStroke() != null) {
            return property.getKeyStroke();
        } else {
            return property.getDefaultKeyStroke();
        }
    }

    /**
     * Creates and returns a new Default Button.
     *
     * @param component the input field the button belongs to
     * @param property the ConfigurableProperty the button belongs to
     * @return JButton with a listener attached
     */
    private JButton createDefaultButton(JComponent component, ConfigurableProperty property) {
        JButton button = new JButton("default");
        button.addActionListener(e -> defaultProperty(component, property));
        return button;
    }

    /**
     * Sets a ConfigurableProperty and its input field to the default value.
     *
     * @param component the component to reset
     * @param property the property to reset
     */
    private void defaultProperty(JComponent component, ConfigurableProperty property) {
        _propertyManager.stageChange(property, property.getDefaultValue());
        resetValue(component, property);
        SettingsController.notifySettingsChanged();
    }

    /**
     * Creates and returns a new Reset Button.
     *
     * @param component the input field the button belongs to
     * @param property the ConfigurableProperty the button belongs to
     * @return JButton with a listener attached
     */
    private JButton createResetButton(JComponent component, ConfigurableProperty property) {
        JButton button = new JButton("reset");
        button.addActionListener(e -> resetProperty(component, property));
        return button;
    }

    /**
     * Resets a ConfigurableProperty and its input field to the previously saved value.
     *
     * @param component the component to reset
     * @param property the property to reset
     */
    private void resetProperty(JComponent component, ConfigurableProperty property) {
        _propertyManager.unstageChange(property);
        resetValue(component, property);
        SettingsController.notifySettingsChanged();
    }

    /**
     * Sets the value of the component to the current value of the property.
     *
     * @param component the component to reset
     * @param property the property to read from
     */
    private void resetValue(JComponent component, ConfigurableProperty property) {
        _propertyManager.disableStaging();
        if (component instanceof JCheckBox checkBox) {
            checkBox.setSelected(Boolean.parseBoolean(getPropertyValue(property)));
        } else if (component instanceof JTextField textField) {
            if (component instanceof HotkeyField hotkeyField) {
                if (property instanceof ConfigurableHotkeyProperty hotkeyProperty) {
                    hotkeyField.setKeyStroke(getHotkeyPropertyValue(hotkeyProperty));
                }
            } else {
                textField.setText(getPropertyValue(property));
            }
        } else if (component instanceof JComboBox comboBox) {
            comboBox.setSelectedItem(getPropertyValue(property));
        }
        _propertyManager.enableStaging();
    }

    /**
     * Resets all properties to the default value.
     */
    public void defaultAll() {
        for (Map.Entry<ConfigurableProperty, JComponent> entry : _buttonMap.entrySet()) {
            defaultProperty(entry.getValue(), entry.getKey());
        }
    }

    /**
     * Resets all properties to the previously saved value.
     */
    public void resetAll() {
        for (Map.Entry<ConfigurableProperty, JComponent> entry : _buttonMap.entrySet()) {
            resetProperty(entry.getValue(), entry.getKey());
        }
    }

    /**
     * Reacts to some settings having changed.
     */
    public void settingsChanged() {
        _resetButtonMap.forEach(
            (key, value) -> value.setEnabled(
                ConfigurablePropertyManager.getInstance().getStagedChange(key) != null));
        _defaultButtonMap.forEach((key, value) -> value.setEnabled(checkDefault(key)));
    }

    /**
     * Checks whether the Property is currently in a state, such that the default button should be disabled
     *
     * @param e the Property to check
     * @return true, if the default Button would change the Property.
     */
    private static boolean checkDefault(ConfigurableProperty e) {
        final ConfigurablePropertyManager propManager = ConfigurablePropertyManager.getInstance();
        if (e instanceof ConfigurableHotkeyProperty eh) {
            return !(propManager.getStagedChange(eh) == null)
                && !eh.getDefaultKeyStroke().equals(propManager.getStagedChange(eh))
                || propManager.getStagedChange(eh) == null && !(eh.isDefault());
        }
        return !(propManager.getStagedChange(e) == null)
            && !e.getDefaultValue().equals(propManager.getStagedChange(e))
            || propManager.getStagedChange(e) == null && !(e.isDefault());
    }
}
