/*
 * 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
 *
 *   https://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.accumulo.core.client;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.accumulo.core.conf.ClientProperty.CONDITIONAL_WRITER_THREADS_MAX;
import static org.apache.accumulo.core.conf.ClientProperty.CONDITIONAL_WRITER_TIMEOUT_MAX;

import java.util.concurrent.TimeUnit;

import org.apache.accumulo.core.conf.ConfigurationTypeHelper;
import org.apache.accumulo.core.security.Authorizations;

/**
 * @since 1.6.0
 */
public class ConditionalWriterConfig {

  private static final Long DEFAULT_TIMEOUT = getDefaultTimeout();
  private static final Integer DEFAULT_MAX_WRITE_THREADS =
      Integer.parseInt(CONDITIONAL_WRITER_THREADS_MAX.getDefaultValue());

  private Long timeout = null;
  private Integer maxWriteThreads = null;
  private Authorizations auths = null;
  private Durability durability = null;
  private String classLoaderContext = null;

  /**
   * A set of authorization labels that will be checked against the column visibility of each key in
   * order to filter data. The authorizations passed in must be a subset of the accumulo user's set
   * of authorizations. If the accumulo user has authorizations (A1, A2) and authorizations (A2, A3)
   * are passed, then an exception will be thrown.
   *
   * <p>
   * Any condition that is not visible with this set of authorizations will fail.
   */
  public ConditionalWriterConfig setAuthorizations(Authorizations auths) {
    checkArgument(auths != null, "auths is null");
    this.auths = auths;
    return this;
  }

  /**
   * Sets the maximum amount of time an unresponsive server will be re-tried. When this timeout is
   * exceeded, the {@link ConditionalWriter} should return the mutation with an exception.<br>
   * For no timeout, set to zero, or {@link Long#MAX_VALUE} with {@link TimeUnit#MILLISECONDS}.
   *
   * <p>
   * {@link TimeUnit#MICROSECONDS} or {@link TimeUnit#NANOSECONDS} will be truncated to the nearest
   * {@link TimeUnit#MILLISECONDS}.<br>
   * If this truncation would result in making the value zero when it was specified as non-zero,
   * then a minimum value of one {@link TimeUnit#MILLISECONDS} will be used.
   *
   * <p>
   * <b>Default:</b> {@link Long#MAX_VALUE} (no timeout)
   *
   * @param timeout the timeout, in the unit specified by the value of {@code timeUnit}
   * @param timeUnit determines how {@code timeout} will be interpreted
   * @throws IllegalArgumentException if {@code timeout} is less than 0
   * @return {@code this} to allow chaining of set methods
   */
  public ConditionalWriterConfig setTimeout(long timeout, TimeUnit timeUnit) {
    if (timeout < 0) {
      throw new IllegalArgumentException("Negative timeout not allowed " + timeout);
    }

    if (timeout == 0) {
      this.timeout = Long.MAX_VALUE;
    } else {
      // make small, positive values that truncate to 0 when converted use the minimum millis
      // instead
      this.timeout = Math.max(1, timeUnit.toMillis(timeout));
    }
    return this;
  }

  /**
   * Sets the maximum number of threads to use for writing data to the tablet servers.
   *
   * <p>
   * <b>Default:</b> 3
   *
   * @param maxWriteThreads the maximum threads to use
   * @throws IllegalArgumentException if {@code maxWriteThreads} is non-positive
   * @return {@code this} to allow chaining of set methods
   */
  public ConditionalWriterConfig setMaxWriteThreads(int maxWriteThreads) {
    if (maxWriteThreads <= 0) {
      throw new IllegalArgumentException("Max threads must be positive " + maxWriteThreads);
    }

    this.maxWriteThreads = maxWriteThreads;
    return this;
  }

  /**
   * Sets the Durability for the mutation, if applied.
   * <p>
   * <b>Default:</b> Durability.DEFAULT: use the table's durability configuration.
   *
   * @return {@code this} to allow chaining of set methods
   * @since 1.7.0
   */
  public ConditionalWriterConfig setDurability(Durability durability) {
    this.durability = durability;
    return this;
  }

  public Authorizations getAuthorizations() {
    return auths != null ? auths : Authorizations.EMPTY;
  }

  public long getTimeout(TimeUnit timeUnit) {
    return timeUnit.convert(timeout != null ? timeout : DEFAULT_TIMEOUT, MILLISECONDS);
  }

  public int getMaxWriteThreads() {
    return maxWriteThreads != null ? maxWriteThreads : DEFAULT_MAX_WRITE_THREADS;
  }

  public Durability getDurability() {
    return durability != null ? durability : Durability.DEFAULT;
  }

  private static long getDefaultTimeout() {
    long defVal =
        ConfigurationTypeHelper.getTimeInMillis(CONDITIONAL_WRITER_TIMEOUT_MAX.getDefaultValue());
    return defVal != 0L ? defVal : Long.MAX_VALUE;
  }

  /**
   * Sets the name of the classloader context on this scanner. See the administration chapter of the
   * user manual for details on how to configure and use classloader contexts.
   *
   * @param classLoaderContext name of the classloader context
   * @throws NullPointerException if context is null
   * @since 1.8.0
   */
  public void setClassLoaderContext(String classLoaderContext) {
    requireNonNull(classLoaderContext, "context name cannot be null");
    this.classLoaderContext = classLoaderContext;
  }

  /**
   * Clears the current classloader context set on this scanner
   *
   * @since 1.8.0
   */
  public void clearClassLoaderContext() {
    this.classLoaderContext = null;
  }

  /**
   * Returns the name of the current classloader context set on this scanner
   *
   * @return name of the current context
   * @since 1.8.0
   */
  public String getClassLoaderContext() {
    return this.classLoaderContext;
  }

  private static <T> T merge(T o1, T o2) {
    if (o1 != null) {
      return o1;
    }
    return o2;
  }

  /**
   * Merge this ConditionalWriterConfig with another. If config is set in both, preference will be
   * given to this config.
   *
   * @param other Another ConditionalWriterConfig
   * @return Merged ConditionalWriterConfig
   * @since 2.1.0
   */
  public ConditionalWriterConfig merge(ConditionalWriterConfig other) {
    ConditionalWriterConfig result = new ConditionalWriterConfig();
    result.timeout = merge(this.timeout, other.timeout);
    result.maxWriteThreads = merge(this.maxWriteThreads, other.maxWriteThreads);
    result.durability = merge(this.durability, other.durability);
    result.auths = merge(this.auths, other.auths);
    return result;
  }

}
