/*******************************************************************************
 * Copyright (c) 2016 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation
 ******************************************************************************/
package org.eclipse.persistence.indirection;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collection;
import java.util.Map;

import org.eclipse.persistence.config.SystemProperties;
import org.eclipse.persistence.internal.helper.JavaSEPlatform;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedClassForName;
import org.eclipse.persistence.internal.security.PrivilegedGetSystemProperty;
import org.eclipse.persistence.logging.AbstractSessionLog;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.logging.SessionLogEntry;

/**
 * Provides factory methods to create JDK specific implementation
 * of particular type of {@link IndirectCollection}.
 *
 * @author Lukas Jungmann
 * @see IndirectCollection
 * @see IndirectList
 * @see IndirectMap
 * @see IndirectSet
 * @since EclipseLink 2.6.0
 */
public final class IndirectCollectionsFactory {

    //loaded using reflection to avoid runtime (and compile time) dependency on JDK 8
    private static final String JDK8_SUPPORT_PROVIDER = "org.eclipse.persistence.internal.indirection.jdk8.IndirectCollectionsProvider";

    private static final IndirectCollectionsProvider provider = getProvider();

    /**
     * Class implementing {@link IndirectList}.
     */
    public static final Class IndirectList_Class = provider.getListClass();

    /**
     * Class implementing {@link IndirectSet}.
     */
    public static final Class IndirectSet_Class = provider.getSetClass();

    /**
     * Class implementing {@link IndirectMap}.
     */
    public static final Class IndirectMap_Class = provider.getMapClass();

    /**
     * Construct an empty {@link IndirectList} with the default initial capacity (10)
     * and default capacity increment (0).
     *
     * @param <E> the class of the objects in the list
     * @return an empty {@link IndirectList} with the default initial capacity
     *      and default capacity increment
     */
    public static <E> IndirectList<E> createIndirectList() {
        return provider.createIndirectList(10, 0);
    }

    /**
     * Construct an empty {@link IndirectList} with the specified initial capacity
     * and default capacity increment (0).
     *
     * @param <E> the class of the objects in the list
     * @param initialCapacity the initial capacity of the vector
     *
     * @return an empty {@link IndirectList} with the specified initial capacity
     *      and default capacity increment
     * @throws IllegalArgumentException if the specified initial capacity
     *      is negative
     */
    public static <E> IndirectList<E> createIndirectList(int initialCapacity) {
        return provider.createIndirectList(initialCapacity, 0);
    }

    /**
     * Construct an {@link IndirectList} containing the elements of the specified
     * collection, in the order they are returned by the collection's iterator.
     *
     * @param <E> the class of the objects in the list
     * @param collection a collection containing the elements to construct
     *      the {@link IndirectList} with.
     * @return an {@link IndirectList} containing the elements of the specified
     *      collection
     */
    public static <E> IndirectList<E> createIndirectList(Collection<? extends E> collection) {
        return  provider.createIndirectList(collection);
    }

    /**
     * Construct an empty {@link IndirectSet} with the default initial capacity (10)
     * and the default load factor (0.75).
     *
     * @return an empty {@link IndirectSet} with the default initial capacity
     *      and the default load factor
     */
    public static <E> IndirectSet<E> createIndirectSet() {
        return provider.createIndirectSet(10, 0.75f);
    }

    /**
     * Construct an empty {@link IndirectSet} with the specified initial capacity
     * and the default load factor (0.75).
     *
     * @param initialCapacity the initial capacity of the set
     *
     * @return an empty {@link IndirectSet} with the specified initial capacity
     *      and the default load factor
     * @throws IllegalArgumentException if the specified initial capacity is negative
     */
    public static <E> IndirectSet<E> createIndirectSet(int initialCapacity) {
        return provider.createIndirectSet(initialCapacity, 0.75f);
    }

    /**
     * Constructs an {@link IndirectSet} containing the elements of the specified
     * collection.
     *
     * @param collection a collection containing the elements to construct
     *      the {@link IndirectSet} with
     *
     * @return an {@link IndirectSet} containing the elements of the specified collection
     * @throws NullPointerException if the specified collection is null
     */
    public static <E> IndirectSet<E> createIndirectSet(Collection<? extends E> collection) {
        return provider.createIndirectSet(collection);
    }

    /**
     * Construct a new, empty {@link IndirectMap} with the default initial
     * capacity (11) and the default load factor (0.75).
     *
     * @return a new, empty {@link IndirectMap} with the default initial
     *      capacity and the default load factor
     */
    public static <K, V> IndirectMap<K, V> createIndirectMap() {
        return provider.createIndirectMap(11, 0.75f);
    }

