/*
 * 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.feature.builder;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Objects;
import java.lang.reflect.Array;
import org.opengis.util.GenericName;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.feature.DefaultAttributeType;
import org.apache.sis.feature.FeatureOperations;
import org.apache.sis.internal.util.CollectionsExt;
import org.apache.sis.internal.util.SetOfUnknownSize;
import org.apache.sis.internal.util.AbstractIterator;
import org.apache.sis.internal.feature.AttributeConvention;
import org.apache.sis.internal.feature.Geometries;
import org.apache.sis.internal.feature.Resources;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.Classes;
import org.apache.sis.util.ObjectConverters;
import org.apache.sis.util.UnconvertibleObjectException;

// Branch-dependent imports


/**
 * Describes one {@code AttributeType} which will be part of the feature type to be built by
 * a {@code FeatureTypeBuilder}. An attribute can be for example a city name, a temperature
 * (together with its units of measurement and uncertainty if desired) or a geometric shape.
 * Attribute types contain the following information:
 *
 * <ul>
 *   <li>the name        — a unique name which can be defined within a scope (or namespace).</li>
 *   <li>the definition  — a concise definition of the element.</li>
 *   <li>the designation — a natural language designator for the element for user interfaces.</li>
 *   <li>the description — information beyond that required for concise definition of the element.</li>
 *   <li>the value class — often {@link String}, {@link Float} or {@link com.esri.core.geometry.Geometry}.
 *       Must be specified at {@linkplain FeatureTypeBuilder#addAttribute(Class) construction time}.</li>
 *   <li>a default value — to be used when an attribute instance does not provide an explicit value.</li>
 *   <li>characteristics — for example the units of measurement for all attributes of the same type.</li>
 *   <li>multiplicity    — the minimum and maximum occurrences of attribute values.</li>
 * </ul>
 *
 * @author  Johann Sorel (Geomatys)
 * @author  Martin Desruisseaux (Geomatys)
 * @version 0.8
 *
 * @param <V>  the class of attribute values.
 *
 * @see FeatureTypeBuilder#addAttribute(Class)
 * @see org.apache.sis.feature.DefaultAttributeType
 *
 * @since 0.8
 * @module
 */
public final class AttributeTypeBuilder<V> extends PropertyTypeBuilder {
    /**
     * The class of attribute values. Can not be changed after construction
     * because this value determines the parameterized type {@code <V>}.
     */
    private final Class<V> valueClass;

    /**
     * The default value for the attribute, or {@code null} if none.
     */
    private V defaultValue;

    /**
     * Whether this attribute will be used in a {@linkplain FeatureOperations#compound compound key} named
     * {@code "sis:identifier"}. If only one attribute has this flag and {@link FeatureTypeBuilder#idPrefix} and
     * {@code isSuffix} are null, then {@code "sis:identifier"} will be a {@linkplain FeatureOperations#link link}
     * to {@code idAttributes[0]}.
     *
     * @see #addRole(AttributeRole)
     */
    private boolean isIdentifier;

    /**
     * Builders for the characteristics associated to the attribute.
     */
    final List<CharacteristicTypeBuilder<?>> characteristics;

    /**
     * The attribute type created by this builder, or {@code null} if not yet created.
     * This field must be cleared every time that a setter method is invoked on this builder.
     */
    private transient DefaultAttributeType<V> property;

    /**
     * Creates a new builder initialized to the values of the given builder.
     * This constructor is for {@link #setValueClass(Class)} implementation only.
     *
     * @throws UnconvertibleObjectException if the default value can not be converted to the given class.
     */
    private AttributeTypeBuilder(final AttributeTypeBuilder<?> builder, final Class<V> valueClass)
            throws UnconvertibleObjectException
    {
        super(builder);
        this.valueClass = valueClass;
        defaultValue = ObjectConverters.convert(builder.defaultValue, valueClass);
        isIdentifier = builder.isIdentifier;
        characteristics = builder.characteristics;
    }

    /**
     * Creates a new {@code AttributeType} builder for values of the given class.
     *
     * @param owner       the builder of the {@code FeatureType} for which to add the attribute.
     * @param valueClass  the class of attribute values.
     */
    AttributeTypeBuilder(final FeatureTypeBuilder owner, final Class<V> valueClass) {
        super(owner);
        this.valueClass = valueClass;
        characteristics = new ArrayList<>();
    }

