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

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

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.logging.Logger;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.TransferHandler;
import javax.swing.UIManager;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.PrimitiveId;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
import org.openstreetmap.josm.plugins.turnrestrictions.dnd.PrimitiveIdListProvider;
import org.openstreetmap.josm.plugins.turnrestrictions.dnd.PrimitiveIdListTransferHandler;
import org.openstreetmap.josm.plugins.turnrestrictions.dnd.PrimitiveIdTransferable;
import org.openstreetmap.josm.tools.CheckParameterUtil;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.Shortcut;
import org.openstreetmap.josm.tools.Utils;

/**
 * This is an editor for one of the two legs of a turn restriction.
 */
public class TurnRestrictionLegEditor extends JPanel implements Observer, PrimitiveIdListProvider {
    //private static final Logger logger = Logger.getLogger(TurnRestrictionLegEditor.class.getName());

    private JLabel lblOsmObject;
    private final Set<OsmPrimitive> legs = new HashSet<>();
    private TurnRestrictionEditorModel model;
    private TurnRestrictionLegRole role;
    private DeleteAction actDelete;
    private CopyAction actCopy;
    private PasteAction actPaste;
    private AcceptAction actAccept;
    private TransferHandler transferHandler;

    /**
     * builds the UI
     */
    protected void build() {
        setLayout(new BorderLayout());
        add(lblOsmObject = new JLabel(), BorderLayout.CENTER);
        lblOsmObject.setOpaque(true);
        lblOsmObject.setBorder(null);
        setBorder(
                BorderFactory.createCompoundBorder(
                        BorderFactory.createEtchedBorder(),
                        BorderFactory.createEmptyBorder(1, 1, 1, 1)
                )
        );

        JPanel pnlButtons = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
        pnlButtons.setBorder(null);
        JButton btn;
        actDelete = new DeleteAction();
        pnlButtons.add(btn = new JButton(actDelete));
        btn.setFocusable(false);
        btn.setText(null);
        btn.setBorder(BorderFactory.createRaisedBevelBorder());

        actAccept = new AcceptAction();
        pnlButtons.add(btn = new JButton(actAccept));
        btn.setFocusable(false);
        btn.setText(null);
        btn.setBorder(BorderFactory.createRaisedBevelBorder());
        add(pnlButtons, BorderLayout.EAST);

        // focus handling
        FocusHandler fh = new FocusHandler();
        lblOsmObject.setFocusable(true);
        lblOsmObject.addFocusListener(fh);
        this.addFocusListener(fh);

        // mouse event handling
        MouseEventHandler meh = new MouseEventHandler();
        lblOsmObject.addMouseListener(meh);
        addMouseListener(meh);
        lblOsmObject.addMouseListener(new PopupLauncher());

        // enable DEL to remove the object from the turn restriction
        registerKeyboardAction(actDelete, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

        getInputMap().put(Shortcut.getCopyKeyStroke(), TransferHandler.getCopyAction().getValue(Action.NAME));;
        getInputMap().put(Shortcut.getPasteKeyStroke(), TransferHandler.getPasteAction().getValue(Action.NAME));;
        getActionMap().put(TransferHandler.getCopyAction().getValue(Action.NAME), TransferHandler.getCopyAction());
        getActionMap().put(TransferHandler.getPasteAction().getValue(Action.NAME), TransferHandler.getPasteAction());
        lblOsmObject.setTransferHandler(transferHandler = new LegEditorTransferHandler(this));
        lblOsmObject.addMouseMotionListener(new MouseMotionAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                JComponent c = (JComponent) e.getSource();
                TransferHandler th = c.getTransferHandler();
                th.exportAsDrag(c, e, TransferHandler.COPY);
            }
        });
        actCopy = new CopyAction();
        actPaste = new PasteAction();
    }

    /**
     * Constructor
     *
     * @param model the model. Must not be null.
     * @param role the leg role of the leg this editor is editing. Must not be null.
     * @exception IllegalArgumentException thrown if model is null
     * @exception IllegalArgumentException thrown if role is null
     */
    public TurnRestrictionLegEditor(TurnRestrictionEditorModel model, TurnRestrictionLegRole role) {
        CheckParameterUtil.ensureParameterNotNull(model, "model");
        CheckParameterUtil.ensureParameterNotNull(role, "role");

        this.model = model;
        this.role = role;
        build();
        model.addObserver(this);
        refresh();
    }

    protected void refresh() {
        legs.clear();
        legs.addAll(model.getTurnRestrictionLeg(role));
        if (legs.isEmpty()) {
            lblOsmObject.setFont(UIManager.getFont("Label.font").deriveFont(Font.ITALIC));
            lblOsmObject.setIcon(null);
            lblOsmObject.setText(tr("please select a way"));
            lblOsmObject.setToolTipText(null);
        } else if (legs.size() == 1) {
            OsmPrimitive leg = legs.iterator().next();
            lblOsmObject.setFont(UIManager.getFont("Label.font"));
            lblOsmObject.setIcon(ImageProvider.get("data", "way"));
            lblOsmObject.setText(leg.getDisplayName(DefaultNameFormatter.getInstance()));
            lblOsmObject.setToolTipText(DefaultNameFormatter.getInstance().buildDefaultToolTip(leg));
        } else {
            lblOsmObject.setFont(UIManager.getFont("Label.font").deriveFont(Font.ITALIC));
            lblOsmObject.setIcon(null);
            lblOsmObject.setText(tr("multiple objects with role ''{0}''", this.role.getOsmRole()));
            lblOsmObject.setToolTipText(null);
        }
        renderColors();
        actDelete.updateEnabledState();
    }

    /**
     * Render the foreground and background color
     */
    protected void renderColors() {
        if (lblOsmObject.hasFocus()) {
            setBackground(UIManager.getColor("List.selectionBackground"));
            setForeground(UIManager.getColor("List.selectionForeground"));
            lblOsmObject.setBackground(UIManager.getColor("List.selectionBackground"));
            lblOsmObject.setForeground(UIManager.getColor("List.selectionForeground"));
        } else {
            lblOsmObject.setBackground(UIManager.getColor("List.background"));
            lblOsmObject.setForeground(UIManager.getColor("List.foreground"));
        }
    }

    /**
     * Replies the model for this editor
     *
     * @return the model
     */
    public TurnRestrictionEditorModel getModel() {
        return model;
    }

    /**
     * Replies the role of this editor
     *
     * @return the role
     */
    public TurnRestrictionLegRole getRole() {
        return role;
    }

    /* ----------------------------------------------------------------------------- */
    /* interface Observer                                                            */
    /* ----------------------------------------------------------------------------- */
    @Override
    public void update(Observable o, Object arg) {
        refresh();
    }

    /* ----------------------------------------------------------------------------- */
    /* interface PrimitiveIdListProvider                                                            */
    /* ----------------------------------------------------------------------------- */
    @Override
    public List<PrimitiveId> getSelectedPrimitiveIds() {
        if (legs.size() == 1) {
            return Collections.singletonList(legs.iterator().next().getPrimitiveId());
        }
        return Collections.emptyList();
    }

    /* ----------------------------------------------------------------------------- */
    /* inner classes                                                                 */
    /* ----------------------------------------------------------------------------- */
    /**
     * Responds to focus change events
     */
    class FocusHandler extends FocusAdapter {
        @Override
        public void focusGained(FocusEvent e) {
            renderColors();
        }

        @Override
        public void focusLost(FocusEvent e) {
            renderColors();
        }
    }

    class MouseEventHandler extends MouseAdapter {
        @Override
        public void mouseClicked(MouseEvent e) {
            lblOsmObject.requestFocusInWindow();
        }
    }

    /**
     * Deletes the way from the turn restriction
     */
    class DeleteAction extends AbstractAction {
        DeleteAction() {
            putValue(SHORT_DESCRIPTION, tr("Delete from turn restriction"));
            putValue(NAME, tr("Delete"));
            new ImageProvider("deletesmall").getResource().attachImageIcon(this);
            putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0));
            updateEnabledState();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            model.setTurnRestrictionLeg(role, null);
        }

        public void updateEnabledState() {
            setEnabled(legs.size() > 0);
        }
    }

    /**
     * Accepts the currently selected way as turn restriction leg. Only enabled,
     * if there is exactly one way selected
     */
    class AcceptAction extends AbstractAction implements ListSelectionListener {

        AcceptAction() {
             putValue(SHORT_DESCRIPTION, tr("Accept the currently selected way"));
             putValue(NAME, tr("Accept"));
             new ImageProvider("accept").getResource().attachImageIcon(this);
             model.getJosmSelectionListModel().getListSelectionModel().addListSelectionListener(this);
             updateEnabledState();
        }

         @Override
        public void actionPerformed(ActionEvent e) {
             List<Way> selWays = new ArrayList<>(Utils.filteredCollection(model.getJosmSelectionListModel().getSelected(), Way.class));
             if (selWays.size() != 1) return;
             Way w = selWays.get(0);
             model.setTurnRestrictionLeg(role, w);
         }

         public void updateEnabledState() {
            setEnabled(Utils.filteredCollection(model.getJosmSelectionListModel().getSelected(), Way.class).size() == 1);
         }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            updateEnabledState();
        }
    }

    /**
     * The transfer handler for Drag-and-Drop.
     */
    class LegEditorTransferHandler extends PrimitiveIdListTransferHandler {
        Logger logger = Logger.getLogger(LegEditorTransferHandler.class.getName());

        LegEditorTransferHandler(PrimitiveIdListProvider provider) {
            super(provider);
        }

        @SuppressWarnings("unchecked")
        @Override
        public boolean importData(JComponent comp, Transferable t) {
            try {
                List<PrimitiveId> ids = (List<PrimitiveId>) t.getTransferData(PrimitiveIdTransferable.PRIMITIVE_ID_LIST_FLAVOR);
                if (ids.size() != 1) {
                    return false;
                }
                PrimitiveId id = ids.get(0);
                if (!id.getType().equals(OsmPrimitiveType.WAY)) return false;
                model.setTurnRestrictionLeg(role, id);
                return true;
            } catch (IOException e) {
                // ignore
                return false;
            } catch (UnsupportedFlavorException e) {
                // ignore
                return false;
            }
        }

        @Override
        protected Transferable createTransferable(JComponent c) {
            if (legs.size() != 1) return null;
            return super.createTransferable(c);
        }
    }

    class PopupLauncher extends PopupMenuLauncher {
        @Override
        public void launch(MouseEvent evt) {
            new PopupMenu().show(lblOsmObject, evt.getX(), evt.getY());
        }
    }

    class PopupMenu extends JPopupMenu {
        PopupMenu() {
            actCopy.updateEnabledState();
            JMenuItem item = add(actCopy);
            item.setTransferHandler(transferHandler);
            actPaste.updateEnabledState();
            item = add(actPaste);
            item.setTransferHandler(transferHandler);
            addSeparator();
            add(actDelete);
        }
    }

    class CopyAction extends AbstractAction {
        private Action delegate;

        CopyAction() {
            putValue(NAME, tr("Copy"));
            putValue(SHORT_DESCRIPTION, tr("Copy to the clipboard"));
            new ImageProvider("copy").getResource().attachImageIcon(this);
            putValue(ACCELERATOR_KEY, Shortcut.getCopyKeyStroke());
            delegate = TurnRestrictionLegEditor.this.getActionMap().get("copy");
            updateEnabledState();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            delegate.actionPerformed(e);
        }

        public void updateEnabledState() {
            setEnabled(legs.size() == 1);
        }
    }

    class PasteAction extends AbstractAction {
        private Action delegate;

        public boolean canPaste() {
            Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
            for (DataFlavor df: clipboard.getAvailableDataFlavors()) {
                if (df.equals(PrimitiveIdTransferable.PRIMITIVE_ID_LIST_FLAVOR)) return true;
            }
            // FIXME: check whether there are selected objects in the JOSM copy/paste buffer
            return false;
        }

        PasteAction() {
            putValue(NAME, tr("Paste"));
            putValue(SHORT_DESCRIPTION, tr("Paste from the clipboard"));
            new ImageProvider("paste").getResource().attachImageIcon(this);
            putValue(ACCELERATOR_KEY, Shortcut.getPasteKeyStroke());
            delegate = TurnRestrictionLegEditor.this.getActionMap().get("paste");
        }

        public void updateEnabledState() {
            setEnabled(canPaste());
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            delegate.actionPerformed(e);
        }
    }
}
