/*
 * Copyright 2015-present Facebook, Inc.
 *
 * 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.facebook.buck.event.listener;

import com.facebook.buck.artifact_cache.HttpArtifactCacheEvent;
import com.facebook.buck.event.NetworkEvent.BytesReceivedEvent;
import com.facebook.buck.model.Pair;
import com.facebook.buck.timing.Clock;
import com.facebook.buck.timing.DefaultClock;
import com.facebook.buck.util.concurrent.TimeSpan;
import com.facebook.buck.util.unit.SizeUnit;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;


public class NetworkStatsKeeper {
  private static final TimeSpan DOWNLOAD_SPEED_CALCULATION_INTERVAL =
      new TimeSpan(1000, TimeUnit.MILLISECONDS);

  private final AtomicLong bytesDownloaded;
  private final AtomicLong bytesDownloadedInLastInterval;
  private final AtomicLong artifactDownloaded;
  private long artifactDownloadInProgressCount;
  private double downloadSpeedForLastInterval;
  private long firstDownloadStartTimestamp;
  private long lastDownloadFinishedTimeMs;
  private long totalDownloadTimeMillis;
  private long currentIntervalDownloadTimeMillis;
  private final ScheduledExecutorService scheduler;
  private Clock clock;

  public NetworkStatsKeeper() {
    this.bytesDownloaded = new AtomicLong(0);
    this.bytesDownloadedInLastInterval = new AtomicLong(0);
    this.artifactDownloaded = new AtomicLong(0);
    this.artifactDownloadInProgressCount = 0;
    this.downloadSpeedForLastInterval = 0;
    this.firstDownloadStartTimestamp = 0;
    this.lastDownloadFinishedTimeMs = 0;
    this.totalDownloadTimeMillis = 0;
    this.currentIntervalDownloadTimeMillis = 0;
    this.clock = new DefaultClock();
    this.scheduler = Executors.newScheduledThreadPool(1,
        new ThreadFactoryBuilder().setNameFormat(getClass().getSimpleName() + "-%d").build());
    scheduleDownloadSpeedCalculation();
  }

  private void scheduleDownloadSpeedCalculation() {
    long calculationInterval = DOWNLOAD_SPEED_CALCULATION_INTERVAL.getDuration();
    TimeUnit timeUnit = DOWNLOAD_SPEED_CALCULATION_INTERVAL.getUnit();

    scheduler.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
        calculateDownloadSpeedInLastInterval();
      }
    }, /* initialDelay */ calculationInterval, /* period */ calculationInterval,
        timeUnit);
  }

  @VisibleForTesting
  void calculateDownloadSpeedInLastInterval() {
    synchronized (this) {
      long timeSpentDownloadingInThisInterval = getDownloadTimeForThisInterval();
      downloadSpeedForLastInterval =
          calculateDownloadSpeed(timeSpentDownloadingInThisInterval);
      totalDownloadTimeMillis += timeSpentDownloadingInThisInterval;
      currentIntervalDownloadTimeMillis = 0;
    }

  }

  private double calculateDownloadSpeed(long timeSpentDownloadingInThisInterval) {
    if (timeSpentDownloadingInThisInterval <= 0) {
      return 0.0;
    }
    return ((double) 1000 * bytesDownloadedInLastInterval.getAndSet(0)) /
        timeSpentDownloadingInThisInterval;

  }

  private long getDownloadTimeForThisInterval() {
    long timeSpentDownloadingInThisInterval = currentIntervalDownloadTimeMillis;
    //Downloads may be interleaved.
    if (artifactDownloadInProgressCount != 0) {
      long currentTime = clock.currentTimeMillis();
      timeSpentDownloadingInThisInterval += (currentTime - firstDownloadStartTimestamp);
      firstDownloadStartTimestamp = currentTime;
    }
    return timeSpentDownloadingInThisInterval;
  }

  public void bytesReceived(BytesReceivedEvent bytesReceivedEvent) {
    bytesDownloaded.getAndAdd(bytesReceivedEvent.getBytesReceived());
    bytesDownloadedInLastInterval.getAndAdd(
        bytesReceivedEvent.getBytesReceived());
  }

  public Pair<Long, SizeUnit> getBytesDownloaded() {
    return new Pair<>(bytesDownloaded.get(), SizeUnit.BYTES);
  }

  public Pair<Double, SizeUnit> getDownloadSpeed() {
    return new Pair<>(downloadSpeedForLastInterval, SizeUnit.BYTES);
  }

  public Pair<Double, SizeUnit> getAverageDownloadSpeed() {
    if (totalDownloadTimeMillis <= 0) {
      return new Pair<>(0.0, SizeUnit.BYTES);
    }
    double avgSpeed = ((double) 1000 * bytesDownloaded.get()) /
        totalDownloadTimeMillis;
    return new Pair<>(avgSpeed, SizeUnit.BYTES);
  }

  public void stopScheduler() {
    scheduler.shutdownNow();
  }

  public long getDownloadedArtifactDownloaded() {
    return artifactDownloaded.get();
  }

  public void artifactDownloadFinished(HttpArtifactCacheEvent.Finished event) {
    artifactDownloaded.incrementAndGet();
    synchronized (this) {
      --artifactDownloadInProgressCount;
      if (event.getTimestamp() > lastDownloadFinishedTimeMs) {
        lastDownloadFinishedTimeMs = event.getTimestamp();
      }
      // this is for calculating avg download speed accurately. Think the case where
      // download is interleaved.
      if (artifactDownloadInProgressCount == 0) {
        currentIntervalDownloadTimeMillis +=
            (lastDownloadFinishedTimeMs - firstDownloadStartTimestamp);
        firstDownloadStartTimestamp = 0;
      }
    }
  }

  public void artifactDownloadedStarted(HttpArtifactCacheEvent.Started event) {
    synchronized (this) {
      ++artifactDownloadInProgressCount;
      if (firstDownloadStartTimestamp == 0 ||
          event.getTimestamp() < firstDownloadStartTimestamp) {
        firstDownloadStartTimestamp = event.getTimestamp();
      }
    }
  }

  //only for testing
  protected void setClock(Clock clock){
    this.clock = clock;
  }

}