/**
 * 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.hbs;

import static java.util.Objects.requireNonNull;

import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import org.jooby.Env;
import org.jooby.Jooby;
import org.jooby.Renderer;
import org.jooby.internal.hbs.ConfigValueResolver;
import org.jooby.internal.hbs.HbsEngine;
import org.jooby.internal.hbs.HbsHelpers;
import org.jooby.internal.hbs.RequestValueResolver;
import org.jooby.internal.hbs.SessionValueResolver;

import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.ValueResolver;
import com.github.jknack.handlebars.cache.GuavaTemplateCache;
import com.github.jknack.handlebars.cache.NullTemplateCache;
import com.github.jknack.handlebars.context.FieldValueResolver;
import com.github.jknack.handlebars.context.JavaBeanValueResolver;
import com.github.jknack.handlebars.context.MapValueResolver;
import com.github.jknack.handlebars.context.MethodValueResolver;
import com.github.jknack.handlebars.io.ClassPathTemplateLoader;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheBuilderSpec;
import com.google.inject.Binder;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.name.Names;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;

/**
 * <h1>handlebars</h1>
 * <p>
 * Logic-less and semantic Mustache templates via
 * <a href="https://github.com/jknack/handlebars.java">handlebars.java</a>.
 * </p>
 *
 * <h2>exports</h2>
 * <ul>
 * <li>{@link Handlebars} object.</li>
 * <li>{@link Renderer} object.</li>
 * </ul>
 *
 * <h2>usage</h2>
 * <p>
 * It is pretty straightforward:
 * </p>
 *
 * <pre>
 * {
 *   use(new Hbs());
 *
 *   get("/", req {@literal ->} Results.html("index").put("model", new MyModel());
 * }
 * </pre>
 * <p>
 * public/index.html:
 * </p>
 *
 * <pre>
 *   {{model}}
 * </pre>
 *
 * <p>
 * Templates are loaded from root of classpath: <code>/</code> and must end with: <code>.html</code>
 * file extension.
 * </p>
 *
 * <h2>options</h2>
 * <h3>helpers</h3>
 * <p>
 * Simple/basic helpers are add it at startup time:
 * </p>
 *
 * <pre>
 * {
 *   use(new Hbs().doWith((hbs, config) {@literal ->} {
 *     hbs.registerHelper("myhelper", (ctx, options) {@literal ->} {
 *       return ...;
 *     });
 *     hbs.registerHelpers(Helpers.class);
 *   });
 * }
 * </pre>
 * <p>
 * Now, if the helper depends on a service and require injection:
 * </p>
 *
 * <pre>
 * {
 *   use(new Hbs().with(Helpers.class));
 * }
 * </pre>
 *
 * <p>
 * The <code>Helpers</code> will be injected by Guice and Handlebars will scan and discover any
 * helper method.
 * </p>
 *
 * <h3>template loader</h3>
 * <p>
 * Templates are loaded from the root of classpath and must end with <code>.html</code>. You can
 * change the default template location and extensions too:
 * </p>
 *
 * <pre>
 * {
 *   use(new Hbs("/", ".hbs"));
 * }
 * </pre>
 *
 * <h3>cache</h3>
 * <p>
 * Cache is OFF when <code>env=dev</code> (useful for template reloading), otherwise is ON.
 * </p>
 * <p>
 * Cache is backed by Guava and the default cache will expire after <code>100</code> entries.
 * </p>
 * <p>
 * If <code>100</code> entries is not enough or you need a more advanced cache setting, just set the
 * <code>hbs.cache</code> option:
 * </p>
 *
 * <pre>
 * hbs.cache = "expireAfterWrite=1h"
 * </pre>
 *
 * <p>
 * See {@link CacheBuilderSpec}.
 * </p>
 *
 * <p>
 * That's all folks! Enjoy it!!!
 * </p>
 *
 * @author edgar
 * @since 0.5.0
 */
public class Hbs implements Jooby.Module {

  private final Handlebars hbs;

  private BiConsumer<Handlebars, Config> callback;

  private Set<Class<?>> helpers = new HashSet<>();

  private Deque<ValueResolver> resolvers = new LinkedList<>();

