/*   Copyright 2004 The Apache Software Foundation
 *
 *   Licensed 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.xmlbeans.impl.schema;

import org.apache.xmlbeans.*;
import org.apache.xmlbeans.impl.common.QNameHelper;
import org.apache.xmlbeans.impl.common.SystemCache;
import org.apache.xmlbeans.impl.common.XBeanDebug;
import org.apache.xmlbeans.impl.xb.xsdschema.SchemaDocument;

import javax.xml.namespace.QName;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.util.*;

import static org.apache.xmlbeans.impl.schema.SchemaTypeSystemImpl.METADATA_PACKAGE_GEN;

public class SchemaTypeLoaderImpl extends SchemaTypeLoaderBase {
    private final ResourceLoader _resourceLoader;
    private final ClassLoader _classLoader;
    private final SchemaTypeLoader[] _searchPath;

    private Map<String, SchemaTypeSystemImpl> _classpathTypeSystems;
    private Map<String, SchemaTypeSystemImpl> _classLoaderTypeSystems;
    private Map<QName, Object> _elementCache;
    private Map<QName, Object> _attributeCache;
    private Map<QName, Object> _modelGroupCache;
    private Map<QName, Object> _attributeGroupCache;
    private Map<QName, Object> _idConstraintCache;
    private Map<QName, Object> _typeCache;
    private Map<QName, Object> _documentCache;
    private Map<QName, Object> _attributeTypeCache;
    private Map<String, Object> _classnameCache;
    private final String _metadataPath;

    public static String METADATA_PACKAGE_LOAD = METADATA_PACKAGE_GEN;
    private static final Object CACHED_NOT_FOUND = new Object();

    private static final String[] basePackage = {"org.apache.xmlbeans.metadata", "schemaorg_apache_xmlbeans"};
    private static final String[] baseSchemas = {"sXMLCONFIG", "sXMLLANG", "sXMLSCHEMA", "sXMLTOOLS"};


    private static class SchemaTypeLoaderCache extends SystemCache {
        // The following maintains a cache of SchemaTypeLoaders per ClassLoader per Thread.
        // I use soft references to allow the garbage collector to reclain the type loaders
        // and/pr class loaders at will.

        private final ThreadLocal<List<SoftReference<SchemaTypeLoaderImpl>>> _cachedTypeSystems = ThreadLocal.withInitial(ArrayList::new);

        @Override
        public void clearThreadLocals() {
            _cachedTypeSystems.remove();

            super.clearThreadLocals();
        }

        public SchemaTypeLoader getFromTypeLoaderCache(ClassLoader cl) {
            List<SoftReference<SchemaTypeLoaderImpl>> a = _cachedTypeSystems.get();

            int candidate = -1;
            SchemaTypeLoaderImpl result = null;

            for (int i = 0; i < a.size(); i++) {
                SchemaTypeLoaderImpl tl = a.get(i).get();

                if (tl == null) {
                    a.remove(i--);
                } else if (tl._classLoader == cl) {
                    candidate = i;
                    result = tl;
                    break;
                }
            }

            // Make sure the most recently accessed entry is at the beginning of the array

            if (candidate > 0) {
                SoftReference<SchemaTypeLoaderImpl> t = a.get(0);
                a.set(0, a.get(candidate));
                a.set(candidate, t);
            }

            return result;
        }

        public void addToTypeLoaderCache(SchemaTypeLoader stl, ClassLoader cl) {
            assert (stl instanceof SchemaTypeLoaderImpl) &&
                   ((SchemaTypeLoaderImpl) stl)._classLoader == cl;

            List<SoftReference<SchemaTypeLoaderImpl>> a = _cachedTypeSystems.get();
            // Make sure this entry is at the top of the stack
            if (a.size() > 0) {
                SoftReference<SchemaTypeLoaderImpl> t = a.get(0);
                a.set(0, new SoftReference<>((SchemaTypeLoaderImpl) stl));
                a.add(t);
            } else {
                a.add(new SoftReference<>((SchemaTypeLoaderImpl) stl));
            }
        }
    }

    public static SchemaTypeLoaderImpl getContextTypeLoader() {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        SchemaTypeLoaderImpl result = (SchemaTypeLoaderImpl)
            SystemCache.get().getFromTypeLoaderCache(cl);

        if (result == null) {
            result =
                new SchemaTypeLoaderImpl(
                    new SchemaTypeLoader[]{BuiltinSchemaTypeSystem.get()}, null, cl, null);
            SystemCache.get().addToTypeLoaderCache(result, cl);
        }

        return result;
    }

    public static SchemaTypeLoader build(SchemaTypeLoader[] searchPath, ResourceLoader resourceLoader, ClassLoader classLoader) {
        return build(searchPath, resourceLoader, classLoader, null);
    }

    /**
     * Initialize a SchemaTypeLoader via the given loaders and paths
     *
     * @param searchPath     the searchPath to use
     * @param resourceLoader the resourceLoader to use
     * @param classLoader    the classLoader to use
     * @param metadataPath   the custom metadata path
     * @return the schemaTypeLoader
     * @since XmlBeans 3.1.0
     */
    public static SchemaTypeLoader build(final SchemaTypeLoader[] searchPath, ResourceLoader resourceLoader, ClassLoader classLoader, String metadataPath) {
        // assemble a flattened search path with no duplicates
        SubLoaderList list = new SubLoaderList();

        list.add(searchPath);

        ClassLoader cl = (classLoader == null) ? SchemaDocument.class.getClassLoader() : classLoader;

        for (String prefix : basePackage) {
            for (String holder : baseSchemas) {
                String clName = prefix + ".system." + holder + ".TypeSystemHolder";
                if (cl.getResource(clName.replace(".", "/") + ".class") == null) {
                    // if the first class isn't found in the package, continue with the next package
                    break;
                }
                try {
                    @SuppressWarnings("unchecked")
                    Class<? extends SchemaTypeLoader> cls = (Class<? extends SchemaTypeLoader>) Class.forName(clName, true, cl);
                    list.add((SchemaTypeLoader) cls.getDeclaredField("typeSystem").get(null));
                } catch (Exception e) {
                    throw new XmlRuntimeException(e);
                }
            }
        }

        return new SchemaTypeLoaderImpl(list.toArray(), resourceLoader, classLoader, metadataPath);
    }

    /**
     * Just used to avoid duplicate path entries
     */
    private static class SubLoaderList {
        private final List<SchemaTypeLoader> theList = new ArrayList<>();
        private final Map<SchemaTypeLoader, Object> seen = new IdentityHashMap<>();

        void add(SchemaTypeLoader[] searchPath) {
            if (searchPath == null) {
                return;
            }
            for (SchemaTypeLoader stl : searchPath) {
                if (stl instanceof SchemaTypeLoaderImpl) {
                    SchemaTypeLoaderImpl sub = (SchemaTypeLoaderImpl) stl;
                    if (sub._classLoader != null || sub._resourceLoader != null) {
                        add(sub);
                    } else {
                        add(sub._searchPath);
                    }
                } else {
                    add(stl);
                }
            }
        }

        void add(SchemaTypeLoader loader) {
            if (loader != null && !seen.containsKey(loader)) {
                theList.add(loader);
                seen.put(loader, null);
            }
        }

        SchemaTypeLoader[] toArray() {
            return theList.toArray(EMPTY_SCHEMATYPELOADER_ARRAY);
        }
    }

    /**
     * Constructs a SchemaTypeLoaderImpl that searches for objects
     * in the following order:
     * <p>
     * (1) First on the searchPath of other SchemaTypeSystems supplied,
     * in order that they are listed.
     * (2) Next on the classpath of .jar files or directories supplied,
     * in the order that they are listed. When types are returned in
     * this way, they are instantiated from a private typesystem.
     * In other words, if a type is loaded from another SchemaTypeLoaderImpl
     * that was initialized on the same file, the instance of the type will
     * be different.
     * (3) Finally on the classloader supplied.
     */
    private SchemaTypeLoaderImpl(SchemaTypeLoader[] searchPath, ResourceLoader resourceLoader, ClassLoader classLoader, String metadataPath) {
        _searchPath = (searchPath == null) ? EMPTY_SCHEMATYPELOADER_ARRAY : searchPath;
        _resourceLoader = resourceLoader;
        _classLoader = classLoader;

        if (metadataPath != null) {
            this._metadataPath = metadataPath;
        } else {
            final String path26 = "schema" + METADATA_PACKAGE_LOAD.replace("/", "_");
            this._metadataPath = (isPath30(_classLoader)) ? METADATA_PACKAGE_LOAD : path26;
        }

        initCaches();
    }

    private static boolean isPath30(ClassLoader loader) {
        final String path30 = METADATA_PACKAGE_LOAD + "/system";
        final ClassLoader cl = (loader != null) ? loader : SchemaDocument.class.getClassLoader();
        return cl.getResource(path30) != null;
    }

    /**
     * Initializes the caches.
     */
    private void initCaches() {
        _classpathTypeSystems = Collections.synchronizedMap(new HashMap<>());
        _classLoaderTypeSystems = Collections.synchronizedMap(new HashMap<>());
        _elementCache = Collections.synchronizedMap(new HashMap<>());
        _attributeCache = Collections.synchronizedMap(new HashMap<>());
        _modelGroupCache = Collections.synchronizedMap(new HashMap<>());
        _attributeGroupCache = Collections.synchronizedMap(new HashMap<>());
        _idConstraintCache = Collections.synchronizedMap(new HashMap<>());
        _typeCache = Collections.synchronizedMap(new HashMap<>());
        _documentCache = Collections.synchronizedMap(new HashMap<>());
        _attributeTypeCache = Collections.synchronizedMap(new HashMap<>());
        _classnameCache = Collections.synchronizedMap(new HashMap<>());
    }

    SchemaTypeSystemImpl typeSystemForComponent(String searchdir, QName name) {
        String searchfor = searchdir + QNameHelper.hexsafedir(name) + ".xsb";
        String tsname = null;

        if (_resourceLoader != null) {
            tsname = crackEntry(_resourceLoader, searchfor);
        }

        if (_classLoader != null) {
            tsname = crackEntry(_classLoader, searchfor);
        }

        if (tsname != null) {
            return (SchemaTypeSystemImpl) typeSystemForName(tsname);
        }

        return null;
    }

    public SchemaTypeSystem typeSystemForName(String name) {
        if (_resourceLoader != null) {
            SchemaTypeSystem result = getTypeSystemOnClasspath(name);
            if (result != null) {
                return result;
            }
        }

        if (_classLoader != null) {
            SchemaTypeSystem result = getTypeSystemOnClassloader(name);
            if (result != null) {
                return result;
            }
        }
        return null;
    }

    SchemaTypeSystemImpl typeSystemForClassname(String searchdir, String name) {
        String searchfor = searchdir + name.replace('.', '/') + ".xsb";

        if (_resourceLoader != null) {
            String tsname = crackEntry(_resourceLoader, searchfor);
            if (tsname != null) {
                return getTypeSystemOnClasspath(tsname);
            }
        }

        if (_classLoader != null) {
            String tsname = crackEntry(_classLoader, searchfor);
            if (tsname != null) {
                return getTypeSystemOnClassloader(tsname);
            }
        }

        return null;
    }

    SchemaTypeSystemImpl getTypeSystemOnClasspath(String name) {
        return _classpathTypeSystems.computeIfAbsent(name, n -> new SchemaTypeSystemImpl(_resourceLoader, n, this));
    }

    SchemaTypeSystemImpl getTypeSystemOnClassloader(String name) {
        XBeanDebug.trace(XBeanDebug.TRACE_SCHEMA_LOADING, "Finding type system " + name + " on classloader", 0);
        SchemaTypeSystemImpl result = _classLoaderTypeSystems.get(name);
        if (result == null) {
            XBeanDebug.trace(XBeanDebug.TRACE_SCHEMA_LOADING, "Type system " + name + " not cached - consulting field", 0);
            result = SchemaTypeSystemImpl.forName(name, _classLoader);
            _classLoaderTypeSystems.put(name, result);
        }
        return result;
    }

    static String crackEntry(ResourceLoader loader, String searchfor) {
        InputStream is = loader.getResourceAsStream(searchfor);
        return is == null ? null : crackPointer(is);
    }

    static String crackEntry(ClassLoader loader, String searchfor) {
        InputStream stream = loader.getResourceAsStream(searchfor);
        return stream == null ? null : crackPointer(stream);
    }

    static String crackPointer(InputStream stream) {
        return SchemaTypeSystemImpl.crackPointer(stream);
    }

    public boolean isNamespaceDefined(String namespace) {
        for (SchemaTypeLoader schemaTypeLoader : _searchPath) {
            if (schemaTypeLoader.isNamespaceDefined(namespace)) {
                return true;
            }
        }

        SchemaTypeSystem sts = typeSystemForComponent(_metadataPath + "/namespace/", new QName(namespace, "xmlns"));
        return (sts != null);
    }

    public SchemaType.Ref findTypeRef(QName name) {
        // The maps are synchronized, we use two accesses to the cache (one read and one write), but the code in-between
        // is not synchronized. The assumption is that the underlying datastructures (the search path and the classloader)
        // do not change, so two threads running the code in parallel will come up with the same result.
        Object cached = _typeCache.get(name);
        if (cached == CACHED_NOT_FOUND) {
            return null;
        }
        SchemaType.Ref result = (SchemaType.Ref) cached;
        if (result == null) {
            for (SchemaTypeLoader schemaTypeLoader : _searchPath) {
                if (null != (result = schemaTypeLoader.findTypeRef(name))) {
                    break;
                }
            }
            if (result == null) {
                SchemaTypeSystem ts = typeSystemForComponent(_metadataPath + "/type/", name);
                if (ts != null) {
                    result = ts.findTypeRef(name);
                    assert (result != null) : "Type system registered type " + QNameHelper.pretty(name) + " but does not return it";
                }
            }
            _typeCache.put(name, result == null ? CACHED_NOT_FOUND : result);
        }
        return result;
    }

    public SchemaType typeForClassname(String classname) {
        classname = classname.replace('$', '.');

        Object cached = _classnameCache.get(classname);
        if (cached == CACHED_NOT_FOUND) {
            return null;
        }
        SchemaType result = (SchemaType) cached;
        if (result == null) {
            for (SchemaTypeLoader schemaTypeLoader : _searchPath) {
                if (null != (result = schemaTypeLoader.typeForClassname(classname))) {
                    break;
                }
            }
            if (result == null) {
                SchemaTypeSystem ts = typeSystemForClassname(_metadataPath + "/javaname/", classname);
                if (ts != null) {
                    result = ts.typeForClassname(classname);
                    assert (result != null) : "Type system registered type " + classname + " but does not return it";
                }
            }
            _classnameCache.put(classname, result == null ? CACHED_NOT_FOUND : result);
        }
        return result;
    }

    public SchemaType.Ref findDocumentTypeRef(QName name) {
        Object cached = _documentCache.get(name);
        if (cached == CACHED_NOT_FOUND) {
            return null;
        }
        SchemaType.Ref result = (SchemaType.Ref) cached;
        if (result == null) {
            for (SchemaTypeLoader schemaTypeLoader : _searchPath) {
                if (null != (result = schemaTypeLoader.findDocumentTypeRef(name))) {
                    break;
                }
            }
            if (result == null) {
                SchemaTypeSystem ts = typeSystemForComponent(_metadataPath + "/element/", name);
                if (ts != null) {
                    result = ts.findDocumentTypeRef(name);
                    assert (result != null) : "Type system registered element " + QNameHelper.pretty(name) + " but does not contain document type";
                }
            }
            _documentCache.put(name, result == null ? CACHED_NOT_FOUND : result);
        }
        return result;
    }

    public SchemaType.Ref findAttributeTypeRef(QName name) {
        Object cached = _attributeTypeCache.get(name);
        if (cached == CACHED_NOT_FOUND) {
            return null;
        }
        SchemaType.Ref result = (SchemaType.Ref) cached;
        if (result == null) {
            for (SchemaTypeLoader schemaTypeLoader : _searchPath) {
                if (null != (result = schemaTypeLoader.findAttributeTypeRef(name))) {
                    break;
                }
            }
            if (result == null) {
                SchemaTypeSystem ts = typeSystemForComponent(_metadataPath + "/attribute/", name);
                if (ts != null) {
                    result = ts.findAttributeTypeRef(name);
                    assert (result != null) : "Type system registered attribute " + QNameHelper.pretty(name) + " but does not contain attribute type";
                }
            }
            _attributeTypeCache.put(name, result == null ? CACHED_NOT_FOUND : result);
        }
        return result;
    }

    public SchemaGlobalElement.Ref findElementRef(QName name) {
        Object cached = _elementCache.get(name);
        if (cached == CACHED_NOT_FOUND) {
            return null;
        }
        SchemaGlobalElement.Ref result = (SchemaGlobalElement.Ref) cached;
        if (result == null) {
            for (SchemaTypeLoader schemaTypeLoader : _searchPath) {
                if (null != (result = schemaTypeLoader.findElementRef(name))) {
                    break;
                }
            }
            if (result == null) {
                SchemaTypeSystem ts = typeSystemForComponent(_metadataPath + "/element/", name);
                if (ts != null) {
                    result = ts.findElementRef(name);
                    assert (result != null) : "Type system registered element " + QNameHelper.pretty(name) + " but does not return it";
                }
            }
            _elementCache.put(name, result == null ? CACHED_NOT_FOUND : result);
        }
        return result;
    }

    public SchemaGlobalAttribute.Ref findAttributeRef(QName name) {
        Object cached = _attributeCache.get(name);
        if (cached == CACHED_NOT_FOUND) {
            return null;
        }
        SchemaGlobalAttribute.Ref result = (SchemaGlobalAttribute.Ref) cached;
        if (result == null) {
            for (SchemaTypeLoader schemaTypeLoader : _searchPath) {
                if (null != (result = schemaTypeLoader.findAttributeRef(name))) {
                    break;
                }
            }
            if (result == null) {
                SchemaTypeSystem ts = typeSystemForComponent(_metadataPath + "/attribute/", name);
                if (ts != null) {
                    result = ts.findAttributeRef(name);
                    assert (result != null) : "Type system registered attribute " + QNameHelper.pretty(name) + " but does not return it";
                }
            }
            _attributeCache.put(name, result == null ? CACHED_NOT_FOUND : result);
        }
        return result;
    }

    public SchemaModelGroup.Ref findModelGroupRef(QName name) {
        Object cached = _modelGroupCache.get(name);
        if (cached == CACHED_NOT_FOUND) {
            return null;
        }
        SchemaModelGroup.Ref result = (SchemaModelGroup.Ref) cached;
        if (result == null) {
            for (SchemaTypeLoader schemaTypeLoader : _searchPath) {
                if (null != (result = schemaTypeLoader.findModelGroupRef(name))) {
                    break;
                }
            }
            if (result == null) {
                SchemaTypeSystem ts = typeSystemForComponent(_metadataPath + "/modelgroup/", name);
                if (ts != null) {
                    result = ts.findModelGroupRef(name);
                    assert (result != null) : "Type system registered model group " + QNameHelper.pretty(name) + " but does not return it";
                }
            }
            _modelGroupCache.put(name, result == null ? CACHED_NOT_FOUND : result);
        }
        return result;
    }

    public SchemaAttributeGroup.Ref findAttributeGroupRef(QName name) {
        Object cached = _attributeGroupCache.get(name);
        if (cached == CACHED_NOT_FOUND) {
            return null;
        }
        SchemaAttributeGroup.Ref result = (SchemaAttributeGroup.Ref) cached;
        if (result == null) {
            for (SchemaTypeLoader schemaTypeLoader : _searchPath) {
                if (null != (result = schemaTypeLoader.findAttributeGroupRef(name))) {
                    break;
                }
            }
            if (result == null) {
                SchemaTypeSystem ts = typeSystemForComponent(_metadataPath + "/attributegroup/", name);
                if (ts != null) {
                    result = ts.findAttributeGroupRef(name);
                    assert (result != null) : "Type system registered attribute group " + QNameHelper.pretty(name) + " but does not return it";
                }
            }
            _attributeGroupCache.put(name, result == null ? CACHED_NOT_FOUND : result);
        }
        return result;
    }

    public SchemaIdentityConstraint.Ref findIdentityConstraintRef(QName name) {
        Object cached = _idConstraintCache.get(name);
        if (cached == CACHED_NOT_FOUND) {
            return null;
        }
        SchemaIdentityConstraint.Ref result = (SchemaIdentityConstraint.Ref) cached;
        if (result == null) {
            for (SchemaTypeLoader schemaTypeLoader : _searchPath) {
                if (null != (result = schemaTypeLoader.findIdentityConstraintRef(name))) {
                    break;
                }
            }
            if (result == null) {
                SchemaTypeSystem ts = typeSystemForComponent(_metadataPath + "/identityconstraint/", name);
                if (ts != null) {
                    result = ts.findIdentityConstraintRef(name);
                    assert (result != null) : "Type system registered identity constraint " + QNameHelper.pretty(name) + " but does not return it";
                }
            }
            _idConstraintCache.put(name, result == null ? CACHED_NOT_FOUND : result);
        }
        return result;
    }

    public InputStream getSourceAsStream(String sourceName) {
        InputStream result = null;

        if (!sourceName.startsWith("/")) {
            sourceName = "/" + sourceName;
        }

        if (_resourceLoader != null) {
            result = _resourceLoader.getResourceAsStream(_metadataPath + "/src" + sourceName);
        }

        if (result == null && _classLoader != null) {
            return _classLoader.getResourceAsStream(_metadataPath + "/src" + sourceName);
        }

        return result;
    }

    private static final SchemaTypeLoader[] EMPTY_SCHEMATYPELOADER_ARRAY = new SchemaTypeLoader[0];

    static {
        if (SystemCache.get() != null) {
            SystemCache.set(new SchemaTypeLoaderCache());
        }
    }
}
