package io.vertx.test.codegen;

/*
 * Copyright 2014 Red Hat, Inc.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution.
 *
 * The Eclipse Public License is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * The Apache License v2.0 is available at
 * http://www.opensource.org/licenses/apache2.0.php
 *
 * You may elect to redistribute this code under either of these licenses.
 */

import io.vertx.codegen.*;
import io.vertx.codegen.Compiler;
import io.vertx.codegen.annotations.ModuleGen;
import io.vertx.codegen.annotations.DataObject;
import io.vertx.codegen.annotations.ProxyGen;
import io.vertx.codegen.annotations.VertxGen;

import javax.annotation.processing.Completion;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;
import java.io.File;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 *
 * @author <a href="http://tfox.org">Tim Fox</a>
 *
 */
public class GeneratorHelper {

  private final DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
  private final List<CodeGen.Converter> converters = new ArrayList<>();

  public GeneratorHelper registerConverter(String type, String serializer, String... names) {
    converters.add(new CodeGen.Converter(type, serializer, Arrays.asList(names)));
    return this;
  }

  public GeneratorHelper registerConverter(Class<?> type, String serializer, String... names) {
    converters.add(new CodeGen.Converter(type.getName(), serializer, Arrays.asList(names)));
    return this;
  }

  public GeneratorHelper registerConverter(Class<?> type, Class<?> serializer, String... names) {
    converters.add(new CodeGen.Converter(type.getName(), serializer.getName(), Arrays.asList(names)));
    return this;
  }

  public PackageModel generatePackage(Class clazz, Set<Class> otherSupportedAnnotations) throws Exception {
    URL url = clazz.getClassLoader().getResource(clazz.getName().replace('.', '/') + ".java");
    File f = new File(url.toURI());
    MyProcessor<PackageModel> processor = new MyProcessor<>(codegen -> codegen.getPackageModel(clazz.getPackage().getName()), otherSupportedAnnotations.stream().map(Class::getCanonicalName).collect(Collectors.toSet()));
    Compiler compiler = new Compiler(processor, collector);
    compiler.compile(f);
    return processor.result;
  }

  public PackageModel generatePackage(Class clazz) throws Exception {
    return this.generatePackage(clazz, new HashSet<>());
  }

  public ModuleModel generateModule(ClassLoader loader, String packageFqn, Set<Class> otherSupportedAnnotations) throws Exception {
    URL url = loader.getResource(packageFqn.replace('.', '/') + "/package-info.java");
    File info = new File(url.toURI());
    File[] files = Files.walk(info.getParentFile().toPath()).filter(Files::isRegularFile).map(Path::toFile).toArray(File[]::new);
    MyProcessor<ModuleModel> processor = new MyProcessor<>(codegen -> codegen.getModuleModel(packageFqn), otherSupportedAnnotations.stream().map(Class::getCanonicalName).collect(Collectors.toSet()));
    Compiler compiler = new Compiler(processor, collector);
    compiler.compile(files);
    return processor.result;
  }

  public ModuleModel generateModule(ClassLoader loader, String packageFqn) throws Exception {
    return this.generateModule(loader, packageFqn, new HashSet<>());
  }

  public DataObjectModel generateDataObject(Class c, Class... rest) throws Exception {
    return generateClass(codegen -> codegen.getDataObjectModel(c.getCanonicalName()), c, rest);
  }

  public ClassModel generateClass(Class c, Class... rest) throws Exception {
    return generateClass(codegen -> codegen.getClassModel(c.getCanonicalName()), c, rest);
  }

  public EnumModel generateEnum(Class c, Class... rest) throws Exception {
    return generateClass(codegen -> codegen.getEnumModel(c.getCanonicalName()), c, rest);
  }

  public <M> M generateClass(Function<CodeGen, M> f, Set<Class> otherSupportedAnnotations, Class c, Class... rest) throws Exception {
    ArrayList<Class> types = new ArrayList<>();
    types.add(c);
    Collections.addAll(types, rest);
    String className = c.getCanonicalName();
    MyProcessor<M> processor = new MyProcessor<>(f, otherSupportedAnnotations.stream().map(Class::getCanonicalName).collect(Collectors.toSet()));
    Compiler compiler = new Compiler(processor, collector);
    compiler.compile(types);
    if (processor.result == null) {
      throw new IllegalArgumentException(className + " not processed.");
    }
    return processor.result;
  }

  public <M> M generateClass(Function<CodeGen, M> f, Class annotation, Class c, Class... rest) throws Exception {
    Set<Class> s = new HashSet<>();
    s.add(annotation);
    return this.generateClass(f, s, c, rest);
  }

  public <M> M generateClass(Function<CodeGen, M> f, Class c, Class... rest) throws Exception {
    return this.generateClass(f, new HashSet<>(), c, rest);
  }

  private class MyProcessor<R> implements Processor {

    private ProcessingEnvironment env;
    private final Function<CodeGen, R> f;
    private R result;
    private Set<String> supportedAnnotations;

    private MyProcessor(Function<CodeGen, R> f, Set<String> otherSupportedAnnotations) {
      this.f = f;
      this.supportedAnnotations = new HashSet<>();
      this.supportedAnnotations.add(ProxyGen.class.getCanonicalName());
      this.supportedAnnotations.add(VertxGen.class.getCanonicalName());
      this.supportedAnnotations.add(DataObject.class.getCanonicalName());
      this.supportedAnnotations.add(ModuleGen.class.getCanonicalName());
      this.supportedAnnotations.addAll(otherSupportedAnnotations);
    }

    @Override
    public Set<String> getSupportedOptions() {
      return Collections.emptySet();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
      return supportedAnnotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
      return SourceVersion.RELEASE_8;
    }

    @Override
    public void init(ProcessingEnvironment processingEnv) {
      this.env = processingEnv;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      if (!roundEnv.processingOver()) {
        CodeGen codegen = new CodeGen(env);
        converters.forEach(codegen::registerConverter);
        codegen.init(roundEnv, Thread.currentThread().getContextClassLoader());
        result = f.apply(codegen);
      }
      return true;
    }

    @Override
    public Iterable<? extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) {
      return Collections.emptyList();
    }
  }
}