    /**
     * Creates a new {@code AttributeType} builder initialized to the values of an existing attribute.
     *
     * @param owner  the builder of the {@code FeatureType} for which to add the attribute.
     */
    AttributeTypeBuilder(final FeatureTypeBuilder owner, final DefaultAttributeType<V> template) {
        super(owner);
        property      = template;
        minimumOccurs = template.getMinimumOccurs();
        maximumOccurs = template.getMaximumOccurs();
        valueClass    = template.getValueClass();
        defaultValue  = template.getDefaultValue();
        final Map<String, DefaultAttributeType<?>> tc = template.characteristics();
        characteristics = new ArrayList<>(tc.size());
        for (final DefaultAttributeType<?> c : tc.values()) {
            characteristics.add(new CharacteristicTypeBuilder<>(this, c));
        }
        initialize(template);
    }

    /**
     * If the {@code AttributeType} created by the last call to {@link #build()} has been cached,
     * clears that cache. This method must be invoked every time that a setter method is invoked.
     */
    @Override
    final void clearCache() {
        property = null;
        super.clearCache();
    }

    /**
     * Returns a default name to use if the user did not specified a name. The first letter will be changed to
     * lower case (unless the name looks like an acronym) for compliance with Java convention on attribute names.
     */
    @Override
    final String getDefaultName() {
        return Classes.getShortName(valueClass);
    }

    /**
     * Sets the {@code AttributeType} name as a generic name.
     * If another name was defined before this method call, that previous value will be discarded.
     *
     * @return {@code this} for allowing method calls chaining.
     */
    @Override
    public AttributeTypeBuilder<V> setName(final GenericName name) {
        super.setName(name);
        return this;
    }

    /**
     * Sets the {@code AttributeType} name as a simple string (local name).
     * The namespace will be the value specified by the last call to {@link FeatureTypeBuilder#setNameSpace(CharSequence)},
     * but that namespace will not be visible in the {@linkplain org.apache.sis.util.iso.DefaultLocalName#toString()
     * string representation} unless the {@linkplain org.apache.sis.util.iso.DefaultLocalName#toFullyQualifiedName()
     * fully qualified name} is requested.
     *
     * <p>This convenience method creates a {@link org.opengis.util.LocalName} instance from
     * the given {@code CharSequence}, then delegates to {@link #setName(GenericName)}.</p>
     *
     * @return {@code this} for allowing method calls chaining.
     */
    @Override
    public AttributeTypeBuilder<V> setName(final CharSequence localPart) {
        super.setName(localPart);
        return this;
    }

    /**
     * Sets the {@code AttributeType} name as a string in the given scope.
     * The {@code components} array must contain at least one element.
     * The last component (the {@linkplain org.apache.sis.util.iso.DefaultScopedName#tip() tip}) will be sufficient
     * in many cases for calls to the {@link org.apache.sis.feature.AbstractFeature#getProperty(String)} method.
     * The other elements before the last one are optional and can be used for resolving ambiguity.
     * They will be visible as the name {@linkplain org.apache.sis.util.iso.DefaultScopedName#path() path}.
     *
     * <div class="note"><b>Example:</b>
     * a call to {@code setName("A", "B", "C")} will create a "A:B:C" name.
     * An attribute built with this name can be obtained from a feature by a call to {@code feature.getProperty("C")}
     * if there is no ambiguity, or otherwise by a call to {@code feature.getProperty("B:C")} (if non-ambiguous) or
     * {@code feature.getProperty("A:B:C")}.</div>
     *
     * In addition to the path specified by the {@code components} array, the name may also contain
     * a namespace specified by the last call to {@link FeatureTypeBuilder#setNameSpace(CharSequence)}.
     * But contrarily to the specified components, the namespace will not be visible in the name
     * {@linkplain org.apache.sis.util.iso.DefaultScopedName#toString() string representation} unless the
     * {@linkplain org.apache.sis.util.iso.DefaultScopedName#toFullyQualifiedName() fully qualified name} is requested.
     *
     * <p>This convenience method creates a {@link org.opengis.util.LocalName} or {@link org.opengis.util.ScopedName}
     * instance depending on whether the {@code names} array contains exactly 1 element or more than 1 element, then
     * delegates to {@link #setName(GenericName)}.</p>
     *
     * @return {@code this} for allowing method calls chaining.
     */
    @Override
    public AttributeTypeBuilder<V> setName(final CharSequence... components) {
        super.setName(components);
        return this;
    }

