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

import java.util.Locale;
import java.util.Collection;
import java.util.Collections;
import java.lang.reflect.Method;
import org.opengis.annotation.UML;
import org.opengis.metadata.Datatype;
import org.opengis.metadata.ExtendedElementInformation;
import org.opengis.metadata.citation.Citation;
import org.opengis.util.CodeList;
import org.opengis.util.InternationalString;
import org.apache.sis.metadata.simple.SimpleIdentifier;
import org.apache.sis.measure.ValueRange;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.iso.Types;
import org.apache.sis.util.collection.CheckedContainer;
import org.apache.sis.util.logging.Logging;

// Specific to the main and geoapi-3.1 branches:
import org.opengis.metadata.Obligation;
import org.opengis.metadata.citation.ResponsibleParty;


/**
 * Description of a metadata property inferred from Java reflection.
 * For a given metadata instances (typically an {@link AbstractMetadata} subclasses,
 * but other types are allowed), instances of {@code PropertyInformation} are obtained
 * indirectly by the {@link MetadataStandard#asInformationMap(Class, KeyNamePolicy)} method.
 *
 * <p>This class implements also the {@link org.opengis.metadata.Identifier} and {@link CheckedContainer} interfaces.
 * Those features are not directly used by this class, but is published in the {@link MetadataStandard} javadoc.</p>
 *
 * <h2>API note</h2>
 * The rational for implementing {@code CheckedContainer} is to consider each {@code ExtendedElementInformation}
 * instance as the set of all possible values for the property. If the information had a {@code contains(E)} method,
 * it would return {@code true} if the given value is valid for that property.
 *
 * <h2>Immutability and thread safety</h2>
 * This final class is immutable and thus thread-safe.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.4
 *
 * @param <E>  the value type, either the method return type if not a collection,
 *             or the type of elements in the collection otherwise.
 *
 * @see InformationMap
 * @see MetadataStandard#asInformationMap(Class, KeyNamePolicy)
 *
 * @since 0.3
 */
