// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.ohe;

import static org.openstreetmap.josm.tools.I18n.tr;
import static org.openstreetmap.josm.tools.I18n.trn;

import java.awt.Component;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.AbstractAction;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;

import org.openstreetmap.josm.actions.JosmAction;
import org.openstreetmap.josm.command.ChangePropertyCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.UndoRedoHandler;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.MainMenu;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.plugins.Plugin;
import org.openstreetmap.josm.plugins.PluginInformation;
import org.openstreetmap.josm.plugins.ohe.gui.OheDialogPanel;
import org.openstreetmap.josm.spi.preferences.Config;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.Shortcut;

/**
 * Opening hours editor plugin.
 */
public class OhePlugin extends Plugin {
    
    /**
     * Strings for choosing which key of an object with given tags should be
     * edited, the order is referencing the preference of the keys, String[] -&gt;
     * {key, value, key-to-edit} key and value can contain regular expressions
     */
    private static final String[][] TAG_EDIT_STRINGS = new String[][] {
            {"opening_hours", ".*", "opening_hours"},
            {"opening_hours:kitchen", ".*", "opening_hours:kitchen"},
            {"collection_times", ".*", "collection_times"},
            {"collection_times:local", ".*", "collection_times:local"},
            {"service_times", ".*", "service_times"},
            {"shop", ".*", "opening_hours"},
            {"amenity", "post_box", "collection_times"},
            {"amenity", ".*", "opening_hours"},
            {"lit", ".*", "lit"},
            {"highway", ".*", "lit"} };

    /**
     * Will be invoked by JOSM to bootstrap the plugin
     * 
     * @param info information about the plugin and its local installation
     */
    public OhePlugin(PluginInformation info) {
        super(info);
        MainMenu.add(MainApplication.getMenu().dataMenu, new OheMenuAction(), false, 0);
    }

    /**
     * this Action is used for calling the OpeningsHourEditor, the selected
     * objects in the active datalayer are edited
     */
    class OheMenuAction extends JosmAction {
        private static final long serialVersionUID = 1456257438391417756L;

        OheMenuAction() {
            super(tr("Edit opening hours"), "opening_hours.png",
                    tr("Edit time-tag of selected element in a graphical interface"), Shortcut.registerShortcut(
                            "tools:opening_hourseditor", tr("Data: {0}", tr("Edit opening hours")), KeyEvent.VK_O,
                            Shortcut.ALT_CTRL_SHIFT), true);
        }

        @Override
        protected void updateEnabledState() {
            if (getLayerManager().getEditDataSet() == null) {
                // if there is no current dataset, then the action is disabled
                setEnabled(false);
            } else {
                updateEnabledState(getLayerManager().getEditDataSet().getSelected());
            }
        }

        @Override
        protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
            // only enable the action if something is selected
            setEnabled(selection != null && !selection.isEmpty());
        }

        @SuppressWarnings("unchecked")
        @Override
        public void actionPerformed(ActionEvent evt) {
            // fetch active Layer
            OsmDataLayer osmlayer = getLayerManager().getEditLayer();
            if (osmlayer == null)
                return;
            Collection<OsmPrimitive> selection = osmlayer.data.getSelected();

            // handling of multiple objects and their tags
            // copied from
            // org.openstreetmap.josm.gui.dialogs.properties.PropertiesDialog[rev4079][line802]
            Map<String, Integer> keyCount = new HashMap<>();
            Map<String, Map<String, Integer>> valueCount = new TreeMap<>();
            for (OsmPrimitive osm : selection) {
                for (String key : osm.keySet()) {
                    String value = osm.get(key);
                    keyCount.put(key, keyCount.containsKey(key) ? keyCount.get(key) + 1 : 1);
                    if (valueCount.containsKey(key)) {
                        Map<String, Integer> v = valueCount.get(key);
                        v.put(value, v.containsKey(value) ? v.get(value) + 1 : 1);
                    } else {
                        TreeMap<String, Integer> v = new TreeMap<>();
                        v.put(value, 1);
                        valueCount.put(key, v);
                    }
                }
            }

            DefaultTableModel propertyData = new DefaultTableModel() {
                @Override
                public boolean isCellEditable(int row, int column) {
                    return false;
                }

                @Override
                public Class<?> getColumnClass(int columnIndex) {
                    return String.class;
                }
            };
            propertyData.setColumnIdentifiers(new String[] {tr("Key"), tr("Value")});
            for (Entry<String, Map<String, Integer>> e : valueCount.entrySet()) {
                int count = 0;
                for (Entry<String, Integer> e1 : e.getValue().entrySet()) {
                    count += e1.getValue();
                }
                if (count < selection.size()) {
                    e.getValue().put("", selection.size() - count);
                }
                propertyData.addRow(new Object[] {e.getKey(), e.getValue()});
            }
            final JTable propertyTable = new JTable(propertyData);
            propertyTable.getColumnModel().getColumn(1).setCellRenderer(new DefaultTableCellRenderer() {
                @Override
                public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
                        boolean hasFocus, int row, int column) {
                    Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
                    if (value == null)
                        return this;
                    if (c instanceof JLabel) {
                        String str = null;
                        if (value instanceof String) {
                            str = (String) value;
                        } else if (value instanceof Map<?, ?>) {
                            Map<?, ?> v = (Map<?, ?>) value;
                            if (v.size() != 1) {
                                str = tr("<different>");
                                c.setFont(c.getFont().deriveFont(Font.ITALIC));
                            } else {
                                final Map.Entry<?, ?> entry = v.entrySet().iterator().next();
                                str = (String) entry.getKey();
                            }
                        }
                        ((JLabel) c).setText(str);
                    }
                    return c;
                }
            });
            // end copy

