// ============================== HOW TO RUN THIS TEST: ====================================
//
// single thread:
// java -jar log4j-perf/target/benchmarks.jar ".*SimpleDateFormat.*" -f 1 -wi 5 -i 5
//
// multiple threads (for example, 4 threads):
// java -jar log4j-perf/target/benchmarks.jar ".*SimpleDateFormat.*" -f 1 -wi 5 -i 5 -t 4 -si true
//
// Usage help:
// java -jar log4j-perf/target/benchmarks.jar -help
//
@State(Scope.Benchmark)
public class SimpleDateFormatBenchmark {

  private final Date date = new Date();
  private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
  private final ThreadLocal<SimpleDateFormat> threadLocal =
      new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
          return new SimpleDateFormat("HH:mm:ss.SSS");
        }
      };

  private final ThreadLocal<Formatter> localFormat =
      new ThreadLocal<Formatter>() {
        @Override
        protected Formatter initialValue() {
          return new Formatter();
        }
      };

  private final FastDateFormat fastFormat = FastDateFormat.getInstance("HH:mm:ss.SSS");

  private class CurrentTime {
    private final long timestamp;
    private final String formatted;

    public CurrentTime(final long timestamp) {
      this.timestamp = timestamp;
      this.formatted = fastFormat.format(timestamp);
    }
  }

  private class Formatter {
    private final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.SSS");
    private long timestamp;
    private String formatted;

    public Formatter() {
      this.timestamp = 0;
    }

    public String format(final long timestamp) {
      if (timestamp != this.timestamp) {
        this.timestamp = timestamp;
        formatted = format.format(timestamp);
      }
      return formatted;
    }
  }

  private final long currentTimestamp = 0;
  private String cachedTime = null;

  private final AtomicReference<CurrentTime> currentTime =
      new AtomicReference<>(new CurrentTime(System.currentTimeMillis()));

  public static void main(final String[] args) {}

  @Benchmark
  @BenchmarkMode(Mode.SampleTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  public void baseline() {}

  @Benchmark
  @BenchmarkMode(Mode.SampleTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  public String synchronizedFormat() {
    final long timestamp = System.currentTimeMillis();
    synchronized (simpleDateFormat) {
      if (timestamp != currentTimestamp) {
        cachedTime = simpleDateFormat.format(date);
      }
      return cachedTime;
    }
  }

  @Benchmark
  @BenchmarkMode(Mode.SampleTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  public String threadLocalFormat() {
    final long timestamp = System.currentTimeMillis();
    return threadLocal.get().format(timestamp);
  }

  @Benchmark
  @BenchmarkMode(Mode.SampleTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  public String cachedFormat() {
    final long timestamp = System.currentTimeMillis();
    return localFormat.get().format(timestamp);
  }

  @Benchmark
  @BenchmarkMode(Mode.SampleTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  public String fastFormat() {
    return fastFormat.format(System.currentTimeMillis());
  }

  @Benchmark
  @BenchmarkMode(Mode.SampleTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  public String atomicFormat() {
    final long timestamp = System.currentTimeMillis();
    final CurrentTime current = currentTime.get();
    if (timestamp != current.timestamp) {
      final CurrentTime newTime = new CurrentTime(timestamp);
      if (currentTime.compareAndSet(current, newTime)) {
        return newTime.formatted;
      } else {
        return currentTime.get().formatted;
      }
    }
    return current.formatted;
  }
}
 @Benchmark
 @BenchmarkMode(Mode.SampleTime)
 @OutputTimeUnit(TimeUnit.NANOSECONDS)
 public String fastFormat() {
   return fastFormat.format(System.currentTimeMillis());
 }