/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sis.gui.referencing;

import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Objects;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WritableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Menu;
import javafx.concurrent.Task;
import org.opengis.util.FactoryException;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.ReferenceSystem;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.crs.DerivedCRS;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.datum.PixelInCell;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.geometry.ImmutableEnvelope;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
import org.apache.sis.referencing.gazetteer.MilitaryGridReferenceSystem;
import org.apache.sis.referencing.gazetteer.GazetteerException;
import org.apache.sis.referencing.gazetteer.GazetteerFactory;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.gui.internal.BackgroundThreads;
import org.apache.sis.gui.internal.ExceptionReporter;
import org.apache.sis.gui.internal.GUIUtilities;
import org.apache.sis.gui.internal.NonNullObjectProperty;
import org.apache.sis.gui.internal.RecentChoices;
import org.apache.sis.gui.internal.io.OptionalDataDownloader;
import org.apache.sis.util.internal.Strings;
import org.apache.sis.util.internal.UnmodifiableArrayList;

import static org.apache.sis.gui.internal.LogHandler.LOGGER;


/**
 * A short list (~10 items) of most recently used {@link ReferenceSystem}s.
 * The list can be shown in a {@link ChoiceBox} or in a list of {@link MenuItem} controls.
 * The last choice is an "Other…" item which, when selected, popups the {@link CRSChooser}.
 *
 * <p>The choices are listed in following order:</p>
 * <ul>
 *   <li>The first choice is the native or preferred reference system of visualized data.
 *       That choice stay always in the first position.</li>
 *   <li>The last choice is "Other…" and stay always in the last position.</li>
 *   <li>All other choices between first and last are ordered with most recently used first.</li>
 * </ul>
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.4
 * @since   1.1
 */
public class RecentReferenceSystems {
    /**
     * Number of reference systems to always show before all other reference systems.
     * They are the native of preferred reference system for the visualized data.
     */
    private static final int NUM_CORE_ITEMS = 1;

    /**
     * Number of reference systems to show in {@link ChoiceBox} or {@link MenuItem}s.
     * The {@value #NUM_CORE_ITEMS} core systems are included but not {@link #OTHER}.
     */
    private static final int NUM_SHOWN_ITEMS = 9;

    /**
     * Number of reference systems to keep at the end of the list.
     */
    private static final int NUM_OTHER_ITEMS = 1;

    /**
     * Key for use with the {@linkplain Menu#getProperties() property map} for storing the selected item.
     * Used for providing the functionality of {@link javafx.scene.control.CheckBox#selectedProperty()}
     * on controls that do not have an explicit selected property.
     */
    private static final String SELECTED_ITEM_KEY = "SelectedItem";

    /**
     * A pseudo-reference system for the "Other…" choice. We use a null value because {@link ChoiceBox}
     * seems to insist for inserting a null value in the items list when we remove the selected item.
     *
     * <h4>Maintenance note</h4>
     * If this field is changed to a non-null value,
     * search also for usages of {@code Object::nonNull} predicate.
     */
    static final ReferenceSystem OTHER = null;

    /**
     * The factory to use for creating a Coordinate Reference System from an authority code.
     * If {@code null}, then a default factory will be fetched when first needed.
     */
    private volatile CRSAuthorityFactory factory;

    /**
     * The area of interest, or {@code null} if none. This is used for filtering the reference systems added by
     * {@code addAlternatives(…)} and for providing some guidance to user when {@link CRSChooser} is shown.
     *
     * <h4>API note</h4>
     * We do not provide getter/setter for this property; use {@link ObjectProperty#set(Object)}
     * directly instead. We omit the "Property" suffix for making this operation more natural.
     */
    public final ObjectProperty<Envelope> areaOfInterest;

    /**
     * Area of interest converted to geographic coordinates, or {@code null} if none.
     */
    private ImmutableEnvelope geographicAOI;

    /**
     * The comparison criterion for considering two reference systems as a duplication.
     * The default value is {@link ComparisonMode#ALLOW_VARIANT}, i.e. axis orders are ignored.
     *
     * <h4>API note</h4>
     * We do not provide getter/setter for this property; use {@link ObjectProperty#set(Object)}
     * directly instead. We omit the "Property" suffix for making this operation more natural.
     */
    public final ObjectProperty<ComparisonMode> duplicationCriterion;

    /**
     * Values of controls created by this {@code RecentReferenceSystems} instance. We retain those properties
     * because modifying the {@link #referenceSystems} list sometimes causes controls to clear their selection
     * if we removed the selected item from the list. We use {@code controlValues} for saving currently selected
     * values before to modify the item list, and restore selections after we finished to modify the list.
     */
    private final List<WritableValue<ReferenceSystem>> controlValues;

    /**
     * The preferred locale for displaying object name, or {@code null} for the default locale.
     */
    final Locale locale;

    /**
     * Wrapper for a {@link ReferenceSystem} which has not yet been compared with authoritative definitions.
     * Those wrappers are created when {@link ReferenceSystem} instances have been specified to {@code setPreferred(…)}
     * or {@code addAlternatives(…)} methods with {@code replaceByAuthoritativeDefinition} argument set to {@code true}.
     *
     * @see #setPreferred(boolean, ReferenceSystem)
     * @see #addAlternatives(boolean, ReferenceSystem...)
     */
    private static final class Unverified {
        /** The reference system to verify. */
        private final ReferenceSystem system;

        /** Flags the given reference system as unverified. */
        Unverified(final ReferenceSystem system) {
            this.system = system;
        }

        /** Returns the verified (if possible) reference system. */
        ReferenceSystem find(final IdentifiedObjectFinder finder) throws FactoryException {
            if (finder != null) {
                final IdentifiedObject replacement = finder.findSingleton(system);
                if (replacement instanceof ReferenceSystem) {
                    return (ReferenceSystem) replacement;
                }
            }
            return system;
        }
    }

