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

import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
import static org.openstreetmap.josm.tools.I18n.tr;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.KeyboardFocusManager;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EventObject;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

import javax.swing.AbstractAction;
import javax.swing.CellEditor;
import javax.swing.DefaultListSelectionModel;
import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.TableColumn;
import javax.swing.text.JTextComponent;

import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.CopyAction;
import org.openstreetmap.josm.actions.PasteTagsAction;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.PrimitiveData;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.Tag;
import org.openstreetmap.josm.gui.dialogs.relation.RunnableAction;
import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.TextTagParser;
import org.openstreetmap.josm.tools.Utils;

/**
 * This is the tabular editor component for OSM tags.
 *
 */
public class TagTable extends JTable  {
    /** the table cell editor used by this table */
    private TagCellEditor editor = null;
    private final TagEditorModel model;
    private Component nextFocusComponent;

    /** a list of components to which focus can be transferred without stopping
     * cell editing this table.
     */
    private final CopyOnWriteArrayList<Component> doNotStopCellEditingWhenFocused = new CopyOnWriteArrayList<>();
    private transient CellEditorRemover editorRemover;

    /**
     * The table has two columns. The first column is used for editing rendering and
     * editing tag keys, the second for rendering and editing tag values.
     *
     */
    static class TagTableColumnModel extends DefaultTableColumnModel {
        public TagTableColumnModel(DefaultListSelectionModel selectionModel) {
            setSelectionModel(selectionModel);
            TableColumn col = null;
            TagCellRenderer renderer = new TagCellRenderer();

            // column 0 - tag key
            col = new TableColumn(0);
            col.setHeaderValue(tr("Key"));
            col.setResizable(true);
            col.setCellRenderer(renderer);
            addColumn(col);

            // column 1 - tag value
            col = new TableColumn(1);
            col.setHeaderValue(tr("Value"));
            col.setResizable(true);
            col.setCellRenderer(renderer);
            addColumn(col);
        }
    }

    /**
     * Action to be run when the user navigates to the next cell in the table,
     * for instance by pressing TAB or ENTER. The action alters the standard
     * navigation path from cell to cell:
     * <ul>
     *   <li>it jumps over cells in the first column</li>
     *   <li>it automatically add a new empty row when the user leaves the
     *   last cell in the table</li>
     * </ul>
     *
     */
    class SelectNextColumnCellAction extends AbstractAction  {
        @Override
        public void actionPerformed(ActionEvent e) {
            run();
        }

        public void run() {
            int col = getSelectedColumn();
            int row = getSelectedRow();
            if (getCellEditor() != null) {
                getCellEditor().stopCellEditing();
            }

            if (row == -1 && col == -1) {
                requestFocusInCell(0, 0);
                return;
            }

            if (col == 0) {
                col++;
            } else if (col == 1 && row < getRowCount()-1) {
                col = 0;
                row++;
            } else if (col == 1 && row == getRowCount()-1) {
                // we are at the end. Append an empty row and move the focus to its second column
                String key = ((TagModel) model.getValueAt(row, 0)).getName();
                if (!key.trim().isEmpty()) {
                    model.appendNewTag();
                    col = 0;
                    row++;
                } else {
                    clearSelection();
                    if (nextFocusComponent != null)
                        nextFocusComponent.requestFocusInWindow();
                    return;
                }
            }
            requestFocusInCell(row, col);
        }
    }

    /**
     * Action to be run when the user navigates to the previous cell in the table,
     * for instance by pressing Shift-TAB
     *
     */
    class SelectPreviousColumnCellAction extends AbstractAction  {

        @Override
        public void actionPerformed(ActionEvent e) {
            int col = getSelectedColumn();
            int row = getSelectedRow();
            if (getCellEditor() != null) {
                getCellEditor().stopCellEditing();
            }

            if (col <= 0 && row <= 0) {
                // change nothing
            } else if (col == 1) {
                col--;
            } else {
                col = 1;
                row--;
            }
            requestFocusInCell(row, col);
        }
    }

