/*
 * 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.apache.spark.sql.jdbc

import org.apache.spark.sql.connector.expressions.filter.Predicate
import org.apache.spark.sql.execution.datasources.jdbc.{JDBCOptions, JDBCPartition}
import org.apache.spark.sql.execution.datasources.v2.TableSampleInfo

/**
 * The builder to build a single SELECT query.
 *
 * Note: All the `withXXX` methods will be invoked at most once. The invocation order does not
 * matter, as all these clauses follow the natural SQL order: sample the table first, then filter,
 * then group by, then sort, then offset, then limit.
 *
 * @since 3.5.0
 */
class JdbcSQLQueryBuilder(dialect: JdbcDialect, options: JDBCOptions) {

  /**
   * `columns`, but as a String suitable for injection into a SQL query.
   */
  protected var columnList: String = "1"

  /**
   * A WHERE clause representing both `filters`, if any, and the current partition.
   */
  protected var whereClause: String = ""

  /**
   * A GROUP BY clause representing pushed-down grouping columns.
   */
  protected var groupByClause: String = ""

  /**
   * A ORDER BY clause representing pushed-down sort of top n.
   */
  protected var orderByClause: String = ""

  /**
   * A LIMIT value representing pushed-down limit.
   */
  protected var limit: Int = -1

  /**
   * A OFFSET value representing pushed-down offset.
   */
  protected var offset: Int = -1

  /**
   * A table sample clause representing pushed-down table sample.
   */
  protected var tableSampleClause: String = ""

  /**
   * The columns names that following dialect's SQL syntax.
   * e.g. The column name is the raw name or quoted name.
   */
  def withColumns(columns: Array[String]): JdbcSQLQueryBuilder = {
    if (columns.nonEmpty) {
      columnList = columns.mkString(",")
    }
    this
  }

  /**
   * Constructs the WHERE clause that following dialect's SQL syntax.
   */
  def withPredicates(predicates: Array[Predicate], part: JDBCPartition): JdbcSQLQueryBuilder = {
    // `filters`, but as a WHERE clause suitable for injection into a SQL query.
    val filterWhereClause: String = {
      predicates.flatMap(dialect.compileExpression(_)).map(p => s"($p)").mkString(" AND ")
    }

    // A WHERE clause representing both `filters`, if any, and the current partition.
    whereClause = if (part.whereClause != null && filterWhereClause.length > 0) {
      "WHERE " + s"($filterWhereClause)" + " AND " + s"(${part.whereClause})"
    } else if (part.whereClause != null) {
      "WHERE " + part.whereClause
    } else if (filterWhereClause.length > 0) {
      "WHERE " + filterWhereClause
    } else {
      ""
    }

    this
  }

  /**
   * Constructs the GROUP BY clause that following dialect's SQL syntax.
   */
  def withGroupByColumns(groupByColumns: Array[String]): JdbcSQLQueryBuilder = {
    if (groupByColumns.nonEmpty) {
      // The GROUP BY columns should already be quoted by the caller side.
      groupByClause = s"GROUP BY ${groupByColumns.mkString(", ")}"
    }

    this
  }

  /**
   * Constructs the ORDER BY clause that following dialect's SQL syntax.
   */
  def withSortOrders(sortOrders: Array[String]): JdbcSQLQueryBuilder = {
    if (sortOrders.nonEmpty) {
      orderByClause = s" ORDER BY ${sortOrders.mkString(", ")}"
    }

    this
  }

  /**
   * Saves the limit value used to construct LIMIT clause.
   */
  def withLimit(limit: Int): JdbcSQLQueryBuilder = {
    this.limit = limit

    this
  }

  /**
   * Saves the offset value used to construct OFFSET clause.
   */
  def withOffset(offset: Int): JdbcSQLQueryBuilder = {
    this.offset = offset

    this
  }

  /**
   * Constructs the table sample clause that following dialect's SQL syntax.
   */
  def withTableSample(sample: TableSampleInfo): JdbcSQLQueryBuilder = {
    tableSampleClause = dialect.getTableSample(sample)

    this
  }

  /**
   * Build the final SQL query that following dialect's SQL syntax.
   */
  def build(): String = {
    // Constructs the LIMIT clause that following dialect's SQL syntax.
    val limitClause = dialect.getLimitClause(limit)
    // Constructs the OFFSET clause that following dialect's SQL syntax.
    val offsetClause = dialect.getOffsetClause(offset)

    options.prepareQuery +
      s"SELECT $columnList FROM ${options.tableOrQuery} $tableSampleClause" +
      s" $whereClause $groupByClause $orderByClause $limitClause $offsetClause"
  }
}