final class PropertyInformation<E> extends SimpleIdentifier           // Implementing Identifier is part of SIS public API.
        implements ExtendedElementInformation, CheckedContainer<E>
{
    /**
     * For cross-versions compatibility.
     */
    private static final long serialVersionUID = 6279709738674566891L;

    /**
     * The interface which contain this property.
     *
     * @see #getParentEntity()
     */
    private final Class<?> parent;

    /**
     * The value type, either the method return type if not a collection,
     * or the type of elements in the collection otherwise.
     *
     * @see #getDataType()
     * @see #getElementType()
     */
    private final Class<E> elementType;

    /**
     * The minimum number of occurrences.
     * A {@code minimumOccurs} value of -1 means that the property is conditional,
     * i.e. the actual {@code minimumOccurs} value can either 0 or 1 depending on
     * the value of another property.
     *
     * @see #getObligation()
     */
    private final byte minimumOccurs;

    /**
     * The maximum number of occurrences as an unsigned number.
     * Value 255 (or -1 as a signed number) shall be understood as {@link Integer#MAX_VALUE}.
     *
     * @see #getMaximumOccurrence()
     */
    private final byte maximumOccurs;

    /**
     * The domain of valid values, or {@code null} if none. If non-null, then this is set to an
     * instance of {@link ValueRange} at construction time, then replaced by an instance of
     * {@link DomainRange} when first needed by the {@link #getDomainValue()} method.
     *
     * @see #getDomainValue()
     */
    @SuppressWarnings("serial")         // Not statically typed as Serializable.
    private volatile Object domainValue;

    /**
     * Creates a new {@code PropertyInformation} instance from the annotations on the given getter method.
     *
     * @param  standard     the international standard that define the property, or {@code null} if none.
     * @param  property     the property name as defined by the international {@code standard}.
     * @param  getter       the getter method defined in the interface.
     * @param  elementType  the value type, either the method return type if not a collection,
     *                      or the type of elements in the collection otherwise.
     * @param  range        the range of valid values, or {@code null} if none. This information is associated to the
     *                      implementation method rather than the interface one, because it is specific to SIS.
     */
    @SuppressWarnings({"unchecked","rawtypes"})
    PropertyInformation(final Citation standard, final String property, final Method getter,
                        final Class<E> elementType, final ValueRange range)
    {
        super(standard, property, getter.isAnnotationPresent(Deprecated.class));
        parent = getter.getDeclaringClass();
        this.elementType = elementType;
        final UML uml = getter.getAnnotation(UML.class);
        byte minimumOccurs = 0;
        byte maximumOccurs = 1;
        if (uml != null) {
            switch (uml.obligation()) {
                case MANDATORY:   minimumOccurs =  1; break;
                case FORBIDDEN:   maximumOccurs =  0; break;
                case CONDITIONAL: minimumOccurs = -1; break;
            }
        }
        if (maximumOccurs != 0) {
            final Class<?> c = getter.getReturnType();
            if (c.isArray() || Collection.class.isAssignableFrom(c)) {
                maximumOccurs = -1;
            }
        }
        this.minimumOccurs = minimumOccurs;
        this.maximumOccurs = maximumOccurs;
        this.domainValue   = range;
    }

    /**
     * Returns the primary name by which this metadata element is identified.
     */
    @Override
    public String getName() {
        return code;
    }

    /**
     * Returns the ISO name of the class containing the property,
     * or the simple class name if the ISO name is undefined.
     *
     * @see #getParentEntity()
     */
    @Override
    public final String getCodeSpace() {
        String codespace = Types.getStandardName(parent);
        if (codespace == null) {
            codespace = parent.getSimpleName();
        }
        return codespace;
    }

    /**
     * Unconditionally returns {@code null}.
     *
     * @deprecated This property was defined in the 2003 edition of ISO 19115,
     *             but has been removed in the 2014 edition.
     */
    @Override
    @Deprecated
    public String getShortName() {
        return null;
    }

    /**
     * Unconditionally returns {@code null}.
     *
     * @deprecated This property was defined in the 2003 edition of ISO 19115,
     *             but has been removed in the 2014 edition.
     */
    @Override
    @Deprecated
    public Integer getDomainCode() {
        return null;
    }

    /**
     * Returns the definition of this property, or {@code null} if none.
     */
    @Override
    public final InternationalString getDefinition() {
        return Types.getDescription(parent, code);
    }

    /**
     * Returns the obligation of the element.
     */
    @Override
    public Obligation getObligation() {
        switch (minimumOccurs) {
            case -1: return Obligation.CONDITIONAL;
            case  0: return Obligation.OPTIONAL;
            default: return Obligation.MANDATORY;
        }
    }

    /**
     * Returns the condition under which the extended element is mandatory.
     * Current implementation always return {@code null}, since the condition
     * is not yet documented programmatically.
     */
    @Override
    public InternationalString getCondition() {
        return null;
    }

    /**
     * Returns the kind of value provided in the extended element.
     * This is a generic code that describe the element type.
     * For more accurate information, see {@link #getElementType()}.
     */
    @Override
    public Datatype getDataType() {
        if (CharSequence.class.isAssignableFrom(elementType)) return Datatype.CHARACTER_STRING;
        if (CodeList    .class.isAssignableFrom(elementType)) return Datatype.CODE_LIST;
        if (Enum        .class.isAssignableFrom(elementType)) return Datatype.ENUMERATION;
        if (Numbers.isInteger(elementType)) {
            return Datatype.INTEGER;
        }
        // TODO: check the org.opengis.annotation.Classifier annotation here.
        return Datatype.TYPE_CLASS;
    }

    /**
     * Returns the case type of values to be stored in the property.
     * If the property type is an array or a collection, then this method
     * returns the type of elements in the array or collection.
     *
     * @see TypeValuePolicy#ELEMENT_TYPE
     */
    @Override
    public Class<E> getElementType() {
        return elementType;
    }

    /**
     * Returns the maximum number of times that values are required.
     * This method returns 0 if the property is forbidden, {@link Integer#MAX_VALUE}
     * if the property is an array or a collection, or 1 otherwise.
     */
    @Override
    public Integer getMaximumOccurrence() {
        final int n = Byte.toUnsignedInt(maximumOccurs);
        return (n == 0xFF) ? Integer.MAX_VALUE : n;
    }

    /**
     * Returns valid values that can be assigned to the extended element, or {@code null} if none.
     * In the particular case of SIS implementation, this method may return a subclass of
     * {@link org.apache.sis.measure.NumberRange}.
     */
    @Override
    @SuppressWarnings({"unchecked","rawtypes"})
    public InternationalString getDomainValue() {
        Object domain = domainValue;
        if (domain != null) {
            if (!(domain instanceof DomainRange)) {
                try {
                    // Not a big deal if we create two instances of that in two concurrent threads.
                    domain = new DomainRange(elementType, (ValueRange) domain);
                } catch (IllegalArgumentException e) {
                    /*
                     * May happen only if a ValueRange annotation is applied on the wrong method.
                     * The JUnit tests ensure that this never happen at least for the SIS metadata
                     * implementation. If this error happen anyway, the user probably doesn't expect
                     * to have an IllegalArgumentException while he didn't provided any argument.
                     * Returning null as a fallback is compliant with the method contract.
                     */
                    Logging.unexpectedException(StandardImplementation.LOGGER, PropertyInformation.class, "getDomainValue", e);
                    domain = null;
                }
                domainValue = domain;
            }
        }
        return (DomainRange) domain;
    }

    /**
     * Returns the name of the metadata entity under which this metadata element may appear.
     * The name may be standard metadata element or other extended metadata element.
     *
     * @see #getCodeSpace()
     */
    @Override
    public Collection<String> getParentEntity() {
        return Collections.singleton(getCodeSpace());
    }

    /**
     * Specifies how the extended element relates to other existing elements and entities.
     * The current implementation always return {@code null}.
     */
    @Override
    public InternationalString getRule() {
        return null;
    }

    /**
     * Unconditionally returns {@code null}.
     */
    public InternationalString getRationale() {
        return null;
    }

    /**
     * Unconditionally returns an empty list.
     */
    @Override
    @Deprecated
    public Collection<InternationalString> getRationales() {
        return Collections.emptyList();
    }

    /**
     * Returns the name of the person or organization creating the element.
     */
    @Override
    public Collection<? extends ResponsibleParty> getSources() {
        return authority.getCitedResponsibleParties();
    }

    /**
     * Compares the given object with this element information for equality.
     *
     * @param  obj  the object to compare with this element information for equality.
     * @return {@code true} if both objects are equal.
     */
    @Override
    public boolean equals(final Object obj) {
        if (obj == this) {
            return true;
        }
        if (super.equals(obj)) {
            final PropertyInformation<?> that = (PropertyInformation<?>) obj;
            return this.parent        == that.parent        &&
                   this.elementType   == that.elementType   &&
                   this.minimumOccurs == that.minimumOccurs &&
                   this.maximumOccurs == that.maximumOccurs;

        }
        return false;
    }

    /**
     * Computes a hash code value only from the code space and property name.
     * We don't need to use the other properties, because the fully qualified
     * property name should be a sufficient discriminator.
     */
    @Override
    public final int hashCode() {
        return (parent.hashCode() + 31 * code.hashCode()) ^ (int) serialVersionUID;
    }

    /**
     * Invoked by {@link #toString()} in order to append additional information after the identifier.
     */
    @Override
    protected void appendStringTo(final StringBuilder buffer) {
        buffer.append(" : ").append(Types.getCodeLabel(getDataType()))
              .append(", ").append(getObligation().name().toLowerCase(Locale.US))
              .append(", maxOccurs=");
        final int n = getMaximumOccurrence();
        if (n != Integer.MAX_VALUE) {
            buffer.append(n);
        } else {
            buffer.append('∞');
        }
        final InternationalString domainValue = getDomainValue();
        if (domainValue != null) {
            buffer.append(", domain=").append(domainValue);
        }
    }
}