    /**
     * Action to be run when the user invokes a delete action on the table, for
     * instance by pressing DEL.
     *
     * Depending on the shape on the current selection the action deletes individual
     * values or entire tags from the model.
     *
     * If the current selection consists of cells in the second column only, the keys of
     * the selected tags are set to the empty string.
     *
     * If the current selection consists of cell in the third column only, the values of the
     * selected tags are set to the empty string.
     *
     *  If the current selection consists of cells in the second and the third column,
     *  the selected tags are removed from the model.
     *
     *  This action listens to the table selection. It becomes enabled when the selection
     *  is non-empty, otherwise it is disabled.
     *
     *
     */
    class DeleteAction extends RunnableAction implements ListSelectionListener {

        public DeleteAction() {
            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
            putValue(SHORT_DESCRIPTION, tr("Delete the selection in the tag table"));
            getSelectionModel().addListSelectionListener(this);
            getColumnModel().getSelectionModel().addListSelectionListener(this);
            updateEnabledState();
        }

        /**
         * delete a selection of tag names
         */
        protected void deleteTagNames() {
            int[] rows = getSelectedRows();
            model.deleteTagNames(rows);
        }

        /**
         * delete a selection of tag values
         */
        protected void deleteTagValues() {
            int[] rows = getSelectedRows();
            model.deleteTagValues(rows);
        }

        /**
         * delete a selection of tags
         */
        protected void deleteTags() {
            int[] rows = getSelectedRows();
            model.deleteTags(rows);
        }

        @Override
        public void run() {
            if (!isEnabled())
                return;
            switch(getSelectedColumnCount()) {
            case 1:
                if (getSelectedColumn() == 0) {
                    deleteTagNames();
                } else if (getSelectedColumn() == 1) {
                    deleteTagValues();
                }
                break;
            case 2:
                deleteTags();
                break;
            }

            if (isEditing()) {
                CellEditor editor = getCellEditor();
                if (editor != null) {
                    editor.cancelCellEditing();
                }
            }

            if (model.getRowCount() == 0) {
                model.ensureOneTag();
                requestFocusInCell(0, 0);
            }
        }

        /**
         * listens to the table selection model
         */
        @Override
        public void valueChanged(ListSelectionEvent e) {
            updateEnabledState();
        }