    /**
     * The reference systems either as {@link ReferenceSystem} instances, {@link Unverified} wrappers or
     * {@link String} codes. All {@code String} elements should be authority codes that {@link #factory}
     * can recognize. The first item in this list should be the native or preferred reference system.
     * The {@link #OTHER} reference system is <em>not</em> included in this list.
     *
     * <p>The list content is specified by calls to {@code setPreferred(…)} and {@code addAlternatives(…)} methods,
     * then is filtered by {@link #filterReferenceSystems(ImmutableEnvelope, ComparisonMode)} for resolving authority
     * codes and removing duplicated elements.</p>
     *
     * <p>All accesses to this field and to {@link #isModified} field shall be done in a block synchronized
     * on {@code systemsOrCodes}.</p>
     */
    private final List<Object> systemsOrCodes;

    /**
     * The {@link #systemsOrCodes} elements with all codes or wrappers replaced by {@link ReferenceSystem}
     * instances and duplicated values removed. This is the list given to JavaFX controls that we build.
     * This list includes {@link #OTHER} as its last item.
     *
     * <p>This list is initially null and created only when first needed. After the list has been created,
     * this reference is never modified. As long as the reference is null, we can skip the synchronization
     * of this list content with the {@link #systemsOrCodes} content when the latter changed. Because that
     * synchronization may involve accesses to the EPSG database, it is potentially costly.</p>
     *
     * @see #getReferenceSystems(boolean)
     */
    private ObservableList<ReferenceSystem> referenceSystems;

    /**
     * A view of {@link #referenceSystems} with only items that are instances of {@link CoordinateReferenceSystem}.
     * This list includes also {@link #OTHER} as its last item. This list is used for menus shown in contexts where
     * identifiers cannot be used, for example for selecting the CRS to use for displaying a map.
     *
     * <p>This list is lazily created when first needed,
     * because it depends on {@link #referenceSystems} which is itself lazily created.</p>
     *
     * @see #getReferenceSystems(boolean)
     */
    private ObservableList<ReferenceSystem> coordinateReferenceSystems;

    /**
     * A filtered view of {@link #referenceSystems} without the {@link #OTHER} item.
     * This is the list returned to users by public API, but otherwise it is not used by this class.
     *
     * <h4>Design notes</h4>
     * The {@link #OTHER} item needs to exist in the list used internally by this class because those lists
     * are used directly by controls like {@code ChoiceBox<ReferenceSystem>}, with the {@link #OTHER} value
     * handled in a special way by {@link ObjectStringConverter} for making the "Other…" item present in the
     * list of choices. But since {@link #OTHER} is not a real CRS, we want to hide that trick to users.
     *
     * <p>This list is lazily created when first needed,
     * because it depends on {@link #referenceSystems} which is itself lazily created.</p>
     *
     * @see #getItems()
     */
    private ObservableList<ReferenceSystem> publicItemList;

    /**
     * Coordinate reference systems used for computing cell indices of grid coverages.
     * Those reference systems are offered in a sub-menu and are not included in {@link #publicItemList}.
     * The content of this list depends on the grid coverages shown in the widget.
     *
     * @see #setGridReferencing(boolean, Map)
     */
    private final List<DerivedCRS> cellIndiceSystems;

    /**
     * {@code true} if the {@link #referenceSystems} list needs to be rebuilt from {@link #systemsOrCodes} content.
     * This field shall be read and modified in a block synchronized on {@link #systemsOrCodes}.
     *
     * @see #listModified()
     */
    private boolean isModified;

    /**
     * {@code true} if {@code RecentReferenceSystems} is in the process of modifying {@link #referenceSystems} list.
     * In such case we want to temporarily disable the {@link SelectionListener}.
     * This field is read and updated in JavaFX thread.
     */
    private boolean isAdjusting;

    /**
     * Creates a builder which will use a default authority factory.
     * The factory will be capable to handle at least some EPSG codes.
     */
    public RecentReferenceSystems() {
        this(null, null);
    }

    /**
     * Creates a builder which will use the specified authority factory.
     *
     * @param  factory  the factory to use for building CRS from authority codes, or {@code null} for the default.
     * @param  locale   the preferred locale for displaying object name, or {@code null} for the default locale.
     *
     * @see org.apache.sis.referencing.CRS#getAuthorityFactory(String)
     */
    public RecentReferenceSystems(final CRSAuthorityFactory factory, final Locale locale) {
        this.factory         = factory;
        this.locale          = locale;
        systemsOrCodes       = new ArrayList<>();
        cellIndiceSystems    = new ArrayList<>();
        areaOfInterest       = new SimpleObjectProperty<>(this, "areaOfInterest");
        duplicationCriterion = new NonNullObjectProperty<>(this, "duplicationCriterion", ComparisonMode.ALLOW_VARIANT);
        controlValues        = new ArrayList<>();
        duplicationCriterion.addListener((e) -> listModified());
        areaOfInterest.addListener((e,o,n) -> {
            geographicAOI = Utils.toGeographic(RecentReferenceSystems.class, "areaOfInterest", n);
            listModified();
        });
    }

