from . import abstract


class Constructs(abstract.Container):
    """A container for metadata constructs.

    The following metadata constructs can be included:

    * domain axis constructs
    * dimension coordinate constructs
    * auxiliary coordinate constructs
    * coordinate reference constructs
    * domain ancillary constructs
    * cell measure constructs
    * domain topology constructs
    * cell connectivity constructs
    * cell method constructs
    * field ancillary constructs

    The container is used by used by `Field` and `Domain` instances.

    The container is like a dictionary in many ways, in that it stores
    key/value pairs where the key is the unique construct key with
    corresponding metadata construct value, and provides some of the
    usual dictionary methods.

    .. versionadded:: (cfdm) 1.7.0

    """

    def __init__(
        self,
        auxiliary_coordinate=None,
        dimension_coordinate=None,
        domain_ancillary=None,
        field_ancillary=None,
        cell_measure=None,
        coordinate_reference=None,
        domain_axis=None,
        domain_topology=None,
        cell_connectivity=None,
        cell_method=None,
        source=None,
        copy=True,
        _use_data=True,
        _view=False,
        _ignore=(),
    ):
        """**Initialisation**

        :Parameters:

            auxiliary_coordinate: `str`, optional
                The base name for keys of auxiliary coordinate
                constructs.

                *Parameter example:*
                  ``auxiliary_coordinate='auxiliarycoordinate'``

            dimension_coordinate: `str`, optional
                The base name for keys of dimension coordinate
                constructs.

                *Parameter example:*
                  ``dimension_coordinate='dimensioncoordinate'``

            domain_ancillary: `str`, optional
                The base name for keys of domain ancillary constructs.

                *Parameter example:*
                  ``domain_ancillary='domainancillary'``

            field_ancillary: `str`, optional
                The base name for keys of field ancillary constructs.

                *Parameter example:*
                  ``field_ancillary='fieldancillary'``

            cell_measure: `str`, optional
                The base name for keys of cell measure constructs.

                *Parameter example:*
                  ``cell_measure='cellmeasure'``

            coordinate_reference: `str`, optional
                The base name for keys of coordinate reference
                constructs.

                *Parameter example:*
                  ``coordinate_reference='coordinatereference'``

            domain_axis: `str`, optional
                The base name for keys of domain axis constructs.

                *Parameter example:*
                  ``domain_axis='domainaxis'``

            domain_topology: `str`, optional
                The base name for keys of domain topology constructs.

                *Parameter example:*
                  ``'domaintopology'``

                .. versionadded:: (cfdm) 1.11.0.0

            cell_connectivity: `str`, optional
                The base name for keys of cell connectivity
                constructs.

                *Parameter example:*
                  ``'cellconnectivity'``

                .. versionadded:: (cfdm) 1.11.0.0

            cell_method: `str`, optional
                The base name for keys of cell method constructs.

                *Parameter example:*
                  ``cell_method='cellmethod'``

            {{init source: optional}}

            copy: `bool`, optional
                If True (the default) then deep copy metadata constructs
                from those of *source* prior to initialisation, else they
                are not deep copied.

            _ignore: sequence of `str`, optional
                Ignores the given construct types.

                *Parameter example:*
                  ``_ignore=('cell_method', 'field_ancillary')``

        """
        _ignore = tuple(_ignore)

        if source is not None:
            if _view:
                self.__dict__ = source.__dict__.copy()
                self._ignore = _ignore
                return

            source_constructs = source._constructs

            self._field_data_axes = source._field_data_axes

            self._key_base = source._key_base.copy()
            self._array_constructs = source._array_constructs.copy()
            self._non_array_constructs = source._non_array_constructs.copy()
            self._construct_axes = source._construct_axes.copy()
            self._construct_type = source._construct_type.copy()

            self_construct_axes = self._construct_axes
            self_construct_type = self._construct_type

            d = {}
            for construct_type in source._array_constructs:
                if construct_type in _ignore:
                    for cid in source_constructs.get(construct_type, ()):
                        self_construct_axes.pop(cid, None)
                        self_construct_type.pop(cid, None)

                    continue

                if construct_type not in source_constructs:
                    continue

                if copy:
                    new_v = {}
                    for cid, construct in source_constructs[
                        construct_type
                    ].items():
                        new_v[cid] = construct.copy(data=_use_data)
                else:
                    new_v = source_constructs[construct_type].copy()

                d[construct_type] = new_v

            for construct_type in source._non_array_constructs:
                if construct_type in _ignore:
                    for cid in source_constructs.get(construct_type, ()):
                        self_construct_type.pop(cid, None)

                    continue

                if construct_type not in source_constructs:
                    continue

                if copy:
                    new_v = {}
                    for cid, construct in source_constructs[
                        construct_type
                    ].items():
                        new_v[cid] = construct.copy()
                else:
                    new_v = source_constructs[construct_type].copy()

                d[construct_type] = new_v

            self._constructs = d

            self._ignore = ()

            return

        self._ignore = _ignore

        self._field_data_axes = None

        self._key_base = {}

        self._array_constructs = set()
        self._non_array_constructs = set()

        self._construct_axes = {}

        # The construct type for each key. For example:
        # {'domainaxis1':'domain_axis',
        #  'auxiliarycoordinate3':'auxiliary_coordinate'}
        self._construct_type = {}

        self._constructs = {}

        if auxiliary_coordinate:
            self._key_base["auxiliary_coordinate"] = auxiliary_coordinate
            self._array_constructs.add("auxiliary_coordinate")

        if dimension_coordinate:
            self._key_base["dimension_coordinate"] = dimension_coordinate
            self._array_constructs.add("dimension_coordinate")

        if domain_ancillary:
            self._key_base["domain_ancillary"] = domain_ancillary
            self._array_constructs.add("domain_ancillary")

        if field_ancillary:
            self._key_base["field_ancillary"] = field_ancillary
            self._array_constructs.add("field_ancillary")

        if cell_measure:
            self._key_base["cell_measure"] = cell_measure
            self._array_constructs.add("cell_measure")

        if domain_topology:
            self._key_base["domain_topology"] = domain_topology
            self._array_constructs.add("domain_topology")

        if cell_connectivity:
            self._key_base["cell_connectivity"] = cell_connectivity
            self._array_constructs.add("cell_connectivity")

        if domain_axis:
            self._key_base["domain_axis"] = domain_axis
            self._non_array_constructs.add("domain_axis")

        if coordinate_reference:
            self._key_base["coordinate_reference"] = coordinate_reference
            self._non_array_constructs.add("coordinate_reference")

        if cell_method:
            self._key_base["cell_method"] = cell_method
            self._non_array_constructs.add("cell_method")

        for x in self._array_constructs:
            self._constructs[x] = {}

        for x in self._non_array_constructs:
            self._constructs[x] = {}

    def __contains__(self, key):
        """Implements membership test operators for construct keys.

        x.__contains__(y) <==> y in x

        .. versionadded:: (cfdm) 1.7.0

        """
        return self.construct_type(key) is not None

    def __copy__(self):
        """Called by the `copy.copy` standard library function.

        .. versionadded:: (cfdm) 1.7.0

        """
        return self.shallow_copy()

    def __deepcopy__(self, memo):
        """Called by the `copy.deepcopy` standard library function.

        .. versionadded:: (cfdm) 1.7.0

        """
        return self.copy()

    def __getitem__(self, key):
        """Return a construct with the given key.

        x.__getitem__(y) <==> x[y]

        .. versionadded:: (cfdm) 1.7.0

        """
        construct_type = self.construct_type(key)
        if construct_type is None:
            raise KeyError(key)

        return self._constructs[construct_type][key]

    def __iter__(self):
        """Called when an iterator is required.

        x.__iter__() <==> iter(x)

        .. versionadded:: (cfdm) 1.7.0

        """
        return iter(self.todict())

    def __len__(self):
        """Return the number of constructs.

        x.__len__() <==> len(x)

        .. versionadded:: (cfdm) 1.7.0

        """
        n = len(self._construct_type)
        ignore = self._ignore
        if ignore:
            constructs = self._constructs
            for ctype in ignore:
                n -= len(constructs[ctype])

        return n

    def _construct_dict(self, construct_type, copy=False):
        """Return the dictionary of constructs of a particular type.

        .. versionadded:: (cfdm) 1.8.9.0

        :Parameters:

            copy: `bool`, optional
                If True and some constructs exisit then the dictionary
                is a shallow copy of that stored in the `_constructs`
                attribute, otherwise it is the same dictionary.

        :Returns:

            `dict`
                The dictionary, keyed by construct identifiers with
                construct values.

        """
        if construct_type in self._ignore:
            return {}

        out = self._constructs.get(construct_type)
        if out is None:
            return {}

        if copy:
            out = out.copy()

        return out

    def _del_data_axes(self, k, *d):
        """Remove and return a construct's axes, if any.

        If k is not found, d is returned if given, otherwise KeyError is
        raised

        """
        return self._construct_axes.pop(k, *d)

    def _view(self, ignore=()):
        """Returns a new container with a view of the same constructs.

        A new `{{class}}` instance is returned that contains the same
        metadata construct instances

        .. versionadded:: (cfdm) 1.7.0

        :Parameters:

            ignore: sequence of `str`, optional
                Return a view that ignores the given construct types.

                *Parameter example:*
                  ``ignore=('cell_method', 'field_ancillary')``

        :Returns:

            `{{class}}`
                The new constructs container.

        """
        return type(self)(source=self, _ignore=ignore, _view=True)

    def _check_construct_type(self, construct_type, default=ValueError()):
        """Check the type of a metadata construct is valid.

        .. versionadded:: (cfdm) 1.7.0

        :Parameters:

            construct_type: `str`
                The construct type to be checked.

            default: `bool`, optional
                Return the value of the *default* parameter if
                construct type is not valid.

                {{default Exception}}

        :Returns:

            `str` or `None`
                Return the type of of the construct, or if the input
                construct was given as `None`, `None` is returned.

        """
        if (
            construct_type not in self._ignore
            and construct_type in self._key_base
        ) or construct_type is None:
            return construct_type

        if default is None:
            return default

        return self._default(
            default, f"Invalid construct type {construct_type!r}"
        )

    def _construct_type_description(self, construct_type):
        """Format the description of the type of a metadata construct.

        Type name components are formatted to be whitespace-delimited to
        effective words for the purposes of printing to the user.

        """
        return construct_type.replace("_", " ")

    def _del_construct(self, key, default=ValueError()):
        """Remove a metadata construct.

        If a domain axis construct is selected for removal then it
        can't be spanned by any metadata construct data arrays, nor be
        referenced by any cell method constructs.

        However, a domain ancillary construct may be removed even if
        it is referenced by coordinate reference construct. In this
        case the reference is replace with `None`.

        .. versionadded:: (cfdm) 1.7.0

        .. seealso:: `_get_construct`, `_set_construct`

        :Parameters:

            key: `str`
                The key of the construct to be removed.

                *Parameter example:*
                  ``key='auxiliarycoordinate0'``

            default: optional
                Return the value of the *default* parameter if the
                construct can not be removed, or does not exist.

                {{default Exception}}

        :Returns:

                The removed construct.

        **Examples**

        >>> f = {{package}}.example_field(3)
        >>> c = f.constructs
        >>> x = c._del_construct('auxiliarycoordinate2')

        """
        data_axes = self.data_axes()

        domain_axes = self._construct_dict("domain_axis")

        if key in domain_axes:
            # Fail if the domain axis construct is spanned by a data
            # array
            for xid, axes in data_axes.items():
                if key in axes:
                    if default is None:
                        return default

                    raise ValueError(
                        f"Can't remove domain axis construct {key!r} that "
                        f"spans the data array of metadata construct {xid!r}"
                    )

            cell_methods = self._construct_dict("cell_method")
            for xid, cm in cell_methods.items():
                if key in cm.get_axes(()):
                    if default is None:
                        return default

                    raise ValueError(
                        f"Can't remove domain axis construct {key!r} "
                        "that is referenced by cell method construct "
                        f"{xid!r}"
                    )
        else:
            # Remove references to the removed construct in coordinate
            # reference constructs
            coordinate_references = self._construct_dict(
                "coordinate_reference"
            )
            for ref in coordinate_references.values():
                coordinate_conversion = ref.coordinate_conversion
                for (
                    term,
                    value,
                ) in coordinate_conversion.domain_ancillaries().items():
                    if key == value:
                        coordinate_conversion.set_domain_ancillary(term, None)

                ref.del_coordinate(key, None)

        out = self._pop(key, None)

        if out is None:
            if default is None:
                return default

            return self._default(
                default, f"Can't remove non-existent construct {key!r}"
            )

        return out

    def _dictionary(self, copy=False):
        """Deprecated at 1.8.9.0.

        Use `todict` instead.

        """
        return self.todict()  # pragma: no cover

    def _set_construct(self, construct, key=None, axes=None, copy=True):
        """Set a metadata construct.

        .. versionadded:: (cfdm) 1.7.0

        .. seealso:: `_del_construct`, `_get_construct`,
                     `_set_construct_data_axes`

        :Parameters:

            construct:
                The metadata construct to be inserted.

            key: `str`, optional
                The construct identifier to be used for the
                construct. If not set then a new, unique identifier is
                created automatically. If the identifier already
                exists then the existing construct will be replaced.

                *Parameter example:*
                  ``key='cellmeasure0'``

            axes: (sequence of) `str`, optional
                The construct identifiers of the domain axis
                constructs spanned by the data array. An exception is
                raised if used for a metadata construct that can not
                have a data array, i.e. domain axis, cell method and
                coordinate reference constructs.

                The axes may also be set afterwards with the
                `_set_construct_data_axes` method.

                *Parameter example:*
                  ``axes='domainaxis1'``

                *Parameter example:*
                  ``axes=['domainaxis1']``

                *Parameter example:*
                  ``axes=('domainaxis1', 'domainaxis0')``

            copy: `bool`, optional
                If True then return a copy of the unique selected
                construct. By default the construct is copied.

        :Returns:

             `str`
                The construct identifier for the construct.

        **Examples**

        >>> f = {{package}}.example_field(3)
        >>> c = f.constructs
        >>> c
        <{{repr}}Constructs: auxiliary_coordinate(6), domain_axis(2)>
        >>> d = {{package}}.DomainAxis(10)
        >>> key1 = c._set_construct(d, copy=False)
        >>> key1
        'domainaxis2'
        >>> print(c.filter_by_type('domain_axis'))
        Constructs:
        {'domainaxis0': <{{repr}}DomainAxis: size(4)>,
         'domainaxis1': <{{repr}}DomainAxis: size(9)>,
         'domainaxis2': <{{repr}}DomainAxis: size(10)>}

        >>> a = {{package}}.CellMeasure(measure='area', properties={'units': 'm2'})
        >>> key2 = c._set_construct(a, key='my_area_cell_measure')
        >>> key2
        'my_area_cell_measure'
        >>> v = {{package}}.CellMeasure(measure='volume', properties={'units': 'm3'})
        >>> key3 = c._set_construct(
        ...     v, key='my_volume_cell_measure', axes='domainaxis2')
        >>> key3
        'my_volume_cell_measure'
        >>> c
        <{{repr}}Constructs: auxiliary_coordinate(6), cell_measure(2), domain_axis(3)>
        >>> print(c.filter_by_type('cell_measure'))
        Constructs:
        {'my_area_cell_measure': <{{repr}}CellMeasure: measure:area m2>,
         'my_volume_cell_measure': <{{repr}}CellMeasure: measure:volume m3>}

        """
        construct_type = self._check_construct_type(construct.construct_type)

        if key is None:
            # Create a new construct identifier
            key = self.new_identifier(construct_type)

        if construct_type in self._array_constructs:
            # ---------------------------------------------------------
            # The construct could have a data array
            # ---------------------------------------------------------
            if axes is not None:
                self._set_construct_data_axes(
                    key=key, axes=axes, construct=construct
                )
        elif axes is not None:
            raise ValueError(
                f"Can't set {construct!r}: Can't provide domain axis "
                "constructs for "
                f"{self._construct_type_description(construct_type)} "
                "construct"
            )

        # Record the construct type
        self._construct_type[key] = construct_type

        if copy:
            # Create a deep copy of the construct
            construct = construct.copy()

        # Insert the construct
        self._constructs[construct_type][key] = construct

        # Return the identifier of the construct
        return key

    def _set_construct_data_axes(self, key, axes, construct=None):
        """Set domain axis constructs for construct identifiers.

        .. versionadded:: (cfdm) 1.7.0

        :Parameters:

            key: `str`, optional
                The construct identifier of metadata construct.

                *Parameter example:*
                  ``key='cellmeasure0'``

            axes: (sequence of) `str`
                The construct identifiers of the domain axis
                constructs spanned by the data array. An exception is
                raised if used for a metadata construct that can not
                have a data array, such as a domain axis construct.

                *Parameter example:*
                  ``axes='domainaxis1'``

                *Parameter example:*
                  ``axes=['domainaxis1']``

                *Parameter example:*
                  ``axes=['domainaxis1', 'domainaxis0']``

        :Returns:

            `None`

        **Examples**

        >>> f = {{package}}.example_field(3)
        >>> c = f.constructs
        >>> c
        <{{repr}}Constructs: auxiliary_coordinate(6), domain_axis(2)>
        >>> a = {{package}}.CellMeasure(measure='area', properties={'units': 'm2'})
        >>> area_measure_key = c.set_construct(a)
        >>> f._set_construct_data_axes(area_measure_key, axes='domainaxis0')

        """
        if construct is None:
            if self.construct_type(key) is None:
                raise ValueError(
                    "Can't set axes for non-existent construct "
                    f"identifier {key!r}"
                )

            construct = self[key]

        if isinstance(axes, str):
            axes = (axes,)

        domain_axes = self._construct_dict("domain_axis")

        axes_shape = []
        for axis in axes:
            if axis not in domain_axes:
                raise ValueError(
                    f"Can't set {construct!r} domain axes: "
                    f"Domain axis {axis!r} does not exist"
                )

            axes_shape.append(domain_axes[axis].get_size())

        axes_shape = tuple(axes_shape)
        try:
            # Note: The construct shape might be different from the
            #       construct's data shape. For instance, this will be
            #       the case for an auxiliary coordinate construct
            #       that has bounds but no cell coordinates; or for
            #       any domain topology construct or cell connectivity
            #       construct.
            if construct.shape != axes_shape:
                raise ValueError(
                    f"Can't set {construct!r}: Shape of {construct.shape!r} "
                    "does not match the shape required by domain axes "
                    f"{tuple(axes)}: {axes_shape}"
                )
        except AttributeError:
            pass

        self._construct_axes[key] = tuple(axes)

    # ----------------------------------------------------------------
    # Private dictionary-like methods
    # ----------------------------------------------------------------
    def _clear(self):
        """D.clear() -> None.

        Remove all items from D.

        """
        self._ignore = ()
        self._key_base.clear()
        self._array_constructs.clear()
        self._non_array_constructs.clear()
        self._construct_axes.clear()
        self._construct_type.clear()
        self._constructs.clear()

    def _pop(self, k, *d):
        """Removes specified key and returns the corresponding value.

        D.pop(k[,d]) -> v, remove specified key and return the
        corresponding value. If key is not found, d is returned if
        given, otherwise KeyError is raised.

        """
        construct_type = self.construct_type(k)
        if construct_type is not None:
            self._del_data_axes(k, None)
            del self._construct_type[k]
            return self._constructs[construct_type].pop(k)

        if d:
            return d[0]

        raise KeyError(k)

    def _update(self, other):
        """D.update(E) -> None.

        Update D from E.

        """
        self._ignore = tuple(set(self._ignore).union(other._ignore))

        self._key_base.update(other._key_base)
        self._array_constructs.update(other._array_constructs)
        self._non_array_constructs.update(other._non_array_constructs)
        self._construct_axes.update(other._construct_axes)
        self._construct_type.update(other._construct_type)
        self._constructs.update(other._constructs)

    # ----------------------------------------------------------------
    # Dictionary-like methods
    # ----------------------------------------------------------------
    def get(self, key, default=None):
        """Returns the construct for the key, else default.

        .. versionadded:: (cfdm) 1.7.0

        .. seealso:: `items`, `keys`, `values`

        """
        construct_type = self.construct_type(key)
        if construct_type is not None:
            return self._constructs[construct_type][key]

        return default

    def items(self):  # SB NOTE: flaky doctest due to dict_items order
        """Return the items as (construct key, construct) pairs.

        .. versionadded:: (cfdm) 1.7.0

        .. seealso:: `get`, `keys`, `values`

        :Returns:

            `dict_items`
                The construct key and constructs respectively as
                key-value pairs in a Python `dict_items` iterator.

        **Examples**

        >>> f = {{package}}.example_field(0)
        >>> c = f.constructs
        >>> c_items = c.items()
        >>> print(c_items)
        dict_items([('dimensioncoordinate0', <{{repr}}DimensionCoordinate: latitude(5) degrees_north>),
                    ('dimensioncoordinate1', <{{repr}}DimensionCoordinate: longitude(8) degrees_east>),
                    ('dimensioncoordinate2', <{{repr}}DimensionCoordinate: time(1) days since 2018-12-01 >),
                    ('domainaxis0', <{{repr}}DomainAxis: size(5)>),
                    ('domainaxis1', <{{repr}}DomainAxis: size(8)>),
                    ('domainaxis2', <{{repr}}DomainAxis: size(1)>),
                    ('cellmethod0', <{{repr}}CellMethod: area: mean>)])
        >>> type(c_items)
        <class 'dict_items'>
        >>> dict(c_items)
        {'dimensioncoordinate0': <{{repr}}DimensionCoordinate: latitude(5) degrees_north>,
         'dimensioncoordinate1': <{{repr}}DimensionCoordinate: longitude(8) degrees_east>,
         'dimensioncoordinate2': <{{repr}}DimensionCoordinate: time(1) days since 2018-12-01 >,
         'cellmethod0': <{{repr}}CellMethod: area: mean>,
         'domainaxis0': <{{repr}}DomainAxis: size(5)>,
         'domainaxis1': <{{repr}}DomainAxis: size(8)>,
         'domainaxis2': <{{repr}}DomainAxis: size(1)>}

        """
        return self.todict().items()

    def keys(self):  # SB NOTE: flaky doctest due to dict_items order
        """Return all of the construct keys, in arbitrary order.

        .. versionadded:: (cfdm) 1.7.0

        .. seealso:: `get`, `items`, `values`

        :Returns:

            `dict_keys`
                The construct keys as a Python `dict_keys` iterator.

        **Examples**

        >>> f = {{package}}.example_field(0)
        >>> c = f.constructs
        >>> c_keys = c.keys()
        >>> print(c_keys)
        dict_keys(['domainaxis0',
                   'domainaxis1',
                   'domainaxis2',
                   'dimensioncoordinate0',
                   'dimensioncoordinate1',
                   'dimensioncoordinate2',
                   'cellmethod0'])
        >>> type(c_keys)
        <class 'dict_keys'>
        >>> list(c_keys)
        ['domainaxis0',
         'domainaxis1',
         'domainaxis2',
         'dimensioncoordinate0',
         'dimensioncoordinate1',
         'dimensioncoordinate2',
         'cellmethod0']

        """
        return self.todict().keys()

    def values(self):  # SB NOTE: flaky doctest due to dict_items order
        """Return all of the metadata constructs, in arbitrary order.

        .. versionadded:: (cfdm) 1.7.0

        .. seealso:: `get`, `items`, `keys`

        :Returns:

            `dict_values`
                The constructs as a Python `dict_values` iterator.

        **Examples**

        >>> f = {{package}}.example_field(0)
        >>> c = f.constructs
        >>> c_values = c.values()
        >>> print(c_values)
        dict_values([<{{repr}}DimensionCoordinate: latitude(5) degrees_north>,
                     <{{repr}}DimensionCoordinate: longitude(8) degrees_east>,
                     <{{repr}}DimensionCoordinate: time(1) days since 2018-12-01 >,
                     <{{repr}}DomainAxis: size(5)>,
                     <{{repr}}DomainAxis: size(8)>,
                     <{{repr}}DomainAxis: size(1)>,
                     <{{repr}}CellMethod: area: mean>])
        >>> type(c_values)
        <class 'dict_values'>
        >>> list(c_values)
        [<{{repr}}DimensionCoordinate: latitude(5) degrees_north>,
         <{{repr}}DimensionCoordinate: longitude(8) degrees_east>,
         <{{repr}}DimensionCoordinate: time(1) days since 2018-12-01 >,
         <{{repr}}DomainAxis: size(5)>,
         <{{repr}}DomainAxis: size(8)>,
         <{{repr}}DomainAxis: size(1)>,
         <{{repr}}CellMethod: area: mean>]

        """
        return self.todict().values()

    def construct_type(self, key):
        """Return the type of a construct for a given key.

        .. versionadded:: (cfdm) 1.7.0

        .. seealso:: `construct_types`

        :Parameters:

            key: `str`
                A construct identifier.

        :Returns:

            `str` or `None`
                The construct type, or `None` if the *key* is not
                present.

        """
        ctype = self._construct_type.get(key)
        if ctype is not None and ctype in self._ignore:
            return

        return ctype

    def construct_types(self):
        """Return all of the construct types for all keys.

        .. versionadded:: (cfdm) 1.7.0

        .. seealso:: `construct_type`

        """
        out = self._construct_type.copy()

        ignore = self._ignore
        if ignore:
            for key, ctype in self._construct_type.items():
                if ctype in ignore:
                    del out[key]

        return out

    def copy(self, data=True):
        """Return a deep copy.

        ``f.copy()`` is equivalent to ``copy.deepcopy(f)``.

        .. versionadded:: (cfdm) 1.7.0

        :Parameters:

            {{data: `bool`, optional}}

        :Returns:

            `{{class}}`
                The deep copy.

        **Examples**

        >>> f = {{package}}.example_field(0)
        >>> c = f.constructs
        >>> k = c.copy()
        >>> k = c.copy(data=False)

        """
        return type(self)(
            source=self,
            copy=True,
            _ignore=self._ignore,
            _view=False,
            _use_data=data,
        )

    def data_axes(self):
        """Returns the axes spanned by the data.

        Specifically, returns the domain axis constructs spanned by
        metadata construct data.

        .. versionadded:: (cfdm) 1.7.0

        :Returns:

            `dict`
                The keys of the domain axes constructs spanned by
                metadata construct data.

        **Examples**

        >>> f = {{package}}.example_field(0)
        >>> c = f.constructs
        >>> print(c)
        Constructs:
        {'cellmethod0': <{{repr}}CellMethod: area: mean>,
         'dimensioncoordinate0': <{{repr}}DimensionCoordinate: latitude(5) degrees_north>,
         'dimensioncoordinate1': <{{repr}}DimensionCoordinate: longitude(8) degrees_east>,
         'dimensioncoordinate2': <{{repr}}DimensionCoordinate: time(1) days since 2018-12-01 >,
         'domainaxis0': <{{repr}}DomainAxis: size(5)>,
         'domainaxis1': <{{repr}}DomainAxis: size(8)>,
         'domainaxis2': <{{repr}}DomainAxis: size(1)>}
        >>> c.data_axes()
        {'dimensioncoordinate0': ('domainaxis0',),
         'dimensioncoordinate1': ('domainaxis1',),
         'dimensioncoordinate2': ('domainaxis2',)}

        """
        out = self._construct_axes.copy()

        ignore = self._ignore
        if not ignore:
            return out

        non_data_constructs = self._non_array_constructs
        for construct_type, keys in self._constructs.items():
            if construct_type in non_data_constructs:
                continue

            if construct_type in ignore:
                for key in keys:
                    out.pop(key, None)

        return out

    def filter_by_type(self, *types, todict=True, cached=None):
        """Select metadata constructs by type.

        .. versionadded:: (cfdm) 1.7.0

        :Parameters:

            types: optional
                Select constructs that have are of any of the given
                types.

                A type is specified by one of the following strings:

                ==========================  ==========================
                *type*                      Construct selected
                ==========================  ==========================
                ``'domain_ancillary'``      Domain ancillary
                ``'dimension_coordinate'``  Dimension coordinate
                ``'domain_axis'``           Domain axis
                ``'auxiliary_coordinate'``  Auxiliary coordinate
                ``'cell_measure'``          Cell measure
                ``'coordinate_reference'``  Coordinate reference
                ``'domain_topology'``       Domain topology
                ``'cell_connectivity'``     Cell connectivity
                ``'cell_method'``           Cell method
                ``'field_ancillary'``       Field ancillary
                ==========================  ==========================

                If no types are provided then all constructs are
                selected.

        :Parameters:

            {{todict: `bool`, optional}}

            {{cached: optional}}

        :Returns:

            `{{class}}` or `dict`
                The selected constructs and their construct keys.

        **Examples**

        Select dimension coordinate constructs:

        >>> f = {{package}}.example_field(1)
        >>> c = f.constructs
        >>> d = c.filter_by_type('dimension_coordinate')
        >>> d
        <{{repr}}Constructs: dimension_coordinate(4)>

        Select dimension coordinate and field ancillary constructs:

        >>> k = c.filter_by_type(
        ...     'dimension_coordinate', 'field_ancillary')
        >>> k
        <{{repr}}Constructs: dimension_coordinate(4), field_ancillary(1)>

        """
        if cached is not None:
            return cached

        if todict:
            # Return a dictionary
            if not types:
                return self.todict()

            out = self._construct_dict(types[0], copy=True)

            if len(types) > 1:
                ignore = self._ignore
                constructs = self._constructs
                for t in types[1:]:
                    if t in constructs and t not in ignore:
                        out.update(constructs[t])

            return out

        # Still here? Then return a `Constructs` object
        if types:
            # Ignore the all but the requested construct types
            ignore = set(self._key_base)
            ignore.difference_update(types)
            ignore.update(self._ignore)
        else:
            # Keep all construct types
            ignore = self._ignore

        return self.shallow_copy(_ignore=ignore)

    def get_data_axes(self, key, default=ValueError()):
        """Return the keys of the axes spanned by a construct's data.

        Specifically, returns the keys of the domain axis constructs
        spanned by the data of a metadata construct.

        .. versionadded:: (cfdm) 1.8.9.0

        :Parameters:

            key: `str`
                Specify a metadata construct.

                *Parameter example:*
                  ``key='auxiliarycoordinate0'``

            default: optional
                Return the value of the *default* parameter if the data
                axes have not been set.

                {{default Exception}}

        :Returns:

            `tuple`
                The keys of the domain axis constructs spanned by the data.

        """
        data_axes = self._construct_axes.get(key)
        if data_axes is not None:
            ignore = self._ignore
            if not ignore or self._construct_type[key] not in ignore:
                return data_axes

        if default is None:
            return default

        return self._default(default, f"No data axes for construct {key!r}")

    def key(self, default=ValueError()):
        """Return the construct key of the sole metadata construct.

        .. versionadded:: (cfdm) 1.7.0

        .. seealso:: `get`, `keys`, `value`

        :Parameters:

            default: optional
                Return the value of the *default* parameter if there
                is not exactly one construct.

                {{default Exception}}

        :Returns:

            `str`
                The construct key.

        **Examples**

        >>> f = {{package}}.Field()
        >>> c = f.constructs
        >>> d = {{package}}.DimensionCoordinate(
        ...     properties={
        ...         'standard_name': 'latitude', 'units': 'degrees_north'},
        ...     data={{package}}.Data(range(5))
        ... )
        >>> c._set_construct(d)
        'dimensioncoordinate0'
        >>> print(c)
        Constructs:
        {'dimensioncoordinate0': <{{repr}}DimensionCoordinate: latitude(5) degrees_north>}
        >>> c.key()
        'dimensioncoordinate0'
        >>> c.value()
        <{{repr}}DimensionCoordinate: latitude(5) degrees_north>

        """
        d = self.todict()
        n = len(d)
        if n == 1:
            key, _ = d.popitem()
            return key

        if default is None:
            return default

        if not n:
            return self._default(default, "Can't get key for zero constructs")

        return self._default(
            default, f"Can't get key for more than one ({n}) constructs"
        )

    def new_identifier(self, construct_type):
        """Return a new, unused construct key.

        .. versionadded:: (cfdm) 1.7.0

        :Parameters:

            construct_type: `str`
                The construct type for which the identifier is being
                created.

                *Parameter example:*
                  ``construct_type='dimension_coordinate'``

        :Returns:

            `str`
                The new construct identifier.

        **Examples**

        >>> f = {{package}}.example_field(0)
        >>> c = f.constructs
        >>> c.keys()
        ['domainaxis0',
         'domainaxis1',
         'domainaxis2',
         'dimensioncoordinate0',
         'dimensioncoordinate1',
         'dimensioncoordinate2',
         'cellmethod0']
        >>> c.new_identifier('domain_axis')
        'domainaxis3'
        >>> c.new_identifier('cell_method')
        'cellmethod1'

        """
        construct_type = self._check_construct_type(construct_type)

        keys = self._constructs[construct_type]

        n = len(keys)
        key_base = self._key_base[construct_type]
        key = f"{key_base}{n}"
        while key in keys:
            n += 1
            key = f"{key_base}{n}"

        return key

    def replace(self, key, construct, axes=None, copy=True):
        """Replace one metadata construct with another.

        .. note:: No checks on the axes are done.

        """
        construct_type = self.construct_types().get(key)
        if construct_type is None:
            raise ValueError(f"Can't replace non-existent construct {key!r}")

        if axes is not None and construct_type in self._array_constructs:
            self._construct_axes[key] = tuple(axes)

        if copy:
            construct = construct.copy()

        self._constructs[construct_type][key] = construct

    def shallow_copy(self, _ignore=None):
        """Return a shallow copy.

        ``copy.copy(f)`` is equivalent to ``f.shallow_copy()``.

        Any in-place changes to the actual constructs of the copy will
        not be seen in the original `{{class}}` object, and vice
        versa, but the copy and filter history are otherwise
        independent.

        .. versionadded:: (cfdm) 1.8.9.0

        .. seealso:: `view`

        :Returns:

            `{{class}}`
                The shallow copy.

        **Examples**

        >>> f = {{package}}.example_field(0)
        >>> c = f.constructs
        >>> k = c.shallow_copy()

        """
        if _ignore is None:
            _ignore = self._ignore

        return type(self)(
            source=self, copy=False, _ignore=_ignore, _view=False
        )

    def todict(self, copy=False):
        """Return a dictionary of the metadata constructs.

        .. versionadded:: (cfdm) 1.8.9.0

        :Parameters:

            copy: `bool`, optional
                If True then deep copy the metadata construct values.

        :Returns:

            `dict`
                The dictionary representation, keyed by construct
                identifiers with metadata construct values.

        """
        out = {}
        ignore = self._ignore
        for key, value in self._constructs.items():
            if key not in ignore:
                out.update(value)

        if copy:
            for key, construct in out.items():
                out[key] = construct.copy()

        return out

    def value(self, default=ValueError()):
        """Return the sole metadata construct.

        .. versionadded:: (cfdm) 1.7.0

        .. seealso:: `get`, `key`, `values`

        :Parameters:

            default: optional
                Return the value of the *default* parameter if there
                is not exactly one construct.

                {{default Exception}}

        :Returns:

                The metadata construct.

        **Examples**

        >>> f = {{package}}.Field()
        >>> c = f.constructs
        >>> d = {{package}}.DimensionCoordinate(
        ...     properties={
        ...         'standard_name': 'latitude', 'units': 'degrees_north'},
        ...     data={{package}}.Data(range(5))
        ... )
        >>> c._set_construct(d)
        'dimensioncoordinate0'
        >>> print(c)
        Constructs:
        {'dimensioncoordinate0': <{{repr}}DimensionCoordinate: latitude(5) degrees_north>}
        >>> c.key()
        'dimensioncoordinate0'
        >>> c.value()
        <{{repr}}DimensionCoordinate: latitude(5) degrees_north>

        """
        d = self.todict()
        n = len(d)
        if n == 1:
            _, construct = d.popitem()
            return construct

        if default is None:
            return default

        if not d:
            return self._default(default, "Can't return zero constructs")

        return self._default(
            default, f"Can't return more than one ({n}) constructs"
        )
