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

import java.util.BitSet;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;

import javax.persistence.spi.LoadState;

import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.enhance.StateManager;
import org.apache.openjpa.kernel.Broker;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.StateManagerImpl;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.ValueMetaData;
import org.apache.openjpa.util.ImplHelper;

public class OpenJPAPersistenceUtil {

    /**
     * Returns the identifier of the persistent entity.
     * @param entity
     * @return The identifier of the entity or null if the entity
     * is not persistent.
     */
    public static Object getIdentifier(Object entity) {
        return getIdentifier(null, entity);
    }

    /**
     * Get the object identifier for a persistent entity managed by one
     * of the entity managers of the specified entity manager factory.
     * @return The identifier of the entity or null if the entity does
     * not have an identifier assigned or is not managed by any of the
     * entity managers of the entity manager factory.
     */
    public static Object getIdentifier(OpenJPAEntityManagerFactory emf,
        Object entity) {

        if (entity instanceof PersistenceCapable) {
            PersistenceCapable pc = (PersistenceCapable)entity;
            // Per contract, if not managed by the owning emf, return null.
            if (emf != null) {
                if (!isManagedBy(emf, pc)) {
                    return null;
                }
            }
            StateManager sm = pc.pcGetStateManager();

            if (sm != null && sm instanceof OpenJPAStateManager) {
                OpenJPAStateManager osm = (OpenJPAStateManager)sm;
                return osm.getObjectId();
            }
        }
        return null;
    }

    /**
     * Determines whether the specified state manager is managed by an open
     * broker within the persistence unit of the provided EMF instance.
     * @param emf OpenJPAEntityManagerFactory
     * @param sm StateManager
     * @return true if this state manager is managed by a broker within
     * this persistence unit.
     */
    public static boolean isManagedBy(OpenJPAEntityManagerFactory emf, Object entity) {
        // Assert a valid emf was provided, it is open, and the entity is PC
        if (emf == null || !emf.isOpen() || !ImplHelper.isManageable(entity)) {
            return false;
        }
        // Assert the context is a broker
        PersistenceCapable pc = (PersistenceCapable)entity;
        if (!(pc.pcGetGenericContext() instanceof Broker)) {
            return false;
        }
        // Assert the broker is available and open
        Broker broker = (Broker)pc.pcGetGenericContext();
        if (broker == null || broker.isClosed()) {
            return false;
        }
        // Assert the emf associated with the PC is the same as the provided emf
        OpenJPAEntityManagerFactory eemf = JPAFacadeHelper.toEntityManagerFactory(broker.getBrokerFactory());
        if (eemf == emf && eemf.isOpen()) {
            return true;
        }
        return false;
    }

    /**
     * Determines whether the attribute on the specified object is loaded and
     * is managed by one of the entity managers.  Use isManagedBy() to
     * determine if an object is managed by a specific entity manager
     * factory.
     *
     * @return LoadState.LOADED - if the attribute is loaded.
     *         LoadState.NOT_LOADED - if the attribute is not loaded or any
     *         EAGER fetch attributes of the entity are not loaded.
     *         LoadState.UNKNOWN - if the entity is not managed by this
     *         provider or if it does not contain the persistent
     *         attribute.
     */
    public static LoadState isLoaded(Object obj, String attr) {

        if (obj == null) {
            return LoadState.UNKNOWN;
        }

        // If the object has a state manager, call it directly.
        if (obj instanceof PersistenceCapable) {
            PersistenceCapable pc = (PersistenceCapable)obj;
            StateManager sm = pc.pcGetStateManager();
            if (sm != null && sm instanceof OpenJPAStateManager) {
                return isLoaded((OpenJPAStateManager)sm, attr, null);
            }
        }
        return LoadState.UNKNOWN;
    }

    private static LoadState isLoaded(OpenJPAStateManager sm, String attr,
        HashSet<OpenJPAStateManager> pcs) {
        boolean isLoaded = true;
        try {
            BitSet loadSet = sm.getLoaded();
            if (attr != null) {
                FieldMetaData fmd = sm.getMetaData().getField(attr);
                // Could not find field metadata for the specified attribute.
                if (fmd == null) {
                    return LoadState.UNKNOWN;
                }
                // Otherwise, return the load state
                if(!loadSet.get(fmd.getIndex())) {
                    return LoadState.NOT_LOADED;
                }
            }
            if (pcs != null && pcs.contains(sm)) {
                return LoadState.LOADED;
            }
            FieldMetaData[] fmds = sm.getMetaData().getFields();
            // Check load state of all persistent eager fetch attributes
            if (fmds != null && fmds.length > 0) {
                pcs = addToLoadSet(pcs, sm);
                for (FieldMetaData fmd : fmds) {
                    if (requiresFetch(sm, fmd)) {
                        if (!isLoadedField(sm, fmd, pcs)) {
                            isLoaded = false;
                            break;
                        }
                    }
                }
                pcs.remove(sm);
            }
        } catch (RuntimeException e) {
            // treat any exceptions, like UnsupportedOperationException
            // for detached entities, as LoadState.UNKNOWN
            return LoadState.UNKNOWN;
        }
        return isLoaded ? LoadState.LOADED : LoadState.NOT_LOADED;
    }

