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

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.Nullable;

/**
 * Utility classloader that has ability to load classes from external resources.
 */
@SuppressWarnings({"CustomClassloader"})
public class GridTestExternalClassLoader extends URLClassLoader {
    /** */
    private Set<String> excludeClassNames;

    /** */
    private Map<String, byte[]> resourceMap;

    /** */
    private long timeout;

    /**
     * Constructor.
     * @param urls the URLs from which to load classes and resources.
     * @param excludeClassNames list of excluded classes.
     */
    public GridTestExternalClassLoader(URL[] urls, String... excludeClassNames) {
        this(urls, Collections.<String, byte[]>emptyMap(), excludeClassNames);
    }

    /**
     * Constructor.
     * @param urls the URLs from which to load classes and resources.
     * @param resourceMap mapped resources.
     */
    public GridTestExternalClassLoader(URL[] urls, Map<String, byte[]> resourceMap) {
        this(urls, resourceMap, Collections.<String>emptySet());
    }

    /**
     * Constructor.
     * @param urls the URLs from which to load classes and resources.
     * @param resourceMap Resource map.
     * @param excludeClassNames list of excluded classes.
     */
    public GridTestExternalClassLoader(URL[] urls, Map<String, byte[]> resourceMap, String... excludeClassNames) {
        this(urls, resourceMap, new HashSet<>(Arrays.asList(excludeClassNames)));
    }

    /**
     * Constructor.
     * @param urls the URLs from which to load classes and resources.
     * @param resourceMap Resource map.
     * @param excludeClassNames list of excluded classes.
     */
    public GridTestExternalClassLoader(URL[] urls, Map<String, byte[]> resourceMap, Set<String> excludeClassNames) {
        super(urls, GridTestExternalClassLoader.class.getClassLoader());

        this.excludeClassNames = excludeClassNames;

        assert resourceMap != null;

        this.resourceMap = resourceMap;
    }

    /**
     * Sets set of excluded resource paths.
     * @param excludeClassNames excluded resource paths.
     */
    public void setExcludeClassNames(Set<String> excludeClassNames) {
        this.excludeClassNames = excludeClassNames;
    }

    /**
     * Sets set of excluded resource paths.
     * @param excludeClassNames excluded resource paths.
     */
    public void setExcludeClassNames(String... excludeClassNames) {
        setExcludeClassNames(new HashSet<>(Arrays.asList(excludeClassNames)));
    }

    /**
     * @param timeout Timeout.
     */
    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    /**
     * Sleep {@code timeout} period of time.
     */
    private void doTimeout() {
        try {
            Thread.sleep(timeout);
        }
        catch (InterruptedException e) {
            throw new RuntimeException("Thread was interrupted", e);
        }
    }

    /**
     * @param resName Resource name.
     * @return Class name.
     */
    private String resNameToClassName(String resName) {
        if (resName.endsWith(".class"))
            resName = resName.substring(0, resName.length() - ".class".length());

        return resName.replace('/', '.');
    }

    /** {@inheritDoc} */
    @Override protected Class<?> findClass(String name) throws ClassNotFoundException {
        for (String s : excludeClassNames)
            if (s.equals(name))
                throw new ClassNotFoundException(name);

        return super.findClass(name);
    }

    /** {@inheritDoc} */
    @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        if (excludeClassNames.contains(name))
            throw new ClassNotFoundException(name);

        return super.loadClass(name, resolve);
    }

    /** {@inheritDoc} */
    @Nullable @Override public URL findResource(String name) {
        if (excludeClassNames.contains(resNameToClassName(name)))
            return null;

        return super.findResource(name);
    }

    /** {@inheritDoc} */
    @Override public InputStream getResourceAsStream(String name) {
        doTimeout();

        byte[] res = resourceMap.get(name);

        return res == null ? super.getResourceAsStream(name) : new ByteArrayInputStream(res);
    }

    /**
     * @param resourceMap mapped resources.
     */
    public void setResourceMap(Map<String, byte[]> resourceMap) {
        this.resourceMap = resourceMap;
    }

    /**
     * Returns an Enumeration of URLs representing all of the resources on the URL search path having the specified name.
     *
     * @param name the resource name.
     * @return an {@code Enumeration} of {@code URL}s.
     * @throws IOException if an I/O exception occurs.
     */
    @Override public Enumeration<URL> findResources(String name) throws IOException {
        if (excludeClassNames.contains(resNameToClassName(name))) {
            return new Enumeration<URL>() {
                @Override public boolean hasMoreElements() {
                    return false;
                }

                @Override public URL nextElement() {
                    throw new UnsupportedOperationException();
                }
            };
        }

        return super.findResources(name);
    }
}
