package io.vertx.codegen;

import io.vertx.codegen.type.ClassKind;
import org.mvel2.integration.impl.MapVariableResolverFactory;
import org.mvel2.templates.CompiledTemplate;
import org.mvel2.templates.SimpleTemplateRegistry;
import org.mvel2.templates.TemplateCompiler;
import org.mvel2.templates.TemplateError;
import org.mvel2.templates.TemplateRegistry;
import org.mvel2.templates.TemplateRuntime;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

/**
 * @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
 */
public class Template {

  private final String baseURI;
  private final String name;
  private final CompiledTemplate compiled;
  private final HashMap<String, String> options = new HashMap<>();

  public Template(URL url) {
    String file = url.getFile();
    this.name = file.substring(file.lastIndexOf('/') + 1);
    try {
      this.baseURI = url.toURI().toString();
      this.compiled = loadCompiled(url);
    } catch (URISyntaxException e) {
      throw new TemplateError("Could not load template from template " + url, e);
    }
  }

  public Template(String name) {
    this(resolveURL(name));
  }

  public void setOptions(Map<String, String> options) {
    this.options.clear();
    this.options.putAll(options);
  }

  // A global template cache because loading a template is expensive
  private static final Map<URL, CompiledTemplate> templateCache = new HashMap<>();

  /**
   * Load a template given its {@code templateURL}.
   *
   * @param templateURL the template url
   * @return the compiled template
   */
  public static CompiledTemplate loadCompiled(URL templateURL) {
    CompiledTemplate template = templateCache.get(templateURL);
    if (template == null) {
      InputStream is = null;
      try {
        is = templateURL.openStream();
      } catch (IOException e) {
        throw new TemplateError("Could not load template from template " + templateURL, e);
      }
      template = loadCompiled(is);
      templateCache.put(templateURL, template);
    }
    return template;
  }

  /**
   * Load a template given its {@code source}.
   *
   * @param source the template source
   * @return the compiled template
   */
  public static CompiledTemplate loadCompiled(InputStream source) {
    // Load the template
    String template;
    try (Scanner scanner = new Scanner(source, "UTF-8").useDelimiter("\\A")) {
      template = scanner.next();
    }
    // MVEL preserves all whitespace therefore, so we can have readable templates we remove all line breaks
    // and replace all occurrences of "\n" with a line break
    // "\n" signifies we want an actual line break in the output
    // We use actual tab characters in the template so we can see indentation, but we strip these out
    // before parsing.
    // So use tabs for indentation that YOU want to see in the template but won't be in the final output
    // And use spaces for indentation that WILL be in the final output
    template = template.replace("\n", "").replace("\r", "").replace("\\n", System.getProperty("line.separator")).replace("\t", "");

    // Be sure to have mvel classloader as parent during evaluation as it will need mvel classes
    // when generating code
    ClassLoader currentCL = Thread.currentThread().getContextClassLoader();
    Thread.currentThread().setContextClassLoader(TemplateRuntime.class.getClassLoader());
    try {
      return TemplateCompiler.compileTemplate(template);
    } finally {
      Thread.currentThread().setContextClassLoader(currentCL);
    }
  }

  private static URL resolveURL(String name) {
    // Read the template file from the classpath
    URL url = Template.class.getClassLoader().getResource(name);
    if (url == null) {
      throw new IllegalArgumentException("Can't find template file on classpath: " + name);
    }
    return url;
  }

  public String getName() {
    return name;
  }

  public void apply(Model model, String outputFileName) throws Exception {
    apply(model, new File(outputFileName));
  }

  public void apply(Model model, File outputFile) throws Exception {
    apply(model, outputFile, Collections.emptyMap());
  }

  public void apply(Model model, File outputFile, Map<String, Object> vars) throws Exception {
    String output = render(model, vars);
    if (output != null) {
      Helper.ensureParentDir(outputFile);
      try (PrintStream outStream = new PrintStream(new FileOutputStream(outputFile))) {
        outStream.print(output);
        outStream.flush();
      }
    }
  }

  public String render(Model model) {
    return render(model, Collections.emptyMap());
  }

  public String render(Model model, Map<String, Object> vars) {
    vars = new HashMap<>(vars);
    vars.put("options", options);
    vars.put("skipFile", false);
    vars.putAll(model.getVars());
    vars.putAll(ClassKind.vars());
    vars.putAll(MethodKind.vars());
    vars.putAll(Case.vars());

    TemplateRegistry registry = new SimpleTemplateRegistry() {
      @Override
      public CompiledTemplate getNamedTemplate(String name) {
        try {
          return super.getNamedTemplate(name);
        } catch (TemplateError err) {
          // Load error try to resolve from base uri
          try {
            URL url;
            if (name.startsWith("/")) {
              url = resolveURL(name.substring(1));
            } else {
              url = new URL(new URL(baseURI), name);
            }
            CompiledTemplate compiledTemplate = loadCompiled(url);
            addNamedTemplate(name, compiledTemplate);
            return compiledTemplate;
          } catch (Exception ex) {
            throw new TemplateError("Could not load template " + name + " from template " + baseURI, ex);
          }
        }
      }
    };

    ClassLoader currentCL = Thread.currentThread().getContextClassLoader();
    try {
      // Be sure to have mvel classloader as parent during evaluation as it will need mvel classes
      // when generating code
      Thread.currentThread().setContextClassLoader(TemplateRuntime.class.getClassLoader());
      TemplateRuntime runtime = new TemplateRuntime(compiled.getTemplate(), registry, compiled.getRoot(), ".");
      String blah = (String) runtime.execute(new StringBuilder(), null, new MapVariableResolverFactory(vars));
      if (Boolean.TRUE.equals(vars.get("skipFile"))) {
        return null;
      }
      return blah;
    } finally {
      Thread.currentThread().setContextClassLoader(currentCL);
    }
  }
}
