// 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.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.Vector;

import org.apache.log4j.Logger;

import com.datastax.driver.core.BatchStatement;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.BoundStatement;
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 coupons for some random customers repeatedly. The numbers of
 * concurrent writers and readers are configurable.
 */
public class CassandraPersonalization extends AppBase {
  private static final Logger LOG = Logger.getLogger(CassandraPersonalization.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 = "coupon_recommendation";

  // The shared prepared select statement for fetching the data.
  private static volatile PreparedStatement preparedSelect;

  // The shared prepared statement for inserting into the table.
  private static volatile PreparedStatement preparedInsert;

  // Lock for initializing prepared statement objects.
  private static final Object prepareInitLock = new Object();

  static Random rand = new Random();

  Vector<Coupon> coupons;

  static class Coupon {
    public final String code;
    public final Date beginDate;
    public final Date endDate;

    public Coupon(String code) {
      this.code = code;
      this.beginDate = generateRandomDate(true);
      this.endDate = generateRandomDate(false);
    }

    private Date generateRandomDate(boolean past) {
      Calendar cal = Calendar.getInstance();
      cal.set(Calendar.HOUR, 0);
      cal.set(Calendar.MINUTE, 0);
      cal.set(Calendar.SECOND, 0);
      cal.set(Calendar.MILLISECOND, 0);
      cal.add(Calendar.DATE, (past ? -1 : 1) * rand.nextInt(7));
      return cal.getTime();
    }
  }

  public CassandraPersonalization() {
    if (appConfig.readOnly) {
      return;
    }
    coupons = new Vector<Coupon>(appConfig.maxCouponsPerCustomer);
    for (int i = 0; i < appConfig.maxCouponsPerCustomer; i++) {
      coupons.add(new Coupon(Long.toString(800000000000L + (long)i)));
    }
    if (appConfig.numNewCouponsPerCustomer < appConfig.maxCouponsPerCustomer) {
      Collections.shuffle(coupons);
    }
  }

  /**
   * Drop the table created by this app.
   */
  @Override
  public void dropTable() {
    dropCassandraTable(getTableName());
  }

  @Override
  public List<String> getCreateTableStatements() {
    String create_stmt = String.format(
      "CREATE TABLE IF NOT EXISTS %s (" +
      "  customer_id text," +
      "  store_id text," +
      "  coupon_code text," +
      "  begin_date timestamp," +
      "  end_date timestamp," +
      "  relevance_score double," +
      "  PRIMARY KEY ((customer_id, store_id), coupon_code));",
      getTableName());
    return Arrays.asList(create_stmt);
  }

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

  protected PreparedStatement getPreparedSelect(String selectStmt)  {
    if (preparedSelect == null) {
      synchronized (prepareInitLock) {
        if (preparedSelect == null) {
          // Create the prepared statement object.
          preparedSelect = getCassandraClient().prepare(selectStmt);
        }
      }
    }
    return preparedSelect;
  }

  private PreparedStatement getPreparedSelect()  {
    return getPreparedSelect(
      String.format("SELECT * FROM %s WHERE customer_id = ? AND store_id = ?;", getTableName()));
  }

  @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.
    String customerId = key.asString();
    String storeId = Integer.toString(rand.nextInt(appConfig.numStores));
    BoundStatement select = getPreparedSelect().bind(customerId, storeId);
    ResultSet rs = getCassandraClient().execute(select);
    List<Row> rows = rs.all();
    LOG.debug("Read coupon count: " + rows.size());
    return 1;
  }

  protected PreparedStatement getPreparedInsert(String insertStmt)  {
    if (preparedInsert == null) {
      synchronized (prepareInitLock) {
        if (preparedInsert == null) {
          // Create the prepared statement object.
          preparedInsert = getCassandraClient().prepare(insertStmt);
        }
      }
    }
    return preparedInsert;
  }

  protected PreparedStatement getPreparedInsert()  {
    return getPreparedInsert(
      String.format(
        "INSERT INTO %s " +
        "(customer_id, store_id, coupon_code, begin_date, end_date, relevance_score) " +
        "VALUES (?, ?, ?, ?, ?, ?);",
        getTableName()));
  }

  private double generateRandomRelevanceScore() {
    return rand.nextDouble();
  }

  private int generateRandomCustomerScore() {
    return rand.nextInt(10);
  }

  @Override
  public long doWrite(int threadIdx) {
    BatchStatement batch = new BatchStatement();
    PreparedStatement insert = getPreparedInsert();
    Key key = getSimpleLoadGenerator().getKeyToWrite();
    try {
      int totalCouponCount = 0;
      for (int i = 0; i < appConfig.numStores; i++) {
        String customerId = key.asString();
        String storeId = Integer.toString(i);
        int couponCount = appConfig.numNewCouponsPerCustomer / appConfig.numStores;
        for (int j = 0; j < couponCount; j++) {
          Coupon coupon = coupons.elementAt(j);
          batch.add(insert.bind(customerId, storeId, coupon.code, coupon.beginDate, coupon.endDate,
                                Double.valueOf(generateRandomRelevanceScore())));
        }
        totalCouponCount += couponCount;
      }
      ResultSet resultSet = getCassandraClient().execute(batch);
      LOG.debug("Wrote coupon count: " + totalCouponCount + ", return code: " +
                resultSet.toString());
      getSimpleLoadGenerator().recordWriteSuccess(key);
      return 1;
    } catch (Exception e) {
      getSimpleLoadGenerator().recordWriteFailure(key);
      throw e;
    }
  }

  @Override
  public synchronized void resetClients() {
    synchronized (prepareInitLock) {
      preparedInsert = null;
      preparedSelect = null;
    }
    super.resetClients();
  }

  @Override
  public synchronized void destroyClients() {
    synchronized (prepareInitLock) {
      preparedInsert = null;
      preparedSelect = null;
    }
    super.destroyClients();
  }

  @Override
  public void appendMessage(StringBuilder sb) {
    super.appendMessage(sb);
    sb.append("maxWrittenKey: " + getSimpleLoadGenerator().getMaxWrittenKey() +  " | ");
    sb.append("maxGeneratedKey: " + getSimpleLoadGenerator().getMaxGeneratedKey() +  " | ");
  }

  public void appendParentMessage(StringBuilder sb) {
    super.appendMessage(sb);
  }

  @Override
  public List<String> getWorkloadDescription() {
    return Arrays.asList(
      "User personalization app. Writes unique customer ids, each with a set of coupons for different stores.",
      " There are multiple readers and writers that update these keys and read them indefinitely. ",
      "Note that 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,
      "--num_threads_read " + appConfig.numReaderThreads,
      "--num_threads_write " + appConfig.numWriterThreads,
      "--num_stores " + appConfig.numStores,
      "--num_new_coupons_per_customer " + appConfig.numNewCouponsPerCustomer,
      "--max_coupons_per_customer " + appConfig.maxCouponsPerCustomer);
  }
}