    /**
     * Sets the reference systems, area of interest and "referencing by grid indices" systems.
     * This method performs all the following work:
     *
     * <ul>
     *   <li>Invokes {@link #setPreferred(boolean, ReferenceSystem)} with the first CRS in iteration order.</li>
     *   <li>Invokes {@link #addAlternatives(boolean, ReferenceSystem...)} for all other CRS (single call).</li>
     *   <li>Sets the {@link #areaOfInterest} to the union of all envelopes.</li>
     *   <li>Sets the content of "Referencing by cell indices" sub-menu.</li>
     * </ul>
     *
     * @param  replaceByAuthoritativeDefinition  whether the reference systems should be replaced by authoritative definition.
     * @param  geometries  grid coverage names together with their grid geometry. May be empty.
     *
     * @since 1.3
     */
    public void setGridReferencing(final boolean replaceByAuthoritativeDefinition,
            final Map<String,GridGeometry> geometries)
    {
        /*
         * Fetch or compute information needed, but without modifying the state of this object yet.
         * All assignments to `this` should be done inside the `try … finally` block.
         */
        int countEnv = 0;
        int countCRS = 0;
        int countCIR = 0;
        final Envelope[] envelopes = new Envelope[geometries.size()];
        final DerivedCRS[] derived = new DerivedCRS[geometries.size()];
        final CoordinateReferenceSystem[] alt = new CoordinateReferenceSystem[Math.max(derived.length - 1, 0)];
        CoordinateReferenceSystem preferred = null;
        for (final Map.Entry<String,GridGeometry> entry : geometries.entrySet()) {
            final GridGeometry gg = entry.getValue();
            if (gg.isDefined(GridGeometry.ENVELOPE)) {
                envelopes[countEnv++] = gg.getEnvelope();
            }
            if (gg.isDefined(GridGeometry.CRS)) {
                final CoordinateReferenceSystem crs = gg.getCoordinateReferenceSystem();
                if (preferred == null) {
                    preferred = crs;
                } else {
                    alt[countCRS++] = crs;
                }
                if (gg.isDefined(GridGeometry.GRID_TO_CRS | GridGeometry.EXTENT)) {
                    derived[countCIR++] = gg.createImageCRS(entry.getKey(), PixelInCell.CELL_CENTER);
                }
            }
        }
        Envelope aoi = null;
        try {
            aoi = Envelopes.union(envelopes);       // No need to trim null elements.
        } catch (TransformException e) {
            errorOccurred("setGridReferencing", e);
        }
        /*
         * Modify now the state of `this` object but with `listModified()` made almost no-op.
         * The intent is to have only one effective call to `listModified()` at the end,
         * in order to have only one call to `filterReferenceSystems(…)`.
         */
        final ObservableList<ReferenceSystem> savedReferenceSystemList = referenceSystems;
        try {
            referenceSystems = null;
            if (preferred != null) {
                setPreferred(replaceByAuthoritativeDefinition, preferred);
                addAlternatives(replaceByAuthoritativeDefinition, alt);         // No need to trim null elements.
                cellIndiceSystems.clear();
                cellIndiceSystems.addAll(UnmodifiableArrayList.wrap(derived, 0, countCIR));
            }
            areaOfInterest.set(aoi);
        } finally {
            referenceSystems = savedReferenceSystemList;
        }
        listModified();
    }

    /**
     * Sets the native or preferred reference system. This is the system to always show as the first
     * choice and should typically be the native {@link CoordinateReferenceSystem} of visualized data.
     * If a previous preferred system existed, the previous system will be moved to alternative choices.
     *
     * <p>The {@code replaceByAuthoritativeDefinition} argument specifies whether the given reference system should
     * be replaced by authoritative definition. If {@code true} then for example a <cite>"WGS 84"</cite> geographic
     * CRS with (<var>longitude</var>, <var>latitude</var>) axis order may be replaced by "EPSG::4326" definition with
     * (<var>latitude</var>, <var>longitude</var>) axis order.</p>
     *
     * @param  replaceByAuthoritativeDefinition  whether the given system should be replaced by authoritative definition.
     * @param  system  the native or preferred reference system to show as the first choice.
     */
    public final void setPreferred(final boolean replaceByAuthoritativeDefinition, final ReferenceSystem system) {
        // Final because `setGridReferencing(…)` needs to be sure that `referenceSystems` is not rebuilt.
        ArgumentChecks.ensureNonNull("system", system);
        synchronized (systemsOrCodes) {
            systemsOrCodes.add(0, replaceByAuthoritativeDefinition ? new Unverified(system) : system);
            listModified();
        }
    }

    /**
     * Sets the native or preferred reference system as an authority code. This is the system to always show as
     * the first choice and should typically be the native {@link CoordinateReferenceSystem} of visualized data.
     * If a previous preferred system existed, the previous system will be moved to alternative choices.
     *
     * <p>If the given code is not recognized, then the error will be notified at some later time by a call to
     * {@link #errorOccurred(FactoryException)} in a background thread and the given code will be silently ignored.
     * This behavior allows the use of codes that depend on whether an optional dependency is present or not,
     * in particular the <a href="https://sis.apache.org/epsg.html">EPSG dataset</a>.</p>
     *
     * @param  code  authority code of the native of preferred reference system to show as the first choice.
     */
    public void setPreferred(final String code) {
        ArgumentChecks.ensureNonEmpty("code", code);
        synchronized (systemsOrCodes) {
            systemsOrCodes.add(0, code);
            listModified();
        }
    }

    /**
     * Invoked when a new CRS is selected and that CRS has not been found in the list.
     * The new CRS is added after the CRS and menu items will be added in background thread.
     */
    final void addSelected(final ReferenceSystem system) {
        if (isAccepted(system)) {
            synchronized (systemsOrCodes) {
                systemsOrCodes.add(Math.min(systemsOrCodes.size(), NUM_CORE_ITEMS), system);
                listModified();
            }
        }
    }

