/*
 * 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.quarkus.component.csimple.deployment;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.quarkus.bootstrap.classloading.ClassPathElement;
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.deployment.dev.CompilationProvider;
import io.quarkus.deployment.dev.CompilationProvider.Context;
import io.quarkus.deployment.dev.JavaCompilationProvider;
import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.deployment.recording.RecorderContext;
import io.quarkus.runtime.RuntimeValue;
import org.apache.camel.Exchange;
import org.apache.camel.language.csimple.CSimpleCodeGenerator;
import org.apache.camel.language.csimple.CSimpleGeneratedCode;
import org.apache.camel.language.csimple.CSimpleHelper;
import org.apache.camel.language.csimple.CSimpleLanguage;
import org.apache.camel.language.csimple.CSimpleLanguage.Builder;
import org.apache.camel.quarkus.component.csimple.CSimpleLanguageRecorder;
import org.apache.camel.quarkus.core.deployment.spi.CamelBeanBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelContextBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CompiledCSimpleExpressionBuildItem;
import org.apache.camel.quarkus.core.deployment.util.CamelSupport;
import org.apache.camel.quarkus.core.util.FileUtils;
import org.apache.camel.quarkus.support.language.deployment.ExpressionBuildItem;
import org.apache.camel.quarkus.support.language.deployment.ExpressionExtractionResultBuildItem;
import org.apache.camel.util.PropertiesHelper;
import org.jboss.logging.Logger;

class CSimpleProcessor {

    private static final Logger LOG = Logger.getLogger(CSimpleProcessor.class);
    private static final String CLASS_NAME = "CompiledExpression";
    private static final String PACKAGE_NAME = "org.apache.camel.quarkus.component.csimple.generated";
    static final String CLASS_EXT = ".class";
    private static final String FEATURE = "camel-csimple";

    @BuildStep
    FeatureBuildItem feature() {
        return new FeatureBuildItem(FEATURE);
    }

    @BuildStep
    void collectCSimpleExpressions(
            ExpressionExtractionResultBuildItem result,
            List<ExpressionBuildItem> expressions,
            BuildProducer<CSimpleExpressionSourceBuildItem> csimpleExpressions) {

        if (result.isSuccess()) {
            int counter = 0;
            for (ExpressionBuildItem expression : expressions) {
                if ("csimple".equals(expression.getLanguage())) {
                    csimpleExpressions.produce(
                            new CSimpleExpressionSourceBuildItem(
                                    expression.getExpression(),
                                    expression.isPredicate(),
                                    String.format("%s.%s_%d", PACKAGE_NAME, CLASS_NAME, ++counter)));
                }
            }
        }
    }

    @BuildStep
    void compileCSimpleExpressions(
            BuildSystemTargetBuildItem buildSystemTargetBuildItem,
            List<CSimpleExpressionSourceBuildItem> expressionSources,
            BuildProducer<CompiledCSimpleExpressionBuildItem> compiledCSimpleExpression,
            BuildProducer<GeneratedClassBuildItem> generatedClasses) throws IOException {

        if (!expressionSources.isEmpty()) {
            final Set<String> imports = new TreeSet<>();
            final Map<String, String> aliases = new LinkedHashMap<>();
            final ClassLoader loader = Thread.currentThread().getContextClassLoader();
            if (!(loader instanceof QuarkusClassLoader)) {
                throw new IllegalStateException(
                        QuarkusClassLoader.class.getSimpleName() + " expected as the context class loader");
            }
            final QuarkusClassLoader quarkusClassLoader = (QuarkusClassLoader) loader;
            readConfig(imports, aliases, loader);
            final CSimpleCodeGenerator generator = new CSimpleCodeGenerator();
            generator.setAliases(aliases);
            generator.setImports(imports);

            final Path projectDir = Paths.get(".").toAbsolutePath().normalize();
            final Path outputDirectory = buildSystemTargetBuildItem.getOutputDirectory();
            final Path csimpleGeneratedSourceDir = outputDirectory.resolve("generated/csimple");
            Files.createDirectories(csimpleGeneratedSourceDir);

            final Set<File> filesToCompile = new LinkedHashSet<>();

            /* We do not want to compile the same source twice, so we store here what we have compiled already */
            final Map<Boolean, Set<String>> compiledExpressions = new HashMap<>();
            compiledExpressions.put(true, new HashSet<>());
            compiledExpressions.put(false, new HashSet<>());

            /* Generate Java classes for the language expressions */
            for (CSimpleExpressionSourceBuildItem expr : expressionSources) {
                final boolean predicate = expr.isPredicate();
                final String script = expr.getSourceCode();
                if (!compiledExpressions.get(predicate).contains(script)) {
                    final CSimpleGeneratedCode code = predicate
                            ? generator.generatePredicate(expr.getClassNameBase(), script)
                            : generator.generateExpression(expr.getClassNameBase(), script);

                    compiledCSimpleExpression
                            .produce(new CompiledCSimpleExpressionBuildItem(code.getCode(), predicate, code.getFqn()));

                    final Path javaCsimpleFile = csimpleGeneratedSourceDir
                            .resolve(code.getFqn().replace('.', '/') + ".java");
                    Files.createDirectories(javaCsimpleFile.getParent());
                    Files.write(javaCsimpleFile, code.getCode().getBytes(StandardCharsets.UTF_8));
                    filesToCompile.add(javaCsimpleFile.toFile());
                    compiledExpressions.get(predicate).add(script);
                }
            }

            final Path csimpleClassesDir = outputDirectory.resolve("csimple-classes");
            Files.createDirectories(csimpleClassesDir);

            /* Compile the generated sources */
            try (JavaCompilationProvider compiler = new JavaCompilationProvider()) {
                final Context context = compilationContext(projectDir, csimpleClassesDir, quarkusClassLoader);
                compiler.compile(filesToCompile, context);
            }

            /* Register the compiled classes via Quarkus GeneratedClassBuildItem */
            try (Stream<Path> classFiles = Files.walk(csimpleClassesDir)) {
                classFiles
                        .filter(Files::isRegularFile)
                        .filter(p -> p.getFileName().toString().endsWith(CLASS_EXT))
                        .forEach(p -> {
                            final Path relPath = csimpleClassesDir.relativize(p);
                            String className = FileUtils.nixifyPath(relPath.toString());
                            className = className.substring(0, className.length() - CLASS_EXT.length());
                            try {
                                final GeneratedClassBuildItem item = new GeneratedClassBuildItem(true, className,
                                        Files.readAllBytes(p));
                                generatedClasses.produce(item);
                            } catch (IOException e) {
                                throw new RuntimeException("Could not read " + p);
                            }
                        });
            }

        }
    }

    @Record(ExecutionTime.STATIC_INIT)
    @BuildStep
    @Consume(CamelContextBuildItem.class)
    CamelBeanBuildItem configureCSimpleLanguage(
            RecorderContext recorderContext,
            CSimpleLanguageRecorder recorder,
            ExpressionExtractionResultBuildItem result,
            CurateOutcomeBuildItem curateOutcomeBuildItem,
            List<CompiledCSimpleExpressionBuildItem> compiledCSimpleExpressions) {

        if (result.isSuccess()) {
            final RuntimeValue<Builder> builder = recorder.csimpleLanguageBuilder();
            for (CompiledCSimpleExpressionBuildItem expr : compiledCSimpleExpressions) {
                recorder.addExpression(builder, recorderContext.newInstance(expr.getClassName()));
            }

            final RuntimeValue<?> csimpleLanguage = recorder.buildCSimpleLanguage(builder);
            return new CamelBeanBuildItem("csimple", CSimpleLanguage.class.getName(), csimpleLanguage);
        } else if (curateOutcomeBuildItem.getApplicationModel().getDependencies().stream().noneMatch(
                x -> x.getGroupId().equals("org.apache.camel") && x.getArtifactId().equals("camel-csimple-joor"))) {
            LOG.warn(
                    "The expression extraction process has been disabled or failed, please add camel-csimple-joor to your classpath to compile the expressions at runtime");
        }
        return null;
    }

    static void readConfig(Set<String> imports, Map<String, String> aliases, ClassLoader cl) throws IOException {
        Enumeration<URL> confiUrls = cl.getResources("camel-csimple.properties");
        while (confiUrls.hasMoreElements()) {
            final URL configUrl = confiUrls.nextElement();
            try (BufferedReader r = new BufferedReader(new InputStreamReader(configUrl.openStream(), StandardCharsets.UTF_8))) {
                String line;
                while ((line = r.readLine()) != null) {
                    line = line.trim();
                    // skip comments
                    if (line.startsWith("#")) {
                        continue;
                    }
                    // imports
                    if (line.startsWith("import ")) {
                        imports.add(line);
                        continue;
                    }
                    // aliases as key=value
                    final int eqPos = line.indexOf('=');
                    final String key = line.substring(0, eqPos).trim();
                    final String value = line.substring(eqPos + 1).trim();
                    aliases.put(key, value);

                }
            } catch (IOException e) {
                throw new RuntimeException("Could not read from " + configUrl);
            }
        }
    }

    private Context compilationContext(final Path projectDir, final Path csimpleClassesDir,
            QuarkusClassLoader quarkusClassLoader) {
        Set<File> classPathElements = Stream.of(CSimpleHelper.class, Exchange.class, PropertiesHelper.class)
                .map(clazz -> clazz.getName().replace('.', '/') + CLASS_EXT)
                .flatMap(className -> (Stream<ClassPathElement>) quarkusClassLoader.getElementsWithResource(className).stream())
                .map(ClassPathElement::getRoot)
                .filter(Objects::nonNull)
                .map(Path::toFile)
                .collect(Collectors.toSet());

        return new CompilationProvider.Context(
                "csimple-project",
                classPathElements,
                classPathElements,
                projectDir.toFile(),
                projectDir.resolve("src/main/java").toFile(),
                csimpleClassesDir.toFile(),
                StandardCharsets.UTF_8.name(),
                Collections.emptyMap(),
                CamelSupport.COMPILATION_JVM_TARGET,
                null,
                null,
                Collections.emptyList(),
                Collections.emptyList(),
                null,
                Collections.emptySet(),
                Collections.emptyList(),
                null);
    }

}
