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

import static javax.persistence.metamodel.Type.PersistenceType.BASIC;
import static javax.persistence.metamodel.Type.PersistenceType.EMBEDDABLE;
import static javax.persistence.metamodel.Type.PersistenceType.ENTITY;
import static javax.persistence.metamodel.Type.PersistenceType.MAPPED_SUPERCLASS;

import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.security.AccessController;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EmbeddableType;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.ManagedType;
import javax.persistence.metamodel.MappedSuperclassType;
import javax.persistence.metamodel.Metamodel;
import javax.persistence.metamodel.PluralAttribute.CollectionType;
import javax.persistence.metamodel.StaticMetamodel;
import javax.persistence.metamodel.Type;
import javax.persistence.metamodel.Type.PersistenceType;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.kernel.QueryContext;
import org.apache.openjpa.kernel.exps.AggregateListener;
import org.apache.openjpa.kernel.exps.FilterListener;
import org.apache.openjpa.kernel.exps.Resolver;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.MetaDataRepository;
import org.apache.openjpa.persistence.meta.Members.Member;
import org.apache.openjpa.util.InternalException;

/**
 * Adapts JPA Metamodel to OpenJPA meta-data repository.
 *
 * @author Pinaki Poddar
 *
 */
public class MetamodelImpl implements Metamodel, Resolver {
    private final MetaDataRepository repos;
    private Map<Class<?>, Type<?>> _basics = new HashMap<>();
    private Map<Class<?>, EntityType<?>> _entities = new HashMap<>();
    private Set<EntityType<?>> _entitiesOnlySet = null;
    private Map<Class<?>, EmbeddableType<?>> _embeddables = new HashMap<>();
    private Map<Class<?>, MappedSuperclassType<?>> _mappedsupers = new HashMap<>();
    private Map<Class<?>, Types.PseudoEntity<?>> _pseudos = new HashMap<>();

    private static Localizer _loc = Localizer.forPackage(MetamodelImpl.class);

    /**
     * Constructs a model with the current content of the supplied non-null repository.
     *
     */
    public MetamodelImpl(MetaDataRepository repos) {
        this.repos = repos;
        Collection<Class<?>> classes = repos.loadPersistentTypes(true, null);
        for (Class<?> cls : classes) {
            if (repos.skipMetadata(cls)) { // AttributeConverters, enums etc....
                continue;
            }

        	ClassMetaData meta = repos.getMetaData(cls, null, true);
            PersistenceType type = getPersistenceType(meta);
            switch (type) {
            case ENTITY:
                find(cls, _entities, ENTITY, false);
                if (meta.isEmbeddable())
                    find(cls, _embeddables, EMBEDDABLE, false);
                break;
            case EMBEDDABLE:
                find(cls, _embeddables, EMBEDDABLE, false);
                break;
            case MAPPED_SUPERCLASS:
                find(cls, _mappedsupers, MAPPED_SUPERCLASS, false);
                break;
            default:
            }
        }
    }

    public MetaDataRepository getRepository() {
        return repos;
    }

    /**
     *  Return the metamodel embeddable type representing the embeddable class.
     *
     *  @param cls  the type of the represented embeddable class
     *  @return the metamodel embeddable type
     *  @throws IllegalArgumentException if not an embeddable class
     */
    @Override
    public <X> EmbeddableType<X> embeddable(Class<X> clazz) {
        return (EmbeddableType<X>)find(clazz, _embeddables, EMBEDDABLE, false);
    }

    /**
     *  Return the metamodel entity type representing the entity.
     *  @param cls  the type of the represented entity
     *  @return the metamodel entity type
     *  @throws IllegalArgumentException if not an entity
     */
    @Override
    public <X> EntityType<X> entity(Class<X> clazz) {
        return (EntityType<X>) find(clazz, _entities, ENTITY, false);
    }

    public <X> EntityType<X> entityImpl(Class<X> clazz) {
        return (EntityType<X>) find(clazz, _entities, ENTITY, true);
    }

    /*
     * Return the most up-to-date entity only set in the current meta model.
     */
    private Collection<EntityType<?>> getEntityValuesOnly() {
        if (_entitiesOnlySet == null) {
            _entitiesOnlySet = new HashSet<>();
            for (Class<?> cls : _entities.keySet()) {
                // if key indicates it is a embeddable, do not add to the _entitiesOnlySet.
                if (!_embeddables.containsKey(cls)) {
                    _entitiesOnlySet.add(_entities.get(cls));
                }
            }
        }
        return _entitiesOnlySet;
    }

    /**
     * Return the metamodel embeddable types.
     * @return the metamodel embeddable types
     */
    @Override
    public Set<EmbeddableType<?>> getEmbeddables() {
        return unmodifiableSet(_embeddables.values());
    }

    /**
     * Return the metamodel entity types.
     * @return the metamodel entity types
     */
    @Override
    public Set<EntityType<?>> getEntities() {
        return unmodifiableSet(getEntityValuesOnly());
    }