            // showing the tags in a dialog
            propertyTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
            JScrollPane sp = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
                    ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
            sp.setViewportView(propertyTable);

            final JComboBox<String> newTagField = new JComboBox<>(new String[]{
                    "opening_hours", "opening_hours:kitchen", "collection_times", "collection_times:local", "service_times", "lit"});

            JRadioButton editButton = new JRadioButton(new AbstractAction(tr("edit existing tag")) {
                @Override
                public void actionPerformed(ActionEvent e) {
                    propertyTable.setEnabled(true);
                    newTagField.setEnabled(false);
                }
            });
            JRadioButton newButton = new JRadioButton(new AbstractAction(tr("edit new tag")) {
                @Override
                public void actionPerformed(ActionEvent e) {
                    propertyTable.setEnabled(false);
                    newTagField.setEnabled(true);
                }
            });
            ButtonGroup group = new ButtonGroup();
            group.add(newButton);
            group.add(editButton);

            // search through the tags and choose which one should be selected
            String preSelectedKey = "";
            searchLoop: for (String[] pattern : TAG_EDIT_STRINGS) {
                Pattern keyPattern = Pattern.compile(pattern[0]);
                Pattern valuePattern = Pattern.compile(pattern[1]);
                for (int i = 0; i < propertyData.getRowCount(); ++i) {
                    Matcher keyMatcher = keyPattern.matcher((String) propertyData.getValueAt(i, 0));
                    if (keyMatcher.matches()) {
                        Object value = propertyData.getValueAt(i, 1);
                        if (value instanceof String && valuePattern.matcher((String) value).matches()) {
                            preSelectedKey = pattern[2];
                            break searchLoop;
                        } else if (value instanceof Map<?, ?>) {
                            for (String v : ((Map<String, Integer>) value).keySet()) {
                                if (valuePattern.matcher(v).matches()) {
                                    preSelectedKey = pattern[2];
                                    break searchLoop;
                                }
                            }
                        }
                    }
                }
            }
            int preSelectedRow = -1;
            for (int i = 0; i < propertyData.getRowCount(); ++i) {
                if (preSelectedKey.equals(propertyData.getValueAt(i, 0))) {
                    preSelectedRow = i;
                }
            }
            if (preSelectedRow != -1) {
                propertyTable.setEnabled(true);
                newTagField.setEnabled(false);
                propertyTable.setRowSelectionInterval(preSelectedRow, preSelectedRow);
                editButton.setSelected(true);
            } else {
                propertyTable.setEnabled(false);
                newTagField.setEnabled(true);
                newTagField.setSelectedItem(preSelectedKey);
                newButton.setSelected(true);
            }

            // load the preference for the clocksystem (12h/24h)
            ClockSystem clockSystem = ClockSystem.valueOf(Config.getPref().get("ohe.clocksystem",
                    ClockSystem.getClockSystem(Locale.getDefault()).toString()));

            JCheckBox useTwelveHourClock = new JCheckBox(tr("Display clock in 12h mode."),
                    clockSystem == ClockSystem.TWELVE_HOURS);

