/**
 * Copyright © CENRA (2016)
 *
 * remi.clement@espaces-naturels.fr
 *
 * This software is a multi-platform and portable application based on
 * the scientifical toolbox to monitor wetlands. With this toolbox it’s
 * possible to calculate indicators «RhoMéO» to monitor quality,
 * fonctionalities and urban and agricultural pressures of wetlands.
 * Indicators are calculated using naturalist data entered and/or imported,
 * and geographic data downloaded from a geographic server.
 *
 * This software is governed by the CeCILL license under French law and
 * abiding by the rules of distribution of free software.  You can  use,
 * modify and/ or redistribute the software under the terms of the CeCILL
 * license as circulated by CEA, CNRS and INRIA at the following URL
 * "http://www.cecill.info".
 *
 * As a counterpart to the access to the source code and  rights to copy,
 * modify and redistribute granted by the license, users are provided only
 * with a limited warranty  and the software's author,  the holder of the
 * economic rights,  and the successive licensors  have only  limited
 * liability.
 *
 * In this respect, the user's attention is drawn to the risks associated
 * with loading,  using,  modifying and/or developing or reproducing the
 * software by the user in light of its specific status of free software,
 * that may mean  that it is complicated to manipulate,  and  that  also
 * therefore means  that it is reserved for developers  and  experienced
 * professionals having in-depth computer knowledge. Users are therefore
 * encouraged to load and test the software's suitability as regards their
 * requirements in conditions enabling the security of their systems and/or
 * data to be ensured and,  more generally, to use and operate it in the
 * same conditions as regards security.
 *
 * The fact that you are presently reading this means that you have had
 * knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.core.util;

import fr.cenra.rhomeo.api.Version;
import fr.cenra.rhomeo.api.annotations.RefersTo;
import fr.cenra.rhomeo.api.data.DataContext;
import fr.cenra.rhomeo.api.data.Reference;
import fr.cenra.rhomeo.api.data.ReferenceDescription;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.RhomeoRuntimeException;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.data.ReferenceManager;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.logging.Level;
import javafx.beans.value.ObservableValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * Tries to find boundaries declared by the {@link RefersTo} annotation.
 *
 * @author Alexis Manin (Geomatys)
 */
@Component
public class RelationResolver {

    @Autowired
    private Session session;

    /**
     * Try to find all available objects of the type matching input annotation
     * referred class.
     *
     * @param reference The annotation describing class to get instances for.
     * @return A set of available values, or an empty set if we cannot find any.
     * Never null.
     * @throws IntrospectionException
     */
    public Set getTargetObjects(final RefersTo reference) throws IntrospectionException {
        final Class referedType = reference.type();
        Set result = Collections.EMPTY_SET;
        if (Reference.class.isAssignableFrom(referedType)) {
            ReferenceDescription desc = session.getDescription(referedType);
            if (desc != null)
                result = getReferences(desc);

        } else if (ReferenceDescription.class.isAssignableFrom(referedType)) {
            final ReferenceDescription desc = (ReferenceDescription) session.getBean(referedType);
            if (desc != null)
                result = getReferences(desc);

        } else if (Enum.class.isAssignableFrom(referedType)) {
            result = new LinkedHashSet(Arrays.asList(referedType.getEnumConstants()));

        } else {
            // TODO : try to acquire a repository.
        }

        if (!result.isEmpty()) {
            final Class<? extends Predicate> filter = reference.filterClass();
            try {
                if (filter != null && !filter.isInterface() && !Modifier.isAbstract(filter.getModifiers())) {
                    final Predicate p = filter.newInstance();
                    result.removeIf(p.negate());
                }
            } catch (Exception e) {
                RhomeoCore.LOGGER.log(Level.WARNING, "Cannot filter objects !", e);
            }
        }
        return result;
    }

