/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.annotations.javac;


import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

import javax.annotation.processing.Processor;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.ToolProvider;
import net.ranides.assira.text.Charsets;

/**
 * This class allows you to invoke javac compiler using quite simple fluent builder pattern.
 *
 * It supports invocation of annotation processors
 *
 * It works only if application is executed by JDK with "javac" available.
 *
 * It could be used for dynamic compilation, altough getting generated bytecode must be done
 * manually, by inspection files inside returned JavaCompilationResult.
 *
 * Please note, that all generated results are stored in memory by default.
 * JavaCompilation does not create temporary files.
 */
public final class JavaCompilation {

    private final List<String> options;

    private final List<Processor> processors = new ArrayList<>();
    
    private final List<JavaFileObject> sources = new ArrayList<>();
    
    private final List<String> resources = new ArrayList<>();

    /**
     * Creates new compilation session.
     * You passes additional options to "javac"
     *
     * @param options options
     */
    public JavaCompilation(String... options) {
        this.options = Arrays.asList(options);
    }

    /**
     * Registers annotation processor which will be invoked by javac.
     * It is possible to register many processors.
     *
     * @param processor processor
     * @return this
     */
    public JavaCompilation processor(Processor processor) {
        processors.add(processor);
        return this;
    }

    /**
     * Specifies source to compile.
     * You can specify multiple source files.
     *
     * It takes both class name and class source code.
     * Source code must contain correct package and class declarations,
     * compatible with passed "fqname".
     *
     * @param fqname fqname
     * @param source source
     * @return this
     */
    public JavaCompilation source(String fqname, String source) {
        sources.add(new JavaCompilationFM.CFOString(fqname, source));
        return this;
    }

    /**
     * Specifies source to compile.
     * You can specify multiple source files.
     *
     * URL must point to file located in directory structure compatible with package declaration.
     * URL must be accessible and readable by {@link URL#openStream()}.
     *
     * @param resource resource
     * @return this
     */
    public JavaCompilation source(URL resource) {
        sources.add(new JavaCompilationFM.CFOResource(resource));
        return this;
    }

    /**
     * Specifies resource file visible to javac.
     * You can specify multiple resource files.
     *
     * Resource name must ne readable by ClassLoader.
     *
     * Resources are copied to JavaCompilationResult
     *
     * @param resource resource
     * @return this
     */
    public JavaCompilation resource(String resource) {
        resources.add(resource);
        return this;
    }

    /**
     * Executes "javac" and returns compilation result.
     *
     * This result contains all compile messages and generated files.
     * By default, result is stored in memory and does not use temporary files.
     *
     * @return result
     * @throws IOException if some input file can't be accessed because of I/O error
     * @throws IllegalStateException if "javac" is not present
     */
    public JavaCompilationResult compile() throws IOException {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        if (compiler == null) {
            throw new IllegalStateException("Java Compiler is not present. May be, you need to include tools.jar on your dependency list.");
        }
        DiagnosticCollector<JavaFileObject> dc = new DiagnosticCollector<>();

        JavaCompilationFM fm = new JavaCompilationFM(compiler.getStandardFileManager(dc, Locale.getDefault(), Charsets.UTF8));
        
        for(String rs : resources) {
            fm.exportResource(rs);
        }

        CompilationTask task = compiler.getTask(
            null, // explicitly use the default because old versions of javac log some output on stderr
            fm,
            dc,
            options,
            Collections.emptyList(),
            sources
        );
        task.setProcessors(processors);
        boolean successful = task.call();
        return new JavaCompilationResult(successful, dc.getDiagnostics(), fm.getOutputFiles());
    }

}
