/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.jooby.jade;

import static java.util.Objects.requireNonNull;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import org.jooby.Env;
import org.jooby.Jooby;
import org.jooby.Renderer;

import com.google.inject.Binder;
import com.google.inject.multibindings.Multibinder;
import com.typesafe.config.Config;

import de.neuland.jade4j.JadeConfiguration;
import de.neuland.jade4j.template.ClasspathTemplateLoader;
import de.neuland.jade4j.template.TemplateLoader;

/**
 * <h1>jade</h1>
 * <p>
 * <a href="https://github.com/neuland/jade4j">jade4j's</a> intention is to be able to process jade
 * templates in Java without the need of a JavaScript environment, while being fully compatible with
 * the original jade syntax.
 * </p>
 *
 * <h2>usage</h2>
 *
 * <pre>
 * {
 *   use(new Jade());
 *
 *   get("/", req {@literal ->} Results.html("index").put("model", new MyModel());
 *
 *   // or via API
 *   get("/jade-api", req {@literal ->} {
 *     JadeConfiguration jade = req.require(JadeConfiguration.class);
 *     JadeTemplate template = jade.getTemplate("index");
 *     template.renderTemplate(...);
 *   });
 * }
 * </pre>
 *
 * <p>
 * Templates are loaded from root of classpath: <code>/</code> and must ends with:
 * <code>.jade</code>
 * file extension.
 * </p>
 *
 * <h2>req locals</h2>
 *
 * <p>
 * A template engine has access to ```request locals``` (a.k.a attributes). Here is an example:
 * </p>
 * <pre>
 * {
 *   use(new Jade());
 *
 *   get("*", req {@literal ->} {
 *     req.set("req", req);
 *     req.set("session", req.session());
 *   });
 * }
 * </pre>
 *
 * <p>
 * By default, there is no access to ```req``` or ```session``` from your template. This example
 * shows how to do it.
 * </p>
 *
 * <h2>template loader</h2>
 * <p>
 * Templates are loaded from the root of classpath and must ends with <code>.jade</code>. You can
 * change the extensions too:
 * </p>
 *
 * <pre>
 * {
 *   use(new Jade(".html"));
 * }
 * </pre>
 *
 * <p>
 * Keep in mind if you change it file name must ends with: <code>.html.jade</code>.
 * </p>
 *
 * <h2>template cache</h2>
 * <p>
 * Cache is OFF when <code>application.env = dev</code> (useful for template reloading), otherwise
 * is ON and does not expire, unless you explicitly set <code>jade.caching</code>.
 * </p>
 *
 * <h2>pretty print</h2>
 * <p>
 * Pretty print is on when <code>application.env = dev </code>, otherwise is off, unless unless you
 * explicitly set <code>jade.prettyprint</code>.
 * </p>
 *
 * <p>
 * That's all folks! Enjoy it!!!
 * </p>
 *
 * @since 1.0.0
 */
public class Jade implements Jooby.Module {

  static class IOTemplateLoader implements TemplateLoader {

    private TemplateLoader loader;

    public IOTemplateLoader(final TemplateLoader loader) {
      this.loader = loader;
    }

    @Override
    public long getLastModified(final String name) throws IOException {
      return loader.getLastModified(name);
    }

    @Override
    public Reader getReader(final String name) throws IOException {
      try {
        return loader.getReader(name);
      } catch (NullPointerException ex) {
        throw new FileNotFoundException(name);
      }
    }

  }

  private BiConsumer<JadeConfiguration, Config> callback;

  private String suffix;

  /**
   * Creates a {@link Jade} instance with a custom suffix.
   *
   * @param suffix A suffix like <code>.html</code>. But keep in mind the final extension will be
   *        <code>.html.jade</code>
   */
  public Jade(final String suffix) {
    this.suffix = requireNonNull(suffix, "Suffix is required.");
  }

  /**
   * Creates a {@link Jade} instance with default suffix <code>.jade</code>.
   */
  public Jade() {
    this(".jade");
  }

  /**
   * Configure callback that let you tweak or modified a {@link JadeConfiguration}.
   *
   * @param callback A callback.
   * @return This module.
   */
  public Jade doWith(final Consumer<JadeConfiguration> callback) {
    requireNonNull(callback, "Configurer callback is required.");
    return doWith((j, c) -> callback.accept(j));
  }

  /**
   * Configure callback that let you tweak or modified a {@link JadeConfiguration}.
   *
   * @param callback A callback.
   * @return This module.
   */
  public Jade doWith(final BiConsumer<JadeConfiguration, Config> callback) {
    this.callback = requireNonNull(callback, "Configurer callback is required.");
    return this;
  }

  @Override
  public void configure(final Env env, final Config conf, final Binder binder) {
    JadeConfiguration jadeconf = new JadeConfiguration();
    boolean dev = env.name().equals("dev");
    boolean caching = conf.hasPath("jade.caching")
        ? conf.getBoolean("jade.caching")
        : !dev;
    boolean prettyPrint = conf.hasPath("jade.prettyprint")
        ? conf.getBoolean("jade.prettyprint")
        : dev;

    jadeconf.setCaching(caching);
    jadeconf.setPrettyPrint(prettyPrint);

    Map<String, Object> sharedVariables = new HashMap<>(2);
    sharedVariables.put("env", env);
    sharedVariables.put("xss", new XssHelper(env));
    jadeconf.setSharedVariables(sharedVariables);

    jadeconf.setTemplateLoader(new ClasspathTemplateLoader());

    if (callback != null) {
      callback.accept(jadeconf, conf);
    }

    // rewrite template loader avoid NPE
    jadeconf.setTemplateLoader(new IOTemplateLoader(jadeconf.getTemplateLoader()));

    binder.bind(JadeConfiguration.class)
        .toInstance(jadeconf);

    Multibinder.newSetBinder(binder, Renderer.class).addBinding()
        .toInstance(new Engine(jadeconf, suffix));
  }

}
