/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.painless;

import org.apache.logging.log4j.Logger;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.painless.Definition.Field;
import org.elasticsearch.painless.Definition.Method;
import org.elasticsearch.painless.Definition.Struct;
import org.elasticsearch.painless.Definition.Type;
import org.elasticsearch.painless.api.Augmentation;

import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Consumer;

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;

/**
 * Generates an API reference from the method and type whitelists in {@link Definition}.
 */
public class PainlessDocGenerator {
    private static final Logger logger = ESLoggerFactory.getLogger(PainlessDocGenerator.class);
    private static final Comparator<Field> FIELD_NAME = comparing(f -> f.name);
    private static final Comparator<Method> METHOD_NAME = comparing(m -> m.name);
    private static final Comparator<Method> NUMBER_OF_ARGS = comparing(m -> m.arguments.size());

    public static void main(String[] args) throws IOException {
        Path apiRootPath = PathUtils.get(args[0]);

        // Blow away the last execution and recreate it from scratch
        IOUtils.rm(apiRootPath);
        Files.createDirectories(apiRootPath);

        Path indexPath = apiRootPath.resolve("index.asciidoc");
        logger.info("Starting to write [index.asciidoc]");
        try (PrintStream indexStream = new PrintStream(
                Files.newOutputStream(indexPath, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE),
                false, StandardCharsets.UTF_8.name())) {
            emitGeneratedWarning(indexStream);
            List<Type> types = Definition.allSimpleTypes().stream().sorted(comparing(t -> t.name)).collect(toList());
            for (Type type : types) {
                if (type.sort.primitive) {
                    // Primitives don't have methods to reference
                    continue;
                }
                if ("def".equals(type.name)) {
                    // def is special but doesn't have any methods all of its own.
                    continue;
                }
                indexStream.print("include::");
                indexStream.print(type.struct.name);
                indexStream.println(".asciidoc[]");

                Path typePath = apiRootPath.resolve(type.struct.name + ".asciidoc");
                logger.info("Writing [{}.asciidoc]", type.name);
                try (PrintStream typeStream = new PrintStream(
                        Files.newOutputStream(typePath, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE),
                        false, StandardCharsets.UTF_8.name())) {
                    emitGeneratedWarning(typeStream);
                    typeStream.print("[[");
                    emitAnchor(typeStream, type.struct);
                    typeStream.print("]]++");
                    typeStream.print(type.name);
                    typeStream.println("++::");

                    Consumer<Field> documentField = field -> PainlessDocGenerator.documentField(typeStream, field);
                    Consumer<Method> documentMethod = method -> PainlessDocGenerator.documentMethod(typeStream, method);
                    type.struct.staticMembers.values().stream().sorted(FIELD_NAME).forEach(documentField);
                    type.struct.members.values().stream().sorted(FIELD_NAME).forEach(documentField);
                    type.struct.staticMethods.values().stream().sorted(METHOD_NAME.thenComparing(NUMBER_OF_ARGS)).forEach(documentMethod);
                    type.struct.constructors.values().stream().sorted(NUMBER_OF_ARGS).forEach(documentMethod);
                    Map<String, Struct> inherited = new TreeMap<>();
                    type.struct.methods.values().stream().sorted(METHOD_NAME.thenComparing(NUMBER_OF_ARGS)).forEach(method -> {
                        if (method.owner == type.struct) {
                            documentMethod(typeStream, method);
                        } else {
                            inherited.put(method.owner.name, method.owner);
                        }
                    });

                    if (false == inherited.isEmpty()) {
                        typeStream.print("* Inherits methods from ");
                        boolean first = true;
                        for (Struct inheritsFrom : inherited.values()) {
                            if (first) {
                                first = false;
                            } else {
                                typeStream.print(", ");
                            }
                            typeStream.print("++");
                            emitStruct(typeStream, inheritsFrom);
                            typeStream.print("++");
                        }
                        typeStream.println();
                    }
                }
            }
        }
        logger.info("Done writing [index.asciidoc]");
    }

    private static void documentField(PrintStream stream, Field field) {
        stream.print("** [[");
        emitAnchor(stream, field);
        stream.print("]]");

        if (Modifier.isStatic(field.modifiers)) {
            stream.print("static ");
        }

        emitType(stream, field.type);
        stream.print(' ');

        String javadocRoot = javadocRoot(field);
        emitJavadocLink(stream, javadocRoot, field);
        stream.print('[');
        stream.print(field.name);
        stream.print(']');

        if (javadocRoot.equals("java8")) {
            stream.print(" (");
            emitJavadocLink(stream, "java9", field);
            stream.print("[java 9])");
        }

        stream.println();
    }

