/*
 * 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.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collection;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;

import org.apache.openjpa.enhance.PCRegistry;
import org.apache.openjpa.kernel.Broker;
import org.apache.openjpa.kernel.BrokerFactory;
import org.apache.openjpa.kernel.BrokerImpl.StateManagerId;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.util.ApplicationIds;
import org.apache.openjpa.util.BigDecimalId;
import org.apache.openjpa.util.BigIntegerId;
import org.apache.openjpa.util.ByteId;
import org.apache.openjpa.util.CharId;
import org.apache.openjpa.util.DoubleId;
import org.apache.openjpa.util.FloatId;
import org.apache.openjpa.util.Id;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.IntId;
import org.apache.openjpa.util.LongId;
import org.apache.openjpa.util.ObjectId;
import org.apache.openjpa.util.OpenJPAId;
import org.apache.openjpa.util.ShortId;
import org.apache.openjpa.util.StringId;
import org.apache.openjpa.util.UserException;

/**
 * Helper class for switching between OpenJPA's JPA facade and the underlying
 * Broker kernel.
 *
 * @since 1.0.0
 */
public class JPAFacadeHelper {

    public static final String EM_KEY =
        "org.apache.openjpa.persistence.EntityManager";
    public static final String EMF_KEY =
        "org.apache.openjpa.persistence.EntityManagerFactory";

    private static final Localizer _loc =
        Localizer.forPackage(JPAFacadeHelper.class);

    public static OpenJPAEntityManagerFactory toEntityManagerFactory(
        BrokerFactory factory) {
        if (factory == null)
            return null;

        factory.lock();
        try {
            OpenJPAEntityManagerFactory emf = (OpenJPAEntityManagerFactory)
                factory.getUserObject(EMF_KEY);
            if (emf == null) {
                emf = EntityManagerFactoryValue.newFactory(factory);
                factory.putUserObject(EMF_KEY, emf);
            }
            return emf;
        } catch (Exception e) {
            throw PersistenceExceptions.toPersistenceException(e);
        } finally {
            factory.unlock();
        }
    }

    /**
     * Return the underlying broker factory for the given persistence manager
     * factory facade.
     */
    public static BrokerFactory toBrokerFactory(EntityManagerFactory emf) {
        if (emf == null)
            return null;
        if (!(emf instanceof EntityManagerFactoryImpl)) {
            Class c = emf.getClass();
            try {
                // either cast here may fail
                emf = (EntityManagerFactoryImpl) ((OpenJPAEntityManagerFactory)
                    emf).getUserObject(EMF_KEY);
            } catch (ClassCastException cce) {
                throw new ArgumentException(_loc.get(
                    "cant-convert-brokerfactory", c), null, null, false);
            }
        }
        return ((EntityManagerFactoryImpl) emf).getBrokerFactory();
    }

    /**
     * Return a persistence manager facade to the given broker retaining
     * previously associated persistence context type.
     */
    public static OpenJPAEntityManager toEntityManager(Broker broker) {
        if (broker == null)
            return null;

        broker.lock();
        try {
            OpenJPAEntityManager em = (OpenJPAEntityManager)
                broker.getUserObject(EM_KEY);
            if (em == null) {
                EntityManagerFactoryImpl emf = (EntityManagerFactoryImpl)
                    toEntityManagerFactory(broker.getBrokerFactory());
                em = emf.newEntityManagerImpl(broker);
                broker.putUserObject(EM_KEY, em);
            }
            return em;
        } catch (Exception e) {
            throw PersistenceExceptions.toPersistenceException(e);
        } finally {
            broker.unlock();
        }
    }

    /**
     * Return the underlying broker for the given entity manager facade.
     */
    public static Broker toBroker(EntityManager em) {
        if (em == null)
            return null;
        if (!(em instanceof EntityManagerImpl)) {
            Class c = em.getClass();
            try {
                // either cast here may fail
                em = (EntityManagerImpl) ((OpenJPAEntityManager) em).
                    getUserObject(EM_KEY);
            } catch (ClassCastException cce) {
                throw new ArgumentException(_loc.get("cant-convert-broker", c),
                    new Throwable[] { cce }, null, false);
            }
        }
        return ((EntityManagerImpl) em).getBroker();
    }

    /**
     * Returns the {@link org.apache.openjpa.meta.ClassMetaData} associated with
     * the persistent object <code>o</code>.
     */
    public static ClassMetaData getMetaData(Object o) {
        if (o == null)
            return null;
        EntityManager em = OpenJPAPersistence.getEntityManager(o);
        return (em == null) ? null : getMetaData(em,
            ImplHelper.getManagedInstance(o).getClass());
    }

    /**
     * Returns the {@link org.apache.openjpa.meta.ClassMetaData} associated
     * with the persistent type <code>cls</code>.
     */
    public static ClassMetaData getMetaData(EntityManager em, Class cls) {
        if (em == null)
            throw new NullPointerException("em == null");

        OpenJPAEntityManagerSPI kem = (OpenJPAEntityManagerSPI)
            OpenJPAPersistence.cast(em);
        try {
            return kem.getConfiguration().getMetaDataRepositoryInstance().
                getMetaData(cls, kem.getClassLoader(), false);
        } catch (Exception e) {
            throw PersistenceExceptions.toPersistenceException(e);
        }
    }

