/*
 * 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
 *
 *      https://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.commons.jexl3.internal;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.introspection.JexlPropertyGet;
import org.apache.commons.jexl3.introspection.JexlUberspect;

/**
 * Helper resolving a simple class name into a Fully Qualified Class Name (hence FqcnResolver) using
 * package names and classes as roots of import.
 * <p>This only keeps the names of the classes to avoid any class loading/reloading/permissions issue.</p>
 */
public class FqcnResolver implements JexlUberspect.ClassConstantResolver {

    /**
     * The uberspect.
     */
    private final JexlUberspect uberspect;

    /**
     * The set of packages to be used as import roots.
     */
    private final Set<String> imports = Collections.synchronizedSet(new LinkedHashSet<>());

    /**
     * The map of solved fqcns based on imports keyed on (simple) name,
     * valued as fully qualified class name.
     */
    private final Map<String, String> fqcns = new ConcurrentHashMap<>();

    /**
     * Optional parent solver.
     */
    private final FqcnResolver parent;

    /**
     * Creates a class name solver.
     *
     * @param solver the parent solver
     * @throws NullPointerException if parent solver is null
     */
    FqcnResolver(final FqcnResolver solver) {
        this.parent = Objects.requireNonNull(solver, "solver");
        this.uberspect = solver.uberspect;
    }

    /**
     * Creates a class name solver.
     *
     * @param uber     the optional class loader
     * @param packages the optional package names
     */
    FqcnResolver(final JexlUberspect uber, final Iterable<String> packages) {
        this.uberspect = Objects.requireNonNull(uber, "uberspect");
        this.parent = null;
        importCheck(packages);
    }

    /**
     * Gets a fully qualified class name from a simple class name and imports.
     *
     * @param name the simple name
     * @return the fqcn
     */
    String getQualifiedName(final String name) {
        String fqcn;
        if (parent != null && (fqcn = parent.getQualifiedName(name)) != null) {
            return fqcn;
        }
        return fqcns.computeIfAbsent(name, this::solveClassName);
    }

    /**
     * Attempts to solve a fully qualified class name from a simple class name.
     * <p>It tries to solve the class name as package.classname or package$classname (inner class).</p>
     *
     * @param name the simple class name
     * @return the fully qualified class name or null if not found
     */
    private String solveClassName(final String name) {
        for (final String pkg : imports) {
            // try package.classname or fqcn$classname (inner class)
            for (final char dot : new char[]{'.', '$'}) {
                final Class<?> clazz = uberspect.getClassByName(pkg + dot + name);
                // solved it
                if (clazz != null) {
                    return clazz.getName();
                }
            }
        }
        return null;
    }

    /**
     * Adds a collection of packages/classes as import root, check each name point to one or the other.
     *
     * @param names the package names
     */
    private void importCheck(final Iterable<String> names) {
        if (names != null) {
            names.forEach(this::importCheck);
        }
    }

    /**
     * Adds a package as import root, checks the name points to a package or a class.
     *
     * @param name the package name
     */
    private void importCheck(final String name) {
        if (name == null || name.isEmpty()) {
            return;
        }
        // check the package name actually points to a package to avoid clutter
        final Package pkg = Package.getPackage(name);
        if (pkg == null) {
            // if it is a class, solve it now
            final Class<?> clazz = uberspect.getClassByName(name);
            if (clazz == null) {
                throw new JexlException(null, "Cannot import '" + name + "' as it is neither a package nor a class");
            }
            fqcns.put(name, clazz.getName());
        }
        imports.add(name);
    }

    /**
     * Imports a list of packages as solving roots.
     *
     * @param packages the packages
     * @return this solver
     */
    FqcnResolver importPackages(final Iterable<String> packages) {
        if (packages != null) {
            if (parent == null) {
                importCheck(packages);
            } else {
                packages.forEach(pkg -> {
                    if (!parent.isImporting(pkg)) {
                        importCheck(pkg);
                    }
                });
            }
        }
        return this;
    }

    /**
     * Checks is a package is imported by this solver of one of its ascendants.
     *
     * @param pkg the package name
     * @return true if an import exists for this package, false otherwise
     */
    boolean isImporting(final String pkg) {
        if (parent != null && parent.isImporting(pkg)) {
            return true;
        }
        return imports.contains(pkg);
    }

    @Override
    public String resolveClassName(final String name) {
        return getQualifiedName(name);
    }

    @Override
    public Object resolveConstant(final String cname) {
        return getConstant(cname.split("\\."));
    }

    private Object getConstant(final String... ids) {
        if (ids.length == 1) {
            final String pname = ids[0];
            for (final String cname : fqcns.keySet()) {
                final Object constant = getConstant(cname, pname);
                if (constant != JexlEngine.TRY_FAILED) {
                    return constant;
                }
            }
        } else if (ids.length == 2) {
            final String cname = ids[0];
            final String id = ids[1];
            final String fqcn = resolveClassName(cname);
            if (fqcn != null) {
                final Class<?> clazz = uberspect.getClassByName(fqcn);
                if (clazz != null) {
                    final JexlPropertyGet getter = uberspect.getPropertyGet(clazz, id);
                    if (getter != null && getter.isConstant()) {
                        try {
                            return getter.invoke(clazz);
                        } catch (final Exception xany) {
                            // ignore
                        }
                    }
                }
            }
        }
        return JexlEngine.TRY_FAILED;
    }
}
