/*
 * 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.Map;
import java.util.Collection;
import java.lang.reflect.Array;
import org.apache.sis.xml.NilObject;
import org.apache.sis.xml.NilReason;


/**
 * Whatever {@link MetadataStandard#asValueMap MetadataStandard.asValueMap(…)} shall contain entries for null,
 * {@linkplain org.apache.sis.xml.NilObject nil} or empty values. By default the value map does not provide
 * {@linkplain java.util.Map.Entry entries} for {@code null} metadata properties, nil objects or
 * {@linkplain java.util.Collection#isEmpty() empty collections}.
 * This enumeration allows to control this behavior.
 *
 * <h2>Difference between null and nil</h2>
 * A null property is a reference which is {@code null} in the Java sense.
 * Null references can be used for missing properties when no information is provided about why the property is missing.
 * On the other hand, a nil object is a placeholder for a missing property similar in purpose to {@code null} references,
 * except that an explanation about why the property is missing can be attached to those objects.
 * Those explanations can be obtained by calls to the {@link org.apache.sis.xml.NilReason#forObject(Object)} method.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 0.8
 *
 * @see MetadataStandard#asValueMap(Object, Class, KeyNamePolicy, ValueExistencePolicy)
 *
 * @since 0.3
 */
public enum ValueExistencePolicy {
    /*
     * Implementation note: enumeration order matter.
     * The `acceptNilValues()` method relies on it.
     */

    /**
     * Includes all entries in the map, including those having a null value or an empty collection.
     */
    ALL() {
        /** Never skip values. */
        @Override boolean isSkipped(final Object value) {
            return false;
        }

        /** Substitutes null or empty collections by a null singleton element
            in order to make the property visible in {@code TreeNode}. */
        @Override boolean substituteByNullElement(final Collection<?> values) {
            return (values == null) || values.isEmpty();
        }
    },

    /**
     * Includes only the non-null properties.
     * {@link org.apache.sis.xml.NilObject}s are included.
     * Collections are included no matter if they are empty or not.
     *
     * <p>The set of {@code NON_NULL} properties is a subset of {@link #ALL} properties.</p>
     */
    NON_NULL() {
        /** Skips all null values. */
        @Override boolean isSkipped(final Object value) {
            return (value == null);
        }

        /** Substitutes empty collections by a null singleton element, but not
            null references since they are supposed to be skipped by this policy. */
        @Override boolean substituteByNullElement(final Collection<?> values) {
            return (values != null) && values.isEmpty();
        }
    },

    /**
     * Includes only the non-null and non-nil properties.
     * Collections are included no matter if they are empty or not.
     *
     * <p>The set of {@code NON_NIL} properties is a subset of {@link #NON_NULL} properties.</p>
     *
     * @since 0.4
     */
    NON_NIL() {
        /** Skips all null or nil values. */
        @Override boolean isSkipped(final Object value) {
            return (value == null) || (value instanceof NilObject) || NilReason.forObject(value) != null;
        }

        /** Substitutes empty collections by a null singleton element, but not
            null references since they are supposed to be skipped by this policy. */
        @Override boolean substituteByNullElement(final Collection<?> values) {
            return (values != null) && values.isEmpty();
        }
    },

    /**
     * Includes only the properties that are non-null, non-nil and non empty.
     * A non-null and non-nil property is considered empty in any of the following cases:
     *
     * <ul>
     *   <li>It is a character sequence containing only {@linkplain Character#isWhitespace(int) whitespaces}.</li>
     *   <li>It is an {@linkplain Collection#isEmpty() empty collection}.</li>
     *   <li>It is an {@linkplain Map#isEmpty() empty map}.</li>
     *   <li>It is an empty array (of length 0).</li>
     * </ul>
     *
     * This is the default behavior of {@link AbstractMetadata#asMap()}.
     *
     * <p>The set of {@code NON_EMPTY} properties is a subset of {@link #NON_NIL} properties.</p>
     */
    NON_EMPTY() {
        /** Skips all null or empty values. */
        @Override boolean isSkipped(final Object value) {
            return isNullOrEmpty(value);
        }

        /** Never substitute null or empty collections since they should be skipped. */
        @Override boolean substituteByNullElement(final Collection<?> values) {
            return false;
        }
    },

