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

import static java.util.Objects.requireNonNull;

import java.net.URI;
import java.util.concurrent.TimeUnit;

import javax.inject.Provider;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.jooby.Env;
import org.jooby.Env.ServiceKey;
import org.jooby.Jooby;

import com.google.inject.Binder;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 * Redis cache and key/value data store for Jooby. Exposes a {@link Jedis} service.
 *
 * <h1>usage</h1>
 * <p>
 * It is pretty straightforward:
 * </p>
 *
 * <pre>
 * # define a database URI
 * db = "redis://localhost:6379"
 * </pre>
 *
 * <pre>
 * {
 *   use(new Redis());
 *
 *    get("/:key/:value", req {@literal ->} {
 *      try (Jedis jedis = req.require(Jedis.class)) {
 *        jedis.set(req.param("key").value(), req.param("value").value());
 *        return jedis.get(req.param("key").value());
 *      }
 *    });
 * }
 * </pre>
 *
 * <h1>pool configuration</h1>
 * <p>
 * This module creates a {@link JedisPool}. A default pool is created with a max of <code>128</code>
 * instances.
 * </p>
 *
 * <p>
 * The pool can be customized from your <code>application.conf</code>:
 * </p>
 *
 * <pre>
 * db = "redis://localhost:6379"
 *
 * # increase pool size to 200
 * jedis.pool.maxTotal = 200
 * </pre>
 *
 * <h2>two or more redis connections</h2>
 * <p>
 * In case you need two or more Redis connection, just do:
 * </p>
 *
 * <pre>
 * {
 *   use(new Redis()); // default is "db"
 *   use(new Redis("db1"));
 *
 *   get("/:key/:value", req {@literal ->} {
 *     try (Jedis jedis = req.require("db1", Jedis.class)) {
 *       jedis.set(req.param("key").value(), req.param("value").value());
 *       return jedis.get(req.param("key").value());
 *     }
 *   });
 * }
 * </pre>
 *
 * application.conf:
 *
 * <pre>
 * db = "redis://localhost:6379/0"
 *
 * db1 = "redis://localhost:6379/1"
 * </pre>
 *
 * Pool configuration for <code>db1</code> is inherited from <code>jedis.pool</code>. If you need
 * to tweak the pool configuration for <code>db1</code> just do:
 *
 * <pre>
 * db1 = "redis://localhost:6379/1"
 *
 * # ONLY 10 for db1
 * jedis.db1.maxTotal = 10
 * </pre>
 *
 * <p>
 * For more information about <a href="https://github.com/xetorthio/jedis">Jedis</a> checkout the <a
 * href="https://github.com/xetorthio/jedis/wiki">wiki</a>
 * </p>
 *
 * <p>
 * That's all folks! Enjoy it!
 * </p>
 *
 * TBD: Object mapping? https://github.com/xetorthio/johm?
 *
 * @author edgar
 * @since 0.5.0
 */
public class Redis implements Jooby.Module {

  /**
   * Database name.
   */
  private String name;

  /**
   * <p>
   * Creates a new {@link Redis} instance and connect to the provided database. Please note, the
   * name is a property in your <code>application.conf</code> file with Redis URI.
   * </p>
   *
   * <pre>
   * {
   *   use(new Redis("db1"));
   * }
   * </pre>
   *
   * application.conf
   *
   * <pre>
   * db1 = ""redis://localhost:6379""
   * </pre>
   *
   * Default database name is: <code>db</code>
   *
   * @param name A database name.
   */
  public Redis(final String name) {
    this.name = requireNonNull(name, "A db property is required.");
  }

  /**
   * A new {@link Redis} instance, with a default database name of: <code>db</code>.
   */
  public Redis() {
    this("db");
  }

  @Override
  public void configure(final Env env, final Config config, final Binder binder) {
    /**
     * Pool
     */
    GenericObjectPoolConfig poolConfig = poolConfig(config, name);
    int timeout = (int) config.getDuration("jedis.timeout", TimeUnit.MILLISECONDS);
    URI uri = URI.create(config.getString(name));
    JedisPool pool = new JedisPool(poolConfig, uri, timeout);

    RedisProvider provider = new RedisProvider(pool, uri, poolConfig);
    env.onStart(provider::start);
    env.onStop(provider::stop);

    Provider<Jedis> jedis = (Provider<Jedis>) () -> pool.getResource();

    ServiceKey serviceKey = env.serviceKey();
    serviceKey.generate(JedisPool.class, name, k -> binder.bind(k).toInstance(pool));
    serviceKey.generate(Jedis.class, name,
        k -> binder.bind(k).toProvider(jedis));
  }

  private GenericObjectPoolConfig poolConfig(final Config config, final String name) {
    Config poolConfig = config.getConfig("jedis.pool");
    String override = "jedis." + name;
    if (config.hasPath(override)) {
      poolConfig = config.getConfig(override).withFallback(poolConfig);
    }
    return poolConfig(poolConfig);
  }

  private GenericObjectPoolConfig poolConfig(final Config config) {
    GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
    poolConfig.setBlockWhenExhausted(config.getBoolean("blockWhenExhausted"));
    poolConfig.setEvictionPolicyClassName(config.getString("evictionPolicyClassName"));
    poolConfig.setJmxEnabled(config.getBoolean("jmxEnabled"));
    poolConfig.setJmxNamePrefix(config.getString("jmxNamePrefix"));
    poolConfig.setLifo(config.getBoolean("lifo"));
    poolConfig.setMaxIdle(config.getInt("maxIdle"));
    poolConfig.setMaxTotal(config.getInt("maxTotal"));
    poolConfig.setMaxWaitMillis(config.getDuration("maxWait", TimeUnit.MILLISECONDS));
    poolConfig.setMinEvictableIdleTimeMillis(config.getDuration("minEvictableIdle",
        TimeUnit.MILLISECONDS));
    poolConfig.setMinIdle(config.getInt("minIdle"));
    poolConfig.setNumTestsPerEvictionRun(config.getInt("numTestsPerEvictionRun"));
    poolConfig.setSoftMinEvictableIdleTimeMillis(
        config.getDuration("softMinEvictableIdle", TimeUnit.MILLISECONDS));
    poolConfig.setTestOnBorrow(config.getBoolean("testOnBorrow"));
    poolConfig.setTestOnReturn(config.getBoolean("testOnReturn"));
    poolConfig.setTestWhileIdle(config.getBoolean("testWhileIdle"));
    poolConfig.setTimeBetweenEvictionRunsMillis(config.getDuration("timeBetweenEvictionRuns",
        TimeUnit.MILLISECONDS));

    return poolConfig;
  }

  @Override
  public Config config() {
    return ConfigFactory.parseResources(getClass(), "jedis.conf");
  }

}
