content: /*
   * Copyright (C) 2015 Actor LLC. <https://actor.im>
   */
  
  package im.actor.core.modules.file;
  
  import im.actor.core.entity.FileReference;
  import im.actor.core.modules.ModuleContext;
  import im.actor.core.modules.ModuleActor;
  import im.actor.runtime.HTTP;
  import im.actor.runtime.Log;
  import im.actor.runtime.Storage;
  import im.actor.runtime.actors.ActorRef;
  import im.actor.runtime.actors.ActorCancellable;
  import im.actor.runtime.files.FileSystemReference;
  import im.actor.runtime.files.OutputFile;
  import im.actor.runtime.http.HTTPError;
  
  public class DownloadTask extends ModuleActor {
  
      private static final int SIM_BLOCKS_COUNT = 4;
      private static final int NOTIFY_THROTTLE = 1000;
      private static final int DEFAULT_RETRY = 15;
  
      private final String TAG;
      private final boolean LOG;
  
      private FileReference fileReference;
      private ActorRef manager;
  
      private FileSystemReference destReference;
      private OutputFile outputFile;
  
      private boolean isCompleted;
  
      private long lastNotifyDate;
      private float currentProgress;
      private ActorCancellable notifyCancellable;
  
      private String fileUrl;
      private int blockSize = 128 * 1024;
      private int blocksCount;
      private int nextBlock = 0;
      private int currentDownloads = 0;
      private int downloaded = 0;
  
      public DownloadTask(FileReference fileReference, ActorRef manager, ModuleContext context) {
          super(context);
          this.TAG = "DownloadTask{" + fileReference.getFileId() + "}";
          this.LOG = context.getConfiguration().isEnableFilesLogging();
          this.fileReference = fileReference;
          this.manager = manager;
      }
  
      @Override
      public void preStart() {
          if (LOG) {
              Log.d(TAG, "Creating file...");
          }
  
          destReference = Storage.createTempFile();
          if (destReference == null) {
              reportError();
              if (LOG) {
                  Log.d(TAG, "Unable to create reference");
              }
              return;
          }
  
          destReference.openWrite(fileReference.getFileSize()).then(r -> {
              outputFile = r;
              requestUrl();
          }).failure(e -> {
              reportError();
              if (LOG) {
                  Log.d(TAG, "Unable to write wile");
              }
          });
      }
  
      @Override
      public void onReceive(Object message) {
          if (message instanceof Retry) {
              Retry retry = (Retry) message;
              retryPart(retry.getBlockIndex(), retry.getFileOffset(), retry.getAttempt());
          } else {
              super.onReceive(message);
          }
      }
  
      private void requestUrl() {
          if (LOG) {
              Log.d(TAG, "Loading url...");
          }
  
          context().getFilesModule().getFileUrlInt().askForUrl(fileReference.getFileId(), fileReference.getAccessHash()).then(url -> {
              fileUrl = url;
              if (LOG) {
                  Log.d(TAG, "Loaded file url: " + fileUrl);
              }
              startDownload();
          }).failure(e -> {
              if (LOG) {
                  Log.d(TAG, "Unable to load file url");
              }
              reportError();
          });
      }
  
      private void startDownload() {
          blocksCount = fileReference.getFileSize() / blockSize;
          if (fileReference.getFileSize() % blockSize != 0) {
              blocksCount++;
          }
  
          if (LOG) {
              Log.d(TAG, "Starting downloading " + blocksCount + " blocks");
          }
          checkQueue();
      }
  
      private void completeDownload() {
          if (isCompleted) {
              return;
          }
  
          if (LOG) {
              Log.d(TAG, "Closing file...");
          }
          if (!outputFile.close()) {
              reportError();
              return;
          }
  
          FileSystemReference reference = Storage.commitTempFile(destReference, fileReference.getFileId(),
                  fileReference.getFileName());
          if (reference == null) {
              reportError();
              return;
          }
  
          if (LOG) {
              Log.d(TAG, "Complete download {" + reference.getDescriptor() + "}");
          }
          reportComplete(reference);
      }
  
      private void checkQueue() {
          if (isCompleted) {
              return;
          }
  
          if (LOG) {
              Log.d(TAG, "checkQueue " + currentDownloads + "/" + nextBlock);
          }
          if (currentDownloads == 0 && nextBlock >= blocksCount) {
              completeDownload();
          } else if (currentDownloads < SIM_BLOCKS_COUNT && nextBlock < blocksCount) {
              currentDownloads++;
              int blockIndex = nextBlock++;
              int offset = blockIndex * blockSize;
  
              if (LOG) {
                  Log.d(TAG, "Starting part #" + blockIndex + " download");
              }
  
              downloadPart(blockIndex, offset, 0);
  
              checkQueue();
          } else {
              if (LOG) {
                  Log.d(TAG, "Task queue is full");
              }
          }
      }
  
      private void retryPart(int blockIndex, int fileOffset, int attempt) {
          if (isCompleted) {
              return;
          }
  
          if (LOG) {
              Log.d(TAG, "Trying again part #" + blockIndex + " download");
          }
  
          downloadPart(blockIndex, fileOffset, attempt);
      }
  
      private void downloadPart(final int blockIndex, final int fileOffset, final int attempt) {
          HTTP.getMethod(fileUrl, fileOffset, blockSize, fileReference.getFileSize()).then(r -> {
              downloaded++;
              if (LOG) {
                  Log.d(TAG, "Download part #" + blockIndex + " completed");
              }
              if (!outputFile.write(fileOffset, r.getContent(), 0, r.getContent().length)) {
                  reportError();
                  return;
              }
              currentDownloads--;
              reportProgress(downloaded / (float) blocksCount);
              checkQueue();
          }).failure(e -> {
              if ((e instanceof HTTPError)
                      && ((((HTTPError) e).getErrorCode() >= 500
                      && ((HTTPError) e).getErrorCode() < 600)
                      || ((HTTPError) e).getErrorCode() == 0)) {
                  // Server on unknown error
                  int retryInSecs = DEFAULT_RETRY;
  
                  if (LOG) {
                      Log.w(TAG, "Download part #" + blockIndex + " failure #" + ((HTTPError) e).getErrorCode() + " trying again in " + retryInSecs + " sec, attempt #" + (attempt + 1));
                  }
  
                  self().send(new Retry(blockIndex, fileOffset, attempt + 1));
              } else {
                  if (LOG) {
                      Log.d(TAG, "Download part #" + blockIndex + " failure");
                  }
                  reportError();
              }
          });
      }
  
      private void reportError() {
          if (isCompleted) {
              return;
          }
          isCompleted = true;
          manager.send(new DownloadManager.OnDownloadedError(fileReference.getFileId()));
      }
  
      private void reportProgress(float progress) {
          if (isCompleted) {
              return;
          }
  
          if (progress > currentProgress) {
              currentProgress = progress;
          }
  
          long delta = im.actor.runtime.Runtime.getActorTime() - lastNotifyDate;
  
          if (notifyCancellable != null) {
              notifyCancellable.cancel();
              notifyCancellable = null;
          }
  
          if (delta > NOTIFY_THROTTLE) {
              lastNotifyDate = im.actor.runtime.Runtime.getActorTime();
              performReportProgress();
          } else {
              notifyCancellable = schedule((Runnable) () -> performReportProgress(), delta);
          }
      }
  
      private void performReportProgress() {
          if (isCompleted) {
              return;
          }
          manager.send(new DownloadManager.OnDownloadProgress(fileReference.getFileId(), currentProgress));
      }
  
      private void reportComplete(FileSystemReference reference) {
          if (isCompleted) {
              return;
          }
          isCompleted = true;
          manager.send(new DownloadManager.OnDownloaded(fileReference.getFileId(), reference));
      }
  
      private class Retry {
  
          private int blockIndex;
          private int fileOffset;
          private int attempt;
  
          public Retry(int blockIndex, int fileOffset, int attempt) {
              this.blockIndex = blockIndex;
              this.fileOffset = fileOffset;
              this.attempt = attempt;
          }
  
          public int getBlockIndex() {
              return blockIndex;
          }
  
          public int getFileOffset() {
              return fileOffset;
          }
  
          public int getAttempt() {
              return attempt;
          }
      }
  }
  