    /**
     *  Return the metamodel managed types.
     *  @return the metamodel managed types
     */
    @Override
    public Set<ManagedType<?>> getManagedTypes() {
        Set<ManagedType<?>> result = new HashSet<>();
        result.addAll(getEntityValuesOnly());
        result.addAll(_embeddables.values());
        result.addAll(_mappedsupers.values());
        return result;
    }

    /**
     *  Return the metamodel managed type representing the
     *  entity, mapped superclass, or embeddable class.
     *  @param cls  the type of the represented managed class
     *  @return the metamodel managed type
     *  @throws IllegalArgumentException if not a managed class
     */
    @Override
    public <X> ManagedType<X> managedType(Class<X> clazz) {
        if (_embeddables.containsKey(clazz))
            return (EmbeddableType<X>) _embeddables.get(clazz);
        if (_entities.containsKey(clazz))
            return (EntityType<X>) _entities.get(clazz);
        if (_mappedsupers.containsKey(clazz))
            return (MappedSuperclassType<X>) _mappedsupers.get(clazz);
        throw new IllegalArgumentException(_loc.get("type-not-managed", clazz)
            .getMessage());
    }

    /**
     *  Return the type representing the basic, entity, mapped superclass, or embeddable class.
     *  This method differs from {@linkplain #type(Class)} as it also creates a basic or pesudo
     *  type for the given class argument if not already available in this receiver.
     *
     *  @param cls  the type of the represented managed class
     *  @return the metamodel managed type
     *  @throws IllegalArgumentException if not a managed class
     */
    public <X> Type<X> getType(Class<X> cls) {
        try {
            return managedType(cls);
        } catch (IllegalArgumentException ex) {
            if (_basics.containsKey(cls))
                return (Type<X>)_basics.get(cls);
            if (_pseudos.containsKey(cls))
                return (Type<X>)_pseudos.get(cls);
            if (java.util.Map.class.isAssignableFrom(cls)) {
                Types.PseudoEntity<X> pseudo = new Types.PseudoEntity(cls, this);
                _pseudos.put(cls, new Types.PseudoEntity(cls, this));
                return pseudo;
            } else {
                Type<X> basic = new Types.Basic<>(cls);
                _basics.put(cls, basic);
                return basic;
            }
        }
    }

    public static PersistenceType getPersistenceType(ClassMetaData meta) {
        if (meta == null)
            return BASIC;
        if (meta.isAbstract())
            return MAPPED_SUPERCLASS;
        if (meta.isEmbeddable())
            return EMBEDDABLE;
        return ENTITY;
    }

    /**
     * Looks up the given container for the managed type representing the given Java class.
     * The managed type may become instantiated as a side-effect.
     */
    private <V extends ManagedType<?>> V find(Class<?> cls, Map<Class<?>,V> container,
            PersistenceType expected, boolean implFind) {
        if (container.containsKey(cls)) {
            if (implFind || expected != ENTITY || !_embeddables.containsKey(cls)) {
                return container.get(cls);
            }
        }
        ClassMetaData meta = repos.getMetaData(cls, null, false);
        if (meta != null) {
            instantiate(cls, meta, container, expected);
        }
        return container.get(cls);
    }

    /**
     * Instantiate
     * @param <X>
     * @param <V>
     * @param cls
     * @param container
     * @param expected
     */
    private <X,V extends ManagedType<?>> void instantiate(Class<X> cls, ClassMetaData meta,
            Map<Class<?>,V> container, PersistenceType expected) {
        PersistenceType actual = getPersistenceType(meta);
        if (actual != expected) {
            if (!meta.isEmbeddable() || actual != PersistenceType.ENTITY ||
                expected != PersistenceType.EMBEDDABLE)
                throw new IllegalArgumentException( _loc.get("type-wrong-category",
                    cls, actual, expected).getMessage());
        }
        switch (actual) {
        case EMBEDDABLE:
            Types.Embeddable<X> embedded = new Types.Embeddable<>(meta, this);
            _embeddables.put(cls, embedded);
            populate(embedded);
            // no break : embeddables are stored as both entity and embeddable containers
        case ENTITY:
        	Types.Entity<X> entity = new Types.Entity<>(meta, this);
            _entities.put(cls, entity);
            _entitiesOnlySet = null;
            populate(entity);
            break;
        case MAPPED_SUPERCLASS:
            Types.MappedSuper<X> mapped = new Types.MappedSuper<>(meta, this);
            _mappedsupers.put(cls, mapped);
            populate(mapped);
            break;
        default:
            throw new InternalException(cls.getName());
        }
    }

    public <T> Set<T> unmodifiableSet(Collection<T> coll) {
        HashSet<T> result = new HashSet<>();
        for (T t : coll)
            result.add(t);
        return result;
    }