    private static boolean requiresFetch(OpenJPAStateManager sm, FieldMetaData fmd) {
        if (sm instanceof StateManagerImpl)
            return ((StateManagerImpl)sm).requiresFetch(fmd);
        return fmd.isInDefaultFetchGroup();
    }

    private static HashSet<OpenJPAStateManager> addToLoadSet(
        HashSet<OpenJPAStateManager> pcs, OpenJPAStateManager sm) {
        if (pcs == null) {
            pcs = new HashSet<>();
        }
        pcs.add(sm);
        return pcs;
    }

    private static boolean isLoadedField(OpenJPAStateManager sm,
        FieldMetaData fmd, HashSet<OpenJPAStateManager> pcs) {
        BitSet loadSet = sm.getLoaded();

        // Simple load state check for the field
        if (!loadSet.get(fmd.getIndex()))
            return false;

        Object field = sm.fetchField(fmd.getIndex(), false);

        // Get the state manager for the field, if it is a PC
        OpenJPAStateManager ofsm = getStateManager(field);

        // Prevent circular load state evaluation for this sm.
        if (ofsm != null && pcs.contains(ofsm))
            return true;

        // If a collection type, determine if it is loaded
        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.COLLECTION:
                return isLoadedCollection(sm, fmd.getElement(),
                    (Collection<?>)field, pcs);
            case JavaTypes.MAP:
                return isLoadedMap(sm, fmd,
                    (Map<?,?>)field, pcs);
            case JavaTypes.ARRAY:
                return isLoadedArray(sm, fmd.getElement(),
                    (Object[])field, pcs);
        }
        // If other PC type, determine if it is loaded
        if (ofsm != null && fmd.isDeclaredTypePC()) {
            return isLoaded(ofsm, null, pcs) ==
                LoadState.LOADED;
        }

        return true;
    }

    private static boolean isLoadedCollection(OpenJPAStateManager sm,
        ValueMetaData vmd, Collection<?> coll, HashSet<OpenJPAStateManager> pcs) {

        // This field passed the load state check in isLoadedField, so
        // if any of these conditions are true the collection is loaded.
        if (sm == null || coll == null || coll.size() == 0) {
            return true;
        }

        // Convert to array to prevent concurrency issues
        Object[] arr = coll.toArray();

        return isLoadedArray(sm, vmd, arr, pcs);
    }

    private static boolean isLoadedArray(OpenJPAStateManager sm,
        ValueMetaData vmd, Object[] arr,
        HashSet<OpenJPAStateManager> pcs) {

        // This field passed the load state check in isLoadedField, so
        // if any of these conditions are true the array is loaded.
        if (sm == null || arr == null || arr.length == 0) {
            return true;
        }

        // Not a collection of PC's
        if (!vmd.isDeclaredTypePC()) {
          return true;
        }

        for (Object pc : arr) {
            OpenJPAStateManager esm = getStateManager(pc);
            if (esm == null) {
                return true;
            }
            if (!(isLoaded(esm, null, pcs) == LoadState.LOADED))
                return false;
        }
        return true;
    }

    private static boolean isLoadedMap(OpenJPAStateManager sm,
        FieldMetaData fmd, Map<?,?> map, HashSet<OpenJPAStateManager> pcs) {

        // This field passed the load state check in isLoadedField, so
        // if any of these conditions are true the map is loaded.
        if (sm == null || map == null || map.size() == 0) {
            return true;
        }

        boolean keyIsPC = fmd.getKey().isDeclaredTypePC();
        boolean valIsPC = fmd.getElement().isDeclaredTypePC();

        // Map is does not contain PCs in either keys or values
        if (!(keyIsPC || valIsPC)) {
          return true;
        }

        Object[] arr = map.keySet().toArray();

        for (Object key : arr) {
            if (keyIsPC) {
                OpenJPAStateManager ksm = getStateManager(key);
                if (ksm == null) {
                    return true;
                }
                if (!(isLoaded(ksm, null, pcs) == LoadState.LOADED))
                    return false;
            }
            if (valIsPC) {
                Object value = map.get(key);
                OpenJPAStateManager vsm = getStateManager(value);
                if (vsm == null) {
                    return true;
                }
                if (!(isLoaded(vsm, null, pcs) == LoadState.LOADED))
                    return false;
            }
        }
        return true;
    }

    private static OpenJPAStateManager getStateManager(Object obj) {
        if (obj == null || !(obj instanceof PersistenceCapable)) {
            return null;
        }

        PersistenceCapable pc = (PersistenceCapable)obj;
        StateManager sm = pc.pcGetStateManager();
        if (sm == null || !(sm instanceof OpenJPAStateManager)) {
            return null;
        }
        return (OpenJPAStateManager)sm;
    }
}
