/*
 * 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.jena.sparql.util;

import org.apache.jena.atlas.lib.Lib ;
import org.apache.jena.atlas.logging.Log;
import org.apache.jena.sparql.ARQConstants ;
import org.apache.jena.sparql.ARQInternalErrorException ;

/**
 * Helper for loading class instances
 * <p>
 * This is primarily used as a helper by {@link MappedLoader} to dynamically
 * load in functions without a need to pre-register them. Since these class
 * names originate from URIs which may contain characters which are not valid in
 * Java class names this class implements a simple escaping scheme.
 * </p>
 * <h3>Escaping Scheme</h3>
 * <p>
 * Escaping is applied only to the last portion of the class name, typically
 * {@link MappedLoader} takes care of mapping a function library namespace
 * prefix into a java package name and likely only the last portion (the
 * function name) will require escaping.
 * </p>
 * <p>
 * If the first character of the class name is invalid it is replaced with
 * {@code F_}. If any subsequent characters are invalid they are ignored and the
 * subsequent valid character (if any) is promoted to upper case giving a camel
 * case style valid class name.
 * </p>
 * <p>
 * For example if the last portion of the class name were {@code foo-bar-faz}
 * then we would end up with an escaped class name of {@code fooBarFaz}.
 * </p>
 */
public class ClsLoader {
    static public Class<?> loadClass(String classNameOrURI) {
        return loadClass(classNameOrURI, null);
    }

    static public Class<?> loadClass(String classNameOrURI, Class<?> requiredClass) {
        if (classNameOrURI == null)
            throw new ARQInternalErrorException("Null classNameorIRI");

        if (classNameOrURI.startsWith("http:"))
            return null;
        if (classNameOrURI.startsWith("urn:"))
            return null;

        String className = classNameOrURI;

        if (classNameOrURI.startsWith(ARQConstants.javaClassURIScheme))
            className = classNameOrURI.substring(ARQConstants.javaClassURIScheme.length());

        Class<?> classObj = null;

        try {
            classObj = Class.forName(className);
        } catch (ClassNotFoundException ex) {
            // It is possible that when coming from a URI we might have
            // characters which aren't valid as Java identifiers
            // We should see if we can load the class with the escaped class
            // name instead
            String baseUri = className.substring(0, className.lastIndexOf('.') + 1);
            String escapedClassName = escape(className.substring(className.lastIndexOf('.') + 1));
            try {
                classObj = Class.forName(baseUri + escapedClassName);
            } catch (ClassNotFoundException innerEx) {
                // Ignore, handled in the outer catch
            }

            if (classObj == null) {
                Log.warn(ClsLoader.class, "Class not found: " + className);
                return null;
            }
        }

        if (requiredClass != null && !requiredClass.isAssignableFrom(classObj)) {
            Log.warn(ClsLoader.class, "Class '" + className + "' found but not a " + Lib.classShortName(requiredClass));
            return null;
        }
        return classObj;
    }

    static public Object loadAndInstantiate(String uri, Class<?> requiredClass) {
        Class<?> classObj = loadClass(uri, requiredClass);
        if (classObj == null)
            return null;

        Object module = null;
        try {
            module = classObj.getConstructor().newInstance();
        } catch (Exception ex) {
            String className = uri.substring(ARQConstants.javaClassURIScheme.length());
            Log.warn(ClsLoader.class, "Exception during instantiation '" + className + "': " + ex.getMessage());
            return null;
        }
        return module;
    }

    static public String escape(String className) {
        StringBuilder builder = new StringBuilder();
        boolean upgrade = false;

        for (int offset = 0; offset < className.length();) {
            int cp = className.codePointAt(offset);
            if (builder.length() == 0) {
                if (Character.isJavaIdentifierStart(cp)) {
                    // Start character is valid
                    builder.append(Character.toChars(cp));
                } else {
                    // Illegal start character so use F_ as prefix
                    builder.append("F_");
                }
            } else {
                if (Character.isJavaIdentifierPart(cp)) {
                    // Upgrade to upper case if previous character was illegal
                    if (upgrade) {
                        cp = Character.toUpperCase(cp);
                        upgrade = false;
                    }
                    // Valid character
                    builder.append(Character.toChars(cp));
                } else {
                    // Skip illegal characters, the next valid character will be
                    // upgraded to upper case
                    upgrade = true;
                }
            }

            offset += Character.charCount(cp);
        }

        return builder.toString();
    }
}