    static CollectionType categorizeCollection(Class<?> cls) {
        if (Set.class.isAssignableFrom(cls))
            return CollectionType.SET;
        if (List.class.isAssignableFrom(cls))
            return CollectionType.LIST;
        if (Collection.class.isAssignableFrom(cls))
            return CollectionType.COLLECTION;
        if (Map.class.isAssignableFrom(cls))
            return CollectionType.MAP;

        throw new InternalException(cls.getName() + " not a collection");
    }

    /**
     * Populate the static fields of the canonical type.
     */
    public <X> void populate(AbstractManagedType<X> type) {
        Class<X> cls = type.getJavaType();
        Class<?> mcls = repos.getMetaModel(cls, true);
        if (mcls == null)
            return;
        StaticMetamodel anno = mcls.getAnnotation(StaticMetamodel.class);
        if (anno == null)
            throw new IllegalArgumentException(_loc.get("meta-class-no-anno",
                    mcls.getName(), cls.getName(), StaticMetamodel.class.getName()).getMessage());

        if (cls != anno.value()) {
            throw new IllegalStateException(_loc.get("meta-class-mismatch",
                    mcls.getName(), cls.getName(), anno.value()).getMessage());
        }

        ParameterizedType mfType = null;
        Attribute<? super X, ?> f = null;
        Field[] mfields = AccessController.doPrivileged(J2DoPrivHelper.getDeclaredFieldsAction(mcls));
        for (Field mf : mfields) {
            try {
                mfType = getParameterizedType(mf); // metamodel type
                if (mfType == null) {
                    continue;
                }
                f = type.getAttribute(mf.getName()); // persistent type

                // populate the static field with persistent type information
                mf.set(null, f);
            } catch (Exception e) {
                throw new RuntimeException(_loc.get("meta-field-mismatch",
                        new Object[] { mf.getName(), mcls.getName(), toTypeName(mfType), f.getJavaType().toString() })
                        .getMessage(), e);
            }
        }
    }

    /**
     * Gets the parameterized type of the given field after validating.
     *
     * @return the field's type as a parameterized type. If the field
     * is not parameterized type (that can happen for non-canonical
     * metamodel or weaving process introducing synthetic fields),
     * returns null.
     */
    ParameterizedType getParameterizedType(Field mf) {
        java.lang.reflect.Type t = mf.getGenericType();
        if (!(t instanceof ParameterizedType)) {
        	repos.getLog().warn(_loc.get("meta-field-not-param",
            mf.getDeclaringClass(), mf.getName(), toTypeName(t)).getMessage());
        	return null;
        }
        ParameterizedType mfType = (ParameterizedType)t;
        java.lang.reflect.Type[] args = mfType.getActualTypeArguments();
        if (args.length < 2) {
            throw new IllegalStateException(_loc.get("meta-field-less-param",
            mf.getDeclaringClass(), mf.getName(), toTypeName(t)).getMessage());
        }

        return mfType;
    }

    /**
     * Pretty prints a Type.
     */
    String toTypeName(java.lang.reflect.Type type) {
        if (type instanceof GenericArrayType) {
            return toTypeName(((GenericArrayType)type).
                getGenericComponentType())+"[]";
        }
        if (!(type instanceof ParameterizedType)) {
            Class<?> cls = (Class<?>)type;
            return cls.getName();
        }
        ParameterizedType pType = (ParameterizedType)type;
        java.lang.reflect.Type[] args = pType.getActualTypeArguments();
        StringBuilder tmp = new StringBuilder(pType.getRawType().toString());
        for (int i = 0; i < args.length; i++) {
            tmp.append((i == 0) ? '<' : ',');
            tmp.append(toTypeName(args[i]));
            if (i == args.length-1) tmp.append('>');
        }
        return tmp.toString();
    }

    /**
     * Validates the given field of the meta class matches the given
     * FieldMetaData and
     * @param <X>
     * @param <Y>
     * @param mField
     * @param member
     */
    void validate(Field metaField, FieldMetaData fmd) {

    }

    <X,Y> void validate(Field mField, Member<X, Y> member) {
        if (!ParameterizedType.class.isInstance(mField.getGenericType())) {
            throw new IllegalArgumentException(_loc.get("meta-bad-field",
                mField).getMessage());
        }
        ParameterizedType mfType = (ParameterizedType)mField.getGenericType();
        java.lang.reflect.Type[] args = mfType.getActualTypeArguments();
        java.lang.reflect.Type owner = args[0];
        if (member.getDeclaringType().getJavaType() != owner)
            throw new IllegalArgumentException(_loc.get("meta-bad-field-owner",
                    mField, owner).getMessage());
    }

    @Override
    public Class classForName(String name, String[] imports) {
        throw new UnsupportedOperationException();
    }

    @Override
    public AggregateListener getAggregateListener(String tag) {
        throw new UnsupportedOperationException();
    }

    @Override
    public OpenJPAConfiguration getConfiguration() {
        return repos.getConfiguration();
    }

    @Override
    public FilterListener getFilterListener(String tag) {
        throw new UnsupportedOperationException();
    }

    @Override
    public QueryContext getQueryContext() {
        throw new UnsupportedOperationException();
    }
}
