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

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;

import org.jooby.Env;
import org.jooby.Env.ServiceKey;
import org.jooby.Jooby;

import com.google.inject.Binder;
import com.mongodb.DB;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
import com.mongodb.MongoClientOptions.Builder;
import com.mongodb.MongoClientURI;
import com.mongodb.client.MongoDatabase;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;

/**
 * Exposes {@link MongoClient} and a {@link DB}.
 *
 * <h1>usage</h1>
 *
 * <p>
 * application.conf:
 * </p>
 *
 * <pre>
 * db = "mongodb://localhost/mydb"
 * </pre>
 *
 * <pre>
 * {
 *   use(new Mongodb());
 *
 *   get("/", req {@literal ->} {
 *     MongoClient client = req.require(MongoClient.class);
 *     // work with client
 *     DB = req.require(DB.class);
 *     // work with mydb
 *   });
 * }
 * </pre>
 *
 * Default URI connection property is <code>db</code> but of course you can use any other name:
 *
 * <p>
 * application.conf:
 * </p>
 *
 * <pre>
 * mydb = "mongodb://localhost/mydb"
 * </pre>
 *
 * <pre>
 * {
 *   use(new Mongodb("mydb"));
 *
 *   get("/", req {@literal ->} {
 *     DB mydb = req.require(DB.class);
 *     // work with mydb
 *   });
 * }
 * </pre>
 *
 * <h1>options</h1>
 * <p>
 * Options can be set via <code>.conf</code> file:
 * </p>
 *
 * <pre>
 * mongodb.connectionsPerHost  = 100
 * </pre>
 *
 * <p>
 * or programmatically:
 * </p>
 *
 * <pre>
 * {
 *   use(new Mongodb()
 *     .options((options, config) {@literal ->} {
 *       options.connectionsPerHost(100);
 *     })
 *   );
 * }
 * </pre>
 *
 * <h2>connection URI</h2>
 * <p>
 * Default connection URI is defined by the <code>db</code> property. Mongodb URI looks like:
 * </p>
 *
 * <pre>
 *   mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database[.collection]][?options]]
 * </pre>
 *
 * For more detailed information please check: {@link MongoClientURI}.
 *
 * <h1>two or more connections</h1>
 *
 * <pre>
 * db1 = "mongodb://localhost/mydb1"
 * db2 = "mongodb://localhost/mydb2"
 * </pre>
 *
 * <pre>
 * {
 *   use(new Mongodb("db1"));
 *   use(new Mongodb("db2"));
 *
 *   get("/", req {@literal ->} {
 *     MongoClient client1 = req.require("mydb1", MongoClient.class);
 *     // work with mydb1
 *     MongoClient client2 = req.require("mydb2", MongoClient.class);
 *     // work with mydb1
 *   });
 * }
 * </pre>
 *
 * @author edgar
 * @since 0.5.0
 */
public class Mongodb implements Jooby.Module {

  private final String db;

  private BiConsumer<Builder, Config> options;

  /**
   * Creates a new {@link Mongodb} module.
   *
   * @param db Name of the property with the connection URI.
   */
  public Mongodb(final String db) {
    this.db = requireNonNull(db, "A mongo db is required.");
  }

  /**
   * Creates a new {@link Mongodb} using the default property: <code>db</code>.
   */
  public Mongodb() {
    this("db");
  }

  @Override
  public void configure(final Env env, final Config config, final Binder binder) {
    configure(env, config, binder, (uri, mongo) -> {
    });
  }

  protected void configure(final Env env, final Config config, final Binder binder,
      final BiConsumer<MongoClientURI, MongoClient> callback) {
    MongoClientOptions.Builder options = options(mongodb(config));

    if (this.options != null) {
      this.options.accept(options, config);
    }

    MongoClientURI uri = new MongoClientURI(config.getString(db), options);
    String database = uri.getDatabase();
    checkArgument(database != null, "Database not found: " + uri);
    MongoClient client = new MongoClient(uri);

    ServiceKey serviceKey = env.serviceKey();
    serviceKey.generate(MongoClientURI.class, database, k -> binder.bind(k).toInstance(uri));

    serviceKey.generate(MongoClient.class, database,
        k -> binder.bind(k).toInstance(client));

    MongoDatabase mongodb = client.getDatabase(database);
    serviceKey.generate(MongoDatabase.class, database,
        k -> binder.bind(k).toInstance(mongodb));

    env.onStop(client::close);

    callback.accept(uri, client);
  }

  /**
   * Set an options callback.
   *
   * <pre>
   * {
   *   use(new Mongodb()
   *     .options((options, config) {@literal ->} {
   *       options.connectionsPerHost(100);
   *     })
   *   );
   * }
   * </pre>
   *
   * @param options Configure callback.
   * @return This module
   */
  public Mongodb options(final BiConsumer<MongoClientOptions.Builder, Config> options) {
    this.options = requireNonNull(options, "Options callback is required.");
    return this;
  }

  @Override
  public Config config() {
    return ConfigFactory.parseResources(Mongodb.class, "mongodb.conf");
  }

  private MongoClientOptions.Builder options(final Config config) {
    MongoClientOptions.Builder builder = MongoClientOptions.builder();

    builder.connectionsPerHost(config.getInt("connectionsPerHost"));
    builder.threadsAllowedToBlockForConnectionMultiplier(
        config.getInt("threadsAllowedToBlockForConnectionMultiplier"));
    builder.maxWaitTime((int) config.getDuration("maxWaitTime", TimeUnit.MILLISECONDS));
    builder.connectTimeout((int) config.getDuration("connectTimeout", TimeUnit.MILLISECONDS));
    builder.socketTimeout((int) config.getDuration("socketTimeout", TimeUnit.MILLISECONDS));
    builder.socketKeepAlive(config.getBoolean("socketKeepAlive"));
    builder.cursorFinalizerEnabled(config.getBoolean("cursorFinalizerEnabled"));
    builder.alwaysUseMBeans(config.getBoolean("alwaysUseMBeans"));
    builder.heartbeatFrequency(config.getInt("heartbeatFrequency"));
    builder.minHeartbeatFrequency(config.getInt("minHeartbeatFrequency"));
    builder.heartbeatConnectTimeout(
        (int) config.getDuration("heartbeatConnectTimeout", TimeUnit.MILLISECONDS));
    builder.heartbeatSocketTimeout(
        (int) config.getDuration("heartbeatSocketTimeout", TimeUnit.MILLISECONDS));

    return builder;
  }

  private Config mongodb(final Config config) {
    Config $mongodb = config.getConfig("mongodb");
    if (config.hasPath("mongodb." + db)) {
      $mongodb = config.getConfig("mongodb." + db).withFallback($mongodb);
    }
    return $mongodb;
  }
}