    /**
     * Adds the given reference systems to the list of alternative choices.
     * If there is duplicated values in the given list or with previously added systems,
     * then only the first occurrence of duplicated values is retained.
     * If an {@linkplain #areaOfInterest area of interest} (AOI) is specified,
     * then reference systems that do not intersect the AOI will be hidden.
     *
     * <p>The {@code replaceByAuthoritativeDefinition} argument specifies whether the given reference systems should
     * be replaced by authoritative definitions. If {@code true} then for example a <cite>"WGS 84"</cite> geographic
     * CRS with (<var>longitude</var>, <var>latitude</var>) axis order may be replaced by "EPSG::4326" definition with
     * (<var>latitude</var>, <var>longitude</var>) axis order.</p>
     *
     * @param  replaceByAuthoritativeDefinition  whether the given systems should be replaced by authoritative definitions.
     * @param  systems  the reference systems to add as alternative choices. Null elements are ignored.
     */
    public final void addAlternatives(final boolean replaceByAuthoritativeDefinition, final ReferenceSystem... systems) {
        // Final because `setGridReferencing(…)` needs to be sure that `referenceSystems` is not rebuilt.
        ArgumentChecks.ensureNonNull("systems", systems);
        synchronized (systemsOrCodes) {
            for (final ReferenceSystem system : systems) {
                if (system != null) {
                    systemsOrCodes.add(replaceByAuthoritativeDefinition ? new Unverified(system) : system);
                }
            }
            listModified();
        }
        // Check for duplication will be done in `filterReferenceSystems()` method.
    }

    /**
     * Adds the coordinate reference system identified by the given authority codes.
     * If there is duplicated values in the given list or with previously added systems,
     * then only the first occurrence of duplicated values is retained.
     * If an {@linkplain #areaOfInterest area of interest} (AOI) is specified,
     * then reference systems that do not intersect the AOI will be hidden.
     *
     * <p>If a code is not recognized, then the error will be notified at some later time by a call to
     * {@link #errorOccurred(FactoryException)} in a background thread and the code will be silently ignored.
     * This behavior allows the use of codes that depend on whether an optional dependency is present or not,
     * in particular the <a href="https://sis.apache.org/epsg.html">EPSG dataset</a>.</p>
     *
     * @param  codes  authority codes of the coordinate reference systems to add as alternative choices.
     *                Null or empty elements are ignored.
     */
    public void addAlternatives(final String... codes) {
        ArgumentChecks.ensureNonNull("codes", codes);
        synchronized (systemsOrCodes) {
            for (String code : codes) {
                code = Strings.trimOrNull(code);
                if (code != null) {
                    systemsOrCodes.add(code);
                }
            }
            listModified();
        }
        // Parsing will be done in `filterReferenceSystems()` method.
    }

    /**
     * Adds the coordinate reference systems saved in user preferences. The user preferences are determined
     * from the reference systems observed during current execution or previous executions of JavaFX application.
     * If an {@linkplain #areaOfInterest area of interest} (AOI) is specified,
     * then reference systems that do not intersect the AOI will be ignored.
     */
    public void addUserPreferences() {
        addAlternatives(RecentChoices.getReferenceSystems());
    }

    /**
     * Returns whether the given object is accepted for inclusion in the list of CRS choices.
     * In current implementation we accept a CRS if it has an authority code (typically an EPSG code).
     */
    private static boolean isAccepted(final IdentifiedObject object) {
        return IdentifiedObjects.getIdentifier(object, null) != null;
    }