    /**
     * Construct a new, empty {@link IndirectMap} with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param initialCapacity the initial capacity of the {@link IndirectMap}
     *
     * @return a new, empty {@link IndirectMap} with the specified initial
     *      capacity and the default load factor
     * @throws  IllegalArgumentException  if the initial capacity is less than
     *      or equal to zero
     */
    public static <K, V> IndirectMap<K, V> createIndirectMap(int initialCapacity) {
        return provider.createIndirectMap(initialCapacity, 0.75f);
    }

    /**
     * Construct a new {@link IndirectMap} with the same mappings as the given Map.
     * The {@link IndirectMap} is created with a capacity of twice the number of entries
     * in the given Map or 11 (whichever is greater), and a default load factor, which is 0.75.
     *
     * @param map the map whose mappings are to be placed into created {@link IndirectMap}
     *
     * @return a new {@link IndirectMap} with the same mappings as the given Map
     * @throws NullPointerException if the specified map is null
     */
    public static <K, V> IndirectMap<K, V> createIndirectMap(Map<? extends K, ? extends V> map) {
        return provider.createIndirectMap(map);
    }

    /**
     * As of EclipseLink 2.6.0 this returns Java SE 7- compatible provider by default
     * on Java SE 7 and Java SE 8+ compatible provider if Java SE 8+ is detected.
     *
     * @return default provider responsible for creating Java SE specific implementations
     * of {@link IndirectCollection}s
     */
    private static IndirectCollectionsProvider getProvider() {
        // Bug 485585 - specify the instantiation of the JDK 1.7 API-specific indirect collection classes at runtime 
        // through the use of the (deprecated) SystemProperties.JAVASE7_INDIRECT_COLLECTIONS property.
        String propertyValue = null;
        if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()) {
            propertyValue = AccessController.doPrivileged(new PrivilegedGetSystemProperty(SystemProperties.JAVASE7_INDIRECT_COLLECTIONS));
        } else {
            propertyValue = System.getProperty(SystemProperties.JAVASE7_INDIRECT_COLLECTIONS);
        }
        boolean useJ2SE7IndirectCollections = Boolean.valueOf(propertyValue);
        