  /**
   * Creates a new {@link Hbs} module.
   *
   * @param prefix Template prefix.
   * @param suffix Template suffix.
   * @param helpers Optional list of helpers.
   */
  public Hbs(final String prefix, final String suffix, final Class<?>... helpers) {
    this.hbs = new Handlebars(new ClassPathTemplateLoader(prefix, suffix));
    with(helpers);
    // default value resolvers.
    this.resolvers.add(MapValueResolver.INSTANCE);
    this.resolvers.add(JavaBeanValueResolver.INSTANCE);
    this.resolvers.add(MethodValueResolver.INSTANCE);
    this.resolvers.add(new RequestValueResolver());
    this.resolvers.add(new SessionValueResolver());
    this.resolvers.add(new ConfigValueResolver());
    this.resolvers.add(FieldValueResolver.INSTANCE);
  }

  /**
   * Creates a new {@link Hbs} module.
   *
   * @param prefix Template prefix.
   * @param helpers Optional list of helpers.
   */
  public Hbs(final String prefix, final Class<?>... helpers) {
    this(prefix, ".html", helpers);
  }

  /**
   * Creates a new {@link Hbs} module.
   *
   * @param helpers Optional list of helpers.
   */
  public Hbs(final Class<?>... helpers) {
    this("/", helpers);
  }

  /**
   * Set a handlebars callback. Usage:
   *
   * <pre>{@code
   * {
   *   use(new Hbs().doWith((hbs, conf) -> {
   *     ...
   *   });
   * }
   * }</pre>
   *
   * @param callback Configurer callback.
   * @return This module.
   */
  public Hbs doWith(final BiConsumer<Handlebars, Config> callback) {
    this.callback = requireNonNull(callback, "Configurer is required.");
    return this;
  }

  /**
   * Set a handlebars callback.
   *
   * <pre>{@code
   * {
   *   use(new Hbs().doWith((hbs, conf) -> {
   *     ...
   *   });
   * }
   * }</pre>
   * @param callback Configurer callback.
   * @return This module.
   */
  public Hbs doWith(final Consumer<Handlebars> callback) {
    requireNonNull(callback, "Configurer is required.");
    return doWith((hbs, conf) -> callback.accept(hbs));
  }

  /**
   * Append one or more helper classes.
   *
   * @param helper Helper class.
   * @return This module.
   */
  public Hbs with(final Class<?>... helper) {
    for (Class<?> h : helper) {
      helpers.add(h);
    }
    return this;
  }

  /**
   * Append a {@link ValueResolver}.
   *
   * @param resolver Resolver.
   * @return This module.
   */
  public Hbs with(final ValueResolver resolver) {
    requireNonNull(resolver, "Value resolver is required.");
    this.resolvers.addFirst(resolver);
    return this;
  }

  @Override
  public void configure(final Env env, final Config config, final Binder binder) {

    // cache
    if ("dev".equals(env.name()) || config.getString("hbs.cache").isEmpty()) {
      // noop cache
      hbs.with(NullTemplateCache.INSTANCE);
    } else {
      hbs.with(new GuavaTemplateCache(
          CacheBuilder
              .from(config.getString("hbs.cache"))
              .build()));
    }

    if (callback != null) {
      callback.accept(hbs, config);
    }

    /** XSS */
    hbs.registerHelper("xss", (value, opts) -> {
      String[] xss = new String[opts.params.length];
      System.arraycopy(opts.params, 0, xss, 0, opts.params.length);
      return new Handlebars.SafeString(env.xss(xss).apply(value.toString()));
    });

    binder.bind(Handlebars.class).toInstance(hbs);

    Multibinder<Object> helpersBinding = Multibinder
        .newSetBinder(binder, Object.class, Names.named("hbs.helpers"));
    helpers.forEach(h -> helpersBinding.addBinding().to(h));

    HbsEngine engine = new HbsEngine(hbs, resolvers.toArray(new ValueResolver[resolvers.size()]));

    Multibinder.newSetBinder(binder, Renderer.class).addBinding()
        .toInstance(engine);

    // helper bootstrap
    binder.bind(HbsHelpers.class).asEagerSingleton();
  }

  @Override
  public Config config() {
    return ConfigFactory.empty(Hbs.class.getName())
        .withValue("hbs.cache", ConfigValueFactory.fromAnyRef("maximumSize=100"));
  }

}