        protected final void updateEnabledState() {
            if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
                setEnabled(true);
            } else if (!isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
                setEnabled(true);
            } else if (getSelectedColumnCount() > 1 || getSelectedRowCount() > 1) {
                setEnabled(true);
            } else {
                setEnabled(false);
            }
        }
    }

    /**
     * Action to be run when the user adds a new tag.
     *
     *
     */
    class AddAction extends RunnableAction implements PropertyChangeListener{
        public AddAction() {
            putValue(SMALL_ICON, ImageProvider.get("dialogs", "add"));
            putValue(SHORT_DESCRIPTION, tr("Add a new tag"));
            TagTable.this.addPropertyChangeListener(this);
            updateEnabledState();
        }

        @Override
        public void run() {
            CellEditor editor = getCellEditor();
            if (editor != null) {
                getCellEditor().stopCellEditing();
            }
            final int rowIdx = model.getRowCount()-1;
            String key = ((TagModel) model.getValueAt(rowIdx, 0)).getName();
            if (!key.trim().isEmpty()) {
                model.appendNewTag();
            }
            requestFocusInCell(model.getRowCount()-1, 0);
        }

        protected final void updateEnabledState() {
            setEnabled(TagTable.this.isEnabled());
        }

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            updateEnabledState();
        }
    }

     /**
     * Action to be run when the user wants to paste tags from buffer
     */
    class PasteAction extends RunnableAction implements PropertyChangeListener{
        public PasteAction() {
            putValue(SMALL_ICON, ImageProvider.get("", "pastetags"));
            putValue(SHORT_DESCRIPTION, tr("Paste tags from buffer"));
            TagTable.this.addPropertyChangeListener(this);
            updateEnabledState();
        }

        @Override
        public void run() {
            Relation relation = new Relation();
            model.applyToPrimitive(relation);

            String buf = Utils.getClipboardContent();
            if (buf == null || buf.isEmpty() || buf.matches(CopyAction.CLIPBOARD_REGEXP)) {
                List<PrimitiveData> directlyAdded = Main.pasteBuffer.getDirectlyAdded();
                if (directlyAdded == null || directlyAdded.isEmpty()) return;
                PasteTagsAction.TagPaster tagPaster = new PasteTagsAction.TagPaster(directlyAdded, Collections.<OsmPrimitive>singletonList(relation));
                model.updateTags(tagPaster.execute());
            } else {
                // Paste tags from arbitrary text
                 Map<String, String> tags = TextTagParser.readTagsFromText(buf);
                 if (tags == null || tags.isEmpty()) {
                    TextTagParser.showBadBufferMessage(ht("/Action/PasteTags"));
                 } else if (TextTagParser.validateTags(tags)) {
                     List<Tag> newTags = new ArrayList<>();
                     for (Map.Entry<String, String> entry: tags.entrySet()) {
                        String k = entry.getKey();
                        String v = entry.getValue();
                        newTags.add(new Tag(k, v));
                     }
                     model.updateTags(newTags);
                 }
            }
        }

        protected final void updateEnabledState() {
            setEnabled(TagTable.this.isEnabled());
        }

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            updateEnabledState();
        }
    }

    /** the delete action */
    private RunnableAction deleteAction = null;

    /** the add action */
    private RunnableAction addAction = null;

    /** the tag paste action */
    private RunnableAction pasteAction = null;

    /**
     *
     * @return the delete action used by this table
     */
    public RunnableAction getDeleteAction() {
        return deleteAction;
    }

    public RunnableAction getAddAction() {
        return addAction;
    }

    public RunnableAction getPasteAction() {
        return pasteAction;
    }

    /**
     * initialize the table
     */
    protected final void init() {
        setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        setRowSelectionAllowed(true);
        setColumnSelectionAllowed(true);
        setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);

        // make ENTER behave like TAB
        //
        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
        .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");

        // install custom navigation actions
        //
        getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction());
        getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction());

        // create a delete action. Installing this action in the input and action map
        // didn't work. We therefore handle delete requests in processKeyBindings(...)
        //
        deleteAction = new DeleteAction();

        // create the add action
        //
        addAction = new AddAction();
        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
        .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_MASK), "addTag");
        getActionMap().put("addTag", addAction);

        pasteAction = new PasteAction();

        // create the table cell editor and set it to key and value columns
        //
        TagCellEditor tmpEditor = new TagCellEditor();
        setRowHeight(tmpEditor.getEditor().getPreferredSize().height);
        setTagCellEditor(tmpEditor);
    }

    /**
     * Creates a new tag table
     *
     * @param model the tag editor model
     */
    public TagTable(TagEditorModel model) {
        super(model, new TagTableColumnModel(model.getColumnSelectionModel()), model.getRowSelectionModel());
        this.model = model;
        init();
    }

    @Override
    public Dimension getPreferredSize() {
        Container c = getParent();
        while (c != null && !(c instanceof JViewport)) {
            c = c.getParent();
        }
        if (c != null) {
            Dimension d = super.getPreferredSize();
            d.width = c.getSize().width;
            return d;
        }
        return super.getPreferredSize();
    }

    @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
            int condition, boolean pressed) {

        // handle delete key
        //
        if (e.getKeyCode() == KeyEvent.VK_DELETE) {
            if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1)
                // if DEL was pressed and only the currently edited cell is selected,
                // don't run the delete action. DEL is handled by the CellEditor as normal
                // DEL in the text input.
                //
                return super.processKeyBinding(ks, e, condition, pressed);
            getDeleteAction().run();
        }
        return super.processKeyBinding(ks, e, condition, pressed);
    }

    /**
     * Sets the editor autocompletion list
     * @param autoCompletionList autocompletion list
     */
    public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
        if (autoCompletionList == null)
            return;
        if (editor != null) {
            editor.setAutoCompletionList(autoCompletionList);
        }
    }

    public void setAutoCompletionManager(AutoCompletionManager autocomplete) {
        if (autocomplete == null) {
            Main.warn("argument autocomplete should not be null. Aborting.");
            Thread.dumpStack();
            return;
        }
        if (editor != null) {
            editor.setAutoCompletionManager(autocomplete);
        }
    }

    public AutoCompletionList getAutoCompletionList() {
        if (editor != null)
            return editor.getAutoCompletionList();
        else
            return null;
    }

    public void setNextFocusComponent(Component nextFocusComponent) {
        this.nextFocusComponent = nextFocusComponent;
    }

    public TagCellEditor getTableCellEditor() {
        return editor;
    }

    public void  addOKAccelatorListener(KeyListener l) {
        addKeyListener(l);
        if (editor != null) {
            editor.getEditor().addKeyListener(l);
        }
    }

    /**
     * Inject a tag cell editor in the tag table
     *
     * @param editor tag cell editor
     */
    public void setTagCellEditor(TagCellEditor editor) {
        if (isEditing()) {
            this.editor.cancelCellEditing();
        }
        this.editor = editor;
        getColumnModel().getColumn(0).setCellEditor(editor);
        getColumnModel().getColumn(1).setCellEditor(editor);
    }

    public void requestFocusInCell(final int row, final int col) {
        changeSelection(row, col, false, false);
        editCellAt(row, col);
        Component c = getEditorComponent();
        if (c != null) {
            c.requestFocusInWindow();
            if (c instanceof JTextComponent) {
                 ((JTextComponent) c).selectAll();
            }
        }
        // there was a bug here - on older 1.6 Java versions Tab was not working
        // after such activation. In 1.7 it works OK,
        // previous solution of using awt.Robot was resetting mouse speed on Windows
    }

    public void addComponentNotStoppingCellEditing(Component component) {
        if (component == null) return;
        doNotStopCellEditingWhenFocused.addIfAbsent(component);
    }

    public void removeComponentNotStoppingCellEditing(Component component) {
        if (component == null) return;
        doNotStopCellEditingWhenFocused.remove(component);
    }

    @Override
    public boolean editCellAt(int row, int column, EventObject e) {

        // a snipped copied from the Java 1.5 implementation of JTable
        //
        if (cellEditor != null && !cellEditor.stopCellEditing())
            return false;

        if (row < 0 || row >= getRowCount() ||
                column < 0 || column >= getColumnCount())
            return false;

        if (!isCellEditable(row, column))
            return false;

        // make sure our custom implementation of CellEditorRemover is created
        if (editorRemover == null) {
            KeyboardFocusManager fm =
                KeyboardFocusManager.getCurrentKeyboardFocusManager();
            editorRemover = new CellEditorRemover(fm);
            fm.addPropertyChangeListener("permanentFocusOwner", editorRemover);
        }

        // delegate to the default implementation
        return super.editCellAt(row, column, e);
    }

    @Override
    public void removeEditor() {
        // make sure we unregister our custom implementation of CellEditorRemover
        KeyboardFocusManager.getCurrentKeyboardFocusManager().
        removePropertyChangeListener("permanentFocusOwner", editorRemover);
        editorRemover = null;
        super.removeEditor();
    }

    @Override
    public void removeNotify() {
        // make sure we unregister our custom implementation of CellEditorRemover
        KeyboardFocusManager.getCurrentKeyboardFocusManager().
        removePropertyChangeListener("permanentFocusOwner", editorRemover);
        editorRemover = null;
        super.removeNotify();
    }

    /**
     * This is a custom implementation of the CellEditorRemover used in JTable
     * to handle the client property <tt>terminateEditOnFocusLost</tt>.
     *
     * This implementation also checks whether focus is transferred to one of a list
     * of dedicated components, see {@link TagTable#doNotStopCellEditingWhenFocused}.
     * A typical example for such a component is a button in {@link TagEditorPanel}
     * which isn't a child component of {@link TagTable} but which should respond to
     * to focus transfer in a similar way to a child of TagTable.
     *
     */
    class CellEditorRemover implements PropertyChangeListener {
        private KeyboardFocusManager focusManager;

        public CellEditorRemover(KeyboardFocusManager fm) {
            this.focusManager = fm;
        }

        @Override
        public void propertyChange(PropertyChangeEvent ev) {
            if (!isEditing())
                return;

            Component c = focusManager.getPermanentFocusOwner();
            while (c != null) {
                if (c == TagTable.this)
                    // focus remains inside the table
                    return;
                if (doNotStopCellEditingWhenFocused.contains(c))
                    // focus remains on one of the associated components
                    return;
                else if (c instanceof Window) {
                    if (c == SwingUtilities.getRoot(TagTable.this)) {
                        if (!getCellEditor().stopCellEditing()) {
                            getCellEditor().cancelCellEditing();
                        }
                    }
                    break;
                }
                c = c.getParent();
            }
        }
    }
}
