/*
 * 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.drill.exec.testing;

import java.util.List;

import org.apache.drill.exec.proto.CoordinationProtos.DrillbitEndpoint;

import com.google.common.collect.Lists;

public class Controls {

  private static final String EMPTY_CONTROLS = "{\"injections\" : []}";

  /**
   * Returns a builder that can be used to add injections.
   *
   * @return a builder instance
   */
  public static Builder newBuilder() {
    return new Builder();
  }

  /**
   * Constructor. To disallow building controls without the builder.
   */
  private Controls() {
  }

  /**
   * A builder to create a controls string, a JSON that holds a list of injections that are to be injected in code for
   * testing purposes. This string is passed through the
   * {@link org.apache.drill.exec.ExecConstants#DRILLBIT_CONTROL_INJECTIONS} session option.
   * <p/>
   * The builder class can be reused; it is safe to call build() multiple times to build multiple controls strings in
   * series. Each new controls string contains all the injections added to the builder before it.
   */
  public static class Builder {

    private final List<String> injections = Lists.newArrayList();

    public Builder() {
    }

    /**
     * Adds an exception injection to the controls builder with the given parameters.
     *
     * @param siteClass      class where the exception should be thrown
     * @param desc           descriptor for the exception site in the site class
     * @param exceptionClass class of the exception to throw
     * @param nSkip          number of times to skip before firing
     * @param nFire          number of times to fire the exception
     * @return this builder
     */
    public Builder addException(final Class<?> siteClass, final String desc,
                                final Class<? extends Throwable> exceptionClass, final int nSkip,
                                final int nFire) {
      injections.add(ControlsInjectionUtil.createException(siteClass, desc, nSkip, nFire, exceptionClass));
      return this;
    }

    /**
     * Adds an exception injection to the controls builder with the given parameters. The injection is not skipped, and
     * the exception is thrown when execution reaches the site.
     *
     * @param siteClass      class where the exception should be thrown
     * @param desc           descriptor for the exception site in the site class
     * @param exceptionClass class of the exception to throw
     * @return this builder
     */
    public Builder addException(final Class<?> siteClass, final String desc,
                                final Class<? extends Throwable> exceptionClass) {
      return addException(siteClass, desc, exceptionClass, 0, 1);
    }

    /**
     * Adds an exception injection (for the specified drillbit) to the controls builder with the given parameters.
     *
     * @param siteClass      class where the exception should be thrown
     * @param desc           descriptor for the exception site in the site class
     * @param exceptionClass class of the exception to throw
     * @param endpoint       the endpoint of the drillbit on which to inject
     * @param nSkip          number of times to skip before firing
     * @param nFire          number of times to fire the exception
     * @return this builder
     */
    public Builder addExceptionOnBit(final Class<?> siteClass, final String desc,
                                     final Class<? extends Throwable> exceptionClass,
                                     final DrillbitEndpoint endpoint, final int nSkip,
                                     final int nFire) {
      injections.add(ControlsInjectionUtil.createExceptionOnBit(siteClass, desc, nSkip, nFire, exceptionClass,
        endpoint));
      return this;
    }

    /**
     * Adds an exception injection (for the specified drillbit) to the controls builder with the given parameters. The
     * injection is not skipped, and the exception is thrown when execution reaches the site on the specified drillbit.
     *
     * @param siteClass      class where the exception should be thrown
     * @param desc           descriptor for the exception site in the site class
     * @param exceptionClass class of the exception to throw
     * @param endpoint       endpoint of the drillbit on which to inject
     * @return this builder
     */
    public Builder addExceptionOnBit(final Class<?> siteClass, final String desc,
                                     final Class<? extends Throwable> exceptionClass,
                                     final DrillbitEndpoint endpoint) {
      return addExceptionOnBit(siteClass, desc, exceptionClass, endpoint, 0, 1);
    }

    /**
     * Adds a pause injection to the controls builder with the given parameters.
     *
     * @param siteClass class where the pause should happen
     * @param desc      descriptor for the pause site in the site class
     * @param nSkip     number of times to skip before firing
     * @return this builder
     */
    public Builder addPause(final Class<?> siteClass, final String desc, final int nSkip) {
      injections.add(ControlsInjectionUtil.createPause(siteClass, desc, nSkip));
      return this;
    }

    /**
     * Adds a time-bound pause injection to the controls builder with the given parameters.
     *
     * @param siteClass class where the pause should happen
     * @param desc      descriptor for the pause site in the site class
     * @param nSkip     number of times to skip before firing
     * @param msPause     duration of the pause in millisec
     * @return this builder
     */
    public Builder addTimedPause(final Class<?> siteClass, final String desc, final int nSkip, final long msPause) {
      injections.add(ControlsInjectionUtil.createTimedPause(siteClass, desc, nSkip, msPause));
      return this;
    }

    /**
     * Adds a pause injection to the controls builder with the given parameters. The pause is not skipped i.e. the pause
     * happens when execution reaches the site.
     *
     * @param siteClass class where the pause should happen
     * @param desc      descriptor for the pause site in the site class
     * @return this builder
     */
    public Builder addPause(final Class<?> siteClass, final String desc) {
      return addPause(siteClass, desc, 0);
    }

    /**
     * Adds a pause injection (for the specified drillbit) to the controls builder with the given parameters.
     *
     * @param siteClass class where the pause should happen
     * @param desc      descriptor for the pause site in the site class
     * @param nSkip     number of times to skip before firing
     * @return this builder
     */
    public Builder addPauseOnBit(final Class<?> siteClass, final String desc,
                                 final DrillbitEndpoint endpoint, final int nSkip) {
      injections.add(ControlsInjectionUtil.createPauseOnBit(siteClass, desc, nSkip, endpoint));
      return this;
    }

    /**
     * Adds a time-bound pause injection (for the specified drillbit) to the controls builder with the given parameters.
     *
     * @param siteClass class where the pause should happen
     * @param desc      descriptor for the pause site in the site class
     * @param nSkip     number of times to skip before firing
     * @param msPause     duration of the pause in millisec
     * @return this builder
     */
    public Builder addTimedPauseOnBit(final Class<?> siteClass, final String desc,
                                 final DrillbitEndpoint endpoint, final int nSkip, final long msPause) {
      injections.add(ControlsInjectionUtil.createTimedPauseOnBit(siteClass, desc, nSkip, endpoint, msPause));
      return this;
    }

    /**
     * Adds a pause injection (for the specified drillbit) to the controls builder with the given parameters. The pause
     * is not skipped i.e. the pause happens when execution reaches the site.
     *
     * @param siteClass class where the pause should happen
     * @param desc      descriptor for the pause site in the site class
     * @return this builder
     */
    public Builder addPauseOnBit(final Class<?> siteClass, final String desc,
                                 final DrillbitEndpoint endpoint) {
      return addPauseOnBit(siteClass, desc, endpoint, 0);
    }

    /**
     * Adds a count down latch to the controls builder with the given parameters.
     *
     * @param siteClass class where the latch should be injected
     * @param desc      descriptor for the latch in the site class
     * @return this builder
     */
    public Builder addLatch(final Class<?> siteClass, final String desc) {
      injections.add(ControlsInjectionUtil.createLatch(siteClass, desc));
      return this;
    }

    /**
     * Builds the controls string.
     *
     * @return a validated controls string with the added injections
     * @throws java.lang.AssertionError if controls cannot be validated using
     *                                  {@link org.apache.drill.exec.testing.ExecutionControls#controlsOptionMapper}
     */
    public String build() {
      if (injections.size() == 0) {
        return EMPTY_CONTROLS;
      }

      final StringBuilder builder = new StringBuilder("{ \"injections\" : [");
      for (final String injection : injections) {
        builder.append(injection)
          .append(",");
      }
      builder.setLength(builder.length() - 1); // remove the extra ","
      builder.append("]}");
      final String controls = builder.toString();
      ControlsInjectionUtil.validateControlsString(controls);
      return controls;
    }
  }
}
