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

import java.io.Serializable;
import org.apache.sis.util.ObjectConverter;


/**
 * Holds explicit {@link #sourceClass} and {@link #targetClass} values. Used as key in a hash
 * map of converters. Also used as the base class for converters defined in this package.
 *
 * <p>The only direct subtype allowed is {@link SystemConverter}.
 * <strong>No other direct subtype shall exist</strong>.
 * See {@link #equals(Object)} for an explanation.</p>
 *
 * <h2>Immutability and thread safety</h2>
 * This base class is immutable and thus inherently thread-safe. {@code ClassPair} immutability is necessary
 * for {@link ConverterRegistry}. Subclasses should also be immutable, but this requirement is not as strong
 * as for {@code ClassPair} (because subclasses are not used as keys in hash map).
 *
 * @author  Martin Desruisseaux (Geomatys)
 *
 * @param <S>  the base type of source objects.
 * @param <T>  the base type of converted objects.
 */
class ClassPair<S,T> implements Serializable {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = -5214470401299470687L;

    /**
     * The source class.
     */
    final Class<S> sourceClass;

    /**
     * The target class.
     */
    final Class<T> targetClass;

    /**
     * Creates an entry for the given source and target classes.
     *
     * @param  sourceClass  the {@linkplain ObjectConverter#getSourceClass() source class}.
     * @param  targetClass  the {@linkplain ObjectConverter#getTargetClass() target class}.
     */
    ClassPair(final Class<S> sourceClass, final Class<T> targetClass) {
        this.sourceClass = sourceClass;
        this.targetClass = targetClass;
    }

    /**
     * Returns a key for the parent source, or {@code null} if none.
     * This method applies the following rules:
     *
     * <ul>
     *   <li>If {@link #sourceClass} is a class and have a parent class, then returns a new
     *       {@code ClassPair} having that parent class as the source.</li>
     *   <li>Otherwise if {@link #sourceClass} is an interface extending at least one interface,
     *       then returns a new {@code ClassPair} having the first parent interface as the source.
     *       we select the first interface on the assumption that it is the mean one.</li>
     *   <li>Otherwise (i.e. if there is no parent class or interface), returns {@code null}.</li>
     * </ul>
     *
     * The target class is left unchanged.
     *
     * @return a key for the parent source, or {@code null}.
     */
    final ClassPair<? super S, T> parentSource() {
        final Class<? super S> source;
        if (sourceClass.isInterface()) {
            @SuppressWarnings({"unchecked","rawtypes"})
            final Class<? super S>[] interfaces = (Class[]) sourceClass.getInterfaces();
            if (interfaces.length == 0) {
                return null;
            }
            source = interfaces[0];         // Take only the first interface declaration; ignore others.
        } else {
            source = sourceClass.getSuperclass();
            if (source == null) {
                return null;
            }
        }
        return new ClassPair<>(source, targetClass);
    }

    /**
     * Casts the given converter to the source and target classes of this {@code ClassPair}.
     * This method is not public because the checks are performed using assertions only.
     * If this method was to goes public, the assertions would need to be replaced by
     * unconditional checks.
     *
     * <p>This method is used by {@link ConverterRegistry} after fetching a value from a hash
     * map using this {@code ClassPair} as a key. In this context, the cast should never fail
     * (assuming that the converters do not change their source and target classes).</p>
     */
    @SuppressWarnings("unchecked")
    final ObjectConverter<? super S, ? extends T> cast(final ObjectConverter<?,?> converter) {
        if (converter != null) {
            assert converter.getSourceClass().isAssignableFrom(sourceClass) : sourceClass;
            assert targetClass.isAssignableFrom(converter.getTargetClass()) : targetClass;
        }
        return (ObjectConverter<? super S, ? extends T>) converter;
    }

    /**
     * Compares the given object with this {@code ClassPair} for equality. Two {@code ClassPair}
     * instances are considered equal if they have the same source and target classes, ignoring
     * all other properties eventually defined in subclasses.
     *
     * <p>This method is designed for use by {@link ConverterRegistry} as {@link java.util.HashMap}
     * keys. Its primary purpose is <strong>not</strong> to determine if two objects are converters
     * doing the same conversions. However, the {@link SystemConverter} subclass overrides this
     * method with an additional safety check.</p>
     *
     * @param  other  the object to compare with this {@code ClassPair}.
     * @return {@code true} if the given object is a {@code ClassPair}
     *         having the same source and target classes.
     */
    @Override
    public boolean equals(final Object other) {
        if (other instanceof ClassPair<?,?>) {
            final ClassPair<?,?> that = (ClassPair<?,?>) other;
            return sourceClass == that.sourceClass &&
                   targetClass == that.targetClass;
        }
        return false;
    }

    /**
     * Returns a hash code value for this {@code ClassPair}.
     * See {@link #equals(Object)} javadoc for information on the scope of this method.
     */
    @Override
    public final int hashCode() {
        return sourceClass.hashCode() + 31*targetClass.hashCode();
    }

    /**
     * Returns a string representation for this entry.
     * Used for formatting error messages.
     */
    @Override
    public String toString() {
        return targetClass.getSimpleName() + " ← " + sourceClass.getSimpleName();
    }
}
