/*
 * 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.gora.ignite.utils;

import avro.shaded.com.google.common.collect.Lists;
import com.healthmarketscience.sqlbuilder.BinaryCondition;
import com.healthmarketscience.sqlbuilder.CreateTableQuery;
import com.healthmarketscience.sqlbuilder.CustomSql;
import com.healthmarketscience.sqlbuilder.DeleteQuery;
import com.healthmarketscience.sqlbuilder.DropQuery;
import com.healthmarketscience.sqlbuilder.FunctionCall;
import com.healthmarketscience.sqlbuilder.InsertQuery;
import com.healthmarketscience.sqlbuilder.SelectQuery;
import com.healthmarketscience.sqlbuilder.SqlObject;
import com.healthmarketscience.sqlbuilder.UpdateQuery;
import com.healthmarketscience.sqlbuilder.custom.mysql.MysLimitClause;
import com.healthmarketscience.sqlbuilder.dbspec.Constraint;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbConstraint;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSchema;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSpec;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbTable;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.gora.ignite.store.Column;
import org.apache.gora.ignite.store.IgniteMapping;

/**
 *
 * SQL Builder utility for Ignite.
 */
public class IgniteSQLBuilder {

  /**
   * Returns a SQL query for determine whether a table exists or not.
   *
   * @param tableName The name of the table to be check.
   * @return SQL query
   */
  public static String tableExists(String tableName) {
    DbSpec spec = new DbSpec();
    DbSchema schema = spec.addDefaultSchema();
    DbTable aTable = schema.addTable(tableName);
    return new SelectQuery().addAllColumns().addFromTable(aTable)
        .addCustomization(new MysLimitClause(0)).validate().toString();
  }

  /**
   * Returns a SQL create table statement for initializing a datastore based
   * upon a Ignite Mapping definition.
   *
   * @param mapping The ignite mapping definition of the data store
   * @return SQL create query (DDL).
   */
  public static String createTable(IgniteMapping mapping) {
    DbSpec spec = new DbSpec();
    DbSchema schema = spec.addDefaultSchema();
    DbTable aTable = schema.addTable(mapping.getTableName());
    ArrayList<Column> fieldsList = Lists.newArrayList(mapping.getPrimaryKey());
    fieldsList.addAll(Lists.newArrayList(mapping.getFields().values()));
    for (Column aColumn : fieldsList) {
      String name = aColumn.getName();
      Column.FieldType dataType = aColumn.getDataType();
      aTable.addColumn(name, dataType.toString(), null);
    }
    String[] keys = new String[mapping.getPrimaryKey().size()];
    for (int i = 0; i < mapping.getPrimaryKey().size(); i++) {
      keys[i] = mapping.getPrimaryKey().get(i).getName();
    }
    aTable.addConstraint(new DbConstraint(aTable,
        mapping.getTableName() + "_PRIMARY_KEY",
        Constraint.Type.PRIMARY_KEY, keys));
    return new CreateTableQuery(aTable, true).validate().toString();
  }

  /**
   * Returns a SQL drop table statement for deleting a datastore instance within
   * ignite.
   *
   * @param tableName The name of the table to be dropped.
   * @return SQL drop query (DDL).
   */
  public static String dropTable(String tableName) {
    String statement = DropQuery.dropTable(tableName).validate().toString();
    return statement.substring(0, 11) + "IF EXISTS " + statement.substring(11);
  }

  /**
   * Returns a bare SQL insert statement for adding a new record on a Ignite
   * data store.
   *
   * @param mapping The ignite mapping definition of the data store
   * @param data A map containing the Column-Value pairs of the new record.
   * @return SQL insert statement
   */
  public static String createInsertQuery(IgniteMapping mapping, Map<Column, Object> data) {
    DbSpec spec = new DbSpec();
    DbSchema schema = spec.addDefaultSchema();
    DbTable aTable = schema.addTable(mapping.getTableName());
    InsertQuery insertQuery = new InsertQuery(aTable);
    List<Entry<Column, Object>> list = new ArrayList<>(data.entrySet());
    String[] columns = new String[list.size()];
    for (int i = 0; i < list.size(); i++) {
      columns[i] = list.get(i).getKey().getName();
    }
    return insertQuery.addCustomPreparedColumns(columns).validate().toString()
        .replaceFirst("INSERT", "MERGE");
  }

  /**
   * Fills a SQL PreparedStatement of a insert operation with the actual data to
   * be inserted.
   *
   * @param statement The insertion PreparedStatement to be filled.
   * @param insertData A map containing the Column-Value pairs of the new
   * record.
   * @throws SQLException When invalid values are provided as parameters for the
   * insert statement.
   */
  public static void fillInsertQuery(PreparedStatement statement, Map<Column, Object> insertData) throws SQLException {
    List<Entry<Column, Object>> list = new ArrayList<>(insertData.entrySet());
    for (int i = 0; i < list.size(); i++) {
      int j = i + 1;
      statement.setObject(j, list.get(i).getValue());
    }
  }

