/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed 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 com.android.javac;

import com.google.common.collect.Lists;
import com.google.common.io.Files;

import java.util.Collections;
import java.util.stream.Collectors;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import javax.annotation.processing.Processor;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;

/**
 * Helper class for compiling snippets of Java source and providing access to the resulting class
 * files.
 */
public class Javac {

    private final JavaCompiler mJavac;
    private final StandardJavaFileManager mFileMan;
    private final List<JavaFileObject> mCompilationUnits;
    private final File mClassOutDir;

    public Javac() throws IOException {
        mJavac = ToolProvider.getSystemJavaCompiler();
        mFileMan = mJavac.getStandardFileManager(null, Locale.US, null);
        mClassOutDir = Files.createTempDir();
        mFileMan.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(mClassOutDir));
        mFileMan.setLocation(StandardLocation.CLASS_PATH, Arrays.asList(mClassOutDir));
        mCompilationUnits = new ArrayList<>();
    }

    private String classToFileName(String classname) {
        return classname.replace('.', '/');
    }

    public Javac addSource(String classname, String contents) {
        JavaFileObject java = new SimpleJavaFileObject(URI.create(
                String.format("string:///%s.java", classToFileName(classname))),
                JavaFileObject.Kind.SOURCE
                ){
            @Override
            public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
                return contents;
            }
        };
        mCompilationUnits.add(java);
        return this;
    }

    public void compile() {
        compileWithAnnotationProcessor(null);
    }

    public void compileWithAnnotationProcessor(Processor processor) {
        DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
        JavaCompiler.CompilationTask task = mJavac.getTask(
                null,
                mFileMan,
                diagnosticCollector,
                null,
                null,
                mCompilationUnits);
        if (processor != null) {
            task.setProcessors(Lists.newArrayList(processor));
        } else {
            task.setProcessors(Collections.EMPTY_LIST);
        }
        boolean result = task.call();
        if (!result) {
            throw new IllegalStateException(
                    "Compilation failed:" +
                            diagnosticCollector.getDiagnostics()
                                    .stream()
                                    .map(Object::toString)
                                    .collect(Collectors.joining("\n")));
        }
    }

    public InputStream getOutputFile(String filename) throws IOException {
        Iterable<? extends JavaFileObject> objs = mFileMan.getJavaFileObjects(
                new File(mClassOutDir, filename));
        if (!objs.iterator().hasNext()) {
            return null;
        }
        return objs.iterator().next().openInputStream();
    }

    public InputStream getClassFile(String classname) throws IOException {
        return getOutputFile(String.format("%s.class", classToFileName(classname)));
    }

    public JavaClass getCompiledClass(String classname) throws IOException {
        return new ClassParser(getClassFile(classname),
                String.format("%s.class", classToFileName(classname))).parse();
    }
}
