/*
 This file is part of GNUnet.
 (C) 2011, 2012 Christian Grothoff (and other contributing authors)

 GNUnet is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published
 by the Free Software Foundation; either version 3, or (at your
 option) any later version.

 GNUnet is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with GNUnet; see the file COPYING.  If not, write to the
 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 Boston, MA 02111-1307, USA.
 */

package org.gnunet.construct;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.IOException;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Set;


/**
 * Creates a resource file 'MsgMap.txt' in the package 'org.gnunet.construct' of the source tree.
 */
@SupportedAnnotationTypes("org.gnunet.construct.UnionCase")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class MessageIdAnnotationProcessor extends AbstractProcessor {
    private final Table<String, Integer, String> idToMember = HashBasedTable.create();

    @Override
    public boolean process(Set<? extends TypeElement> typeElements, RoundEnvironment roundEnvironment) {
        if (roundEnvironment.errorRaised()) {
            return false;
        }

        Types types = processingEnv.getTypeUtils();
        Elements elements = processingEnv.getElementUtils();

        if (roundEnvironment.processingOver()) {
            Filer filer = processingEnv.getFiler();
            FileObject outfile;
            try {
                outfile = filer.createResource(StandardLocation.SOURCE_OUTPUT, "org.gnunet.construct", "MsgMap.txt");
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not create MsgMap.txt");
                return false;
            }

            try {
                Writer w = outfile.openWriter();
                for (Table.Cell<String, Integer, String> cell : idToMember.cellSet()) {
                    w.write(cell.getRowKey() + '|' + cell.getColumnKey() + '=' + cell.getValue() + '\n');
                }

                DateFormat fmt = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");

                w.write("# generated " + fmt.format(new Date()) + '\n');
                w.close();
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not write MsgMap.txt");
            }

            processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "message map written to " + outfile.toUri());

        } else {
            for (Element e : roundEnvironment.getElementsAnnotatedWith(UnionCase.class)) {
                UnionCase ann = e.getAnnotation(UnionCase.class);
                // get the uppermost parent class that implements MessageUnion. This is the union type.
                // processingEnv.getElementUtils().
                //processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "element :" + e.toString());
                List<? extends TypeMirror> parents = processingEnv.getTypeUtils().directSupertypes(e.asType());
                TypeMirror msg = elements.getTypeElement("org.gnunet.construct.MessageUnion").asType();
                TypeMirror unionInterface = null;
                for (TypeMirror p : parents) {
                    if (types.isSubtype(p, msg)) {
                        unionInterface = p;
                        break;
                    }
                }
                if (unionInterface == null) {
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format(
                            "class %s annotated with @UnionCase does not implement an interface inheriting MessageUnion", e.getSimpleName()));
                    return false;
                }
                String unionName = getClassName(types.asElement(unionInterface));
                if (idToMember.contains(unionName, ann.value())) {
                    String existingName = idToMember.get(unionName, ann.value());
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format(
                            "duplicate message id %s for union '%s: '%s' and '%s'", ann.value(), unionName,
                            getClassName(e), existingName));
                    return false;
                }
                idToMember.put(unionName, ann.value(), getClassName(e));
            }
        }

        return false;
    }

    /**
     * Get the fully qualified class name, where packages are seperated with '.', and
     * inner classes are separated with '$'
     *
     * @param e the Element representing a class
     * @return the fully qualified class name
     */
    private String getClassName(Element e) {

        assert e.getKind().isClass();

        String name = e.getSimpleName().toString();
        String pkg = processingEnv.getElementUtils().getPackageOf(e).getQualifiedName().toString() + '.';

        String outer = "";

        while (((e = e.getEnclosingElement()) != null) && e.getKind().isClass()) {
            outer = String.format("%s$%s", e.getSimpleName(), outer);
        }

        return pkg + outer + name;
    }
}
