// Copyright (c) YugaByte, Inc.
//
// Licensed 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 com.yugabyte.sample.apps;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

import org.apache.log4j.Logger;

import com.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import com.yugabyte.sample.common.SimpleLoadGenerator.Key;

/**
 * This workload writes and reads some random string keys from a CQL server. By default, this app
 * inserts a million keys, and reads/updates them indefinitely.
 */
public class CassandraTransactionalKeyValue extends CassandraKeyValue {
  private static final Logger LOG = Logger.getLogger(CassandraTransactionalKeyValue.class);

  // Static initialization of this workload's config. These are good defaults for getting a decent
  // read dominated workload on a reasonably powered machine. Exact IOPS will of course vary
  // depending on the machine and what resources it has to spare.
  static {
    // Disable the read-write percentage.
    appConfig.readIOPSPercentage = -1;
    // Set the read and write threads to 1 each.
    appConfig.numReaderThreads = 24;
    appConfig.numWriterThreads = 2;
    // The number of keys to read.
    appConfig.numKeysToRead = -1;
    // The number of keys to write. This is the combined total number of inserts and updates.
    appConfig.numKeysToWrite = -1;
    // The number of unique keys to write. This determines the number of inserts (as opposed to
    // updates).
    appConfig.numUniqueKeysToWrite = NUM_UNIQUE_KEYS;
  }

  // The default table name to create and use for CRUD ops.
  private static final String DEFAULT_TABLE_NAME =
      CassandraTransactionalKeyValue.class.getSimpleName();

  public CassandraTransactionalKeyValue() {
  }

  @Override
  public List<String> getCreateTableStatements() {
    String createStmt = String.format(
        "CREATE TABLE IF NOT EXISTS %s (k varchar, v bigint, primary key (k)) " +
        "WITH transactions = { 'enabled' : true };", getTableName());
    return Arrays.asList(createStmt);
  }

  public String getTableName() {
    return appConfig.tableName != null ? appConfig.tableName : DEFAULT_TABLE_NAME;
  }

  private PreparedStatement getPreparedSelect()  {
    return getPreparedSelect(String.format("SELECT k, v, writetime(v) FROM %s WHERE k in :k;",
                                           getTableName()),
                             appConfig.localReads);
  }

  private void verifyValue(Key key, long value1, long value2) {
    long keyNumber = key.asNumber();
    if (keyNumber != value1 + value2) {
      LOG.fatal("Value mismatch for key: " + key.toString() +
                " != " + value1 + " + " +  value2);
    }
  }

  @Override
  public long doRead() {
    Key key = getSimpleLoadGenerator().getKeyToRead();
    if (key == null) {
      // There are no keys to read yet.
      return 0;
    }
    // Do the read from Cassandra.
    // Bind the select statement.
    BoundStatement select = getPreparedSelect().bind(Arrays.asList(key.asString() + "_1",
                                                                   key.asString() + "_2"));
    ResultSet rs = getCassandraClient().execute(select);
    List<Row> rows = rs.all();
    if (rows.size() != 2) {
      LOG.fatal("Read key: " + key.asString() + " expected 2 row in result, got " + rows.size());
      return 1;
    }
    verifyValue(key, rows.get(0).getLong(1), rows.get(1).getLong(1));
    if (rows.get(0).getLong(2) != rows.get(1).getLong(2)) {
      LOG.fatal("Writetime mismatch for key: " + key.toString() + ", " +
                rows.get(0).getLong(2) + " vs " + rows.get(1).getLong(2));
    }

    LOG.debug("Read key: " + key.toString());
    return 1;
  }

  protected PreparedStatement getPreparedInsert()  {
    return getPreparedInsert(String.format(
        "BEGIN TRANSACTION" +
        "  INSERT INTO %s (k, v) VALUES (:k1, :v1);" +
        "  INSERT INTO %s (k, v) VALUES (:k2, :v2);" +
        "END TRANSACTION;",
        getTableName(),
        getTableName()));
  }

  @Override
  public long doWrite(int threadIdx) {
    Key key = getSimpleLoadGenerator().getKeyToWrite();
    try {
      // Do the write to Cassandra.
      BoundStatement insert = null;
      long keyNum = key.asNumber();
      /* Generate two numbers that add up to the key, and use them as values */
      long value1 = ThreadLocalRandom.current().nextLong(keyNum + 1);
      long value2 = keyNum - value1;
      insert = getPreparedInsert()
                 .bind()
                 .setString("k1", key.asString() + "_1")
                 .setString("k2", key.asString() + "_2")
                 .setLong("v1", value1)
                 .setLong("v2", value2);

      ResultSet resultSet = getCassandraClient().execute(insert);
      LOG.debug("Wrote key: " + key.toString() + ", return code: " + resultSet.toString());
      getSimpleLoadGenerator().recordWriteSuccess(key);
      return 1;
    } catch (Exception e) {
      getSimpleLoadGenerator().recordWriteFailure(key);
      throw e;
    }
  }

  @Override
  public List<String> getWorkloadDescription() {
    return Arrays.asList(
      "Key-value app with multi-row transactions. Each write txn inserts a pair of unique string keys with the same value.",
      " There are multiple readers and writers that update these keys and read them in pair ",
      "indefinitely. The number of reads and writes to perform can be specified as a parameter.");
  }

  @Override
  public List<String> getWorkloadOptionalArguments() {
    return Arrays.asList(
      "--num_unique_keys " + appConfig.numUniqueKeysToWrite,
      "--num_reads " + appConfig.numKeysToRead,
      "--num_writes " + appConfig.numKeysToWrite,
      "--value_size " + appConfig.valueSize,
      "--num_threads_read " + appConfig.numReaderThreads,
      "--num_threads_write " + appConfig.numWriterThreads);
  }
}