/*
 * 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.maven.shared.jar.classes;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantUtf8;
import org.apache.bcel.classfile.EmptyVisitor;
import org.apache.bcel.classfile.JavaClass;
import org.apache.commons.collections4.list.SetUniqueList;

/**
 * Implementation of a BCEL class visitor that analyzes a class and collects imports.
 */
public class ImportVisitor extends EmptyVisitor {
    /**
     * The list of imports discovered.
     */
    private final List<String> imports;

    /**
     * The Java class that is being analyzed.
     */
    private final JavaClass javaClass;

    /**
     * Pattern to detect if the import is qualified and allows retrieval of the actual import name from the string via
     * the group 1.
     */
    private static final Pattern QUALIFIED_IMPORT_PATTERN = Pattern.compile("L([a-zA-Z][a-zA-Z0-9\\.]+);");

    /**
     * Pattern that checks whether a string is valid UTF-8. Imports that are not are ignored.
     */
    private static final Pattern VALID_UTF8_PATTERN = Pattern.compile("^[\\(\\)\\[A-Za-z0-9;/]+$");

    /**
     * Create an Import visitor.
     *
     * @param javaClass the javaclass to work from
     */
    public ImportVisitor(JavaClass javaClass) {
        this.javaClass = javaClass;

        // Create a list that is guaranteed to be unique while retaining it's list qualities (LinkedHashSet does not
        // expose the list interface even if natural ordering is retained)
        this.imports = SetUniqueList.setUniqueList(new ArrayList<>());
    }

    /**
     * Get the list of discovered imports.
     *
     * @return Returns the imports.
     */
    public List<String> getImports() {
        return imports;
    }

    /**
     * Find any formally declared import in the Constant Pool.
     *
     * @see org.apache.bcel.classfile.EmptyVisitor#visitConstantClass(org.apache.bcel.classfile.ConstantClass)
     */
    @Override
    public void visitConstantClass(ConstantClass constantClass) {
        String name = constantClass.getBytes(javaClass.getConstantPool());

        // only strings with '/' character are to be considered.
        if (name.indexOf('/') == -1) {
            return;
        }

        name = name.replace('/', '.');

        if (name.endsWith(".class")) {
            name = name.substring(0, name.length() - 6);
        }

        Matcher mat = QUALIFIED_IMPORT_PATTERN.matcher(name);
        if (mat.find()) {
            this.imports.add(mat.group(1));
        } else {
            this.imports.add(name);
        }
    }

    /**
     * Find any package class Strings in the UTF8 String Pool.
     *
     * @see org.apache.bcel.classfile.EmptyVisitor#visitConstantUtf8(org.apache.bcel.classfile.ConstantUtf8)
     */
    @Override
    public void visitConstantUtf8(ConstantUtf8 constantUtf8) {
        String ret = constantUtf8.getBytes().trim();

        // empty strings are not class names.
        if (ret.length() <= 0) {
            return;
        }

        // Only valid characters please.
        if (!VALID_UTF8_PATTERN.matcher(ret).matches()) {
            return;
        }

        // only strings with '/' character are to be considered.
        if (ret.indexOf('/') == -1) {
            return;
        }

        // Strings that start with '/' are bad too
        // Seen when Pool has regex patterns.
        if (ret.charAt(0) == '/') {
            return;
        }

        // Make string more class-like.
        ret = ret.replace('/', '.');

        // Double ".." indicates a bad class fail-fast.
        // Seen when ConstantUTF8 Pool has regex patterns.
        if (ret.contains("..")) {
            return;
        }

        Matcher mat = QUALIFIED_IMPORT_PATTERN.matcher(ret);
        char prefix = ret.charAt(0);

        if (prefix == '(') {
            // A Method Declaration.

            // Loop for each Qualified Class found.
            while (mat.find()) {
                this.imports.add(mat.group(1));
            }
        } else {
            // A Variable Declaration.
            if (mat.find()) {
                // Add a UTF8 Qualified Class reference.
                this.imports.add(mat.group(1));
            } else {
                // Add a simple Class reference.
                this.imports.add(ret);
            }
        }
    }
}