  /**
   * Returns a bare SQL statement for deleting a record from the Ignite data
   * store.
   *
   * @param mapping The ignite mapping definition of the data store
   * @return SQL delete statement
   */
  public static String createDeleteQuery(IgniteMapping mapping) {
    DbSpec spec = new DbSpec();
    DbSchema schema = spec.addDefaultSchema();
    DbTable aTable = schema.addTable(mapping.getTableName());
    DeleteQuery statement = new DeleteQuery(aTable);
    for (int i = 0; i < mapping.getPrimaryKey().size(); i++) {
      statement.addCondition(new BinaryCondition(BinaryCondition.Op.EQUAL_TO,
          new DbColumn(aTable, mapping.getPrimaryKey().get(i).getName(), null),
          SqlObject.QUESTION_MARK));
    }
    return statement.validate().toString();
  }

  /**
   * Fills a SQL PreparedStatement of a delete operation with the actual key of
   * the record to be deleted
   *
   * @param statement The deletion PreparedStatement to be filled.
   * @param mapping The ignite mapping definition of the data store
   * @param deleteData An Object array containing the primary key values of the
   * record to be deleted
   * @throws SQLException When invalid keys' values are provided as parameters
   */
  public static void fillDeleteQuery(PreparedStatement statement, IgniteMapping mapping, Object... deleteData) throws SQLException {
    assert mapping.getPrimaryKey().size() == deleteData.length;
    for (int i = 0; i < mapping.getPrimaryKey().size(); i++) {
      int j = i + 1;
      statement.setObject(j, deleteData[i]);
    }
  }

  /**
   * Returns a bare SQL statement for checking if a key exists
   *
   * @param mapping The ignite mapping definition of the data store
   * @return SQL select statement
   */
  public static String createSelectQueryExists(IgniteMapping mapping) {
    DbSpec spec = new DbSpec();
    DbSchema schema = spec.addDefaultSchema();
    DbTable aTable = schema.addTable(mapping.getTableName());
    SelectQuery selectQuery = new SelectQuery();
    selectQuery.addCustomColumns(FunctionCall.countAll());
    selectQuery.addFromTable(aTable);
    for (int i = 0; i < mapping.getPrimaryKey().size(); i++) {
      selectQuery.addCondition(new BinaryCondition(BinaryCondition.Op.EQUAL_TO,
          new DbColumn(aTable, mapping.getPrimaryKey().get(i).getName(), null),
          SqlObject.QUESTION_MARK));
    }
    return selectQuery.validate().toString();
  }

  /**
   * Returns a bare SQL statement for retrieving a record from the ignite data
   * store
   *
   * @param mapping The ignite mapping definition of the data store
   * @param columns A list of columns to be retrieved within the select query
   * @return SQL select statement
   */
  public static String createSelectQueryGet(IgniteMapping mapping, List<String> columns) {
    DbSpec spec = new DbSpec();
    DbSchema schema = spec.addDefaultSchema();
    DbTable aTable = schema.addTable(mapping.getTableName());
    SelectQuery selectQuery = new SelectQuery();
    selectQuery.addFromTable(aTable);
    DbColumn[] lsColumns = new DbColumn[columns.size()];
    for (int i = 0; i < columns.size(); i++) {
      lsColumns[i] = aTable.addColumn(columns.get(i));
    }
    selectQuery.addColumns(lsColumns);
    for (int i = 0; i < mapping.getPrimaryKey().size(); i++) {
      selectQuery.addCondition(new BinaryCondition(BinaryCondition.Op.EQUAL_TO,
          new DbColumn(aTable, mapping.getPrimaryKey().get(i).getName(), null),
          SqlObject.QUESTION_MARK));
    }
    return selectQuery.validate().toString();
  }

  /**
   * Fills a SQL PreparedStatement of a select operation with the actual keys of
   * the record to be retrieved
   *
   * @param statement The select PreparedStatement to be filled.
   * @param mapping The ignite mapping definition of the data store
   * @param selectData An Object array containing the primary key values of the
   * record to be retrieved
   * @throws SQLException When invalid keys' values are provided as parameters
   */
  public static void fillSelectQuery(PreparedStatement statement, IgniteMapping mapping, Object... selectData) throws SQLException {
    assert mapping.getPrimaryKey().size() == selectData.length;
    for (int i = 0; i < mapping.getPrimaryKey().size(); i++) {
      int j = i + 1;
      statement.setObject(j, selectData[i]);
    }
  }

