/*
 * 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.internal.netcdf;

import java.util.Map;
import java.util.List;
import java.util.Arrays;
import java.util.Collections;
import java.util.StringJoiner;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.io.IOException;
import java.time.Instant;
import javax.measure.Unit;
import org.opengis.util.FactoryException;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.cs.*;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.crs.SingleCRS;
import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.GeocentricCRS;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.operation.CoordinateOperationFactory;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.Conversion;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.cs.CoordinateSystems;
import org.apache.sis.referencing.crs.AbstractCRS;
import org.apache.sis.referencing.crs.DefaultGeographicCRS;
import org.apache.sis.referencing.crs.DefaultGeocentricCRS;
import org.apache.sis.internal.referencing.provider.Equirectangular;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.internal.util.TemporalUtilities;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.measure.Units;
import org.apache.sis.math.Vector;

// Branch-dependent imports
import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;


/**
 * Temporary object for building a coordinate reference system from the variables in a netCDF file.
 * This class proceeds by inspecting the coordinate system axes. This is a different approach than
 * {@link GridMapping}, which parses Well Known Text or EPSG codes declared in variable attributes.
 *
 * <p>Different instances are required for the geographic, vertical and temporal components of a CRS,
 * or if a netCDF file uses different CRS for different variables. This builder is used as below:</p>
 *
 * <ol>
 *   <li>Invoke {@link #dispatch(List, Axis)} for all axes in a grid.
 *       Builders for CRS components will added in the given list.</li>
 *   <li>Invoke {@link #build(Decoder)} on each builder prepared in above step.</li>
 *   <li>Assemble the CRS components created in above step in a {@code CompoundCRS}.</li>
 * </ol>
 *
 * The builder type is inferred from axes. The axes are identified by their abbreviations,
 * which is a {@linkplain Axis#abbreviation controlled vocabulary} for this implementation.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.0
 * @since   1.0
 * @module
 */
abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
    /**
     * The type of datum as a GeoAPI sub-interface of {@link Datum}.
     * Used for verifying the type of cached datum at {@link #datumIndex}.
     */
    private final Class<D> datumType;

    /**
     * Name of the datum on which the CRS is presumed to be based, or {@code ""}. This is used
     * for building a datum name like <cite>"Unknown datum presumably based on GRS 1980"</cite>.
     */
    private final String datumBase;

    /**
     * Index of the cached datum in a {@code Datum[]} array, from 0 inclusive to {@value #DATUM_CACHE_SIZE} exclusive.
     * The datum type at that index must be an instance of {@link #datumType}. We cache only the datum because they do
     * not depend on the netCDF file content in the common case where the CRS is not explicitly specified.
     */
    private final byte datumIndex;

    /**
     * Specify the range of valid number of dimensions, inclusive.
     * The {@link #dimension} value shall be in that range.
     */
    private final byte minDim, maxDim;

    /**
     * Number of valid elements in the {@link #axes} array. The count should not be larger than 3,
     * even if the netCDF file has more axes, because each {@code CRSBuilder} is only for a subset.
     */
    private byte dimension;

    /**
     * The axes to use for creating the coordinate reference system.
     * They are information about netCDF axes, not yet ISO 19111 axes.
     * The axis are listed in "natural" order (reverse of netCDF order).
     * Only the {@link #dimension} first elements are valid.
     */
    private Axis[] axes;

    /**
     * The datum created by {@link #createDatum(DatumFactory, Map)}.
     */
    protected D datum;

    /**
     * The coordinate system created by {@link #createCS(CSFactory, Map, CoordinateSystemAxis[])}.
     */
    protected CS coordinateSystem;

    /**
     * The coordinate reference system that may have been create by {@link #setPredefinedComponents(Decoder)}.
     */
    protected SingleCRS referenceSystem;

    /**
     * Non-fatal exceptions that may occur while building the coordinate reference system.
     * The same exception may be repeated many time, in which case we will report only the
     * first one.
     *
     * @see #recoverableException(NoSuchAuthorityCodeException)
     */
    private NoSuchAuthorityCodeException warnings;

    /**
     * Creates a new CRS builder based on datum of the given type.
     * This constructor is invoked indirectly by {@link #dispatch(List, Axis)}.
     *
     * @param  datumType   the type of datum as a GeoAPI sub-interface of {@link Datum}.
     * @param  datumBase   name of the datum on which the CRS is presumed to be based, or {@code ""}.
     * @param  datumIndex  index of the cached datum in a {@code Datum[]} array.
     * @param  minDim      minimum number of dimensions (usually 1, 2 or 3).
     * @param  maxDim      maximum number of dimensions (usually 1, 2 or 3).
     */
    private CRSBuilder(final Class<D> datumType, final String datumBase, final byte datumIndex, final byte minDim, final byte maxDim) {
        this.datumType  = datumType;
        this.datumBase  = datumBase;
        this.datumIndex = datumIndex;
        this.minDim     = minDim;
        this.maxDim     = maxDim;
        this.axes       = new Axis[3];
    }

    /**
     * Dispatches the given axis to a {@code CRSBuilder} appropriate for the axis type. The axis type is determined
     * from {@link Axis#abbreviation}, taken as a controlled vocabulary. If no suitable {@code CRSBuilder} is found
     * in the given list, then a new one will be created and added to the list.
     *
     * @param  components  the list of builder where to dispatch the axis. May be modified by this method.
     * @param  axis        the axis to add to a builder in the given list.
     * @throws DataStoreContentException if the given axis can not be added in a builder.
     */
    @SuppressWarnings("fallthrough")
    public static void dispatch(final List<CRSBuilder<?,?>> components, final Axis axis) throws DataStoreContentException {
        final Class<? extends CRSBuilder<?,?>> addTo;
        final Supplier<CRSBuilder<?,?>> constructor;
        int alternative = -1;
        switch (axis.abbreviation) {
            case 'h': for (int i=components.size(); --i >= 0;) {        // Can apply to either Geographic or Projected.
                          if (components.get(i) instanceof Projected) {
                              alternative = i;
                              break;
                          }
                      }                    // Fallthrough
            case 'λ': case 'φ':            addTo =  Geographic.class; constructor =  Geographic::new; break;
            case 'θ': case 'Ω': case 'r':  addTo =   Spherical.class; constructor =   Spherical::new; break;
            case 'E': case 'N':            addTo =   Projected.class; constructor =   Projected::new; break;
            case 'H': case 'D':            addTo =    Vertical.class; constructor =    Vertical::new; break;
            case 't':                      addTo =    Temporal.class; constructor =    Temporal::new; break;
            default:                       addTo = Engineering.class; constructor = Engineering::new; break;
        }
        /*
         * If a builder of 'addTo' class already exists, add the axis in the existing builder.
         * We should have at most one builder of each class. But if we nevertheless have more,
         * add to the most recently used builder. If there is no builder, create a new one.
         */
        for (int i=components.size(); --i >= 0;) {
            final CRSBuilder<?,?> builder = components.get(i);
            if (addTo.isInstance(builder) || i == alternative) {
                builder.add(axis);
                return;
            }
        }
        final CRSBuilder<?,?> builder = constructor.get();
        /*
         * Before to add the axis to a newly created builder, verify if we wrongly associated
         * the ellipsoidal height to Geographic builder before. The issue is that ellipsoidal
         * height can be associated to either Geographic or Projected CRS.  If we do not have
         * more information, our first bet is Geographic. If our bet appears to be wrong, the
         * block below fixes it.
         */
        if (addTo == Projected.class) {
previous:   for (int i=components.size(); --i >= 0;) {
                final CRSBuilder<?,?> replace = components.get(i);
                for (final Axis a : replace.axes) {
                    if (a.abbreviation != 'h') {
                        continue previous;                  // Not a lonely ellipsoidal height in a Geographic CRS.
                    }
                }
                for (final Axis a : replace.axes) {         // Should have exactly one element, but we are paranoiac.
                    builder.add(a);
                }
                components.remove(i);
                break;
            }
        }
        builder.add(axis);
        components.add(builder);            // Add only after we ensured that the builder contains at least one axis.
    }

    /**
     * Adds an axis for the coordinate reference system to build. Adding more than 3 axes is usually an error,
     * but this method nevertheless stores those extraneous axis references for building an error message later.
     *
     * @param  axis  the axis to add.
     * @throws DataStoreContentException if the given axis can not be added in this builder.
     */
    private void add(final Axis axis) throws DataStoreContentException {
        if (dimension == Byte.MAX_VALUE) {
            throw new DataStoreContentException(getFirstAxis().coordinates.errors()
                    .getString(Errors.Keys.ExcessiveListSize_2, "axes", (short) (Byte.MAX_VALUE + 1)));
        }
        if (dimension >= axes.length) {
            axes = Arrays.copyOf(axes, dimension * 2);        // Should not happen (see method javadoc).
        }
        axes[dimension++] = axis;
    }

    /**
     * Returns whether the coordinate system has at least 3 axes.
     */
    final boolean is3D() {
        return dimension >= 3;
    }

    /**
     * Returns the first axis. This method is invoked for coordinate reference systems that are known
     * to contain only one axis, for example temporal coordinate systems.
     */
    final Axis getFirstAxis() {
        return axes[0];
    }

    /**
     * Creates the coordinate reference system.
     * This method can be invoked after all axes have been dispatched.
     *
     * @param  decoder  the decoder of the netCDF from which the CRS are constructed.
     */
    public final SingleCRS build(final Decoder decoder) throws FactoryException, DataStoreException, IOException {
        if (dimension < minDim || dimension > maxDim) {
            final Variable axis = getFirstAxis().coordinates;
            throw new DataStoreContentException(axis.resources().getString(Resources.Keys.UnexpectedAxisCount_4,
                    axis.getFilename(), getClass().getSimpleName(), dimension, NamedElement.listNames(axes, dimension, ", ")));
        }
        /*
         * If the subclass can offer coordinate system and datum candidates based on a brief inspection of axes,
         * set the datum, CS and CRS field values to those candidate. Those values do not need to be exact; they
         * will be overwritten later if they do not match the netCDF file content.
         */
        datum = datumType.cast(decoder.datumCache[datumIndex]);         // Should be before 'setPredefinedComponents' call.
        setPredefinedComponents(decoder);
        /*
         * If `setPredefinedComponents(decoder)` offers a datum, we will used it as-is. Otherwise create the datum now.
         * Datum are often not defined in netCDF files, so the above `setPredefinedComponents` method call may have set
         * EPSG::6019 — "Not specified (based on GRS 1980 ellipsoid)". If not, we build a similar name.
         */
        if (datum == null) {
            // Not localized because stored as a String, possibly exported in WKT or GML, and 'datumBase' is in English.
            createDatum(decoder.getDatumFactory(), properties("Unknown datum presumably based upon ".concat(datumBase)));
        }
        decoder.datumCache[datumIndex] = datum;
        /*
         * Verify if a pre-defined coordinate system can be used. This is often the case, for example
         * the EPSG::6424 coordinate system can be used for (longitude, latitude) axes in degrees.
         * Using a pre-defined CS allows us to get more complete definitions (minimum and maximum values, etc.).
         */
        if (coordinateSystem != null) {
            for (int i=dimension; --i >= 0;) {
                final Axis expected = axes[i];
                if (expected == null || !expected.isSameUnitAndDirection(coordinateSystem.getAxis(i))) {
                    coordinateSystem = null;
                    referenceSystem  = null;
                    break;
                }
            }
        }
        /*
         * If 'setPredefinedComponents(decoder)' did not proposed a coordinate system, or if it proposed a CS
         * but its axes do not match the axes in the netCDF file, then create a new coordinate system here.
         */
        if (referenceSystem == null) {
            final Map<String,?> properties;
            if (coordinateSystem == null) {
                // Fallback if the coordinate system is not predefined.
                final StringJoiner joiner = new StringJoiner(" ");
                final CSFactory csFactory = decoder.getCSFactory();
                final CoordinateSystemAxis[] iso = new CoordinateSystemAxis[dimension];
                for (int i=0; i<iso.length; i++) {
                    final Axis axis = axes[i];
                    joiner.add(axis.getName());
                    iso[i] = axis.toISO(csFactory, i);
                }
                createCS(csFactory, properties(joiner.toString()), iso);
                properties = properties(coordinateSystem.getName());
            } else {
                properties = properties(NamedElement.listNames(axes, dimension, " "));
            }
            createCRS(decoder.getCRSFactory(), properties);
        }
        /*
         * Creates the coordinate reference system using current value of 'datum' and 'coordinateSystem' fields.
         * The coordinate system initially have a [-180 … +180]° longitude range. If the actual coordinate values
         * are outside that range, switch the longitude range to [0 … 360]°.
         */
        final CoordinateSystem cs = referenceSystem.getCoordinateSystem();
        for (int i=cs.getDimension(); --i >= 0;) {
            final CoordinateSystemAxis axis = cs.getAxis(i);
            if (RangeMeaning.WRAPAROUND.equals(axis.getRangeMeaning())) {
                final Vector coordinates = axes[i].read();                          // Typically a cached vector.
                final int length = coordinates.size();
                if (length != 0) {
                    final double first = coordinates.doubleValue(0);
                    final double last  = coordinates.doubleValue(length - 1);
                    if (Math.min(first, last) >= 0 && Math.max(first, last) > axis.getMaximumValue()) {
                        referenceSystem = (SingleCRS) AbstractCRS.castOrCopy(referenceSystem).forConvention(AxesConvention.POSITIVE_RANGE);
                        break;
                    }
                }
            }
        }
        if (warnings != null) {
            decoder.listeners.warning(Level.FINE, null, warnings);
        }
        return referenceSystem;
    }

    /**
     * Reports a non-fatal exception that may occur during {@link #setPredefinedComponents(Decoder)}.
     * In order to avoid repeating the same warning many times, this method collects the warnings
     * together and reports them in a single log record after we finished creating the CRS.
     */
    final void recoverableException(final NoSuchAuthorityCodeException e) {
        if (warnings == null) warnings = e;
        else warnings.addSuppressed(e);
    }

    /**
     * Returns the properties to give to factory {@code create} methods.
     *
     * @param  name  name of the geodetic object (datum, coordinate system, …) to create.
     */
    private static Map<String,?> properties(final Object name) {
        return Collections.singletonMap(IdentifiedObject.NAME_KEY, name);
    }

    /**
     * Returns the EPSG code of a possible coordinate system from EPSG database. This method proceed by brief
     * inspection of axis directions and units; there is no guarantees that the coordinate system returned by
     * this method match the axes defined in the netCDF file. It is caller's responsibility to verify.
     * This is a helper method for {@link #setPredefinedComponents(Decoder)} implementations.
     *
     * @param  defaultUnit  the unit to use if unit definition is missing in the netCDF file.
     * @return EPSG code of a CS candidate, or {@code null} if none.
     *
     * @see Geodetic#isPredefinedCS(Unit)
     */
    final Integer epsgCandidateCS(final Unit<?> defaultUnit) {
        Unit<?> unit = getFirstAxis().getUnit();
        if (unit == null) unit = defaultUnit;
        final AxisDirection[] directions = new AxisDirection[dimension];
        for (int i=0; i<directions.length; i++) {
            directions[i] = axes[i].direction;
        }
        return CoordinateSystems.getEpsgCode(unit, directions);
    }

    /**
     * If a brief inspection of unit and direction of the {@linkplain #getFirstAxis() first axis} suggests
     * that a predefined coordinate system could be used, sets the {@link #coordinateSystem} field to that CS.
     * The coordinate system does not need to be a full match since all axes will be verified by the caller.
     * This method is invoked before to fallback on {@link #createCS(CSFactory, Map, CoordinateSystemAxis[])}.
     *
     * <p>This method may opportunistically set the {@link #datum} and {@link #referenceSystem} fields if it
     * can propose a CRS candidate instead than only a CS candidate.</p>
     */
    abstract void setPredefinedComponents(Decoder decoder) throws FactoryException;

    /**
     * Creates the datum for the coordinate reference system to build. The datum are generally not specified in netCDF files.
     * To make that clearer, this method builds datum with names like <cite>"Unknown datum presumably based on GRS 1980"</cite>.
     * The newly created datum is assigned to the {@link #datum} field.
     *
     * @param  factory     the factory to use for creating the datum.
     * @param  properties  contains the name of the datum to create.
     */
    abstract void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException;

    /**
     * Creates the coordinate system from the given axes. This method is invoked only after we
     * verified that the number of axes is inside the {@link #minDim} … {@link #maxDim} range.
     * The newly created coordinate system is assigned to the {@link #coordinateSystem} field.
     *
     * @param  factory     the factory to use for creating the coordinate system.
     * @param  properties  contains the name of the coordinate system to create.
     * @param  axes        the axes of the coordinate system.
     */
    abstract void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException;

    /**
     * Creates the coordinate reference system from the values in {@link #datum} and {@link #coordinateSystem} fields.
     * This method is invoked only if {@link #referenceSystem} is still {@code null} or its axes do not correspond to
     * the expected axis. The newly created reference system is assigned to the {@link #referenceSystem} field.
     *
     * @param  factory     the factory to use for creating the coordinate reference system.
     * @param  properties  contains the name of the coordinate reference system to create.
     */
    abstract void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException;




    /**
     * Base classes of {@link Spherical}, {@link Geographic} and {@link Projected} builders.
     * They all have in common to be based on a {@link GeodeticDatum}.
     */
    private abstract static class Geodetic<CS extends CoordinateSystem> extends CRSBuilder<GeodeticDatum, CS> {
        /**
         * The coordinate reference system which is presumed the basis of datum on netCDF files.
         */
        protected CommonCRS defaultCRS;

        /**
         * Whether the coordinate system has longitude before latitude.
         * This flag is set as a side-effect of {@link #isPredefinedCS(Unit)} method call.
         */
        protected boolean isLongitudeFirst;

        /**
         * For subclasses constructors.
         *
         * @param  minDim  minimum number of dimensions (2 or 3).
         */
        Geodetic(final byte minDim) {
            super(GeodeticDatum.class, "GRS 1980", (byte) 0, minDim, (byte) 3);
        }

        /**
         * Initializes this builder before {@link #build(Decoder)} execution.
         */
        @Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
            defaultCRS = decoder.convention().defaultHorizontalCRS(false);
        }

        /**
         * Creates a {@link GeodeticDatum} for <cite>"Unknown datum presumably based on GRS 1980"</cite>.
         * This method is invoked only if {@link #setPredefinedComponents(Decoder)} failed to create a datum.
         */
        @Override final void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
            final GeodeticDatum template = defaultCRS.datum();
            datum = factory.createGeodeticDatum(properties, template.getEllipsoid(), template.getPrimeMeridian());
        }

        /**
         * Returns {@code true} if the coordinate system may be one of the predefined CS. A returns value of {@code true}
         * is not a guarantee that the coordinate system in the netCDF file matches the predefined CS; it only tells that
         * this is reasonable chances to be the case based on a brief inspection of the first coordinate system axis.
         * If {@code true}, then {@link #isLongitudeFirst} will have been set to an indication of axis order.
         *
         * @param  expected  the expected unit of measurement of the first axis.
         *
         * @see #epsgCandidateCS(Unit)
         */
        final boolean isPredefinedCS(final Unit<?> expected) {
            final Axis axis = getFirstAxis();
            final Unit<?> unit = axis.getUnit();
            if (unit == null || expected.equals(unit)) {
                isLongitudeFirst = AxisDirection.EAST.equals(axis.direction);
                if (isLongitudeFirst || AxisDirection.NORTH.equals(axis.direction)) {
                    return true;
                }
            }
            return false;
        }
    }




    /**
     * Builder for geocentric CRS with (θ,Ω,r) axes.
     */
    private static final class Spherical extends Geodetic<SphericalCS> {
        /**
         * Creates a new builder (invoked by lambda function).
         */
        public Spherical() {
            super((byte) 3);
        }

        /**
         * Possibly sets {@link #datum} and {@link #coordinateSystem} to predefined objects
         * matching the axes defined in the netCDF file.
         */
        @Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
            super.setPredefinedComponents(decoder);
            if (isPredefinedCS(Units.DEGREE)) {
                GeocentricCRS crs = defaultCRS.spherical();
                if (isLongitudeFirst) {
                    crs = DefaultGeocentricCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED);
                }
                referenceSystem  = crs;
                coordinateSystem = (SphericalCS) crs.getCoordinateSystem();
                datum            = crs.getDatum();
            } else {
                datum = defaultCRS.datum();
            }
        }

        /**
         * Creates the three-dimensional {@link SphericalCS} from given axes. This method is invoked only
         * if {@link #setPredefinedComponents(Decoder)} failed to assign a CS or if {@link #build(Decoder)}
         * found that the {@link #coordinateSystem} does not have compatible axes.
         */
        @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
            coordinateSystem = factory.createSphericalCS(properties, axes[0], axes[1], axes[2]);
        }

        /**
         * Creates the coordinate reference system from datum and coordinate system computed in previous steps.
         * This method is invoked under conditions similar to the ones of above {@code createCS(…)} method.
         */
        @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
            referenceSystem = factory.createGeocentricCRS(properties, datum, coordinateSystem);
        }
    }




    /**
     * Geographic CRS with (λ,φ,h) axes.
     * The height, if present, is ellipsoidal height.
     */
    private static final class Geographic extends Geodetic<EllipsoidalCS> {
        /**
         * Creates a new builder (invoked by lambda function).
         */
        public Geographic() {
            super((byte) 2);
        }

        /**
         * Possibly sets {@link #datum}, {@link #coordinateSystem} and {@link #referenceSystem}
         * to predefined objects matching the axes defined in the netCDF file.
         */
        @Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
            super.setPredefinedComponents(decoder);
            if (isPredefinedCS(Units.DEGREE)) {
                GeographicCRS crs;
                if (is3D()) {
                    crs = defaultCRS.geographic3D();
                    if (isLongitudeFirst) {
                        crs = DefaultGeographicCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED);
                    }
                } else if (isLongitudeFirst) {
                    crs = defaultCRS.normalizedGeographic();
                } else {
                    crs = defaultCRS.geographic();
                }
                referenceSystem  = crs;
                coordinateSystem = crs.getCoordinateSystem();
                datum            = crs.getDatum();
            } else {
                datum = defaultCRS.datum();
                final Integer epsg = epsgCandidateCS(Units.DEGREE);
                if (epsg != null) try {
                    coordinateSystem = decoder.getCSAuthorityFactory().createEllipsoidalCS(epsg.toString());
                } catch (NoSuchAuthorityCodeException e) {
                    recoverableException(e);
                }
            }
        }

        /**
         * Creates the two- or three-dimensional {@link EllipsoidalCS} from given axes. This method is invoked only if
         * {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if {@link #build(Decoder)}
         * found that the {@link #coordinateSystem} does not have compatible axes.
         */
        @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
            if (axes.length > 2) {
                coordinateSystem = factory.createEllipsoidalCS(properties, axes[0], axes[1], axes[2]);
            } else {
                coordinateSystem = factory.createEllipsoidalCS(properties, axes[0], axes[1]);
            }
        }

        /**
         * Creates the coordinate reference system from datum and coordinate system computed in previous steps.
         * This method is invoked under conditions similar to the ones of above {@code createCS(…)} method.
         */
        @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
            referenceSystem = factory.createGeographicCRS(properties, datum, coordinateSystem);
        }
    }




    /**
     * Projected CRS with (E,N,h) axes. There is not enough information in a netCDF files for creating the right
     * map projection, unless {@code "grid_mapping"} attributes are specified. If insufficient information, this
     * class creates an unknown map projection based on Plate Carrée. Note that this map projection may be replaced
     * by {@link GridMapping#crs} at a later stage.
     */
    private static final class Projected extends Geodetic<CartesianCS> {
        /**
         * The spherical variant of {@link #defaultCRS}.
         */
        private CommonCRS sphericalDatum;

        /**
         * Defining conversion for "Not specified (presumed Plate Carrée)". This conversion use spherical formulas.
         * Consequently it should be used with {@link #sphericalDatum} instead of {@link #defaultCRS}.
         */
        private static final Conversion UNKNOWN_PROJECTION;
        static {
            final DefaultCoordinateOperationFactory factory = DefaultFactories.forBuildin(
                    CoordinateOperationFactory.class, DefaultCoordinateOperationFactory.class);
            try {
                final OperationMethod method = factory.getOperationMethod(Equirectangular.NAME);
                UNKNOWN_PROJECTION = factory.createDefiningConversion(
                        properties("Not specified (presumed Plate Carrée)"),
                        method, method.getParameters().createValue());
            } catch (FactoryException e) {
                throw new ExceptionInInitializerError(e);
            }
        }

        /**
         * Creates a new builder (invoked by lambda function).
         */
        public Projected() {
            super((byte) 2);
        }

        /**
         * Possibly sets {@link #datum}, {@link #coordinateSystem} and {@link #referenceSystem}
         * to predefined objects matching the axes defined in the netCDF file.
         */
        @Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
            super.setPredefinedComponents(decoder);
            sphericalDatum = decoder.convention().defaultHorizontalCRS(true);
            datum = sphericalDatum.datum();
            if (isPredefinedCS(Units.METRE)) {
                coordinateSystem = decoder.getStandardProjectedCS();
            }
        }

        /**
         * Creates the two- or three-dimensional {@link CartesianCS} from given axes. This method is invoked only if
         * {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if {@link #build(Decoder)}
         * found that the {@link #coordinateSystem} does not have compatible axes.
         */
        @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
            if (axes.length > 2) {
                coordinateSystem = factory.createCartesianCS(properties, axes[0], axes[1], axes[2]);
            } else {
                coordinateSystem = factory.createCartesianCS(properties, axes[0], axes[1]);
            }
        }

        /**
         * Creates the coordinate reference system from datum and coordinate system computed in previous steps.
         * The datum for this method is based on a sphere.
         */
        @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
            final boolean is3D = (coordinateSystem.getDimension() >= 3);
            GeographicCRS baseCRS = is3D ? sphericalDatum.geographic3D() : sphericalDatum.geographic();
            if (!baseCRS.getDatum().equals(datum)) {
                baseCRS = factory.createGeographicCRS(properties, datum, baseCRS.getCoordinateSystem());
            }
            referenceSystem = factory.createProjectedCRS(properties, baseCRS, UNKNOWN_PROJECTION, coordinateSystem);
        }
    }




    /**
     * Vertical CRS with (H) or (D) axis.
     * Used for mean sea level (not for ellipsoidal height).
     */
    private static final class Vertical extends CRSBuilder<VerticalDatum, VerticalCS> {
        /**
         * Creates a new builder (invoked by lambda function).
         */
        public Vertical() {
            super(VerticalDatum.class, "Mean Sea Level", (byte) 1, (byte) 1, (byte) 1);
        }

        /**
         * Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file.
         */
        @Override void setPredefinedComponents(final Decoder decoder) {
            final Axis axis = getFirstAxis();
            final Unit<?> unit = axis.getUnit();
            final CommonCRS.Vertical predefined;
            if (Units.METRE.equals(unit)) {
                if (AxisDirection.UP.equals(axis.direction)) {
                    predefined = CommonCRS.Vertical.MEAN_SEA_LEVEL;
                } else {
                    predefined = CommonCRS.Vertical.DEPTH;
                }
            } else if (Units.HECTOPASCAL.equals(unit)) {
                predefined = CommonCRS.Vertical.BAROMETRIC;
            } else {
                return;
            }
            coordinateSystem = predefined.crs().getCoordinateSystem();
        }

        /**
         * Creates a {@link VerticalDatum} for <cite>"Unknown datum based on Mean Sea Level"</cite>.
         */
        @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
            datum = factory.createVerticalDatum(properties, VerticalDatumType.GEOIDAL);
        }

        /**
         * Creates the one-dimensional {@link VerticalCS} from given axes. This method is invoked
         * only if {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system
         * or if {@link #build(Decoder)} found that the axis or direction are not compatible.
         */
        @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
            coordinateSystem = factory.createVerticalCS(properties, axes[0]);
        }

        /**
         * Creates the coordinate reference system from datum and coordinate system computed in previous steps.
         */
        @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
            referenceSystem =  factory.createVerticalCRS(properties, datum, coordinateSystem);
        }
    }




    /**
     * Temporal CRS with (t) axis. Its datum need to be built
     * in a special way since it contains the time origin.
     */
    private static final class Temporal extends CRSBuilder<TemporalDatum, TimeCS> {
        /**
         * Creates a new builder (invoked by lambda function).
         */
        public Temporal() {
            super(TemporalDatum.class, "", (byte) 2, (byte) 1, (byte) 1);
        }

        /**
         * Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file.
         */
        @Override void setPredefinedComponents(final Decoder decoder) {
            final Axis axis = getFirstAxis();
            final Unit<?> unit = axis.getUnit();
            final CommonCRS.Temporal predefined;
            if (Units.DAY.equals(unit)) {
                predefined = CommonCRS.Temporal.JULIAN;
            } else if (Units.SECOND.equals(unit)) {
                predefined = CommonCRS.Temporal.UNIX;
            } else if (Units.MILLISECOND.equals(unit)) {
                predefined = CommonCRS.Temporal.JAVA;
            } else {
                return;
            }
            coordinateSystem = predefined.crs().getCoordinateSystem();
        }

        /**
         * Creates a {@link TemporalDatum} for <cite>"Unknown datum based on …"</cite>.
         */
        @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
            final Axis axis = getFirstAxis();
            axis.getUnit();                                     // Force epoch parsing if not already done.
            Instant epoch = axis.coordinates.epoch;
            final CommonCRS.Temporal c = CommonCRS.Temporal.forEpoch(epoch);
            if (c != null) {
                datum = c.datum();
            } else {
                properties = properties("Time since " + epoch);
                datum = factory.createTemporalDatum(properties, TemporalUtilities.toDate(epoch));
            }
        }

        /**
         * Creates the one-dimensional {@link TimeCS} from given axes. This method is invoked only
         * if {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if
         * {@link #build(Decoder)} found that the axis or direction are not compatible.
         */
        @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
            coordinateSystem = factory.createTimeCS(properties, axes[0]);
        }

        /**
         * Creates the coordinate reference system from datum and coordinate system computed in previous steps.
         */
        @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
            properties = properties(getFirstAxis().coordinates.getUnitsString());
            referenceSystem =  factory.createTemporalCRS(properties, datum, coordinateSystem);
        }
    }




    /**
     * Unknown CRS with (x,y,z) axes.
     */
    private static final class Engineering extends CRSBuilder<EngineeringDatum, AffineCS> {
        /**
         * Creates a new builder (invoked by lambda function).
         */
        public Engineering() {
            super(EngineeringDatum.class, "affine coordinate system", (byte) 3, (byte) 2, (byte) 3);
        }

        /**
         * No-op since we have no predefined engineering CRS.
         */
        @Override void setPredefinedComponents(final Decoder decoder) {
        }

        /**
         * Creates a {@link VerticalDatum} for <cite>"Unknown datum based on affine coordinate system"</cite>.
         */
        @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
            datum = factory.createEngineeringDatum(properties);
        }

        /**
         * Creates two- or three-dimensional {@link AffineCS} from given axes.
         */
        @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
            if (axes.length > 2) {
                coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1], axes[2]);
            } else {
                coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1]);
            }
        }

        /**
         * Creates the coordinate reference system from datum and coordinate system computed in previous steps.
         */
        @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
            referenceSystem =  factory.createEngineeringCRS(properties, datum, coordinateSystem);
        }
    }

    /**
     * Maximal {@link #datumIndex} value +1. The maximal value can be seen in the call to {@code super(…)} constructor
     * in the last inner class defined above.
     */
    static final int DATUM_CACHE_SIZE = 4;
}