    /**
     * Sets the minimum number of attribute values. If the given number is greater than the
     * {@linkplain #getMaximumOccurs() maximal number} of attribute values, than the maximum
     * is also set to that value.
     *
     * @param  occurs  the new minimum number of attribute values.
     * @return {@code this} for allowing method calls chaining.
     */
    @Override
    public AttributeTypeBuilder<V> setMinimumOccurs(final int occurs) {
        super.setMinimumOccurs(occurs);
        return this;
    }

    /**
     * Sets the maximum number of attribute values. If the given number is less than the
     * {@linkplain #getMinimumOccurs() minimal number} of attribute values, than the minimum
     * is also set to that value. {@link Integer#MAX_VALUE} means that there is no maximum.
     *
     * @param  occurs  the new maximum number of attribute values.
     * @return {@code this} for allowing method calls chaining.
     */
    @Override
    public AttributeTypeBuilder<V> setMaximumOccurs(final int occurs) {
        super.setMaximumOccurs(occurs);
        return this;
    }

    /**
     * Returns the class of attribute values.
     *
     * @return the class of attribute values.
     *
     * @see #setValueClass(Class)
     */
    public Class<V> getValueClass() {
        return valueClass;
    }

    /**
     * Sets the class of attribute values. Callers <strong>must</strong> use the builder returned by this method
     * instead of {@code this} builder after this method call, since the returned builder may be a new instance.
     *
     * @param  <N>   the compile-time value of the {@code type} argument.
     * @param  type  the new class of attribute values.
     * @return the attribute builder — <em>not necessarily this instance.</em>
     * @throws UnconvertibleObjectException if the {@linkplain #getDefaultValue() default value}
     *         can not be converted to the given {@code <N>} class.
     *
     * @see #getValueClass()
     */
    @SuppressWarnings("unchecked")
    public <N> AttributeTypeBuilder<N> setValueClass(final Class<N> type) throws UnconvertibleObjectException {
        final FeatureTypeBuilder owner = owner();
        ensureNonNull("type", type);
        if (type == valueClass) {
            return (AttributeTypeBuilder<N>) this;
        }
        final AttributeTypeBuilder<N> newb = new AttributeTypeBuilder<>(this, type);
        for (final CharacteristicTypeBuilder<?> c : characteristics) {
            c.owner(newb);
        }
        owner.replace(this, newb);
        dispose();
        return newb;
    }

    /**
     * Returns the default value for the attribute, or {@code null} if none.
     *
     * @return the default attribute value, or {@code null} if none.
     *
     * @see #setDefaultValue(Object)
     */
    public V getDefaultValue() {
        return defaultValue;
    }

    /**
     * Sets the default value for the attribute.
     *
     * @param  value  default attribute value, or {@code null} if none.
     * @return {@code this} for allowing method calls chaining.
     *
     * @see #getDefaultValue()
     */
    public AttributeTypeBuilder<V> setDefaultValue(final V value) {
        if (!Objects.equals(defaultValue, value)) {
            defaultValue = value;
            clearCache();
        }
        return this;
    }

    /**
     * Returns an enumeration of valid values for the attribute, or an empty array if none.
     * This convenience method returns the value of the characteristic set by {@link #setValidValues(Object...)}.
     *
     * @return valid values for the attribute, or an empty array if none.
     */
    @SuppressWarnings("unchecked")
    public V[] getValidValues() {
        final Collection<?> c = CollectionsExt.nonNull((Collection<?>)
                getCharacteristic(AttributeConvention.VALID_VALUES_CHARACTERISTIC));
        final V[] values = (V[]) Array.newInstance(valueClass, c.size());
        int index = 0;
        for (final Object value : c) {
            values[index++] = (V) value;        // ArrayStoreException if 'value' is not the expected type.
        }
        return values;
    }

    /**
     * Sets an enumeration of valid values for the attribute.
     *
     * <p>This is a convenience method for {@link #addCharacteristic(Class)} with a value
     * of type {@link Set} and a conventional name.</p>
     *
     * @param  values  valid values.
     * @return {@code this} for allowing method calls chaining.
     *
     * @see #characteristics()
     * @see AttributeConvention#VALID_VALUES_CHARACTERISTIC
     */
    @SafeVarargs
    public final AttributeTypeBuilder<V> setValidValues(final V... values) {
        return setCharacteristic(AttributeConvention.VALID_VALUES_CHARACTERISTIC,
                Set.class, CollectionsExt.immutableSet(false, values));
    }