    /**
     * Includes non-empty properties but omits {@linkplain TitleProperty title properties}.
     * Values associated to title properties are instead associated with the parent node.
     * This policy is relevant for metadata classes annotated with {@link TitleProperty};
     * for all other classes, this policy is identical to {@link #NON_EMPTY}.
     *
     * <h4>Example</h4>
     * the {@link org.apache.sis.metadata.iso.citation.DefaultCitation} and
     * {@link org.apache.sis.metadata.iso.citation.DefaultCitationDate} classes are annotated with
     * <code>&#64;TitleProperty(name="title")</code> and <code>&#64;TitleProperty(name="date")</code>
     * respectively. The following table compares the trees produced by two policies:
     *
     * <table class="sis">
     *   <caption>Comparison of "non-empty" and "compact" policy on the same metadata</caption>
     *   <tr>
     *     <th>{@code NON_EMPTY}</th>
     *     <th class="sep">{@code COMPACT}</th>
     *   </tr><tr><td>
     *     <pre class="text">
     * Citation
     *  ├─Title……………………… My document
     *  └─Date
     *     ├─Date………………… 2012/01/01
     *     └─Date type…… Creation</pre>
     *   </td><td class="sep">
     *     <pre class="text">
     * Citation……………………… My document
     *  └─Date………………………… 2012/01/01
     *     └─Date type…… Creation</pre>
     *   </td></tr>
     * </table>
     *
     * This policy is the default behavior of {@link AbstractMetadata#asTreeTable()},
     * and consequently defines the default rendering of {@link AbstractMetadata#toString()}.
     *
     * @see TitleProperty
     *
     * @since 0.8
     */
    COMPACT() {
        /** Skips all null or empty values. */
        @Override boolean isSkipped(final Object value) {
            return isNullOrEmpty(value);
        }

        /** Never substitute null or empty collections since they should be skipped. */
        @Override boolean substituteByNullElement(final Collection<?> values) {
            return false;
        }
    };

    /**
     * {@return whether this policy accepts nil values}.
     */
    final boolean acceptNilValues() {
        return ordinal() < NON_NIL.ordinal();
    }

    /**
     * Returns {@code true} if the given value shall be skipped for this policy.
     */
    abstract boolean isSkipped(Object value);

    /**
     * Returns {@code true} if {@link TreeNode} shall substitute the given collection by
     * a singleton containing only a null element.
     *
     * <h4>Purpose</h4>
     * When a collection is null or empty, while not excluded according this {@code ValueExistencePolicy},
     * we need an empty space for making the metadata property visible in {@code TreeNode}.
     */
    abstract boolean substituteByNullElement(Collection<?> values);

    /**
     * Returns {@code true} if the specified object is null or an empty collection, array or string.
     *
     * <p>This method intentionally does not inspect array or collection elements, since this method
     * is invoked from methods doing shallow copy or comparison. If we were inspecting elements,
     * we would need to add a check against infinite recursion.</p>
     *
     * <p>This method does not check for the {@link org.apache.sis.util.Emptiable} interface because
     * the {@code isEmpty()} method may be costly (for example {@link AbstractMetadata#isEmpty()}
     * iterates over all the metadata tree). Instead, the check for {@code Emptiable} will be done
     * explicitly by the caller when appropriate.</p>
     */
    static boolean isNullOrEmpty(final Object value) {
        if (value == null)                  return true;
        if (value instanceof NilObject)     return true;
        if (value instanceof CharSequence)  return isEmpty((CharSequence) value);
        if (value instanceof Collection<?>) return ((Collection<?>) value).isEmpty();
        if (value instanceof Map<?,?>)      return ((Map<?,?>) value).isEmpty();
        if (value.getClass().isArray())     return Array.getLength(value) == 0;
        return NilReason.forObject(value) != null;
    }

    /**
     * Returns {@code true} if the given character sequence shall be considered empty.
     * The current implementation returns {@code true} if the sequence contains only
     * whitespaces in the sense of Java (i.e. ignoring line feeds, but not ignoring
     * non-breaking spaces). The exact criterion is not a committed part of the API
     * and may change in future SIS versions according experiences.
     */
    private static boolean isEmpty(final CharSequence value) {
        final int length = value.length();
        for (int i=0; i<length;) {
            final int c = Character.codePointAt(value, i);
            if (!Character.isWhitespace(c)) {
                return false;
            }
            i += Character.charCount(c);
        }
        return true;
    }
}