    /**
     * Document a method.
     */
    private static void documentMethod(PrintStream stream, Method method) {
        stream.print("* ++[[");
        emitAnchor(stream, method);
        stream.print("]]");

        if (false == method.augmentation && Modifier.isStatic(method.modifiers)) {
            stream.print("static ");
        }

        if (false == method.name.equals("<init>")) {
            emitType(stream, method.rtn);
            stream.print(' ');
        }

        String javadocRoot = javadocRoot(method);
        emitJavadocLink(stream, javadocRoot, method);
        stream.print('[');

        stream.print(methodName(method));

        stream.print("](");
        boolean first = true;
        for (Type arg : method.arguments) {
            if (first) {
                first = false;
            } else {
                stream.print(", ");
            }
            emitType(stream, arg);
        }
        stream.print(")++");

        if (javadocRoot.equals("java8")) {
            stream.print(" (");
            emitJavadocLink(stream, "java9", method);
            stream.print("[java 9])");
        }

        stream.println();
    }

    /**
     * Anchor text for a {@link Struct}.
     */
    private static void emitAnchor(PrintStream stream, Struct struct) {
        stream.print("painless-api-reference-");
        stream.print(struct.name.replace('.', '-'));
    }

    /**
     * Anchor text for a {@link Method}.
     */
    private static void emitAnchor(PrintStream stream, Method method) {
        emitAnchor(stream, method.owner);
        stream.print('-');
        stream.print(methodName(method));
        stream.print('-');
        stream.print(method.arguments.size());
    }

    /**
     * Anchor text for a {@link Field}.
     */
    private static void emitAnchor(PrintStream stream, Field field) {
        emitAnchor(stream, field.owner);
        stream.print('-');
        stream.print(field.name);
    }

    private static String methodName(Method method) {
        return method.name.equals("<init>") ? method.owner.name : method.name;
    }

    /**
     * Emit a {@link Type}. If the type is primitive or an array of primitives this just emits the name of the type. Otherwise this emits an
     * internal link with the text.
     */
    private static void emitType(PrintStream stream, Type type) {
        emitStruct(stream, type.struct);
        for (int i = 0; i < type.dimensions; i++) {
            stream.print("[]");
        }
    }

    /**
     * Emit a {@link Struct}. If the {@linkplain Struct} is primitive or def this just emits the name of the struct. Otherwise this emits an
     * internal link with the name.
     */
    private static void emitStruct(PrintStream stream, Struct struct) {
        if (false == struct.clazz.isPrimitive() && false == struct.name.equals("def")) {
            stream.print("<<");
            emitAnchor(stream, struct);
            stream.print(',');
            stream.print(struct.name);
            stream.print(">>");
        } else {
            stream.print(struct.name);
        }
    }

    /**
     * Emit an external link to Javadoc for a {@link Method}.
     *
     * @param root name of the root uri variable
     */
    private static void emitJavadocLink(PrintStream stream, String root, Method method) {
        stream.print("link:{");
        stream.print(root);
        stream.print("-javadoc}/");
        stream.print((method.augmentation ? Augmentation.class : method.owner.clazz).getName().replace('.', '/'));
        stream.print(".html#");
        stream.print(methodName(method));
        stream.print("%2D");
        boolean first = true;
        if (method.augmentation) {
            first = false;
            stream.print(method.owner.clazz.getName());
        }
        for (Type arg: method.arguments) {
            if (first) {
                first = false;
            } else {
                stream.print("%2D");
            }
            stream.print(arg.struct.clazz.getName());
            if (arg.dimensions > 0) {
                stream.print(":A");
            }
        }
        stream.print("%2D");
    }

    /**
     * Emit an external link to Javadoc for a {@link Field}.
     *
     * @param root name of the root uri variable
     */
    private static void emitJavadocLink(PrintStream stream, String root, Field field) {
        stream.print("link:{");
        stream.print(root);
        stream.print("-javadoc}/");
        stream.print(field.owner.clazz.getName().replace('.', '/'));
        stream.print(".html#");
        stream.print(field.javaName);
    }

    /**
     * Pick the javadoc root for a {@link Method}.
     */
    private static String javadocRoot(Method method) {
        if (method.augmentation) {
            return "painless";
        }
        return javadocRoot(method.owner);
    }

    /**
     * Pick the javadoc root for a {@link Field}.
     */
    private static String javadocRoot(Field field) {
        return javadocRoot(field.owner);
    }

    /**
     * Pick the javadoc root for a {@link Struct}.
     */
    private static String javadocRoot(Struct struct) {
        String classPackage = struct.clazz.getPackage().getName();
        if (classPackage.startsWith("java")) {
            return "java8";
        }
        if (classPackage.startsWith("org.elasticsearch.painless")) {
            return "painless";
        }
        if (classPackage.startsWith("org.elasticsearch")) {
            return "elasticsearch";
        }
        if (classPackage.startsWith("org.joda.time")) {
            return "joda-time";
        }
        if (classPackage.startsWith("org.apache.lucene")) {
            return "lucene-core";
        }
        throw new IllegalArgumentException("Unrecognized packge: " + classPackage);
    }

    private static void emitGeneratedWarning(PrintStream stream) {
        stream.println("////");
        stream.println("Automatically generated by PainlessDocGenerator. Do not edit.");
        stream.println("Rebuild by running `gradle generatePainlessApi`.");
        stream.println("////");
        stream.println();
    }
}