    /**
     * Returns the maximal length that characterizes the {@link CharSequence} values of this attribute.
     * This convenience method returns the value of the characteristic set by {@link #setMaximalLength(Integer)}.
     *
     * @return the maximal length of {@link CharSequence} attribute values, or {@code null}.
     */
    public Integer getMaximalLength() {
        return (Integer) getCharacteristic(AttributeConvention.MAXIMAL_LENGTH_CHARACTERISTIC);
    }

    /**
     * Sets the maximal length that characterizes the {@link CharSequence} values of this attribute.
     * While this characteristic can be applied to any kind of attribute, it is meaningful only with
     * character sequences.
     *
     * <p>This is a convenience method for {@link #addCharacteristic(Class)} with a value
     * of type {@link Integer} and a conventional name.</p>
     *
     * @param  length  maximal length of {@link CharSequence} attribute values, or {@code null}.
     * @return {@code this} for allowing method calls chaining.
     *
     * @see #characteristics()
     * @see AttributeConvention#MAXIMAL_LENGTH_CHARACTERISTIC
     */
    public AttributeTypeBuilder<V> setMaximalLength(final Integer length) {
        return setCharacteristic(AttributeConvention.MAXIMAL_LENGTH_CHARACTERISTIC, Integer.class, length);
    }

    /**
     * Returns the coordinate reference system associated to attribute values.
     * This convenience method returns the value of the characteristic set by {@link #setCRS(CoordinateReferenceSystem)}.
     *
     * @return the coordinate reference system associated to attribute values, or {@code null}.
     */
    public CoordinateReferenceSystem getCRS() {
        return (CoordinateReferenceSystem) getCharacteristic(AttributeConvention.CRS_CHARACTERISTIC);
    }

    /**
     * Sets the coordinate reference system that characterizes the values of this attribute.
     * While this characteristic can be applied to any kind of attribute, it is meaningful
     * only with georeferenced values like geometries or coverages.
     *
     * <p>This is a convenience method for {@link #addCharacteristic(Class)} with a value
     * of type {@link CoordinateReferenceSystem} and a conventional name.</p>
     *
     * @param  crs  coordinate reference system associated to attribute values, or {@code null}.
     * @return {@code this} for allowing method calls chaining.
     *
     * @see #characteristics()
     * @see AttributeConvention#CRS_CHARACTERISTIC
     */
    public AttributeTypeBuilder<V> setCRS(final CoordinateReferenceSystem crs) {
        return setCharacteristic(AttributeConvention.CRS_CHARACTERISTIC, CoordinateReferenceSystem.class, crs);
    }

    /**
     * Implementation of all getter methods for characteristics.
     */
    private Object getCharacteristic(final GenericName name) {
        for (final CharacteristicTypeBuilder<?> characteristic : characteristics) {
            if (name.equals(characteristic.getName())) {
                return characteristic.getDefaultValue();
            }
        }
        return null;
    }

    /**
     * Implementation of all setter methods for characteristics.
     */
    private <C> AttributeTypeBuilder<V> setCharacteristic(final GenericName name, final Class<C> type, final C value) {
        for (final CharacteristicTypeBuilder<?> characteristic : characteristics) {
            if (name.equals(characteristic.getName())) {
                characteristic.set(value);
                clearCache();
                return this;
            }
        }
        addCharacteristic(type).setDefaultValue(value).setName(name);
        return this;
    }

    /**
     * Returns the builder for the characteristic of the given name. The given name does not need to contains
     * all elements of a {@link org.opengis.util.ScopedName}; it is okay to specify only the tip (for example
     * {@code "myName"} instead of {@code "myScope:myName"}) provided that ignoring the name head does not
     * create ambiguity.
     *
     * @param  name   name of the characteristic to search.
     * @return characteristic of the given name, or {@code null} if none.
     * @throws IllegalArgumentException if the given name is ambiguous.
     *
     * @see #characteristics()
     */
    public CharacteristicTypeBuilder<?> getCharacteristic(final String name) {
        return forName(characteristics, name, true);
    }

