/*
 * 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.camel.maven;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.text.html.parser.DTD;

import org.apache.camel.support.component.ApiMethodParser;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;

/**
 * Parses ApiMethod signatures from Javadoc.
 */
@Mojo(name = "fromJavadoc", requiresDependencyResolution = ResolutionScope.TEST, requiresProject = true,
        defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true)
public class JavadocApiMethodGeneratorMojo extends AbstractApiMethodGeneratorMojo {

    static {
        // set Java AWT to headless before using Swing HTML parser
        System.setProperty("java.awt.headless", "true");
    }

    protected static final String DEFAULT_EXCLUDE_PACKAGES = "javax?\\.lang.*";
    private static final Pattern RAW_ARGTYPES_PATTERN = Pattern.compile("\\s*([^<\\s,]+)\\s*(<[^>]+>)?\\s*,?");

    @Parameter(property = PREFIX + "excludePackages", defaultValue = DEFAULT_EXCLUDE_PACKAGES)
    protected String excludePackages;

    @Parameter(property = PREFIX + "excludeClasses")
    protected String excludeClasses;

    @Parameter(property = PREFIX + "includeMethods")
    protected String includeMethods;

    @Parameter(property = PREFIX + "excludeMethods")
    protected String excludeMethods;

    @Parameter(property = PREFIX + "includeStaticMethods")
    protected Boolean includeStaticMethods;

    @Override
    public List<String> getSignatureList() throws MojoExecutionException {
        // signatures as map from signature with no arg names to arg names from JavadocParser
        Map<String, String> result = new HashMap<>();

        final Pattern packagePatterns = Pattern.compile(excludePackages);
        final Pattern classPatterns = (excludeClasses != null) ? Pattern.compile(excludeClasses) : null;
        final Pattern includeMethodPatterns = (includeMethods != null) ? Pattern.compile(includeMethods) : null;
        final Pattern excludeMethodPatterns = (excludeMethods != null) ? Pattern.compile(excludeMethods) : null;

        // for proxy class and super classes not matching excluded packages or classes
        for (Class<?> aClass = getProxyType();
             aClass != null && !packagePatterns.matcher(aClass.getPackage().getName()).matches()
                     && (classPatterns == null || !classPatterns.matcher(aClass.getSimpleName()).matches());
             aClass = aClass.getSuperclass()) {

            log.debug("Processing " + aClass.getName());
            final String javaDocPath = aClass.getName().replace('.', '/').replace('$', '.') + ".html";

            // read javadoc html text for class

            try (InputStream inputStream = getProjectClassLoader().getResourceAsStream(javaDocPath)) {
                if (inputStream == null) {
                    log.debug("JavaDoc not found on classpath for " + aClass.getName());
                    break;
                }
                // transform the HTML to get method summary as text
                // dummy DTD
                final DTD dtd = DTD.getDTD("html.dtd");
                final JavadocParser htmlParser = new JavadocParser(dtd, javaDocPath);
                htmlParser.parse(new InputStreamReader(inputStream, "UTF-8"));

                // look for parse errors
                final String parseError = htmlParser.getErrorMessage();
                if (parseError != null) {
                    throw new MojoExecutionException(parseError);
                }

                // get public method signature
                final Map<String, String> methodMap = htmlParser.getMethodText();
                for (String method : htmlParser.getMethods()) {
                    if (!result.containsKey(method)
                            && (includeMethodPatterns == null || includeMethodPatterns.matcher(method).find())
                            && (excludeMethodPatterns == null || !excludeMethodPatterns.matcher(method).find())) {

                        final int leftBracket = method.indexOf('(');
                        final String name = method.substring(0, leftBracket);
                        final String args = method.substring(leftBracket + 1, method.length() - 1);
                        String[] types;
                        if (args.isEmpty()) {
                            types = new String[0];
                        } else {
                            // get raw types from args
                            final List<String> rawTypes = new ArrayList<>();
                            final Matcher argTypesMatcher = RAW_ARGTYPES_PATTERN.matcher(args);
                            while (argTypesMatcher.find()) {
                                rawTypes.add(argTypesMatcher.group(1));
                            }
                            types = rawTypes.toArray(new String[rawTypes.size()]);
                        }
                        final String resultType = getResultType(aClass, name, types);
                        if (resultType != null) {
                            result.put(method, resultType + " " + name + methodMap.get(method));
                        }
                    }
                }
            } catch (IOException e) {
                throw new MojoExecutionException(e.getMessage(), e);
            }
        }

        if (result.isEmpty()) {
            throw new MojoExecutionException("No public non-static methods found, "
                    + "make sure Javadoc is available as project test dependency");
        }
        return new ArrayList<>(result.values());
    }

    private String getResultType(Class<?> aClass, String name, String[] types) throws MojoExecutionException {
        Class<?>[] argTypes = new Class<?>[types.length];
        final ClassLoader classLoader = getProjectClassLoader();
        for (int i = 0; i < types.length; i++) {
            try {
                try {
                    argTypes[i] = ApiMethodParser.forName(types[i], classLoader);
                } catch (ClassNotFoundException e) {
                    throw new MojoExecutionException(e.getMessage(), e);
                }
            } catch (IllegalArgumentException e) {
                throw new MojoExecutionException(e.getCause().getMessage(), e.getCause());
            }
        }

        // return null for non-public methods, and for non-static methods if includeStaticMethods is null or false
        String result = null;
        try {
            final Method method = aClass.getMethod(name, argTypes);
            int modifiers = method.getModifiers();
            if (!Modifier.isStatic(modifiers) || Boolean.TRUE.equals(includeStaticMethods)) {
                result = method.getReturnType().getName();
            }
        } catch (NoSuchMethodException e) {
            // could be a non-public method
            try {
                aClass.getDeclaredMethod(name, argTypes);
            } catch (NoSuchMethodException e1) {
                throw new MojoExecutionException(e1.getMessage(), e1);
            }
        }

        return result;
    }

}