  /**
   * Returns a base SQL statement for retrieving multiple records from the
   * ignite data store
   *
   * @param mapping The ignite mapping definition of the data store
   * @param selectFields A list of columns to be retrieved within the select
   * query
   * @return SQL select statement
   */
  public static String createSelectQuery(IgniteMapping mapping, List<String> selectFields) {
    DbSpec spec = new DbSpec();
    DbSchema schema = spec.addDefaultSchema();
    DbTable aTable = schema.addTable(mapping.getTableName());
    SelectQuery selectQuery = new SelectQuery();
    selectQuery.addFromTable(aTable);
    List<String> fields = new ArrayList<>();
    for (Column c : mapping.getPrimaryKey()) {
      fields.add(c.getName());
    }
    fields.addAll(selectFields);
    DbColumn[] lsColumns = new DbColumn[fields.size()];
    for (int i = 0; i < fields.size(); i++) {
      lsColumns[i] = aTable.addColumn(fields.get(i));
    }
    selectQuery.addColumns(lsColumns);
    return selectQuery.validate().toString();
  }

  /**
   * Returns a base SQL statement for deleting multiple records from the ignite
   * data store
   *
   * @param mapping The ignite mapping definition of the data store
   * @return SQL delete statement
   */
  public static String createDeleteQueryMultipleRecords(IgniteMapping mapping) {
    DbSpec spec = new DbSpec();
    DbSchema schema = spec.addDefaultSchema();
    DbTable aTable = schema.addTable(mapping.getTableName());
    DeleteQuery deleteQuery = new DeleteQuery(aTable);
    return deleteQuery.validate().toString();
  }

  /**
   * Returns a base SQL statement for deleting fields from records of the ignite
   * data store
   *
   * @param mapping The ignite mapping definition of the data store
   * @param deleteFields A list of columns to be deleted (set to null)
   * @return SQL update statement
   */
  public static String createDeleteQueryWithFields(IgniteMapping mapping, List<String> deleteFields) {
    DbSpec spec = new DbSpec();
    DbSchema schema = spec.addDefaultSchema();
    DbTable aTable = schema.addTable(mapping.getTableName());
    UpdateQuery updateQuery = new UpdateQuery(aTable);
    for (int i = 0; i < deleteFields.size(); i++) {
      updateQuery.addSetClause(new DbColumn(aTable, deleteFields.get(i), null), SqlObject.NULL_VALUE);
    }
    return updateQuery.validate().toString();
  }

  /**
   * Returns a SQL's WHERE segment with proper conditions set for
   * Querying/Deleting/Updating multiple records of a ignite data store
   *
   * @param mapping The ignite mapping definition of the data store
   * @param startKey Start key of the WHERE condition
   * @param endKey End key of the WHERE condition
   * @param limit The maximum number of records to be consider
   * @return SQL WHERE segment
   */
  public static String createWhereClause(IgniteMapping mapping, Object startKey, Object endKey, long limit) {
    DbSpec spec = new DbSpec();
    DbSchema schema = spec.addDefaultSchema();
    DbTable aTable = schema.addTable(mapping.getTableName());
    SelectQuery selectQuery = new SelectQuery();
    selectQuery.addFromTable(aTable);
    String fisrtPart = selectQuery.validate().toString();
    String keycolumn = mapping.getPrimaryKey().get(0).getName();
    if (startKey != null || endKey != null) {
      if (startKey != null && endKey != null && startKey.equals(endKey)) {
        selectQuery.addCondition(new BinaryCondition(BinaryCondition.Op.EQUAL_TO,
            new CustomSql(keycolumn), SqlObject.QUESTION_MARK));
      } else {
        if (startKey != null) {
          selectQuery.addCondition(new BinaryCondition(BinaryCondition.Op.GREATER_THAN_OR_EQUAL_TO,
              new CustomSql(keycolumn), SqlObject.QUESTION_MARK));
        }
        if (endKey != null) {
          selectQuery.addCondition(new BinaryCondition(BinaryCondition.Op.LESS_THAN_OR_EQUAL_TO,
              new CustomSql(keycolumn), SqlObject.QUESTION_MARK));
        }
      }
    }
    if (limit > 0) {
      selectQuery.addCustomization(new MysLimitClause(limit));
    }
    String completeQuery = selectQuery.validate().toString();
    return completeQuery.substring(fisrtPart.length());
  }

  /**
   * Fills a SQL PreparedStatement's WHERE segment of a select/delete/update
   * operation with proper key values
   *
   * @param statement The select PreparedStatement to be filled.
   * @param startKey Start key of the WHERE condition
   * @param endKey End key of the WHERE condition
   * @throws SQLException When invalid keys' values are provided as parameters
   */
  public static void fillWhereClause(PreparedStatement statement, Object startKey, Object endKey) throws SQLException {
    if (startKey != null || endKey != null) {
      if (startKey != null && endKey != null && startKey.equals(endKey)) {
        statement.setObject(1, startKey);
      } else {
        if (startKey != null && endKey != null) {
          statement.setObject(1, startKey);
          statement.setObject(2, endKey);
        } else {
          if (startKey != null) {
            statement.setObject(1, startKey);
          } else {
            statement.setObject(1, endKey);
          }
        }
      }
    }
  }

}
