/*
 * Copyright 2015 Ben Manes. All Rights Reserved.
 *
 * 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.github.benmanes.caffeine.cache.simulator;

import static com.github.benmanes.caffeine.cache.simulator.Simulator.Message.ERROR;
import static com.github.benmanes.caffeine.cache.simulator.Simulator.Message.FINISH;
import static com.github.benmanes.caffeine.cache.simulator.Simulator.Message.START;
import static java.util.stream.Collectors.toList;

import java.io.IOException;
import java.util.List;
import java.util.PrimitiveIterator;
import java.util.stream.LongStream;

import com.github.benmanes.caffeine.cache.simulator.parser.TraceFormat;
import com.github.benmanes.caffeine.cache.simulator.policy.PolicyActor;
import com.github.benmanes.caffeine.cache.simulator.policy.PolicyStats;
import com.github.benmanes.caffeine.cache.simulator.policy.Registry;
import com.github.benmanes.caffeine.cache.simulator.report.Reporter;
import com.google.common.base.Stopwatch;
import com.typesafe.config.Config;

import akka.actor.AbstractActor;
import akka.actor.ActorRef;
import akka.actor.Props;
import akka.routing.ActorRefRoutee;
import akka.routing.BroadcastRoutingLogic;
import akka.routing.Routee;
import akka.routing.Router;
import it.unimi.dsi.fastutil.longs.LongArrayList;

/**
 * A simulator that broadcasts the recorded cache events to each policy and generates an aggregated
 * report. See <tt>reference.conf</tt> for details on the configuration.
 * <p>
 * The simulator reports the hit rate of each of the policy being evaluated. A miss may occur
 * due to,
 * <ul>
 *   <li>Conflict: multiple entries are mapped to the same location
 *   <li>Compulsory: the first reference misses and the entry must be loaded
 *   <li>Capacity: the cache is not large enough to contain the needed entries
 *   <li>Coherence: an invalidation is issued by another process in the system
 * </ul>
 * <p>
 * It is recommended that multiple access traces are used during evaluation to see how the policies
 * handle different workload patterns. When choosing a policy some metrics that are not reported
 * may be relevant, such as the cost of maintaining the policy's internal structures.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
public final class Simulator extends AbstractActor {
  public enum Message { START, FINISH, ERROR }

  private final BasicSettings settings;
  private final Stopwatch stopwatch;
  private final Reporter reporter;
  private final Router router;
  private final int batchSize;
  private int remaining;

  public Simulator() {
    Config config = context().system().settings().config().getConfig("caffeine.simulator");
    settings = new BasicSettings(config);

    List<Routee> routes = makeRoutes();
    router = new Router(new BroadcastRoutingLogic(), routes);
    remaining = routes.size();

    batchSize = settings.batchSize();
    stopwatch = Stopwatch.createStarted();
    reporter = settings.report().format().create(config);
  }

  @Override
  public void preStart() {
    self().tell(START, self());
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder()
        .matchEquals(START, msg -> broadcast())
        .matchEquals(ERROR, msg -> context().stop(self()))
        .match(PolicyStats.class, this::reportStats)
        .build();
  }

  /** Broadcast the trace events to all of the policy actors. */
  private void broadcast() {
    try (LongStream events = eventStream()) {
      LongArrayList batch = new LongArrayList(batchSize);
      for (PrimitiveIterator.OfLong i = events.iterator(); i.hasNext();) {
        batch.add(i.nextLong());
        if (batch.size() == batchSize) {
          router.route(batch, self());
          batch = new LongArrayList(batchSize);
        }
      }
      router.route(batch, self());
      router.route(FINISH, self());
    } catch (Exception e) {
      context().system().log().error(e, "");
      context().stop(self());
    }
  }

  /** Returns a stream of trace events. */
  private LongStream eventStream() throws IOException {
    if (settings.isSynthetic()) {
      return Synthetic.generate(settings);
    }
    List<String> filePaths = settings.traceFiles().paths();
    TraceFormat format = settings.traceFiles().format();
    return format.readFiles(filePaths).events();
  }

  /** Returns the actors to broadcast trace events to. */
  private List<Routee> makeRoutes() {
    return Registry.policies(settings).stream().map(policy -> {
      ActorRef actorRef = context().actorOf(Props.create(PolicyActor.class, policy));
      context().watch(actorRef);
      return new ActorRefRoutee(actorRef);
    }).collect(toList());
  }

  /** Add the stats to the reporter, print if completed, and stop the simulator. */
  private void reportStats(PolicyStats stats) throws IOException {
    reporter.add(stats);
    if (--remaining == 0) {
      reporter.print();
      context().stop(self());
      System.out.println("Executed in " + stopwatch);
    }
  }

  public static void main(String[] args) {
    akka.Main.main(new String[] { Simulator.class.getName() } );
  }
}
