package io.quarkus.logging;

import static java.net.http.HttpResponse.BodyHandlers;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.jboss.logging.BasicLogger;
import org.jboss.logging.Logger;

import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter;
import com.github.javaparser.ast.comments.LineComment;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName;
import com.github.javaparser.ast.stmt.BlockStmt;
import com.github.javaparser.ast.stmt.CatchClause;
import com.github.javaparser.ast.stmt.ReturnStmt;
import com.github.javaparser.ast.stmt.TryStmt;
import com.github.javaparser.ast.type.PrimitiveType;

public class GenerateLog {
    private static final String CLASS_JAVADOC = "" +
            "Copy of {@link org.jboss.logging.BasicLogger}.\n" +
            "Invocations of all {@code static} methods of this class are, during build time, replaced by invocations\n" +
            "of the same methods on a generated instance of {@link Logger}.";

    public static void main(String[] args) throws Exception {
        String source = BasicLogger.class.getProtectionDomain().getCodeSource().getLocation().getPath();
        Matcher matcher = Pattern.compile("\\d+\\.\\d+\\.\\d+\\.Final").matcher(source);
        if (matcher.find()) {
            String version = matcher.group();
            String url = "https://raw.githubusercontent.com/jboss-logging/jboss-logging/" + version
                    + "/src/main/java/org/jboss/logging/BasicLogger.java";
            HttpClient client = HttpClient.newBuilder()
                    .followRedirects(HttpClient.Redirect.NORMAL)
                    .connectTimeout(Duration.ofSeconds(10))
                    .build();
            HttpRequest request = HttpRequest.newBuilder(new URI(url)).build();
            HttpResponse<String> response = client.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8));
            if (response.statusCode() == 200) {
                generateLogClass(response.body());
            } else {
                throw new Exception("Failed fetching " + url);
            }
        } else {
            throw new Exception("Couldn't find JBoss Logging version in " + source);
        }
    }

    private static void generateLogClass(String templateSource) {
        CompilationUnit templateUnit = StaticJavaParser.parse(templateSource);
        ClassOrInterfaceDeclaration templateClass = (ClassOrInterfaceDeclaration) templateUnit.getTypes().get(0);

        CompilationUnit unit = new CompilationUnit();
        unit.setPackageDeclaration("io.quarkus.logging");
        unit.addImport(Logger.class);
        unit.addOrphanComment(new LineComment(" automatically generated by io.quarkus.logging.GenerateLog"));

        ClassOrInterfaceDeclaration clazz = unit.addClass("Log", Modifier.Keyword.PUBLIC, Modifier.Keyword.FINAL)
                .setJavadocComment(CLASS_JAVADOC);

        clazz.addFieldWithInitializer("StackWalker", "stackWalker",
                StaticJavaParser.parseExpression("StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)"),
                Modifier.Keyword.PRIVATE, Modifier.Keyword.STATIC, Modifier.Keyword.FINAL);

        clazz.addFieldWithInitializer(PrimitiveType.booleanType(), "shouldFail",
                StaticJavaParser.parseExpression("!isTest()"),
                Modifier.Keyword.PRIVATE, Modifier.Keyword.STATIC, Modifier.Keyword.FINAL);

        {
            MethodDeclaration method = clazz.addMethod("isTest");
            method.setPrivate(true);
            method.setStatic(true);
            method.setType(PrimitiveType.booleanType());
            BlockStmt body = new BlockStmt();

            BlockStmt tryPart = new BlockStmt();
            tryPart.addStatement("Class.forName(\"org.junit.jupiter.api.Assertions\");");
            tryPart.addStatement("return true;");

            BlockStmt catchPart = new BlockStmt();
            catchPart.addStatement("return false;");
            CatchClause catchClause = new CatchClause(
                    new Parameter(StaticJavaParser.parseType(ClassNotFoundException.class.getName()), "ignored"),
                    catchPart);

            body.addStatement(new TryStmt(tryPart, new NodeList<>(catchClause), null));
            method.setBody(body);
        }

        for (MethodDeclaration methodTemplate : templateClass.getMethods()) {
            MethodDeclaration method = clazz.addMethod(methodTemplate.getNameAsString());
            method.setJavadocComment(methodTemplate.getJavadoc().orElseThrow());
            method.setPublic(true);
            method.setStatic(true);
            method.setType(methodTemplate.getType());
            method.setParameters(methodTemplate.getParameters());
            BlockStmt body = new BlockStmt();
            body.addStatement("if (shouldFail) { throw fail(); }");
            Expression logger = StaticJavaParser
                    .parseExpression("Logger.getLogger(stackWalker.getCallerClass())");
            List<Expression> forwardParams = methodTemplate.getParameters()
                    .stream()
                    .map(NodeWithSimpleName::getNameAsExpression)
                    .collect(Collectors.toList());
            MethodCallExpr forwardCall = new MethodCallExpr(logger, methodTemplate.getName().getIdentifier(),
                    new NodeList<>(forwardParams));
            if (methodTemplate.getType().isVoidType()) {
                body.addStatement(forwardCall);
            } else {
                body.addStatement(new ReturnStmt(forwardCall));
            }
            method.setBody(body);
        }

        {
            MethodDeclaration method = clazz.addMethod("fail", Modifier.Keyword.PRIVATE, Modifier.Keyword.STATIC);
            method.setType(UnsupportedOperationException.class);
            BlockStmt body = new BlockStmt();
            body.addStatement("return new UnsupportedOperationException(\"Using " + Log.class.getName()
                    + " is only possible with Quarkus bytecode transformation\");");
            method.setBody(body);
        }

        System.out.println(unit);
    }
}
