/**
 * This class cyclically dumps all environment measurements, e.g. to Log4J.
 *
 * <p>Calling the constructor takes care of registration with ASysMon.
 *
 * <p>This is a data sink for pragmatic reasons, 'shutdown' integration in particular. It does not
 * actually use hierarchical measurements.
 *
 * @author arno
 */
public abstract class ACyclicMeasurementDumper implements ADataSink, ASysMonAware {
  private static final ASysMonLogger log = ASysMonLogger.get(ACyclicMeasurementDumper.class);

  private final ScheduledExecutorService ec;
  private volatile ASysMonApi sysMon;
  private final int initialDelaySeconds;
  private final int frequencyInSeconds;
  private final int averagingDelayMillis;

  private final Runnable dumper =
      new Runnable() {
        @Override
        public void run() {
          try {
            final Map<String, AScalarDataPoint> m =
                sysMon.getScalarMeasurements(averagingDelayMillis);
            for (String key : m.keySet()) {
              dump("Scalar Measurement: " + key + " = " + m.get(key).getFormattedValue());
            }
          } catch (Exception exc) {
            log.error(exc);
            dump(exc.toString());
          }
        }
      };

  public ACyclicMeasurementDumper(
      int initialDelaySeconds, int frequencyInSeconds, int averagingDelayMillis) {
    this.initialDelaySeconds = initialDelaySeconds;
    this.frequencyInSeconds = frequencyInSeconds;
    this.averagingDelayMillis = averagingDelayMillis;
    ec = Executors.newSingleThreadScheduledExecutor();
  }

  @Override
  public void setASysMon(ASysMonApi sysMon) {
    this.sysMon = sysMon;
    ec.scheduleAtFixedRate(dumper, initialDelaySeconds, frequencyInSeconds, TimeUnit.SECONDS);
  }

  protected abstract void dump(String s);

  @Override
  public void onStartedHierarchicalMeasurement(String identifier) {}

  @Override
  public void onFinishedHierarchicalMeasurement(AHierarchicalDataRoot data) {}

  @Override
  public void shutdown() {
    ec.shutdown();
  }
}
/** @author arno */
public class AHttpJsonOffloadingDataSink implements ADataSink {
  public static final int NO_DATA_SLEEP_MILLIS = 10;

  private static final ASysMonLogger log = ASysMonLogger.get(AHttpJsonOffloadingDataSink.class);

  private final ASysMonConfig config;

  // TODO how many concurrent HTTP connections does this provide? --> configure!
  private final CloseableHttpClient httpClient =
      HttpClients.createDefault(); // TODO make this configurable
  private final URI uri;

  private final String sender;
  private final String senderInstance;

  private final ASoftlyLimitedQueue<AHierarchicalDataRoot> traceQueue;
  private final ASoftlyLimitedQueue<AScalarDataPoint> scalarQueue;

  private final ExecutorService offloadingThreadPool;
  private final ScheduledExecutorService scalarMeasurementPool;

  private volatile boolean isShutDown = false;

  public AHttpJsonOffloadingDataSink(
      final ASysMonApi sysMon,
      String uri,
      String sender,
      String senderInstance,
      int traceQueueSize,
      int scalarQueueSize,
      int numOffloadingThreads,
      int scalarMeasurementFrequencyMillis) {
    this.config = sysMon.getConfig();

    this.uri = URI.create(uri);
    this.sender = sender;
    this.senderInstance = senderInstance;

    this.traceQueue =
        new ASoftlyLimitedQueue<AHierarchicalDataRoot>(
            traceQueueSize, new DiscardedLogger("trace queue overflow - discarding oldest trace"));
    this.scalarQueue =
        new ASoftlyLimitedQueue<AScalarDataPoint>(
            scalarQueueSize,
            new DiscardedLogger("environment queue overflow - discarding oldest data"));

    offloadingThreadPool = Executors.newFixedThreadPool(numOffloadingThreads);
    for (int i = 0; i < numOffloadingThreads; i++) {
      offloadingThreadPool.submit(new OffloadingRunnable());
    }

    scalarMeasurementPool = Executors.newSingleThreadScheduledExecutor();
    scalarMeasurementPool.scheduleAtFixedRate(
        new Runnable() {
          @Override
          public void run() {
            // TODO introduce 'AScalarProvider' interface for callbacks like this
            for (AScalarDataPoint scalar :
                sysMon
                    .getScalarMeasurements()
                    .values()) { // TODO ensure that this ASysMon call will never throw exceptions
              scalarQueue.add(scalar);
            }
          }
        },
        0,
        scalarMeasurementFrequencyMillis,
        TimeUnit.MILLISECONDS);
  }

  @Override
  public void onStartedHierarchicalMeasurement(String identifier) {}

  @Override
  public void onFinishedHierarchicalMeasurement(AHierarchicalDataRoot data) {

    // TODO make resending of arbitrary data idempotent --> send a unique identifier with every
    // *atom* (trace, environment measurement, ...)
    // TODO re-enter the data into the queues when the server returns a non-OK http response code

    traceQueue.add(data);
  }

  private void doOffload() throws Exception {
    final List<AHierarchicalDataRoot> traces = new ArrayList<AHierarchicalDataRoot>();
    AHierarchicalDataRoot candidate;
    while ((candidate = traceQueue.poll()) != null) { // TODO limit number per HTTP request?!
      traces.add(candidate);
    }

    final List<AScalarDataPoint> scalars = new ArrayList<AScalarDataPoint>();
    AScalarDataPoint scalar;
    while ((scalar = scalarQueue.poll()) != null) { // TODO limit number per HTTP request?
      scalars.add(scalar);
    }

    if (traces.isEmpty() && scalars.isEmpty()) {
      Thread.sleep(NO_DATA_SLEEP_MILLIS);
    } else {
      try {
        final HttpPost httpPost = new HttpPost(uri);

        final AJsonOffloadingEntity entity =
            new AJsonOffloadingEntity(traces, scalars, sender, senderInstance);
        httpPost.setEntity(entity);

        final CloseableHttpResponse response = httpClient.execute(httpPost);
        try {
          // TODO response with commands for monitoring this app?!
        } finally {
          response.close();
        }
      } catch (Exception exc) {
        log.error(exc);

        // add the data to the queue again for later retry
        scalarQueue.addAll(scalars);

        System.out.println(" == " + scalarQueue);

        // wait a grace period for the situation to improve
        Thread.sleep(5000); // TODO make this configurable
      }
    }
  }

  @Override
  public void shutdown() throws IOException {
    isShutDown = true;
    httpClient.close();
    scalarMeasurementPool.shutdown();
    offloadingThreadPool.shutdown();
  }

  private static class DiscardedLogger implements Runnable {
    private final String msg;

    private DiscardedLogger(String msg) {
      this.msg = msg;
    }

    @Override
    public void run() {
      log.warn(msg);
    }
  }

  private class OffloadingRunnable implements Runnable {
    @Override
    public void run() {
      while (!isShutDown) {
        try {
          doOffload();
        } catch (Exception e) {
          e.printStackTrace(); // TODO exception handling
        }
      }
    }
  }
}