            JPanel dlgPanel = new JPanel(new GridBagLayout());
            dlgPanel.add(editButton, GBC.std().anchor(GridBagConstraints.WEST));
            dlgPanel.add(sp, GBC.eol().fill(GridBagConstraints.BOTH));
            dlgPanel.add(newButton, GBC.std().anchor(GridBagConstraints.WEST));
            dlgPanel.add(newTagField, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
            dlgPanel.add(useTwelveHourClock, GBC.eol().fill(GridBagConstraints.HORIZONTAL).insets(0, 5, 0, 5));

            JOptionPane optionPane = new JOptionPane(dlgPanel, JOptionPane.QUESTION_MESSAGE,
                    JOptionPane.OK_CANCEL_OPTION);
            JDialog dlg = optionPane.createDialog(MainApplication.getMainFrame(), tr("Choose key"));
            dlg.pack();
            dlg.setResizable(true);
            dlg.setVisible(true);

            Object answer = optionPane.getValue();
            String keyToEdit = null;
            Object valuesToEdit = "";
            if (answer != null && answer != JOptionPane.UNINITIALIZED_VALUE
                    && (answer instanceof Integer && (Integer) answer == JOptionPane.OK_OPTION)) {
                if (editButton.isSelected() && propertyTable.getSelectedRow() != -1) {
                    keyToEdit = (String) propertyData.getValueAt(propertyTable.getSelectedRow(), 0);
                    valuesToEdit = propertyData.getValueAt(propertyTable.getSelectedRow(), 1);
                } else if (newButton.isSelected()) {
                    keyToEdit = newTagField.getSelectedItem().toString();
                }
            }
            if (keyToEdit == null)
                return;

            // save the value for the clocksystem (12h/24h)
            Config.getPref().put("ohe.clocksystem", (useTwelveHourClock.isSelected() ? ClockSystem.TWELVE_HOURS
                    : ClockSystem.TWENTYFOUR_HOURS).toString());

            OheDialogPanel panel = new OheDialogPanel(keyToEdit, valuesToEdit,
                    useTwelveHourClock.isSelected() ? ClockSystem.TWELVE_HOURS : ClockSystem.TWENTYFOUR_HOURS);

            optionPane = new JOptionPane(panel, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION);
            dlg = optionPane.createDialog(MainApplication.getMainFrame(), tr("Edit"));
            dlg.setResizable(true);
            dlg.setVisible(true);

            String[] changedKeyValuePair = null;
            answer = optionPane.getValue();
            if (!(answer == null || answer == JOptionPane.UNINITIALIZED_VALUE 
              || (answer instanceof Integer && (Integer) answer != JOptionPane.OK_OPTION))) {
                changedKeyValuePair = panel.getChangedKeyValuePair();
            }
            if (changedKeyValuePair == null)
                return;
            String key = changedKeyValuePair[0].trim();
            String newkey = changedKeyValuePair[1].trim();
            String value = changedKeyValuePair[2].trim();

            if ("".equals(value)) {
                value = null; // delete the key
            }
            if ("".equals(newkey)) {
                newkey = key;
                value = null; // delete the key instead
            }
            if (key.equals(newkey) && tr("<different>").equals(value))
                return;
            if (key.equals(newkey) || value == null) {
                UndoRedoHandler.getInstance().add(new ChangePropertyCommand(selection, newkey, value));
            } else {
                Collection<Command> commands = new Vector<>();
                commands.add(new ChangePropertyCommand(selection, key, null));
                if (value.equals(tr("<different>"))) {
                    HashMap<String, Vector<OsmPrimitive>> map = new HashMap<>();
                    for (OsmPrimitive osm : selection) {
                        String val = osm.get(key);
                        if (val != null) {
                            if (map.containsKey(val)) {
                                map.get(val).add(osm);
                            } else {
                                Vector<OsmPrimitive> v = new Vector<>();
                                v.add(osm);
                                map.put(val, v);
                            }
                        }
                    }
                    for (Entry<String, Vector<OsmPrimitive>> e : map.entrySet()) {
                        commands.add(new ChangePropertyCommand(e.getValue(), newkey, e.getKey()));
                    }
                } else {
                    commands.add(new ChangePropertyCommand(selection, newkey, value));
                }
                UndoRedoHandler.getInstance().add(new SequenceCommand(trn("Change properties of up to {0} object",
                        "Change properties of up to {0} objects", selection.size(), selection.size()), commands));
            }
        }
    }
}