    /**
     * Returns the {@link org.apache.openjpa.meta.ClassMetaData} associated
     * with the persistent type <code>cls</code>.
     */
    public static ClassMetaData getMetaData(EntityManagerFactory emf,
        Class cls) {
        if (emf == null)
            throw new NullPointerException("emf == null");

        OpenJPAEntityManagerFactorySPI emfSPI =
            (OpenJPAEntityManagerFactorySPI) OpenJPAPersistence.cast(emf);
        try {
            return emfSPI.getConfiguration().getMetaDataRepositoryInstance().
                getMetaData(cls, null, false);
        } catch (Exception e) {
            throw PersistenceExceptions.toPersistenceException(e);
        }
    }

    /**
     * Translate from a OpenJPA identity object to a Persistence one.
     */
    public static Object fromOpenJPAObjectId(Object oid) {
        if (oid instanceof OpenJPAId)
            return ((OpenJPAId) oid).getIdObject();
        return oid;
    }

    /**
     * Translate from a Persistence identity object to a OpenJPA one. If the provided oid isn't of the expected type
     * a UserException will be thrown.
     */
    public static Object toOpenJPAObjectId(ClassMetaData meta, Object oid) {
        if (oid == null || meta == null)
            return null;
        if (oid instanceof OpenJPAId) {
            return oid;
        }

        Class<?> cls = meta.getDescribedType();
        FieldMetaData[] pks = meta.getPrimaryKeyFields();

        Object expected = meta.getObjectIdType();
        try {
            switch (meta.getIdentityType()) {
            case ClassMetaData.ID_DATASTORE:
                if (oid instanceof String && ((String) oid).startsWith(StateManagerId.STRING_PREFIX))
                    return new StateManagerId((String) oid);
                return new Id(cls, ((Number) oid).longValue());
            case ClassMetaData.ID_APPLICATION:
                if (ImplHelper.isAssignable(meta.getObjectIdType(), oid.getClass())) {
                    if (!meta.isOpenJPAIdentity() && meta.isObjectIdTypeShared())
                        return new ObjectId(cls, oid);
                    return oid;
                }

                if (meta.getIdClass() == null) {
                    expected = pks[0].getDeclaredType();
                } else {
                    expected = meta.getIdClass();
                }
                // stringified app id?
                if (oid instanceof String
                    && !meta.getRepository().getConfiguration().getCompatibilityInstance().getStrictIdentityValues()
                    && !Modifier.isAbstract(cls.getModifiers()))
                    return PCRegistry.newObjectId(cls, (String) oid);

                Object[] arr = (oid instanceof Object[]) ? (Object[]) oid : new Object[] { oid };
                Object rtrn = ApplicationIds.fromPKValues(arr, meta);
                if (rtrn != null && meta.getObjectIdType() != null) {
                    if (rtrn instanceof ObjectId) {
                        // embedded id and composite id with a derived id that
                        // uses an embedded id
                        if (pks.length > 0 && (pks[0].isEmbedded() || pks[0].isTypePC())) {
                            Class idClass = meta.getIdClass();
                            if (pks[0].getDeclaredType().equals(oid.getClass()) || idClass != null
                                && idClass.equals(oid.getClass())) {
                                return rtrn;
                            }
                        }
                    } else {
                        if (!(rtrn instanceof StringId) || rtrn instanceof StringId && oid instanceof String) {
                            return rtrn;
                        }
                    }
                }
            default:
                throw new UserException(_loc.get("invalid-oid", new Object[] { expected, oid.getClass() }));
            }
        } catch (RuntimeException re) {
            if (expected == null)
                throw new UserException(_loc.get("invalid-oid", new Object[] { Number.class, oid.getClass() }));
            throw new UserException(_loc.get("invalid-oid", new Object[] { expected, oid.getClass() }));
        }
    }

    /**
     * Return an array of OpenJPA oids for the given native oid array.
     */
    public static Object[] toOpenJPAObjectIds(ClassMetaData meta,
        Object... oids) {
        if (oids == null || oids.length == 0)
            return oids;

        // since the class if fixed for all oids, we can tell if we have to
        // translate the array based on whether the first oid needs translating
        Object oid = toOpenJPAObjectId(meta, oids[0]);
        if (oid == oids[0])
            return oids;

        Object[] copy = new Object[oids.length];
        copy[0] = oid;
        for (int i = 1; i < oids.length; i++)
            copy[i] = toOpenJPAObjectId(meta, oids[i]);
        return copy;
    }

    /**
     * Return a collection of OpenJPA oids for the given native oid collection.
     */
    public static Collection<Object> toOpenJPAObjectIds(ClassMetaData meta, Collection<Object> oids) {
        if (oids == null || oids.size() == 0) {
            return oids;
        }
        return Arrays.asList(toOpenJPAObjectIds(meta, oids.toArray()));
    }

    
    /**
     * Translate from a OpenJPA identity class to a native one.
     */
    public static Class fromOpenJPAObjectIdClass(Class oidClass) {
        if (oidClass == null)
            return null;
        if (oidClass == Id.class)
            return Long.class;
        if (oidClass == ByteId.class)
            return Byte.class;
        if (oidClass == CharId.class)
            return Character.class;
        if (oidClass == DoubleId.class)
            return Double.class;
        if (oidClass == FloatId.class)
            return Float.class;
        if (oidClass == IntId.class)
            return Integer.class;
        if (oidClass == LongId.class)
            return Long.class;
        if (oidClass == ShortId.class)
            return Short.class;
        if (oidClass == StringId.class)
            return String.class;
        if (oidClass == BigDecimalId.class)
            return BigDecimal.class;
        if (oidClass == BigIntegerId.class)
            return BigInteger.class;
        return oidClass;
	}
}
