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

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import net.ranides.assira.collection.maps.HashMultiMap;
import net.ranides.assira.collection.maps.MultiMap;

/**
 * 
 * Based on "org.kohsuke.metainf-services"
 * http://weblogs.java.net/blog/kohsuke/archive/2009/03/my_project_of_t.html 
 * (C) by Kohsuke Kawaguchi(kk@kohsuke.org) under MIT license
 * 
 * @author Ranides Atterwim <ranides@gmail.com>
 */
@SupportedAnnotationTypes(
    "net.ranides.assira.annotations.Meta.JavaService"
)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class JavaServiceProcessor extends AbstractProcessor {
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if ( !roundEnv.processingOver() ) {
            writeMeta( readMeta( getServices(roundEnv) ) );
        }
        return false;
    }
    
    private MultiMap<String, String> getServices(RoundEnvironment roundEnv) {
        
        MultiMap<String, String> services = new HashMultiMap<>();
        Elements elements = processingEnv.getElementUtils();
        
        // discover services from the current compilation sources
        for (Element element : roundEnv.getElementsAnnotatedWith(Meta.JavaService.class)) {
            Meta.JavaService info = element.getAnnotation(Meta.JavaService.class);
            TypeElement type = (TypeElement)element;
            TypeElement contract = getContract(type, info);

            if(null != contract) {
                String cname = elements.getBinaryName(contract).toString();
                String tname = elements.getBinaryName(type).toString();
                services.put(cname, tname);
            }
        }
        return services;
    }

    private MultiMap<String, String> readMeta(MultiMap<String, String> services) {
        
        for (String contract : services.uniqueKeySet()) {
            try(BufferedReader reader = reader(contract)) {
                String line;
                while( null!=(line=reader.readLine()) ) { 
                    services.put(contract, line);
                }
            } catch (FileNotFoundException $0) { // NOPMD
                // ignore, if doesn't exist
            } catch (IOException cause) {
                processingEnv.getMessager().printMessage(Kind.ERROR,"Failed to load existing service definition files: "+cause);
            }
        }
        return services;
    }
    
    private MultiMap<String, String> writeMeta(MultiMap<String, String> services) {
        for (String contract : services.uniqueKeySet()) {
            try(PrintWriter writer = writer(contract)) {
                processingEnv.getMessager().printMessage(Kind.NOTE, "Writing META-INF/services/"+contract);
                for (String value : services.getAll(contract)) {
                    writer.println(value);
                }
            } catch (IOException x) {
                processingEnv.getMessager().printMessage(Kind.ERROR, "Failed to write service definition files: "+x);
            }
        }
        return services;
    }
    
    private PrintWriter writer(String contract) throws IOException {
        return new PrintWriter(new OutputStreamWriter(fo(contract).openOutputStream(), "UTF-8"));
    }
    
    private BufferedReader reader(String contract) throws IOException {
        return new BufferedReader(new InputStreamReader(fi(contract).openInputStream(), "UTF-8"));
    }
    
    private FileObject fo(String contract) throws IOException {
        return processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/services/" +contract);
    }
    
    private FileObject fi(String contract) throws IOException {
        return processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/services/" +contract);
    }
    
    private TypeElement getContract(TypeElement type, Meta.JavaService annotation) {
        TypeMirror mirror = getExportValue(annotation);
        if (mirror.getKind()== TypeKind.VOID) {
            // contract inferred from the signature
            boolean hasBaseClass = type.getSuperclass().getKind()!=TypeKind.NONE && !isObject(type.getSuperclass());
            boolean hasInterfaces = !type.getInterfaces().isEmpty();
            if(hasBaseClass^hasInterfaces) {
                if(hasBaseClass) {
                    return (TypeElement)((DeclaredType)type.getSuperclass()).asElement();
                } else {
                    return (TypeElement)((DeclaredType)type.getInterfaces().get(0)).asElement();
                }
            }

            error(type, "Contract type was not specified, but it couldn't be inferred.");
            return null;
        }

        if (mirror instanceof DeclaredType) {
            return (TypeElement)((DeclaredType)mirror).asElement();
        } else {
            error(type, "Invalid type specified as the contract");
            return null;
        }
    }
    
    private TypeMirror getExportValue(Meta.JavaService annotation) {
        try {
            annotation.value();
            throw new AssertionError();
        } catch (MirroredTypeException exception) {
            return exception.getTypeMirror();
        }
    }

    private boolean isObject(TypeMirror mirror) {
        if (mirror instanceof DeclaredType) {
            DeclaredType dtype = (DeclaredType)mirror;
            TypeElement element = (TypeElement)dtype.asElement();
            return element.getQualifiedName().toString().equals("java.lang.Object");
        }
        return false;
    }

    private void error(Element source, String msg) {
        processingEnv.getMessager().printMessage(Kind.ERROR,msg,source);
    }
    
}
