/*
 * 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.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import jakarta.persistence.Embeddable;
import jakarta.persistence.Entity;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.NamedNativeQueries;
import jakarta.persistence.NamedNativeQuery;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.NamedStoredProcedureQueries;
import jakarta.persistence.NamedStoredProcedureQuery;
import jakarta.persistence.SqlResultSetMapping;
import jakarta.persistence.SqlResultSetMappings;
import jakarta.persistence.metamodel.StaticMetamodel;

import org.apache.openjpa.lib.conf.Configurable;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.conf.GenericConfigurable;
import org.apache.openjpa.lib.meta.ClassAnnotationMetaDataFilter;
import org.apache.openjpa.lib.meta.ClassArgParser;
import org.apache.openjpa.lib.meta.MetaDataFilter;
import org.apache.openjpa.lib.meta.MetaDataParser;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.MultiClassLoader;
import org.apache.openjpa.lib.util.Options;
import org.apache.openjpa.meta.AbstractCFMetaDataFactory;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.MetaDataDefaults;
import org.apache.openjpa.meta.MetaDataFactory;
import org.apache.openjpa.meta.QueryMetaData;
import org.apache.openjpa.meta.SequenceMetaData;
import org.apache.openjpa.util.GeneralException;
import org.apache.openjpa.util.MetaDataException;

/**
 * {@link MetaDataFactory} for JPA metadata.
 *
 * @author Steve Kim
 * @since 0.4.0
 */
