package com.github.x3333.mashbox.generator.impl;

import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PROTECTED;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;

import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementFilter;
import javax.tools.JavaFileObject;

import com.github.x3333.mashbox.Util;
import com.github.x3333.mashbox.annotation.MashGwtEventParam;
import com.github.x3333.mashbox.generator.AbstractGenerator;
import com.squareup.javawriter.JavaWriter;

/**
 * Generator for MashGwtEvent marker interface.
 *
 * @author Tercio Gaudencio Filho (terciofilho [at] gmail.com)
 */
public class MashGwtEventGenerator extends AbstractGenerator {

  public MashGwtEventGenerator(final ProcessingEnvironment processingEnv) {
    super(processingEnv);
  }

  @Override
  public void processElement(final TypeElement element) throws IOException {
    final String elementSimpleName = element.getSimpleName().toString();

    if (!elementSimpleName.endsWith("EventDef")) {
      error("Element '" + element.toString()
          + "', @MashGwtEvent class name must ends with 'EventDef'.");
      return;
    }
    if (element.getKind() != ElementKind.CLASS) {
      error("@MashGwtEvent can only be applied to Class element.");
      return;
    }

    // Find fields annotated with @MashGwtEventParam
    // TODO: Create a map or something to avoid all those .getSimpleName().toString() calls
    final Map<Integer, VariableElement> params = new TreeMap<Integer, VariableElement>();
    for (final VariableElement field : ElementFilter.fieldsIn(element.getEnclosedElements())) {
      final MashGwtEventParam param = field.getAnnotation(MashGwtEventParam.class);
      if (param != null) {
        if (params.containsKey(param.value())) {
          error(String.format("Field %s reused value '%d'.", field.getSimpleName(), param.value()));
          continue;
        }
        params.put(param.value(), field);
      }
    }

    final String packageName = Util.getPackage(element).getQualifiedName().toString();
    final String canonicalName =
        element.getQualifiedName().toString().replaceAll("EventDef$", "Event");
    final String simpleName = element.getSimpleName().toString().replaceAll("EventDef$", "Event");
    final String rawName = element.getSimpleName().toString().replaceAll("EventDef$", "");
    final String gwtEventType =
        Util.getGenericType("GwtEvent", getStrippedTypeName(canonicalName, packageName)
            + ".Handler");

    final JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(canonicalName);
    final JavaWriter writer = new JavaWriter(sourceFile.openWriter());

    // Package
    writer.emitJavadoc("Generated by Mashbox - http://github.com/0x3333/mashbox");
    writer.emitPackage(packageName);

    // Imports
    writer.emitImports("com.google.gwt.event.shared.EventHandler",
        "com.google.gwt.event.shared.GwtEvent", "com.google.gwt.event.shared.HasHandlers",
        "com.google.web.bindery.event.shared.HandlerRegistration");
    writer.emitEmptyLine();

    // Class
    writer.beginType(canonicalName, "class", EnumSet.of(PUBLIC, FINAL), gwtEventType);
    writer.emitEmptyLine();

    // HasHandler
    writer.beginType("HasHandler", "interface", EnumSet.of(PUBLIC), "HasHandlers");
    writer.beginMethod("HandlerRegistration", "add" + rawName + "Handler", EnumSet.of(PUBLIC),
        simpleName + ".Handler", "handler");
    writer.endMethod();
    writer.endType();
    writer.emitEmptyLine();

    // Handler
    writer.beginType("Handler", "interface", EnumSet.of(PUBLIC), "EventHandler");
    writer.beginMethod("void", "on" + rawName, EnumSet.of(PUBLIC), simpleName, "event");
    writer.endMethod();
    writer.endType();
    writer.emitEmptyLine();

    // Static Field
    writer.emitField("Type<Handler>", "TYPE", EnumSet.of(PUBLIC, FINAL, STATIC),
        "new Type<Handler>()");
    writer.emitEmptyLine();

    // Fields
    for (final VariableElement variable : params.values()) {
      writer.emitField(variable.asType().toString(), variable.getSimpleName().toString(), //
          EnumSet.of(PRIVATE, FINAL));
    }
    writer.emitEmptyLine();

    // Constructor
    final List<String> constructorParameters = new ArrayList<String>();
    for (final VariableElement variable : params.values()) {
      constructorParameters.add("final " + variable.asType().toString());
      constructorParameters.add(variable.getSimpleName().toString());
    }
    writer.beginConstructor(EnumSet.of(PUBLIC), constructorParameters, null);
    for (final VariableElement variable : params.values()) {
      writer.emitStatement("this.%1$s = %1$s", variable.getSimpleName().toString());
    }
    writer.endConstructor();
    writer.emitEmptyLine();

    // Fire Event
    constructorParameters.add(0, "hasHandler");
    constructorParameters.add(0, "final HasHandlers");
    final StringBuilder strFields = new StringBuilder();
    for (final VariableElement variable : params.values()) {
      strFields.append(", ");
      strFields.append(variable.getSimpleName().toString());
    }
    if (strFields.length() > 0) {
      strFields.deleteCharAt(0);
      strFields.deleteCharAt(0);
    }
    writer.beginMethod("void", "fire", EnumSet.of(PUBLIC, STATIC), constructorParameters, null);
    writer.emitStatement("hasHandler.fireEvent(new %s(%s))", simpleName, strFields.toString());
    writer.endMethod();
    writer.emitEmptyLine();

    // Getters
    for (final VariableElement variable : params.values()) {
      writer.beginMethod(variable.asType().toString(), Util.getVariableGetter(variable), EnumSet
          .of(PUBLIC));
      writer.emitStatement("return this.%s", variable.getSimpleName().toString());
      writer.endMethod();
      writer.emitEmptyLine();
    }

    // getAssociatedType
    writer.emitAnnotation("Override");
    writer.beginMethod("Type<Handler>", "getAssociatedType", EnumSet.of(PUBLIC));
    writer.emitStatement("return TYPE");
    writer.endMethod();
    writer.emitEmptyLine();

    // dispatch
    writer.emitAnnotation("Override");
    writer.beginMethod("void", "dispatch", EnumSet.of(PROTECTED), "final Handler", "handler");
    writer.emitStatement("handler.on" + rawName + "(this)");
    writer.endMethod();
    writer.emitEmptyLine();

    writer.endType();

    writer.close();
  }

  private String getStrippedTypeName(final String canonicalName, final String packageName) {
    return canonicalName.substring(packageName.isEmpty() ? 0 : packageName.length() + 1);
  }

}

@@ -5345,33 +5345,32 @@
 final HasHandler
-s
 %22);%0A    final St

Revert "Corrected fire method to use HasHandlers instead HasHandler."