    /**
     * Try to find all possible values for the property described by input
     * annotation.
     *
     * @param reference An annotation describing a property of a specific class.
     * @return A set of possible values for the described property. Never null,
     * but can be empty.
     * @throws IntrospectionException
     * @throws NoSuchMethodException
     */
    public Set getPossibleValues(final RefersTo reference) throws IntrospectionException, NoSuchMethodException {
        final Function extractor = acquireExtractor(reference);

        final Set objects = getTargetObjects(reference);
        if (objects.isEmpty()) {
            return Collections.EMPTY_SET;
        }

        final LinkedHashSet result = new LinkedHashSet(objects.size());
        for (final Object o : objects) {
            result.add(extractor.apply(o));
        }

        return result;
    }

    /**
     * Find objects of type specified by given annotation, whose property quoted
     * the same annotation is equal to given object.
     *
     * @param reference The reference annotation giving target type and
     * property.
     * @param sourceValue Value of the property to compare to available
     * referenced objects.
     * @return A set of object whose property described in input annotation is
     * equal to given object.
     * @throws IntrospectionException If we cannot analyze input refered type.
     * @throws NoSuchMethodException If we cannot acquire a getter for refered
     * property.
     */
    public Set getMatchingObjects(final RefersTo reference, final Object sourceValue) throws IntrospectionException, NoSuchMethodException {
        Function extractor = acquireExtractor(reference);

        final HashSet result = new HashSet();
        final Set possibleObjects = getTargetObjects(reference);
        Object extracted;
        for (final Object o : possibleObjects) {
            extracted = extractor.apply(o);
            if (Objects.equals(sourceValue, extracted)) {
                result.add(o);
            }
        }

        return result;
    }

    /**
     * Try to find loaded values for the given reference type.
     *
     * IMPORTANT : This method won't try to load any data. It's the caller job
     * to ensure that queried reference has already been loaded using
     * {@link ReferenceManager} mechanism.
     *
     * @param <T> Type of reference object to get values for.
     * @param desc Descriptor of the wanted type.
     * @return Available values for given reference type.
     * @throws IntrospectionException If we cannot analyze reference type.
     */
    private <T extends Reference> Set<T> getReferences(final ReferenceDescription desc) throws IntrospectionException {
        final DataContext dataContext = session.getDataContext();
        if (dataContext != null) {
            Version refVersion = dataContext.getReferences().get(desc.getReferenceType());
            if (refVersion != null) {
                return new LinkedHashSet<>(ReferenceManager.getOrCreate(desc).getValues(refVersion));
            }
        }
        return Collections.EMPTY_SET;
    }

    /**
     * Create a function to get value of the property referenced by input
     * annotation from an object of the class referenced by input annotation.
     *
     * @param reference The annotation containing target class and property
     * information
     * @return A function, never null.
     * @throws IntrospectionException If the class referenced by input
     * annotation cannot be analyzed.
     * @throws NoSuchMethodException If we cannot find any getter in the
     * referenced class for the wanted property.
     */
    public static Function acquireExtractor(final RefersTo reference) throws IntrospectionException, NoSuchMethodException {
        String pName = reference.property();
        if ((pName == null || (pName = pName.trim()).isEmpty())) {
            throw new IllegalArgumentException("Given reference annotation does not refer to any property !");
        }

        final Class referedType = reference.type();
        final BeanInfo beanInfo = Introspector.getBeanInfo(referedType);
        Function extractor = null;
        for (final PropertyDescriptor d : beanInfo.getPropertyDescriptors()) {
            if (d.getName().equals(pName)) {
                final Method getter = d.getReadMethod();
                if (getter != null) {
                    getter.setAccessible(true);
                    extractor = input -> {
                        try {
                            return getter.invoke(input);
                        } catch (ReflectiveOperationException ex) {
                            throw new RhomeoRuntimeException(ex);
                        }
                    };
                }
                break;
            }
        }

        if (extractor == null) {
            final Method pGetter = referedType.getMethod(pName.concat("Property"));
            if (pGetter != null && ObservableValue.class.isAssignableFrom(pGetter.getReturnType())) {
                pGetter.setAccessible(true);
                extractor = input -> {
                    try {
                        return ((ObservableValue) pGetter.invoke(input)).getValue();
                    } catch (ReflectiveOperationException ex) {
                        throw new RhomeoRuntimeException(ex);
                    }
                };
            }
        }

        return extractor;
    }
}