    /**
     * Filters the {@link #systemsOrCodes} list by making sure that it contains only {@link ReferenceSystem} instances.
     * Authority codes are resolved if possible or removed if they cannot be resolved. Unverified CRSs are compared
     * with authoritative definitions and replaced when a match is found. Duplications are removed.
     * Finally reference systems with a domain of validity outside the {@link #geographicAOI} are omitted
     * from the returned list (but not removed from the original {@link #systemsOrCodes} list).
     *
     * <p>This method can be invoked from any thread. In practice, it is invoked from a background thread.</p>
     *
     * @param  domain  the {@link #areaOfInterest} value read from JavaFX thread, or {@code null} if none.
     * @param  mode    the {@link #duplicationCriterion} value read from JavaFX thread.
     * @return the filtered reference systems, or {@code null} if already filtered.
     */
    private List<ReferenceSystem> filterReferenceSystems(final ImmutableEnvelope domain, final ComparisonMode mode) {
        final List<ReferenceSystem> systems;
        final GazetteerFactory gf = new GazetteerFactory();     // Cheap to construct.
        synchronized (systemsOrCodes) {
            CRSAuthorityFactory factory = this.factory;         // Hide volatile field by local field.
            if (!isModified) {
                return null;                                    // Another thread already did the work.
            }
            boolean noFactoryFound = false;
            boolean searchedFinder = false;
            IdentifiedObjectFinder finder = null;
            for (int i=systemsOrCodes.size(); --i >= 0;) {
                final Object item = systemsOrCodes.get(i);
                if (item instanceof ReferenceSystem) {
                    continue;
                }
                ReferenceSystem system = null;
                if (item != OTHER) try {
                    if (item instanceof String) {
                        /*
                         * The current list element is an authority code such as "EPSG::4326".
                         * Replace that code by the full `CoordinateReferenceSystem` instance.
                         * Note that authority factories are optional, so it is okay if we can
                         * not resolve the code. In such case the item will be removed.
                         */
                        system = gf.forNameIfKnown((String) item).orElse(null);
                        if (system == null && !noFactoryFound) {
                            if (factory == null) {
                                factory = Utils.getDefaultFactory();
                            }
                            system = factory.createCoordinateReferenceSystem((String) item);
                        }
                    } else if (item instanceof Unverified) {
                        /*
                         * The current list element is a `ReferenceSystem` instance but maybe not
                         * conform to authoritative definition, for example regarding axis order.
                         * If we can find an authoritative definition, do the replacement.
                         * If this operation cannot be done, accept the reference system as-is.
                         */
                        if (!searchedFinder) {
                            searchedFinder = true;          // Set now in case an exception is thrown.
                            if (factory instanceof GeodeticAuthorityFactory) {
                                finder = ((GeodeticAuthorityFactory) factory).newIdentifiedObjectFinder();
                            } else {
                                finder = IdentifiedObjects.newFinder(null);
                            }
                            finder.setIgnoringAxes(true);
                        }
                        system = ((Unverified) item).find(finder);
                    }
                } catch (FactoryException e) {
                    errorOccurred(e);
                    noFactoryFound = (factory == null);
                } catch (GazetteerException e) {
                    errorOccurred("getReferenceSystems", e);
                    // Note: `getReferenceSystems(…)` is indirectly the caller of this method.
                }
                if (system != null) {
                    systemsOrCodes.set(i, system);
                } else {
                    systemsOrCodes.remove(i);
                }
            }
            /*
             * Search for duplicated values after we finished filtering. This block is inefficient
             * (execution time of O(N²)) but it should not be an issue if this list is short (e.g.
             * 20 elements). We cut the list if we reach the maximal amount of systems to keep.
             */
            for (int i=0,j; i < (j=systemsOrCodes.size()); i++) {
                if (i >= RecentChoices.MAXIMUM_REFERENCE_SYSTEMS) {
                    systemsOrCodes.subList(i, j).clear();
                    break;
                }
                Object item = systemsOrCodes.get(i);
                while (--j > i) {
                    if (Utilities.deepEquals(item, systemsOrCodes.get(j), mode)) {
                        final Object removed = systemsOrCodes.remove(j);
                        if (isAccepted((IdentifiedObject) removed) && !isAccepted((IdentifiedObject) item)) {
                            /*
                             * Keep the instance which has an identifier. The instance without identifier
                             * is typically a CRS with non-standard axis order. It happens when it is the
                             * CRS associated to an image that has just been read.
                             */
                            systemsOrCodes.set(i, item = removed);
                        }
                    }
                }
            }
            /*
             * Finished to filter the `systemsOrCodes` list: all elements are now guaranteed to be
             * `ReferenceSystem` instances with no duplicated values. Copy those reference systems
             * in a separated list as a protection against changes in `systemsOrCodes` list that
             * could happen after this method returned, and also for retaining only the reference
             * systems that are valid in the area of interest. We do not remove "invalid" CRS
             * because they may become valid later if the area of interest changes.
             */
            final int n = systemsOrCodes.size();
            systems = new ArrayList<>(Math.min(NUM_SHOWN_ITEMS, n) + NUM_OTHER_ITEMS);
            for (int i=0; i<n; i++) {
                final ReferenceSystem system = (ReferenceSystem) systemsOrCodes.get(i);
                if (i >= NUM_CORE_ITEMS && !Utils.intersects(domain, system.getDomainOfValidity())) {
                    continue;
                }
                if (Utils.isIgnoreable(system)) {       // Ignore "Computer display" CRS.
                    continue;
                }
                systems.add(system);
                if (systems.size() >= NUM_SHOWN_ITEMS) break;
            }
            systems.add(OTHER);
            isModified   = false;
            this.factory = factory;         // Save in volatile field.
        }
        return systems;
    }

    /**
     * Invoked when {@link #systemsOrCodes} has been modified. If the modification happens after
     * some controls have been created ({@link ChoiceBox} or {@link MenuItem}s), then this method
     * updates their list of items. The update may happen at some time after this method returned.
     */
    private void listModified() {
        synchronized (systemsOrCodes) {
            isModified = true;
            if (referenceSystems != null) {
                // ChoiceBox or Menu already created. They will observe the changes in item list.
                getReferenceSystems(false);
            }
        }
    }

