/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.invalidation.cache;

import alluxio.AlluxioURI;
import alluxio.BaseFileStructure;
import alluxio.BaseThreadState;
import alluxio.conf.Configuration;
import alluxio.conf.PropertyKey;
import alluxio.file.options.DescendantType;
import alluxio.master.file.meta.UfsSyncPathCache;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.CommandLineOptionException;
import org.openjdk.jmh.runner.options.CommandLineOptions;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;

public class InvalidationSyncCacheBench {

  @State(Scope.Thread)
  public static class ThreadState extends BaseThreadState {

    int mDirectorySyncCount;
    int mFileSyncCount;
    int mTotalSyncChecks;
    Random mRand = new Random();

    @Setup(Level.Trial)
    public void trialSetup() {
      mRand = new Random();
    }

    @Setup(Level.Iteration)
    public void setup() {
      mDirectorySyncCount = 0;
      mFileSyncCount = 0;
      mTotalSyncChecks = 0;
    }

    @TearDown(Level.Iteration)
    public void tearDown() {
      System.out.printf("Directory sync needed %d, file sync needed %d,"
              + " total sync checks %d, %f percent syncs%n",
          mDirectorySyncCount, mFileSyncCount, mTotalSyncChecks,
          100 * (((float) mFileSyncCount + mDirectorySyncCount) / (float) mTotalSyncChecks));
    }

    boolean nextOpIsCheckSync(FileStructure fs) {
      return mRand.nextInt(100) < fs.mCheckSync;
    }

    boolean nextOpIsDirectory(FileStructure fs) {
      return mRand.nextInt(100) < fs.mDirSync;
    }

    AlluxioURI nextPath(BaseFileStructure fs, boolean isDirectory) {
      int depth = nextDepth(fs);
      StringBuilder builder = new StringBuilder();
      for (int i = 0; i < depth; i++) {
        builder.append("/nxt").append(nextWidth(fs));
      }
      if (isDirectory) {
        builder.append("/");
      } else {
        builder.append("/file").append(nextFileId(fs, depth));
      }
      return new AlluxioURI(builder.toString());
    }
  }

  @State(Scope.Benchmark)
  public static class FileStructure extends BaseFileStructure {
    UfsSyncPathCache mCache;

    @Param({"10"})
    public int mDepth;

    @Param({"0"})
    public int mWidth;

    @Param({"1000"})
    public int mCacheSize;

    @Param({"70"})
    public int mCheckSync;

    @Param({"5"})
    public int mDirSync;

    @Param({"ZIPF"})
    public Distribution mInvalDist;

    @Param({"1000"})
    public int mInvalCount;

    BaseFileStructure mInvalidationStructure;

    @Setup(Level.Trial)
    public void setupTrial() throws Exception {
      Configuration.set(PropertyKey.MASTER_UFS_PATH_CACHE_CAPACITY, mCacheSize);

      mInvalidationStructure = new BaseFileStructure();
      mInvalidationStructure.init(mDepth, 0, mInvalCount, mInvalDist);
      mCache = new UfsSyncPathCache(new AtomicClock());
      mCache.notifySyncedPath(new AlluxioURI("/"), DescendantType.ALL,
          mCache.recordStartSync(), null, false);

      // first approximately fill the cache
      BaseFileStructure fs = new BaseFileStructure();
      fs.init(mDepth, mWidth, mInvalCount, Distribution.UNIFORM);
      ThreadState ts = new ThreadState();
      int fillSize = Math.min(mCacheSize, 5000000);
      System.out.println("Filling cache with " + fillSize + " elements");
      for (int i = 0; i < fillSize; i++) {
        AlluxioURI nextPath = ts.nextPath(fs, false);
        mCache.notifyInvalidation(nextPath);
      }
      System.out.println("Done filling cache");
    }

    static class AtomicClock extends Clock {

      private final AtomicLong mTime = new AtomicLong();

      @Override
      public long millis() {
        return mTime.incrementAndGet();
      }

      @Override
      public ZoneId getZone() {
        throw new UnsupportedOperationException();
      }

      @Override
      public Clock withZone(ZoneId zone) {
        throw new UnsupportedOperationException();
      }

      @Override
      public Instant instant() {
        throw new UnsupportedOperationException();
      }
    }
  }

  @Benchmark
  public void fileInvalidationBench(FileStructure fs, ThreadState ts) throws Exception {
    if (ts.nextOpIsCheckSync(fs)) {
      boolean isDirectory = ts.nextOpIsDirectory(fs);
      AlluxioURI path = ts.nextPath(fs, isDirectory);
      if (fs.mCache.shouldSyncPath(path, Long.MAX_VALUE, DescendantType.ONE).isShouldSync()) {
        if (isDirectory) {
          ts.mDirectorySyncCount++;
        } else {
          ts.mFileSyncCount++;
        }
        fs.mCache.notifySyncedPath(path, DescendantType.ONE, fs.mCache.recordStartSync(), null,
            !isDirectory);
      }
      ts.mTotalSyncChecks++;
    } else {
      AlluxioURI path = ts.nextPath(fs.mInvalidationStructure, false);
      fs.mCache.notifyInvalidation(path);
    }
  }

  public static void main(String[] args) throws RunnerException, CommandLineOptionException {
    Options argsCli = new CommandLineOptions(args);
    Options opts = new OptionsBuilder()
        .parent(argsCli)
        .include(InvalidationSyncCacheBench.class.getName())
        .result("results.json")
        .resultFormat(ResultFormatType.JSON)
        .build();
    new Runner(opts).run();
  }
}