        //try this on JDK 8+ only (see bug 464096)
        if (!useJ2SE7IndirectCollections && JavaSEPlatform.CURRENT.atLeast(JavaSEPlatform.v1_8)) {
            try {
                if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()) {
                    final Class support = AccessController.doPrivileged(new PrivilegedClassForName(JDK8_SUPPORT_PROVIDER, true, IndirectCollectionsFactory.class.getClassLoader()));
                    return AccessController.doPrivileged(new PrivilegedAction<IndirectCollectionsProvider>() {

                        @Override
                        public IndirectCollectionsProvider run() {
                            try {
                                return (IndirectCollectionsProvider) support.newInstance();
                            } catch (InstantiationException | IllegalAccessException ex) {
                                throw new RuntimeException(ex);
                            }
                        }
                    });
                } else {
                    Class support = PrivilegedAccessHelper.getClassForName(JDK8_SUPPORT_PROVIDER, true, IndirectCollectionsFactory.class.getClassLoader());
                    return (IndirectCollectionsProvider) PrivilegedAccessHelper.newInstanceFromClass(support);
                }
            } catch (Throwable t) {
                SessionLogEntry sle = new SessionLogEntry(null, t);
                sle.setMessage("IndirectCollections: Using JDK 7 compatible APIs.");
                sle.setLevel(SessionLog.FINEST);
                sle.setNameSpace(SessionLog.MISC);
                //avoid printing date as that would cause an attempt to access
                //a field of this class which has not been initialized yet
                //see DefaultSessionLog.getDateString->ConversionManager.convertObject
                //    ->ClassConstants
                sle.setDate(null);
                AbstractSessionLog.getLog().log(sle);
            }
        }

        return new DefaultProvider();
    }

    /**
     * Define API providers of {@link IndirectCollection} implementations must conform to.
     */
    public static interface IndirectCollectionsProvider {

        /**
         * Class implementing {@link IndirectList}.
         *
         * @return class implementing {@link IndirectList}
         */
        Class getListClass();

        /**
         * Construct an empty {@link IndirectList} with the specified initial capacity
         * and capacity increment.
         *
         * @param <E> the class of the objects in the list
         * @param initialCapacity the initial capacity of the list
         * @param capacityIncrement the amount by which the capacity is increased
         *      when the list overflows
         *
         * @return an empty {@link IndirectList} with the specified initial capacity
         *      and capacity increment
         * @throws IllegalArgumentException if the specified initial capacity is negative
         */
        <E> IndirectList<E> createIndirectList(int initialCapacity, int capacityIncrement);

        /**
         * Constructs an {@link IndirectList} containing the elements of the specified
         * collection, in the order they are returned by the collection's iterator.
         *
         * @param <E> the class of the objects in the list
         * @param collection a collection containing the elements to construct
         *      the {@link IndirectList} with
         *
         * @return an {@link IndirectList} containing the elements of the specified collection
         * @throws NullPointerException if the specified collection is null
         */
        <E> IndirectList<E> createIndirectList(Collection<? extends E> collection);

        /**
         * Class implementing {@link IndirectSet}.
         *
         * @return class implementing {@link IndirectSet}
         */
        Class getSetClass();

        /**
         * Construct an empty {@link IndirectSet} with the specified initial capacity
         * and the specified load factor.
         *
         * @param initialCapacity the initial capacity of the set
         * @param loadFactor the load factor of the set
         *
         * @return an empty {@link IndirectSet} with the specified initial capacity
         *      and the specified load factor
         * @throws IllegalArgumentException if the specified initial capacity is negative
         */
        <E> IndirectSet<E> createIndirectSet(int initialCapacity, float loadFactor);

        /**
         * Constructs an {@link IndirectSet} containing the elements of the specified
         * collection.
         *
         * @param collection a collection containing the elements to construct
         *      the {@link IndirectSet} with
         *
         * @return an {@link IndirectSet} containing the elements of the specified collection
         * @throws NullPointerException if the specified collection is null
         */
        <E> IndirectSet<E> createIndirectSet(Collection<? extends E> collection);

        /**
         * Class implementing {@link IndirectMap}.
         *
         * @return class implementing {@link IndirectMap}
         */
        Class getMapClass();

        /**
         * Construct a new, empty {@link IndirectMap} with the specified initial
         * capacity and the specified load factor.
         *
         * @param initialCapacity the initial capacity of the {@link IndirectMap}
         * @param loadFactor a number between 0.0 and 1.0
         *
         * @return a new, empty {@link IndirectMap} with the specified initial
         *      capacity and the specified load factor
         * @throws  IllegalArgumentException  if the initial capacity is less than
         *      or equal to zero, or if the load factor is less than or equal to zero
         */
        <K, V> IndirectMap<K, V> createIndirectMap(int initialCapacity, float loadFactor);

        /**
         * Construct a new {@link IndirectMap} with the same mappings as the given Map.
         * The {@link IndirectMap} is created with a capacity of twice the number of entries
         * in the given Map or 11 (whichever is greater), and a default load factor, which is 0.75.
         *
         * @param map the map whose mappings are to be placed into created {@link IndirectMap}
         *
         * @return a new {@link IndirectMap} with the same mappings as the given Map
         * @throws NullPointerException if the specified map is null
         */
        <K, V> IndirectMap<K, V> createIndirectMap(Map<? extends K, ? extends V> map);
    }

    /**
     * Provider for creating Java SE 7 (and older) compatible
     * {@link IndirectCollection} implementations.
     */
    private static final class DefaultProvider implements IndirectCollectionsProvider {

        @Override
        public Class getListClass() {
            return IndirectList.class;
        }

        @Override
        public <E> IndirectList<E> createIndirectList(int initialCapacity, int capacityIncrement) {
            return new IndirectList<>(initialCapacity, capacityIncrement);
        }

        @Override
        public <E> IndirectList<E> createIndirectList(Collection<? extends E> collection) {
            return new IndirectList<>(collection);
        }

        @Override
        public Class getSetClass() {
            return IndirectSet.class;
        }

        @Override
        public <E> IndirectSet<E> createIndirectSet(int initialCapacity, float loadFactor) {
            return new IndirectSet<>(initialCapacity, loadFactor);
        }

        @Override
        public <E> IndirectSet<E> createIndirectSet(Collection<? extends E> collection) {
            return new IndirectSet<>(collection);
        }

        @Override
        public Class getMapClass() {
            return IndirectMap.class;
        }

        @Override
        public <K, V> IndirectMap<K, V> createIndirectMap(int initialCapacity, float loadFactor) {
            return new IndirectMap<>(initialCapacity, loadFactor);
        }

        @Override
        public <K, V> IndirectMap<K, V> createIndirectMap(Map<? extends K, ? extends V> map) {
            return new IndirectMap<>(map);
        }
    }
}