    /**
     * Updates {@link #referenceSystems} with the reference systems added to {@link #systemsOrCodes} list.
     * The new items may not be added immediately; instead the CRS will be processed in background thread
     * and copied to the {@link #referenceSystems} list when ready.
     *
     * @param  filtered  whether to filter the list for retaining only {@link CoordinateReferenceSystem} instances.
     * @return the list of items. May be empty on return and filled later.
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    private ObservableList<ReferenceSystem> getReferenceSystems(final boolean filtered) {
        if (referenceSystems == null) {
            referenceSystems = FXCollections.observableArrayList();
        }
        synchronized (systemsOrCodes) {
            /*
             * Prepare a temporary list as the concatenation of all items that are currently visible in JavaFX
             * controls with all items that were specified by `setPreferred(…)` or `addAlternatives(…)` methods.
             * This concatenation creates a lot of duplicated values, but those duplications will be filtered by
             * `filterReferenceSystems(…)` method. The intent is to preserve following order:
             *
             *   - NUM_CORE_ITEMS preferred reference systems first.
             *   - All reference systems that are currently selected by JavaFX controls.
             *   - All reference systems offered as choice in JavaFX controls.
             *   - All reference systems specified by `addAlternatives(…)`.
             *   - NUM_OTHER_ITEMS systems (will be handled in a special way by `filterReferenceSystems(…)`).
             *
             * The list will be truncated to NUM_SHOWN_ITEMS after duplications are removed and before OTHER
             * is added. The first occurrence of duplicated values is kept, which will result in above-cited
             * order as the priority order where to insert the CRS.
             */
            if (isModified) {
                final int insertAt = Math.min(systemsOrCodes.size(), NUM_CORE_ITEMS);
                final List<ReferenceSystem> selected = getSelectedItems();
                systemsOrCodes.addAll(insertAt, selected);
                systemsOrCodes.addAll(insertAt + selected.size(), referenceSystems);
                final ImmutableEnvelope domain = geographicAOI;
                final ComparisonMode mode = duplicationCriterion.get();
                BackgroundThreads.execute(new Task<List<ReferenceSystem>>() {
                    /** Filters the {@link ReferenceSystem}s in a background thread. */
                    @Override protected List<ReferenceSystem> call() {
                        return filterReferenceSystems(domain, mode);
                    }

                    /** Should never happen. */
                    @Override protected void failed() {
                        ExceptionReporter.show(null, this);
                    }

                    /** Sets the {@link ChoiceBox} content to the list computed in background thread. */
                    @Override protected void succeeded() {
                        setReferenceSystems(getValue(), mode);
                    }
                });
            }
        }
        if (filtered) {
            if (coordinateReferenceSystems == null) {
                coordinateReferenceSystems = new FilteredList<>(referenceSystems, RecentReferenceSystems::isCRS);
            }
            return coordinateReferenceSystems;
        }
        return referenceSystems;
    }

    /**
     * Returns {@code true} if the given reference system can be included
     * in the {@link #coordinateReferenceSystems} list.
     */
    private static boolean isCRS(final ReferenceSystem system) {
        return (system == OTHER) || (system instanceof CoordinateReferenceSystem);
    }

    /**
     * Sets the reference systems to the given content. The given list is often similar to current content,
     * for example with only a reference system that moved to a different index. This method compares the
     * given list with current one and tries to fire as few change events as possible.
     *
     * @param  systems  the new reference systems, or {@code null} for no changes.
     * @param  mode     the value of {@link #duplicationCriterion} at the time the
     *                  {@code systems} list has been computed.
     */
    private void setReferenceSystems(final List<ReferenceSystem> systems, final ComparisonMode mode) {
        // Nested calls to this method should never happen, but check !isAdjusting anyway as safety.
        if (systems != null && !isAdjusting) try {
            isAdjusting = true;
            /*
             * The call to `copyAsDiff(…)` may cause some ChoiceBox values to be lost if the corresponding
             * item in the `referenceSystems` list is temporarily removed before to be inserted elsewhere.
             * Save the values before to modify the list. Note that if `referenceSystems` was empty before
             * the copy, `controlValues` should be null before the copy but may become non-null after the
             * copy because listeners will have initialized them to the first `ReferenceSystem` available.
             * Those non-null values will not be reflected in the `values` array, so we should ignore them.
             */
            final ReferenceSystem[] values = controlValues.stream().map(WritableValue::getValue).toArray(ReferenceSystem[]::new);
            if (GUIUtilities.copyAsDiff(systems, referenceSystems)) {
                notifyChanges();
            }
            /*
             * Restore the previous selections. This code also serves another purpose: the previous selection
             * may not be an item in the list.  If the value was set by a call to `ChoiceBox.setValue(…)` and
             * is a `GeographicCRS` with (λ,φ) axis order, it may have been replaced in the list by a CRS with
             * (φ,λ) axis order. We need to replace the previous value by the instance in the list, otherwise
             * `ChoiceBox` will not show the CRS as selected.
             */
            final int n = referenceSystems.size();
            for (int j=0; j<values.length; j++) {
                ReferenceSystem system = values[j];
                if (system != null) {                   // See comment about empty `referenceSystems` list.
                    for (int i=0; i<n; i++) {
                        final ReferenceSystem candidate = referenceSystems.get(i);
                        if (Utilities.deepEquals(candidate, system, mode)) {
                            system = candidate;
                            break;
                        }
                    }
                    controlValues.get(j).setValue(system);
                }
            }
        } finally {
            isAdjusting = false;
        }
    }

    /**
     * Invoked when user selects a reference system. If the choice is "Other…", then {@link CRSChooser} popups
     * and the selected reference system is added to the list of choices. If the selected CRS is different than
     * the previous one, then {@link RecentChoices} is notified and the user-specified listener is notified.
     */
    final class SelectionListener implements ChangeListener<ReferenceSystem> {
        /** The user-specified action to execute when a reference system is selected. */
        final ChangeListener<ReferenceSystem> action;

        /** Creates a new listener of reference system selection. */
        private SelectionListener(final ChangeListener<ReferenceSystem> action) {
            this.action = action;
        }

        /** The manager of reference systems to synchronize with. */
        final RecentReferenceSystems owner() {
            return RecentReferenceSystems.this;
        }

        /** Invoked when the user selects a reference system or the "Other…" item. */
        @SuppressWarnings("unchecked")
        @Override public void changed(final ObservableValue<? extends ReferenceSystem> property,
                                      final ReferenceSystem oldValue, ReferenceSystem newValue)
        {
            if (isAdjusting) {
                action.changed(property, oldValue, newValue);
                return;
            }
            final ComparisonMode mode = duplicationCriterion.get();
            if (newValue == OTHER) {
                final CRSChooser chooser = new CRSChooser(factory, geographicAOI, locale);
                newValue = chooser.showDialog(GUIUtilities.getWindow(property)).orElse(null);
                if (newValue == null) {
                    newValue = oldValue;
                } else {
                    /*
                     * If user selected a CRS in the CRSChooser list, verify if her/his selection is a CRS
                     * already presents in the `referenceSystems` list. We ignore axis order (by default)
                     * because the previous CRS in the list may be a CRS given by `setPreferred(…)` method,
                     * which typically come from a DataStore. That previous CRS will be replaced by the CRS
                     * given by CRSChooser, which is more conform to authoritative definition.
                     */
                    final ObservableList<ReferenceSystem> items = referenceSystems;
                    int count = items.size() - NUM_OTHER_ITEMS;
                    boolean found = false;
                    for (int i=0; i<count; i++) {
                        if (Utilities.deepEquals(newValue, items.get(i), mode)) {
                            if (i >= NUM_CORE_ITEMS) {
                                items.set(i, newValue);
                            }
                            found = true;
                            break;
                        }
                    }
                    /*
                     * If the selected CRS was not present in the list, we may need to remove the last item
                     * for making room for the new one. New item must be added before `property.setValue(…)`
                     * is invoked, otherwise ChoiceBox may add a new item by itself.
                     */
                    if (!found) {
                        if (count >= NUM_SHOWN_ITEMS) {
                            final List<ReferenceSystem> selected = getSelectedItems();
                            for (int i=count; --i >= NUM_CORE_ITEMS;) {
                                if (!selected.contains(items.get(i))) {         // Do not remove selected items.
                                    items.remove(i);                            // Remove an item before `OTHER`.
                                    if (--count < NUM_SHOWN_ITEMS) break;
                                }
                            }
                        }
                        items.add(Math.min(items.size(), NUM_CORE_ITEMS), newValue);
                        notifyChanges();
                    }
                }
                /*
                 * Following cast is safe because this listener is registered only on ObjectProperty
                 * instances, and the ObjectProperty class implements WritableValue.
                 * The effect of this method call is to set the selected value.
                 */
                ((WritableValue<ReferenceSystem>) property).setValue(newValue);
            }
            if (oldValue != newValue) {
                /*
                 * If the selected CRS is already at the beginning of the list, do nothing. The beginning is
                 * either one of the core items (specified by `setPreferred(…)`) or the first item after the
                 * core items.
                 */
                final ObservableList<ReferenceSystem> items = referenceSystems;
                final int count = items.size() - NUM_OTHER_ITEMS;
                for (int i = Math.min(count, NUM_CORE_ITEMS + 1); --i >= 0;) {
                    final ReferenceSystem current = items.get(i);
                    if (Utilities.deepEquals(current, newValue, mode)) {
                        action.changed(property, oldValue, current);
                        return;
                    }
                }
                /*
                 * Move the selected reference system as the first choice after the core systems.
                 * We need to remove the old value before to add the new one, otherwise it seems
                 * to confuse the list.
                 */
                for (int i = count; --i >= NUM_CORE_ITEMS;) {
                    if (Utilities.deepEquals(items.get(i), newValue, mode)) {
                        newValue = items.remove(i);
                        break;
                    }
                }
                items.add(Math.min(items.size(), NUM_CORE_ITEMS), newValue);
                /*
                 * Notify the user-specified listeners. It will typically starts a background process.
                 * We test (oldValue != newValue) again because `newValue` may have been replaced.
                 */
                notifyChanges();
                if (oldValue != newValue) {
                    action.changed(property, oldValue, newValue);       // Typically starts a background process.
                }
                RecentChoices.useReferenceSystem(IdentifiedObjects.toString(IdentifiedObjects.getIdentifier(newValue, null)));
            }
        }
    }

    /**
     * Notifies all {@link MenuSync} that the list of reference systems changed. We send a notification manually
     * instead of relying on {@code ListChangeListener} in order to process only one event after we have done
     * a bunch of changes instead of an event after each individual add or remove operation.
     */
    private void notifyChanges() {
        for (final WritableValue<ReferenceSystem> value : controlValues) {
            if (value instanceof MenuSync) {
                ((MenuSync) value).notifyChanges();
            }
        }
    }

    /**
     * Returns all reference systems in the order they appear in JavaFX controls. The first element
     * is the {@link #setPreferred(boolean, ReferenceSystem) preferred} (or native) reference system.
     * All other elements are {@linkplain #addAlternatives(boolean, ReferenceSystem...) alternatives}.
     *
     * @return all reference systems in the order they appear in JavaFX controls.
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    public ObservableList<ReferenceSystem> getItems() {
        if (publicItemList == null) {
            publicItemList = new FilteredList<>(getReferenceSystems(false), Objects::nonNull);
        }
        return publicItemList;
    }

    /**
     * Returns all currently selected reference systems in the order they appear in JavaFX controls.
     * This method collects selected values of all controls created by a {@code createXXX(…)} method.
     * The returned list does not contain duplicated values.
     *
     * @return currently selected values of all controls, without duplicated values and in the order
     *         they appear in choice lists.
     */
    public List<ReferenceSystem> getSelectedItems() {
        /*
         * Build an array of selected reference systems. This array may contain duplicated elements if two
         * or more JavaFX controls have the same selected value. Those duplications will be resolved later.
         * Conceptually we will use this array as a java.util.Set, except that its length is so small
         * (usually no more than 3 elements) that it is not worth to use HashSet.
         */
        int count = 0;
        final ReferenceSystem[] selected = new ReferenceSystem[controlValues.size()];
        for (final WritableValue<ReferenceSystem> value : controlValues) {
            final ReferenceSystem system = value.getValue();
            if (system != null) selected[count++] = system;
        }
        /*
         * Now filter the `referenceSystems` list, retaining only elements that are present in `selected`.
         * We do that way for having selected elements in the same order as they appear in JavaFX controls.
         */
        final List<ReferenceSystem> ordered = new ArrayList<>(count);
        if (count != 0) {
            // (count > 0) implies (referenceSystems != null).
            for (final ReferenceSystem system : referenceSystems) {
                if (system != OTHER) {
                    for (int i=0; i<count; i++) {
                        if (selected[i] == system) {
                            ordered.add(system);
                            if (--count == 0) return ordered;
                            System.arraycopy(selected, i+1, selected, i, count - i);
                            break;
                        }
                    }
                }
            }
            /*
             * If some selected elements were not found in the `referenceSystems` list, add them last.
             * It should not happen, unless those remaining elements are duplicated values (i.e. two
             * or more controls having the same selection).
             */
next:       for (int i=0; i<count; i++) {
                final ReferenceSystem system = selected[i];
                for (int j=ordered.size(); --j >= 0;) {
                    if (ordered.get(j) == system) {
                        continue next;                  // Skip duplicated value.
                    }
                }
                ordered.add(system);
            }
        }
        return ordered;
    }

    /**
     * Creates a box offering choices among the reference systems specified to this {@code RecentReferenceSystems}.
     * The returned control may be initially empty, in which case its content will be automatically set at
     * a later time (after a background thread finished to process the {@link CoordinateReferenceSystem}s).
     *
     * <p>If the {@code filtered} argument is {@code true}, then the choice box will contain only reference systems
     * that can be used for rendering purposes. That filtered list can contain {@link CoordinateReferenceSystem}
     * instances but not reference systems by identifiers such as {@linkplain MilitaryGridReferenceSystem MGRS}.
     * The latter are usable only for the purposes of formatting coordinate values as texts.</p>
     *
     * <h4>Limitations</h4>
     * There is currently no mechanism for disposing the returned control. For garbage collecting the
     * returned {@code ChoiceBox}, this {@code RecentReferenceSystems} must be garbage-collected as well.
     *
     * @param  filtered  whether the choice box should contain only {@link CoordinateReferenceSystem} instances.
     * @param  action    the action to execute when a reference system is selected.
     * @return a choice box with reference systems specified by {@code setPreferred(…)}
     *         and {@code addAlternatives(…)} methods.
     *
     * @since 1.3
     */
    public ChoiceBox<ReferenceSystem> createChoiceBox(final boolean filtered, final ChangeListener<ReferenceSystem> action) {
        ArgumentChecks.ensureNonNull("action", action);
        final ChoiceBox<ReferenceSystem> choices = new ChoiceBox<>(getReferenceSystems(filtered));
        choices.setConverter(new ObjectStringConverter<>(choices.getItems(), locale));
        choices.valueProperty().addListener(new SelectionListener(action));
        controlValues.add(choices.valueProperty());
        return choices;
    }

    /**
     * Creates menu items offering choices among the reference systems specified to this {@code RecentReferenceSystems}.
     * The items will be inserted in the {@linkplain Menu#getItems() menu list}. The content of that list will
     * change at any time after this method returned: items will be added or removed as a result of user actions.
     *
     * <p>If the {@code filtered} argument is {@code true}, then the menu items will contain only reference systems
     * that can be used for rendering purposes. That filtered list can contain {@link CoordinateReferenceSystem}
     * instances but not reference systems by identifiers such as {@linkplain MilitaryGridReferenceSystem MGRS}.
     * The latter are usable only for the purposes of formatting coordinate values as texts.</p>
     *
     * <h4>Limitations</h4>
     * There is currently no mechanism for disposing the returned control. For garbage collecting the
     * returned {@code Menu}, this {@code RecentReferenceSystems} must be garbage-collected as well.
     *
     * @param  filtered  whether the menu should contain only {@link CoordinateReferenceSystem} instances.
     * @param  action    the action to execute when a reference system is selected.
     * @return the menu containing items for reference systems.
     *
     * @since 1.3
     */
    public Menu createMenuItems(final boolean filtered, final ChangeListener<ReferenceSystem> action) {
        ArgumentChecks.ensureNonNull("action", action);
        final List<ReferenceSystem> main = getReferenceSystems(filtered);
        final List<DerivedCRS> derived = (filtered) ? null : cellIndiceSystems;
        final Menu menu = new Menu(Vocabulary.getResources(locale).getString(Vocabulary.Keys.ReferenceSystem));
        final MenuSync property = new MenuSync(main, !filtered, derived, menu, new SelectionListener(action));
        menu.getProperties().put(SELECTED_ITEM_KEY, property);
        controlValues.add(property);
        return menu;
    }

    /**
     * Returns the property for the selected value in a menu created by {@link #createMenuItems(boolean, ChangeListener)}.
     *
     * @param  menu  the menu, or {@code null} if none.
     * @return the property for the selected value, or {@code null} if none.
     */
    public static ObjectProperty<ReferenceSystem> getSelectedProperty(final Menu menu) {
        if (menu != null) {
            final Object property = menu.getProperties().get(SELECTED_ITEM_KEY);
            if (property instanceof MenuSync) {
                return (MenuSync) property;
            }
        }
        return null;
    }

    /**
     * Invoked when an error occurred while filtering a {@link ReferenceSystem} instance.
     * The error may be a failure to convert an EPSG code to a {@link CoordinateReferenceSystem} instance,
     * or an error during a CRS verification. Some errors may be normal, for example because EPSG dataset
     * is not expected to be present in every runtime environments. The consequence of this error is "only"
     * that the CRS will not be listed among the reference systems that the user can choose.
     *
     * <p>The default implementation popups an alert dialog only if the error occurred after the user
     * accepted to {@linkplain org.apache.sis.setup.OptionalInstallations download optional dependencies},
     * because the error may be caused by a problem related to the download operation.
     * Otherwise this method only logs the error at {@link java.util.logging.Level#FINE}.
     * No other processing is done; user is not notified unless (s)he paid attention to loggings.</p>
     *
     * @param  e  the error that occurred.
     */
    protected void errorOccurred(final FactoryException e) {
        OptionalDataDownloader.reportIfInstalling(e);
        Logging.recoverableException(LOGGER, RecentReferenceSystems.class, "getReferenceSystems", e);
    }

    /**
     * Invoked when an error other than {@link FactoryException} occurred.
     * The error shall be recoverable, e.g. by ignoring a menu item.
     *
     * @param  caller  the method to report as the source the in log record.
     * @param  e  the error that occurred.
     */
    static void errorOccurred(final String caller, final Exception e) {
        Logging.recoverableException(LOGGER, RecentReferenceSystems.class, caller, e);
    }
}