    /**
     * Adds another attribute type that describes this attribute type.
     * See <cite>"Attribute characterization"</cite> in {@link DefaultAttributeType} Javadoc for more information.
     *
     * <p>Usage example:</p>
     * {@preformat java
     *     attribute.addCharacteristic(Unit.class).setName("Unit of measurement").setDefaultValue(Units.CELSIUS);
     * }
     *
     * The default characteristic name is the name of the given type, but callers should invoke one
     * of the {@code CharacteristicTypeBuilder.setName(…)} methods on the returned instance with a better name.
     *
     * @param  <C>   the compile-time type of {@code type} argument.
     * @param  type  the class of characteristic values.
     * @return a builder for a characteristic of this attribute.
     *
     * @see #characteristics()
     */
    public <C> CharacteristicTypeBuilder<C> addCharacteristic(final Class<C> type) {
        ensureNonNull("type", type);
        final CharacteristicTypeBuilder<C> characteristic = new CharacteristicTypeBuilder<>(this, type);
        characteristics.add(characteristic);
        clearCache();
        return characteristic;
    }

    /**
     * Adds another attribute type that describes this attribute type, using an existing one as a template.
     * See <cite>"Attribute characterization"</cite> in {@link DefaultAttributeType} Javadoc for more information.
     *
     * <div class="warning"><b>Warning:</b>
     * The {@code template} argument type will be changed to {@code AttributeType} if and when such interface
     * will be defined in GeoAPI.</div>
     *
     * @param  <C>       the compile-time type of values in the {@code template} argument.
     * @param  template  an existing attribute type to use as a template.
     * @return a builder for a characteristic of this attribute, initialized with the values of the given template.
     *
     * @see #characteristics()
     */
    public <C> CharacteristicTypeBuilder<C> addCharacteristic(final DefaultAttributeType<C> template) {
        ensureNonNull("template", template);
        final CharacteristicTypeBuilder<C> characteristic = new CharacteristicTypeBuilder<>(this, template);
        characteristics.add(characteristic);
        clearCache();
        return characteristic;
    }

    /**
     * Returns a view of all characteristics added to the {@code AttributeType} to build.
     * The returned list is <cite>live</cite>: changes in this builder are reflected in that list and conversely.
     * However the returned list allows only {@linkplain List#remove(Object) remove} operations;
     * new characteristics can be added only by calls to one of the {@code set/addCharacteristic(…)} methods.
     *
     * @return a live list over the characteristics declared to this builder.
     *
     * @see #getCharacteristic(String)
     * @see #addCharacteristic(Class)
     * @see #addCharacteristic(DefaultAttributeType)
     * @see #setValidValues(Object...)
     * @see #setCRS(CoordinateReferenceSystem)
     */
    public List<CharacteristicTypeBuilder<?>> characteristics() {
        return new RemoveOnlyList<>(characteristics);
    }

    /**
     * Returns the roles that the attribute play in the pre-defined operations managed by {@code AttributeTypeBuilder}.
     * The set returned by this method is <cite>live</cite>: additions or removal on that set are reflected back on
     * this builder, and conversely.
     *
     * @return the roles that the attribute play in the pre-defined operations managed by {@code AttributeTypeBuilder}.
     */
    public Set<AttributeRole> roles() {
        return new SetOfUnknownSize<AttributeRole>() {
            @Override public Iterator<AttributeRole> iterator() {return new RoleIter();}
            @Override public boolean add(AttributeRole role)    {return addRole(role);}
        };
    }

    /**
     * The iterator returned by the {@link AttributeTypeBuilder#roles()} set.
     */
    private final class RoleIter extends AbstractIterator<AttributeRole> {
        /**
         * Index of the next {@code AttributeRole} to return.
         */
        private int index;

        /**
         * Prepares the next {@code AttributeRole} on which to iterate and returns
         * {@code true} if such {@code AttributeRole} has been found.
         */
        @Override
        @SuppressWarnings("fallthrough")
        public boolean hasNext() {
            if (next == null) {
                switch (index) {
                    case 0: {
                        if (isIdentifier) {
                            next = AttributeRole.IDENTIFIER_COMPONENT;
                            break;
                        }
                        index++;        // Fall through for testing the case for next 'index' value.
                    }
                    case 1: {
                        if (owner().defaultGeometry == AttributeTypeBuilder.this) {
                            next = AttributeRole.DEFAULT_GEOMETRY;
                            break;
                        }
                        index++;        // Fall through for testing the case for next 'index' value.
                    }
                    default: {
                        return false;
                    }
                }
                index++;
            }
            return true;
        }

        /**
         * Removes the element returned by the last {@link #next()} method.
         */
        @Override
        public void remove() {
            switch (index) {
                case 1: isIdentifier = false; break;
                case 2: owner().defaultGeometry = null; break;
                default: throw new IllegalStateException();
            }
        }
    }

