/*
 * Copyright (C) 2012 The Guava Authors
 *
 * 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.google.common.base;

import com.google.caliper.BeforeExperiment;
import com.google.caliper.Benchmark;
import com.google.caliper.Param;
import java.util.Arrays;
import java.util.Iterator;
import org.jspecify.annotations.NullUnmarked;

/**
 * Benchmarks {@link Joiner} against some common implementations of delimiter-based string joining.
 *
 * @author Adomas Paltanavicius
 */
@NullUnmarked
public class JoinerBenchmark {

  private static final String DELIMITER_STRING = ",";
  private static final char DELIMITER_CHARACTER = ',';

  private static final Joiner JOINER_ON_STRING = Joiner.on(DELIMITER_STRING);
  private static final Joiner JOINER_ON_CHARACTER = Joiner.on(DELIMITER_CHARACTER);

  @Param({"3", "30", "300"})
  int count;

  @Param({"0", "1", "16", "32", "100"})
  int componentLength;

  private Iterable<String> components;

  @BeforeExperiment
  @SuppressWarnings("InlineMeInliner") // String.repeat unavailable under Java 8
  void setUp() {
    String component = Strings.repeat("a", componentLength);
    String[] raw = new String[count];
    Arrays.fill(raw, component);
    components = Arrays.asList(raw);
  }

  /** {@link Joiner} with a string delimiter. */
  @Benchmark
  int joinerWithStringDelimiter(int reps) {
    int dummy = 0;
    for (int i = 0; i < reps; i++) {
      dummy ^= JOINER_ON_STRING.join(components).length();
    }
    return dummy;
  }

  /** {@link Joiner} with a character delimiter. */
  @Benchmark
  int joinerWithCharacterDelimiter(int reps) {
    int dummy = 0;
    for (int i = 0; i < reps; i++) {
      dummy ^= JOINER_ON_CHARACTER.join(components).length();
    }
    return dummy;
  }

  /**
   * Mimics what the {@link Joiner} class does internally when no extra options like ignoring {@code
   * null} values are used.
   */
  @Benchmark
  int joinerInlined(int reps) {
    int dummy = 0;
    for (int i = 0; i < reps; i++) {
      StringBuilder sb = new StringBuilder();
      Iterator<String> iterator = components.iterator();
      if (iterator.hasNext()) {
        sb.append(iterator.next().toString());
        while (iterator.hasNext()) {
          sb.append(DELIMITER_STRING);
          sb.append(iterator.next());
        }
      }
      dummy ^= sb.toString().length();
    }
    return dummy;
  }

  /**
   * Only appends delimiter if the accumulated string is non-empty. Note: this isn't a candidate
   * implementation for Joiner since it fails on leading empty components.
   */
  @Benchmark
  int stringBuilderIsEmpty(int reps) {
    int dummy = 0;
    for (int i = 0; i < reps; i++) {
      StringBuilder sb = new StringBuilder();
      for (String comp : components) {
        if (sb.length() > 0) {
          sb.append(DELIMITER_STRING);
        }
        sb.append(comp);
      }
      dummy ^= sb.toString().length();
    }
    return dummy;
  }

  /**
   * Similar to the above, but keeps a boolean flag rather than checking for the string accumulated
   * so far being empty. As a result, it does not have the above-mentioned bug.
   */
  @Benchmark
  int booleanIfFirst(int reps) {
    int dummy = 0;
    for (int i = 0; i < reps; i++) {
      StringBuilder sb = new StringBuilder();
      boolean append = false;
      for (String comp : components) {
        if (append) {
          sb.append(DELIMITER_STRING);
        }
        sb.append(comp);
        append = true;
      }
      dummy ^= sb.toString().length();
    }
    return dummy;
  }

  /**
   * Starts with an empty delimiter and changes to the desired value at the end of the iteration.
   */
  @Benchmark
  int assignDelimiter(int reps) {
    int dummy = 0;
    for (int i = 0; i < reps; i++) {
      StringBuilder sb = new StringBuilder();
      String delim = "";
      for (String comp : components) {
        sb.append(delim);
        sb.append(comp);
        delim = DELIMITER_STRING;
      }
      dummy ^= sb.toString().length();
    }
    return dummy;
  }

  /**
   * Always append the delimiter after the component, and in the very end shortens the buffer to get
   * rid of the extra trailing delimiter.
   */
  @Benchmark
  int alwaysAppendThenBackUp(int reps) {
    int dummy = 0;
    for (int i = 0; i < reps; i++) {
      StringBuilder sb = new StringBuilder();
      for (String comp : components) {
        sb.append(comp);
        sb.append(DELIMITER_STRING);
      }
      if (sb.length() > 0) {
        sb.setLength(sb.length() - DELIMITER_STRING.length());
      }
      dummy ^= sb.toString().length();
    }
    return dummy;
  }
}
