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

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

import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.ChangePropertyCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.PrimitiveData;
import org.openstreetmap.josm.data.osm.Tag;
import org.openstreetmap.josm.data.osm.TagCollection;
import org.openstreetmap.josm.gui.conflict.tags.PasteTagsConflictResolverDialog;
import org.openstreetmap.josm.tools.Shortcut;
import org.openstreetmap.josm.tools.TextTagParser;
import org.openstreetmap.josm.tools.Utils;

/**
 * Action, to paste all tags from one primitive to another.
 *
 * It will take the primitive from the copy-paste buffer an apply all its tags
 * to the selected primitive(s).
 *
 * @author David Earl
 */
public final class PasteTagsAction extends JosmAction {

    private static final String help = ht("/Action/PasteTags");

    /**
     * Constructs a new {@code PasteTagsAction}.
     */
    public PasteTagsAction() {
        super(tr("Paste Tags"), "pastetags",
                tr("Apply tags of contents of paste buffer to all selected items."),
                Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")),
                KeyEvent.VK_V, Shortcut.CTRL_SHIFT), true);
        putValue("help", help);
    }

    public static class TagPaster {

        private final Collection<PrimitiveData> source;
        private final Collection<OsmPrimitive> target;
        private final List<Tag> commands = new ArrayList<>();

        public TagPaster(Collection<PrimitiveData> source, Collection<OsmPrimitive> target) {
            this.source = source;
            this.target = target;
        }

        /**
         * Replies true if the source for tag pasting is heterogeneous, i.e. if it doesn't consist of
         * {@link OsmPrimitive}s of exactly one type
         */
        protected boolean isHeteogeneousSource() {
            int count = 0;
            count = !getSourcePrimitivesByType(OsmPrimitiveType.NODE).isEmpty() ? count + 1 : count;
            count = !getSourcePrimitivesByType(OsmPrimitiveType.WAY).isEmpty() ? count + 1 : count;
            count = !getSourcePrimitivesByType(OsmPrimitiveType.RELATION).isEmpty() ? count + 1 : count;
            return count > 1;
        }

        /**
         * Replies all primitives of type <code>type</code> in the current selection.
         *
         * @param type  the type
         * @return all primitives of type <code>type</code> in the current selection.
         */
        protected Collection<? extends PrimitiveData> getSourcePrimitivesByType(OsmPrimitiveType type) {
            return PrimitiveData.getFilteredList(source, type);
        }

        /**
         * Replies the collection of tags for all primitives of type <code>type</code> in the current
         * selection
         *
         * @param type  the type
         * @return the collection of tags for all primitives of type <code>type</code> in the current
         * selection
         */
        protected TagCollection getSourceTagsByType(OsmPrimitiveType type) {
            return TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type));
        }

        /**
         * Replies true if there is at least one tag in the current selection for primitives of
         * type <code>type</code>
         *
         * @param type the type
         * @return true if there is at least one tag in the current selection for primitives of
         * type <code>type</code>
         */
        protected boolean hasSourceTagsByType(OsmPrimitiveType type) {
            return !getSourceTagsByType(type).isEmpty();
        }

        protected void buildChangeCommand(Collection<? extends OsmPrimitive> selection, TagCollection tc) {
            for (String key : tc.getKeys()) {
                commands.add(new Tag(key, tc.getValues(key).iterator().next()));
            }
        }

        protected Map<OsmPrimitiveType, Integer> getSourceStatistics() {
            Map<OsmPrimitiveType, Integer> ret = new EnumMap<>(OsmPrimitiveType.class);
            for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
                if (!getSourceTagsByType(type).isEmpty()) {
                    ret.put(type, getSourcePrimitivesByType(type).size());
                }
            }
            return ret;
        }

        protected Map<OsmPrimitiveType, Integer> getTargetStatistics() {
            Map<OsmPrimitiveType, Integer> ret = new EnumMap<>(OsmPrimitiveType.class);
            for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
                int count = OsmPrimitive.getFilteredList(target, type.getOsmClass()).size();
                if (count > 0) {
                    ret.put(type, count);
                }
            }
            return ret;
        }

        /**
         * Pastes the tags from a homogeneous source (the {@link Main#pasteBuffer}s selection consisting
         * of one type of {@link OsmPrimitive}s only).
         *
         * Tags from a homogeneous source can be pasted to a heterogeneous target. All target primitives,
         * regardless of their type, receive the same tags.
         */
        protected void pasteFromHomogeneousSource() {
            TagCollection tc = null;
            for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
                TagCollection tc1 = getSourceTagsByType(type);
                if (!tc1.isEmpty()) {
                    tc = tc1;
                }
            }
            if (tc == null)
                // no tags found to paste. Abort.
                return;

            if (!tc.isApplicableToPrimitive()) {
                PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent);
                dialog.populate(tc, getSourceStatistics(), getTargetStatistics());
                dialog.setVisible(true);
                if (dialog.isCanceled())
                    return;
                buildChangeCommand(target, dialog.getResolution());
            } else {
                // no conflicts in the source tags to resolve. Just apply the tags
                // to the target primitives
                //
                buildChangeCommand(target, tc);
            }
        }

        /**
         * Replies true if there is at least one primitive of type <code>type</code>
         * is in the target collection
         *
         * @param type  the type to look for
         * @return true if there is at least one primitive of type <code>type</code> in the collection
         * <code>selection</code>
         */
        protected boolean hasTargetPrimitives(Class<? extends OsmPrimitive> type) {
            return !OsmPrimitive.getFilteredList(target, type).isEmpty();
        }

        /**
         * Replies true if this a heterogeneous source can be pasted without conflict to targets
         *
         * @param targets the collection of target primitives
         * @return true if this a heterogeneous source can be pasted without conflicts to targets
         */
        protected boolean canPasteFromHeterogeneousSourceWithoutConflict(Collection<OsmPrimitive> targets) {
            for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
                if (hasTargetPrimitives(type.getOsmClass())) {
                    TagCollection tc = TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type));
                    if (!tc.isEmpty() && !tc.isApplicableToPrimitive())
                        return false;
                }
            }
            return true;
        }

        /**
         * Pastes the tags in the current selection of the paste buffer to a set of target
         * primitives.
         */
        protected void pasteFromHeterogeneousSource() {
            if (canPasteFromHeterogeneousSourceWithoutConflict(target)) {
                for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
                    if (hasSourceTagsByType(type) && hasTargetPrimitives(type.getOsmClass())) {
                        buildChangeCommand(target, getSourceTagsByType(type));
                    }
                }
            } else {
                PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent);
                dialog.populate(
                        getSourceTagsByType(OsmPrimitiveType.NODE),
                        getSourceTagsByType(OsmPrimitiveType.WAY),
                        getSourceTagsByType(OsmPrimitiveType.RELATION),
                        getSourceStatistics(),
                        getTargetStatistics()
                );
                dialog.setVisible(true);
                if (dialog.isCanceled())
                    return;
                for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
                    if (hasSourceTagsByType(type) && hasTargetPrimitives(type.getOsmClass())) {
                        buildChangeCommand(OsmPrimitive.getFilteredList(target, type.getOsmClass()), dialog.getResolution(type));
                    }
                }
            }
        }

        public List<Tag> execute() {
            commands.clear();
            if (isHeteogeneousSource()) {
                pasteFromHeterogeneousSource();
            } else {
                pasteFromHomogeneousSource();
            }
            return commands;
        }

    }

    @Override
    public void actionPerformed(ActionEvent e) {
        Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();

        if (selection.isEmpty())
            return;

        String buf = Utils.getClipboardContent();
        if (buf == null || buf.isEmpty() || buf.matches(CopyAction.CLIPBOARD_REGEXP)) {
            pasteTagsFromJOSMBuffer(selection);
        } else {
            // Paste tags from arbitrary text
            pasteTagsFromText(selection, buf);
        }
    }

    /** Paste tags from arbitrary text, not using JOSM buffer
     * @return true if action was successful
     */
    public static boolean pasteTagsFromText(Collection<OsmPrimitive> selection, String text) {
        Map<String, String> tags = TextTagParser.readTagsFromText(text);
        if (tags == null || tags.isEmpty()) {
            TextTagParser.showBadBufferMessage(help);
            return false;
        }
        if (!TextTagParser.validateTags(tags)) return false;

        List<Command> commands = new ArrayList<>(tags.size());
        for (Entry<String, String> entry: tags.entrySet()) {
            String v = entry.getValue();
            commands.add(new ChangePropertyCommand(selection, entry.getKey(), "".equals(v) ? null : v));
        }
        commitCommands(selection, commands);
        return !commands.isEmpty();
    }

    /** Paste tags from JOSM buffer
     * @param selection objects that will have the tags
     * @return false if JOSM buffer was empty
     */
    public static boolean pasteTagsFromJOSMBuffer(Collection<OsmPrimitive> selection) {
        List<PrimitiveData> directlyAdded = Main.pasteBuffer.getDirectlyAdded();
        if (directlyAdded == null || directlyAdded.isEmpty()) return false;

        PasteTagsAction.TagPaster tagPaster = new PasteTagsAction.TagPaster(directlyAdded, selection);
        List<Command> commands = new ArrayList<>();
        for (Tag tag : tagPaster.execute()) {
            commands.add(new ChangePropertyCommand(selection, tag.getKey(), "".equals(tag.getValue()) ? null : tag.getValue()));
        }
        commitCommands(selection, commands);
        return true;
    }

    /**
     * Create and execute SequenceCommand with descriptive title
     * @param commands the commands to perform in a sequential command
     */
    private static void commitCommands(Collection<OsmPrimitive> selection, List<Command> commands) {
        if (!commands.isEmpty()) {
            String title1 = trn("Pasting {0} tag", "Pasting {0} tags", commands.size(), commands.size());
            String title2 = trn("to {0} object", "to {0} objects", selection.size(), selection.size());
            Main.main.undoRedo.add(
                    new SequenceCommand(
                            title1 + " " + title2,
                            commands
                    ));
        }
    }

    @Override
    protected void updateEnabledState() {
        if (getCurrentDataSet() == null) {
            setEnabled(false);
            return;
        }
        // buffer listening slows down the program and is not very good for arbitrary text in buffer
        setEnabled(!getCurrentDataSet().getSelected().isEmpty());
    }

    @Override
    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
        setEnabled(selection != null && !selection.isEmpty());
    }
}