    /**
     * Flags this attribute as an input of one of the pre-defined operations managed by {@code AttributeTypeBuilder}.
     * Invoking this method is equivalent to invoking <code>{@linkplain #roles()}.add(role)</code>.
     *
     * @param  role the role to add to the attribute (shall not be null).
     * @return {@code true} if the given role has been added to the attribute.
     */
    public boolean addRole(final AttributeRole role) {
        final FeatureTypeBuilder owner = owner();
        ensureNonNull("role", role);
        switch (role) {
            case IDENTIFIER_COMPONENT: {
                if (!isIdentifier) {
                    isIdentifier = true;
                    owner.identifierCount++;
                    owner.clearCache();         // The change does not impact this attribute itself.
                    return true;
                }
                break;
            }
            case DEFAULT_GEOMETRY: {
                if (owner.defaultGeometry != this) {
                    if (!Geometries.isKnownType(valueClass)) {
                        throw new IllegalStateException(errors().getString(Errors.Keys.UnsupportedImplementation_1, valueClass));
                    }
                    if (owner.defaultGeometry != null) {
                        throw new IllegalStateException(resources().getString(Resources.Keys.PropertyAlreadyExists_2,
                                owner.getDisplayName(), AttributeConvention.GEOMETRY_PROPERTY));
                    }
                    owner.defaultGeometry = this;
                    owner.clearCache();         // The change does not impact this attribute itself.
                    return true;
                }
                break;
            }
        }
        return false;
    }

    /**
     * Returns {@code true} if {@link AttributeRole#IDENTIFIER_COMPONENT} has been associated to this attribute.
     */
    @Override
    boolean isIdentifier() {
        return isIdentifier;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AttributeTypeBuilder<V> setDefinition(final CharSequence definition) {
        super.setDefinition(definition);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AttributeTypeBuilder<V> setDesignation(final CharSequence designation) {
        super.setDesignation(designation);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AttributeTypeBuilder<V> setDescription(final CharSequence description) {
        super.setDescription(description);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AttributeTypeBuilder<V> setDeprecated(final boolean deprecated) {
        super.setDeprecated(deprecated);
        return this;
    }

    /**
     * Appends a text inside the value returned by {@link #toString()}, before the closing bracket.
     */
    @Override
    final void toStringInternal(final StringBuilder buffer) {
        buffer.append(" : ").append(Classes.getShortName(valueClass));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void remove() {
        if (isIdentifier) {
            isIdentifier = false;
            owner().identifierCount--;      // Owner should never be null since we set 'isIdentifier' to false.
        }
        super.remove();
    }

    /**
     * Builds the attribute type from the information specified to this builder.
     * If a type has already been built and this builder state has not changed since the type creation,
     * then the previously created {@code AttributeType} instance is returned.
     *
     * <div class="note"><b>Example:</b>
     * the following lines of code add a "name" attribute to a "City" feature, then get the corresponding
     * {@code AttributeType<String>} instance. If no setter method is invoked on the builder of the "name"
     * attribute after those lines, then the {@code name} variable below will reference the same instance
     * than the "name" attribute in the {@code city} type.
     *
     * {@preformat java
     *   FeatureTypeBuilder builder = new FeatureTypeBuilder().setName("City");
     *   AttributeType<String> name = builder.addAttribute(String.class).setName("name").build();
     *   FeatureType city = builder.build();
     *
     *   assert city.getProperty("name") == name : "AttributeType instance should be the same.";
     * }
     *
     * Note that {@code city.getProperty("name")} returns {@code AttributeType<?>},
     * i.e. the {@linkplain #getValueClass() value class} is lost at compile-time.
     * By comparison, this {@code build()} method has a more accurate return type.
     * </div>
     *
     * <div class="warning"><b>Warning:</b> In a future SIS version, the return type may be changed to the
     * {@code org.opengis.feature.AttributeType} interface. This change is pending GeoAPI revision.</div>
     *
     * @return the attribute type.
     */
    @Override
    public DefaultAttributeType<V> build() {
        if (property == null) {
            final DefaultAttributeType<?>[] chrts = new DefaultAttributeType<?>[characteristics.size()];
            for (int i=0; i<chrts.length; i++) {
                chrts[i] = characteristics.get(i).build();
            }
            property = new DefaultAttributeType<>(identification(), valueClass, minimumOccurs, maximumOccurs, defaultValue, chrts);
        }
        return property;
    }
}
