/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.uberfire.annotations.processors;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileObject;

/**
 * Contains a series of adaptations and workarounds to make annotation processors work well under Eclipse JDT APT. Does
 * not limit compatibility with other annotation processing environments (such as javac).
 */
public abstract class AbstractErrorAbsorbingProcessor extends AbstractProcessor {

    private Throwable rememberedInitError;

    protected AbstractErrorAbsorbingProcessor() {
        try {
            freemarker.log.Logger.selectLoggerLibrary(freemarker.log.Logger.LIBRARY_NONE);
        } catch (ClassNotFoundException e) {
            rememberedInitError = e;
        }
    }

    private static AnnotationMirror findAnnotationMirror(Element target,
                                                         TypeElement annotationType) {
        final Name annotationTypeName = annotationType.getQualifiedName();
        for (AnnotationMirror am : target.getAnnotationMirrors()) {
            if (GeneratorUtils.getQualifiedName(am).contentEquals(annotationTypeName)) {
                return am;
            }
        }
        return null;
    }

    /**
     * Wraps the given processing environment with one that protects against known bugs in the Eclipse annotation
     * processing implementation.
     */
    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(new EclipseWorkaroundProcessingEnvironment(env));
    }

    @Override
    public final boolean process(Set<? extends TypeElement> annotations,
                                 RoundEnvironment roundEnv) {
        try {
            if (rememberedInitError != null) {
                throw rememberedInitError;
            }
            return processWithExceptions(annotations,
                                         roundEnv);
        } catch (Throwable e) {
            // eclipse JDT goes into an infinite loop when the annotation processor throws any exception
            // so we have to catch EVERYTHING, even Errors.

            StringWriter stringWriter = new StringWriter();
            e.printStackTrace(new PrintWriter(stringWriter));
            final String errorMessage = "Internal error in " + getClass().getName() + stringWriter.toString();

            boolean emittedSpecificError = false;
            for (TypeElement annotation : annotations) {
                for (Element annotationTarget : roundEnv.getElementsAnnotatedWith(annotation)) {
                    processingEnv.getMessager().printMessage(
                            Kind.ERROR,
                            errorMessage,
                            annotationTarget,
                            findAnnotationMirror(annotationTarget,
                                                 annotation));
                    emittedSpecificError = true;
                }
            }

            // if the above loop caught nothing, the type we were called for didn't contain an annotation
            // we handle (maybe it was inherited). In this case, we'll just emit a non-location-specific error
            // so there is at least some sort of diagnostic message for the user to go on!
            if (!emittedSpecificError) {
                processingEnv.getMessager().printMessage(
                        Kind.ERROR,
                        errorMessage);
            }

            return false;
        }
    }

    /**
     * Subclasses must call this from their constructors if something throws an
     * exception during initialization of the instance. Once this method has
     * been called with a non-null throwable, the
     * {@link #processWithExceptions(Set, RoundEnvironment)} method will not be
     * called on this instance.
     * @param t the exception that occurred (and was caught) during instance
     * creation of this annotation processor instance.
     */
    protected void rememberInitializationError(Throwable t) {
        rememberedInitError = t;
    }

    /**
     * Same contract as {@link #process(Set, RoundEnvironment)}, except that any
     * exceptions thrown are caught and printed as messages of type
     * {@link Kind#ERROR}. This is done to keep Eclipse JDT from going into an
     * infinite processing loop.
     */
    protected abstract boolean processWithExceptions(
            Set<? extends TypeElement> annotations,
            RoundEnvironment roundEnv) throws Exception;

    /**
     * Writes the given code to javac's Filer.
     */
    protected final void writeCode(final String packageName,
                                   final String className,
                                   final StringBuffer code) throws IOException {
        JavaFileObject jfo = processingEnv.getFiler().createSourceFile(packageName + "." + className);
        try (Writer w = jfo.openWriter();
             BufferedWriter bw = new BufferedWriter(w)) {
            bw.append(code);
        }
    }
}