public class PersistenceMetaDataFactory
    extends AbstractCFMetaDataFactory
    implements Configurable, GenericConfigurable {

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

    private final PersistenceMetaDataDefaults _def =
        new PersistenceMetaDataDefaults();
    private AnnotationPersistenceMetaDataParser _annoParser = null;
    private AnnotationPersistenceXMLMetaDataParser _annoXMLParser = null;
    private XMLPersistenceMetaDataParser _xmlParser = null;
    private Map<URL, Set<String>> _xml = null; // xml rsrc -> class names
    private Set<URL> _unparsed = null; // xml rsrc
    private boolean _fieldOverride = true;

    protected Stack<XMLPersistenceMetaDataParser> _stack =
        new Stack<>();

    /**
     * Whether to use field-level override or class-level override.
     * Defaults to true.
     */
    public void setFieldOverride(boolean field) {
        _fieldOverride = field;
    }

    /**
     * Whether to use field-level override or class-level override.
     * Defaults to true.
     */
    public boolean getFieldOverride() {
        return _fieldOverride;
    }

    /**
     * Return metadata parser, creating it if it does not already exist.
     */
    public AnnotationPersistenceMetaDataParser getAnnotationParser() {
        if (_annoParser == null) {
            _annoParser = newAnnotationParser();
            _annoParser.setRepository(repos);
        }
        return _annoParser;
    }

    /**
     * Set the metadata parser.
     */
    public void setAnnotationParser(
        AnnotationPersistenceMetaDataParser parser) {
        if (_annoParser != null)
            _annoParser.setRepository(null);
        if (parser != null)
            parser.setRepository(repos);
        _annoParser = parser;
    }

    /**
     * Create a new metadata parser.
     */
    protected AnnotationPersistenceMetaDataParser newAnnotationParser() {
        return new AnnotationPersistenceMetaDataParser
            (repos.getConfiguration());
    }

    /**
     * Create a new annotation serializer.
     */
    @Override
    protected AnnotationPersistenceMetaDataSerializer
        newAnnotationSerializer() {
        return new AnnotationPersistenceMetaDataSerializer
            (repos.getConfiguration());
    }

    /**
     * Return XML metadata parser, creating it if it does not already exist or
     * if the existing parser is parsing.
     */
    public XMLPersistenceMetaDataParser getXMLParser() {
        if (_xmlParser == null || _xmlParser.isParsing()) {
            Class<?> parseCls = null;
            ArrayList<Class<?>> parseList = null;
            // If there is an existing parser and it is parsing, push it on
            // the stack and return a new one.
            if (_xmlParser != null) {
                _stack.push(_xmlParser);
                parseCls = _xmlParser.getParseClass();
                parseList = _xmlParser.getParseList();
            }
            _xmlParser = newXMLParser(true);
            _xmlParser.addToParseList(parseList);
            _xmlParser.addToParseList(parseCls);
            _xmlParser.setRepository(repos);
            if (_fieldOverride)
                _xmlParser.setAnnotationParser(getAnnotationParser());
        }
        return _xmlParser;
    }

    public void resetXMLParser() {
        // If a parser was pushed on the stack due to multi-level parsing,
        // clear the current parser and pop the inner parser off the stack.
        if (!_stack.isEmpty()) {
            _xmlParser.clear();
            _xmlParser = _stack.pop();
        }
    }

    /**
     * Set the metadata parser.
     */
    public void setXMLParser(XMLPersistenceMetaDataParser parser) {
        if (_xmlParser != null)
            _xmlParser.setRepository(null);
        if (parser != null)
            parser.setRepository(repos);
        _xmlParser = parser;
    }

    /**
     * Create a new metadata parser.
     */
    protected XMLPersistenceMetaDataParser newXMLParser(boolean loading) {
        return new XMLPersistenceMetaDataParser(repos.getConfiguration());
    }

    /**
     * Create a new serializer
     */
    protected XMLPersistenceMetaDataSerializer newXMLSerializer() {
        return new XMLPersistenceMetaDataSerializer(repos.getConfiguration());
    }

    @Override
    public void load(Class<?> cls, int mode, ClassLoader envLoader) {
        if (mode == MODE_NONE)
            return;
        if (!strict && (mode & MODE_META) != 0)
            mode |= MODE_MAPPING;

        // getting the list of persistent types runs callbacks to
        // mapPersistentTypeNames if it hasn't been called already, which
        // caches XML resources
        getPersistentTypeNames(false, envLoader);
        URL xml = findXML(cls);

        // we have to parse metadata up-front to register persistence unit
        // defaults and system callbacks
        ClassMetaData meta;
        boolean parsedXML = false;
        if (_unparsed != null && !_unparsed.isEmpty()
            && (mode & MODE_META) != 0) {
            Set<URL> unparsed = new HashSet<>(_unparsed);
            for (URL url : unparsed) {
                parseXML(url, cls, mode, envLoader);
            }
            parsedXML = unparsed.contains(xml);
             _unparsed.clear();

            // XML process check
            meta = repos.getCachedMetaData(cls);
            if (meta != null && (meta.getSourceMode() & mode) == mode) {
                validateStrategies(meta);
                return;
            }
        }

        // might have been looking for system-level query
        if (cls == null)
            return;

        // we may still need to parse XML if this is a redeploy of a class, or
        // if we're in strict query-only mode
        if (!parsedXML && xml != null) {
            parseXML(xml, cls, mode, envLoader);
            // XML process check
            meta = repos.getCachedMetaData(cls);
            if (meta != null && (meta.getSourceMode() & mode) == mode) {
                validateStrategies(meta);
                return;
            }
        }

        AnnotationPersistenceMetaDataParser parser = getAnnotationParser();
        parser.setEnvClassLoader(envLoader);
        parser.setMode(mode);
        parser.parse(cls);

        meta = repos.getCachedMetaData(cls);
        if (meta != null && (meta.getSourceMode() & mode) == mode)
            validateStrategies(meta);
    }

    /**
     * Parse the given XML resource.
     */
    private void parseXML(URL xml, Class<?> cls, int mode,
    	ClassLoader envLoader) {
        // spring needs to use the envLoader first for all class resolution,
        // but we must still fall back on application loader
        ClassLoader loader = repos.getConfiguration().
            getClassResolverInstance().getClassLoader(cls, null);
        if (envLoader != null && envLoader != loader) {
          MultiClassLoader mult = new MultiClassLoader();
          mult.addClassLoader(envLoader);

          // loader from resolver is usually a multi loader itself
          if (loader instanceof MultiClassLoader)
            mult.addClassLoaders((MultiClassLoader)loader);
          else
            mult.addClassLoader(loader);
          loader = mult;
        }

        XMLPersistenceMetaDataParser xmlParser = getXMLParser();
        xmlParser.setClassLoader(loader);
        xmlParser.setEnvClassLoader(envLoader);
        xmlParser.setMode(mode);
        try {
            xmlParser.parse(xml);
        } catch (IOException ioe) {
            throw new GeneralException(ioe);
        }
        finally {
            resetXMLParser();
        }
    }

    /**
     * Locate the XML resource for the given class.
     */
    private URL findXML(Class<?> cls) {
        if (_xml != null && cls != null)
            for (Map.Entry<URL, Set<String>> entry : _xml.entrySet())
                if (entry.getValue().contains(cls.getName()))
                    return entry.getKey();
        return null;
    }

    @Override
    protected void mapPersistentTypeNames(Object rsrc, String[] names) {
        if (rsrc.toString().endsWith(".class")) {
            if (log.isTraceEnabled())
                log.trace(
                    _loc.get("map-persistent-types-skipping-class", rsrc));
            return;
        } else if (!(rsrc instanceof URL)) {
            if (log.isTraceEnabled())
                log.trace(
                    _loc.get("map-persistent-types-skipping-non-url", rsrc));
            return;
        } else if (rsrc.toString().endsWith("/")) {
            // OPENJPA-1546 If the rsrc URL is a directory it should not be
            // added to the list of the unparsed XML files
            if (log.isTraceEnabled())
                log.trace(_loc.get("map-persistent-types-skipping-dir", rsrc));
            return;
        }

        if (log.isTraceEnabled())
            log.trace(_loc.get(
                "map-persistent-type-names", rsrc, Arrays.asList(names)));

        if (_xml == null)
            _xml = new HashMap<>();
        _xml.put((URL) rsrc, new HashSet<>(Arrays.asList(names)));

        if (_unparsed == null)
            _unparsed = new HashSet<>();
        _unparsed.add((URL) rsrc);
    }

    @Override
    public Class<?> getQueryScope(String queryName, ClassLoader loader) {
        if (queryName == null)
            return null;
        Collection<Class<?>> classes = repos.loadPersistentTypes(false, loader);
        for (Class<?> cls :  classes) {
            if (AccessController.doPrivileged(J2DoPrivHelper
                    .isAnnotationPresentAction(cls, NamedQuery.class)) && hasNamedQuery
                (queryName, (NamedQuery) cls.getAnnotation(NamedQuery.class)))
                return cls;
            if (AccessController.doPrivileged(J2DoPrivHelper
                    .isAnnotationPresentAction(cls, NamedQueries.class)) &&
                hasNamedQuery(queryName, ((NamedQueries) cls.
                    getAnnotation(NamedQueries.class)).value()))
                return cls;
            if (AccessController.doPrivileged(J2DoPrivHelper
                    .isAnnotationPresentAction(cls, NamedNativeQuery.class)) &&
                hasNamedNativeQuery(queryName, (NamedNativeQuery) cls.
                    getAnnotation(NamedNativeQuery.class)))
                return cls;
            if (AccessController.doPrivileged(J2DoPrivHelper
                    .isAnnotationPresentAction(cls, NamedNativeQueries.class)) &&
                hasNamedNativeQuery(queryName, ((NamedNativeQueries) cls.
                    getAnnotation(NamedNativeQueries.class)).value()))
                return cls;
            if (isAnnotated(cls, NamedStoredProcedureQuery.class)
                    && hasNamedStoredProcedure(queryName, cls.getAnnotation(NamedStoredProcedureQuery.class)))
                return cls;
            if (isAnnotated(cls, NamedStoredProcedureQueries.class)
                    && hasNamedStoredProcedure(queryName, cls.getAnnotation(NamedStoredProcedureQueries.class).value()))
                return cls;
        }
        return null;
    }

    private boolean isAnnotated(Class<?> cls, Class<? extends Annotation> annotationClazz) {
        return AccessController.doPrivileged(J2DoPrivHelper.isAnnotationPresentAction(cls, annotationClazz));
    }

    @Override
    public Class<?> getResultSetMappingScope(String rsMappingName,
        ClassLoader loader) {
        if (rsMappingName == null)
            return null;

        Collection<Class<?>> classes = repos.loadPersistentTypes(false, loader);
        for (Class<?> cls : classes) {

            if (AccessController.doPrivileged(J2DoPrivHelper
                    .isAnnotationPresentAction(cls, SqlResultSetMapping.class)) &&
                hasRSMapping(rsMappingName, (SqlResultSetMapping) cls.
                getAnnotation(SqlResultSetMapping.class)))
                return cls;

            if (AccessController.doPrivileged(J2DoPrivHelper
                    .isAnnotationPresentAction(cls, SqlResultSetMappings.class)) &&
                hasRSMapping(rsMappingName, ((SqlResultSetMappings) cls.
                getAnnotation(SqlResultSetMappings.class)).value()))
                return cls;
        }
        return null;
    }

    private boolean hasNamedQuery(String query, NamedQuery... queries) {
        for (NamedQuery q : queries) {
            if (query.equals(q.name()))
                return true;
        }
        return false;
    }

    private boolean hasRSMapping(String rsMapping,
        SqlResultSetMapping... mappings) {
        for (SqlResultSetMapping m : mappings) {
            if (rsMapping.equals(m.name()))
                return true;
        }
        return false;
    }

    private boolean hasNamedStoredProcedure(String query, NamedStoredProcedureQuery... queries) {
        for (NamedStoredProcedureQuery q : queries) {
            if (query.equals(q.name()))
                return true;
        }
        return false;
    }

    private boolean hasNamedNativeQuery(String query,
        NamedNativeQuery... queries) {
        for (NamedNativeQuery q : queries) {
            if (query.equals(q.name()))
                return true;
        }
        return false;
    }

    @Override
    protected MetaDataFilter newMetaDataFilter() {
        ClassAnnotationMetaDataFilter camdf = new ClassAnnotationMetaDataFilter(
                new Class[] { Entity.class, Embeddable.class,
                        MappedSuperclass.class });
        camdf.setLog(log);
        return camdf;
    }

    /**
     * Ensure all fields have declared a strategy.
     */
    private void validateStrategies(ClassMetaData meta) {
        StringBuilder buf = null;
        for (FieldMetaData fmd : meta.getDeclaredFields()) {
            if (!fmd.isExplicit()) {
                if (buf == null)
                    buf = new StringBuilder();
                else
                    buf.append(", ");
                buf.append(meta.getDescribedTypeString() + "." + fmd);
            }
        }
        if (buf != null) {
            throw new MetaDataException(_loc.get("no-pers-strat", buf));
        }
    }

    @Override
    public MetaDataDefaults getDefaults() {
        return _def;
    }

    @Override
    public ClassArgParser newClassArgParser() {
        ClassArgParser parser = new ClassArgParser();
        parser.setMetaDataStructure("package", null, new String[]{
            "entity", "embeddable", "mapped-superclass" }, "class");
        return parser;
    }

    @Override
    public void clear() {
        super.clear();
        if (_annoParser != null)
            _annoParser.clear();
        if (_xmlParser != null)
            _xmlParser.clear();
        if (_xml != null)
            _xml.clear();
    }

    @Override
    protected Parser newParser(boolean loading) {
        return newXMLParser(loading);
    }

    @Override
    protected Serializer newSerializer() {
        return newXMLSerializer();
    }

    @Override
    protected void parse(MetaDataParser parser, Class[] cls) {
        parse(parser, Collections.singleton(defaultXMLFile()));
    }

    @Override
    protected File defaultSourceFile(ClassMetaData meta) {
        return defaultXMLFile();
    }

    @Override
    protected File defaultSourceFile(QueryMetaData query, Map clsNames) {
        ClassMetaData meta = getDefiningMetaData(query, clsNames);
        File file = (meta == null) ? null : meta.getSourceFile();
        if (file != null)
            return file;
        return defaultXMLFile();
    }

    @Override
    protected File defaultSourceFile(SequenceMetaData seq, Map clsNames) {
        return defaultXMLFile();
    }

    /**
     * Look for META-INF/orm.xml, and if it doesn't exist, choose a default.
     */
    private File defaultXMLFile() {
        ClassLoader loader = repos.getConfiguration().
            getClassResolverInstance().getClassLoader(getClass(), null);
        URL rsrc = AccessController.doPrivileged(
            J2DoPrivHelper.getResourceAction(loader, "META-INF/orm.xml"));
        if (rsrc != null) {
            File file = new File(rsrc.getFile());
            if (AccessController.doPrivileged(
                    J2DoPrivHelper.existsAction(file)))
                return file;
        }
        return new File(dir, "orm.xml");
    }

    @Override
    public void setConfiguration(Configuration conf) {
    }

    @Override
    public void startConfiguration() {
    }

    @Override
    public void endConfiguration() {
        if (rsrcs == null)
            rsrcs = Collections.singleton("META-INF/orm.xml");
        else
			rsrcs.add("META-INF/orm.xml");
	}

    @Override
    public void setInto(Options opts) {
        opts.keySet().retainAll(opts.setInto(_def).keySet());
    }

    /**
     * Return JAXB XML annotation parser,
     * creating it if it does not already exist.
     */
    public AnnotationPersistenceXMLMetaDataParser getXMLAnnotationParser() {
        if (_annoXMLParser == null) {
            _annoXMLParser = newXMLAnnotationParser();
            _annoXMLParser.setRepository(repos);
        }
        return _annoXMLParser;
    }

    /**
     * Set the JAXB XML annotation parser.
     */
    public void setXMLAnnotationParser(
        AnnotationPersistenceXMLMetaDataParser parser) {
        if (_annoXMLParser != null)
            _annoXMLParser.setRepository(null);
        if (parser != null)
            parser.setRepository(repos);
        _annoXMLParser = parser;
    }

    /**
     * Create a new JAXB XML annotation parser.
     */
    protected AnnotationPersistenceXMLMetaDataParser newXMLAnnotationParser() {
        return new AnnotationPersistenceXMLMetaDataParser
            (repos.getConfiguration());
    }

    @Override
    public void loadXMLMetaData(Class<?> cls) {
        AnnotationPersistenceXMLMetaDataParser parser
            = getXMLAnnotationParser();
        parser.parse(cls);
    }

    private static String UNDERSCORE = "_";

    @Override
    public String getManagedClassName(String mmClassName) {
        if (mmClassName == null || mmClassName.length() == 0)
            return null;
        if (mmClassName.endsWith(UNDERSCORE))
            return mmClassName.substring(0, mmClassName.length()-1);
        return mmClassName;
    }

    @Override
    public String getMetaModelClassName(String managedClassName) {
        if (managedClassName == null || managedClassName.length() == 0)
            return null;
        return managedClassName + UNDERSCORE;
    }

    @Override
    public boolean isMetaClass(Class<?> c) {
        return c != null && c.getAnnotation(StaticMetamodel.class) != null;
    }

    @Override
    public Class<?> getManagedClass(Class<?> c) {
        if (isMetaClass(c)) {
            return c.getAnnotation(StaticMetamodel.class).value();
        }
        return null;
    }
}
