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

import static java.util.Objects.requireNonNull;
import static javaslang.API.Case;
import static javaslang.API.Match;

import java.util.function.BiConsumer;
import java.util.function.Function;

import org.jooby.Env;
import org.jooby.Env.ServiceKey;
import org.jooby.jdbc.Jdbc;

import com.google.inject.Binder;
import com.querydsl.sql.Configuration;
import com.querydsl.sql.DB2Templates;
import com.querydsl.sql.FirebirdTemplates;
import com.querydsl.sql.H2Templates;
import com.querydsl.sql.HSQLDBTemplates;
import com.querydsl.sql.MySQLTemplates;
import com.querydsl.sql.OracleTemplates;
import com.querydsl.sql.PostgreSQLTemplates;
import com.querydsl.sql.SQLQueryFactory;
import com.querydsl.sql.SQLTemplates;
import com.querydsl.sql.SQLiteTemplates;
import com.typesafe.config.Config;

/**
 * <h1>queryDSL</h1>
 *
 * <p>
 * SQL abstraction provided by <a href="http://www.querydsl.com">QueryDSL</a> using plain JDBC
 * underneath.
 * </p>
 *
 * <p>
 * This module depends on {@link Jdbc} module, make sure you read the doc of the {@link Jdbc}
 * module before using this module.
 * </p>
 *
 * <h2>usage</h2>
 *
 * <pre>
 * import org.jooby.querydsl.QueryDSL;
 *
 * {
 *   use(new QueryDSL());
 *
 *   get("/my-api", req {@literal ->} {
 *     SQLQueryFactory queryFactory = req.require(SQLQueryFactory.class);
 *     // Do something with the database
 *     ...
 *   });
 * }
 * </pre>
 *
 * <h2>dialects</h2>
 * <p>
 * Dialect is detected automatically and usually you don't need to do anything. But if the default
 * dialect detector doesn't work and/or you have a custom dialect:
 * </p>
 *
 * <pre>
 * {
 *   use(new QueryDSL().with(new MyCustomTemplates());
 * }
 * </pre>
 *
 * <h2>multiple databases</h2>
 *
 * <pre>
 * import org.jooby.querydsl.QueryDSL;
 *
 * {
 *   use(new QueryDSL("db.main"));
 *   use(new QueryDSL("db.aux"));
 *
 *   get("/my-api", req {@literal ->} {
 *     SQLQueryFactory queryFactory = req.require("db.main", SQLQueryFactory.class);
 *     // Do something with the database
 *   });
 * }
 * </pre>
 *
 * <h2>advanced configuration</h2>
 * <p>
 * This module builds QueryDSL SQLQueryFactories on top of a default {@link Configuration} object, a
 * {@link SQLTemplates} instance and a {@link javax.sql.DataSource} from the {@link Jdbc} module.
 * </p>
 * <p>
 * Advanced configuration can be added by invoking the {@link #doWith(BiConsumer)} method, adding
 * your own settings to the configuration.
 * </p>
 *
 * <pre>
 * {
 *   use(new QueryDSL().doWith(conf {@literal ->} {
 *     conf.set(...);
 *   });
 * }
 * </pre>
 *
 * <h2>code generation</h2>
 * <p>
 * This module does not provide code generation for DB mapping classes. To learn how to create
 * QueryDSL mapping classes using Maven, please consult the
 * <a href="http://www.querydsl.com/static/querydsl/latest/reference/html_single/#d0e725">QueryDSL
 * documentation.</a>
 * </p>
 *
 * @since 1.0.0.CR
 * @author sjackel
 */
public class QueryDSL extends Jdbc {

  private Function<String, SQLTemplates> templates = QueryDSL::toSQLTemplates;

  /**
   * Creates a new {@link QueryDSL} module
   *
   * @param name Database name
   */
  public QueryDSL(final String name) {
    super(name);
  }

  /**
   * Creates a new {@link QueryDSL} module
   */
  public QueryDSL() {
  }

  @Override
  public void configure(final Env env, final Config conf, final Binder binder) {
    super.configure(env, conf, binder, (name, ds) -> {
      SQLTemplates templates = this.templates.apply(dbtype.orElse("unknown"));

      Configuration querydslconf = new Configuration(templates);

      callback(querydslconf, conf);

      SQLQueryFactory sqfp = new SQLQueryFactory(querydslconf, ds);

      ServiceKey serviceKey = env.serviceKey();
      serviceKey.generate(SQLTemplates.class, name, k -> binder.bind(k).toInstance(templates));
      serviceKey.generate(Configuration.class, name, k -> binder.bind(k).toInstance(querydslconf));
      serviceKey.generate(SQLQueryFactory.class, name, k -> binder.bind(k).toInstance(sqfp));
    });
  }

  public QueryDSL with(final SQLTemplates templates) {
    requireNonNull(templates, "SQL templates needs to be defined");
    this.templates = t -> templates;
    return this;
  }

  static SQLTemplates toSQLTemplates(final String type) {
    return Match(type).option(
        Case("db2", DB2Templates::new),
        Case("mysql", MySQLTemplates::new),
        Case("mariadb", MySQLTemplates::new),
        Case("h2", H2Templates::new),
        Case("hsqldb", HSQLDBTemplates::new),
        Case("pgsql", PostgreSQLTemplates::new),
        Case("postgresql", PostgreSQLTemplates::new),
        Case("sqlite", SQLiteTemplates::new),
        Case("oracle", OracleTemplates::new),
        Case("firebirdsql", FirebirdTemplates::new))
        .getOrElseThrow(() -> new IllegalStateException("Unsupported database: " + type));
  }
}
