001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.tagging.ac;
003
004import java.util.Collection;
005import java.util.Objects;
006import java.util.Optional;
007import java.util.Set;
008import java.util.TreeSet;
009import java.util.stream.Collectors;
010
011/**
012 * A sorted set of {@link AutoCompletionItem}s.
013 *
014 * Items are sorted with higher priority first, then according to lexicographic order
015 * on the value of the {@code AutoCompletionListItem}.
016 *
017 * @since 12859 (extracted from {@code gui.tagging.ac.AutoCompletionList})
018 */
019public class AutoCompletionSet extends TreeSet<AutoCompletionItem> {
020
021    // Keep a separate tree set of values for determining fast if a value is present
022    private final Set<String> values = new TreeSet<>();
023
024    @Override
025    public boolean add(AutoCompletionItem e) {
026        // Is there already an item for the value?
027        String value = e.getValue();
028        if (contains(value)) { // Fast
029            Optional<AutoCompletionItem> result = stream().filter(i -> i.getValue().equals(e.getValue())).findFirst(); // Slow
030            if (result.isPresent()) {
031                AutoCompletionItem item = result.get();
032                // yes: merge priorities
033                AutoCompletionPriority newPriority = item.getPriority().mergeWith(e.getPriority());
034                // if needed, remove/re-add the updated item to maintain set ordering
035                if (!item.getPriority().equals(newPriority)) {
036                    super.remove(item);
037                    item.setPriority(newPriority);
038                    return super.add(item);
039                } else {
040                    return false;
041                }
042            } else {
043                // Should never happen if values is correctly synchronized with this set
044                throw new IllegalStateException(value);
045            }
046        } else {
047            values.add(value);
048            return super.add(e);
049        }
050    }
051
052    @Override
053    public boolean remove(Object o) {
054        if (o instanceof AutoCompletionItem) {
055            values.remove(((AutoCompletionItem) o).getValue());
056        }
057        return super.remove(o);
058    }
059
060    @Override
061    public void clear() {
062        values.clear();
063        super.clear();
064    }
065
066    /**
067     * Adds a list of strings to this list. Only strings which
068     * are not null and which do not exist yet in the list are added.
069     *
070     * @param values a list of strings to add
071     * @param priority the priority to use
072     * @return {@code true} if this set changed as a result of the call
073     */
074    public boolean addAll(Collection<String> values, AutoCompletionPriority priority) {
075        return addAll(values.stream().filter(Objects::nonNull).map(v -> new AutoCompletionItem(v, priority)).collect(Collectors.toList()));
076    }
077
078    /**
079     * Adds values that have been entered by the user.
080     * @param values values that have been entered by the user
081     * @return {@code true} if this set changed as a result of the call
082     */
083    public boolean addUserInput(Collection<String> values) {
084        int i = 0;
085        boolean modified = false;
086        for (String value : values) {
087            if (value != null && add(new AutoCompletionItem(value, new AutoCompletionPriority(false, false, false, i++)))) {
088                modified = true;
089            }
090        }
091        return modified;
092    }
093
094    /**
095     * Checks whether an item with the given value is already in the list. Ignores priority of the items.
096     *
097     * @param value the value of an auto completion item
098     * @return true, if value is in the list; false, otherwise
099     */
100    public boolean contains(String value) {
101        return values.contains(value);
102    }
103
104    /**
105     * Removes the auto completion item with key <code>key</code>
106     * @param key the key
107     * @return {@code true} if an element was removed
108     */
109    public boolean remove(String key) {
110        return values.remove(key) && removeIf(i -> i.getValue().equals(key));
111    }
112}