import { Injectable } from "@angular/core";
import { marker } from "@biesbjerg/ngx-translate-extract-marker";
import { Subject } from "rxjs";
import { DriveFile } from "../../components/resource-page/drive-window/drive-layout/drive-file";
import { FileSystemWritableFileStreamCommonInterface } from "../nano/nano-requests";
import { NanoService } from "../nano/nano.service";
import { FormatBytesPipe } from "../pipes/format-bytes.pipe";
import { CordovaService } from "./cordova.service";
import { DialogService } from "./dialog.service";
import { RouterHandler } from "./router-handler.service";

export enum FileManagerDirection {
  UP = "up",
  DOWN = "down",
}

export type FileManagerType = {
  direction: string; // up, down
  file: DriveFile;
  resourceId;
  path: string;
  currPart: number;
  countPart: number;
  speed: string;
  timeNeeded: string;
  started: Date;
  progress: number;
  done: boolean;
  error: boolean;
  requestId: number;
  doneCallback?: Function;
  progressCallback?: Function;
  errorCallback?: Function;
  /**
   * Data could be stored in cache for a short time. It is still available for instant download
   */
  objectURL?: string; // window.URL.createObjectURL
};

export let downloadProgressObserver = new Subject<FileManagerType>();
export let downloadStartedObserver = new Subject<FileManagerType>();

@Injectable({
  providedIn: "root",
})
export class DownloadManagerService {
  private fileList: FileManagerType[] = [];

  // store data in cache after download
  public static STORE_TIME_DOWNLOADED_DATA_IN_CACHE = 120 * 1000; // 2 min

  public getFileList(): FileManagerType[] {
    return this.fileList;
  }

  constructor(
    private nanoService: NanoService,
    private dialogService: DialogService,
    private formatBytesPipe: FormatBytesPipe,
    private routerHandler: RouterHandler,
    private cordovaService: CordovaService
  ) {}

  private getDeviceSpecificFileWriter(
    file: DriveFile
  ): Promise<FileSystemWritableFileStreamCommonInterface | null> {
    if (this.cordovaService.isOnApp()) {
      let ask = Promise.resolve(true);

      if (localStorage.getItem("beta-download") !== "1") {
        ask = this.dialogService
          .openConfirmDialog(
            marker("Beta Function"),
            marker(
              "The download on Android and iOS application is experimental. The current implementation can slow down, freeze or crash the application, but soon it will be replaced with a stable one. Are you sure you want to download the file?"
            )
          )
          .toPromise();
      }

      return ask.then((res) => {
        if (res) {
          localStorage.setItem("beta-download", "1");
          return this.cordovaService.getFileWriter(file.fullName);
        }
      });
    } else {
      if (window["showSaveFilePicker"]) {
        return window["showSaveFilePicker"]({
          suggestedName: file.fullName,
          types: [
            {
              //description: 'Images',
              accept: { [file.mime]: "." + [file.ext] },
            },
          ],
          excludeAcceptAllOption: false,
          multiple: false,
        })
          .then((f) => {
            return f.createWritable();
          })
          .catch((err) => {
            throw new Error("cancel");
          });
      } else {
        // return null, so without writablestream we will use blob
        if (localStorage.getItem("big-file-download-warning") != "1") {
          // ask only the first time
          return this.dialogService
            .openConfirmDialog(
              marker("File download"),
              marker(
                "Downloading large files using this browser will use the computer's memory as temporary storage. If the file is too large to fit inside the available memory, the download may fail. To download large files optimally, use a browser with file-streaming support. (Chrome or Edge)"
              ),
              marker("Download large file without streaming support"),
              marker("Cancel")
            )
            .toPromise()
            .then((response) => {
              if (response) {
                localStorage.setItem("big-file-download-warning", "1");
                return null;
              } else {
                throw new Error("cancel");
              }
            });
        } else {
          return Promise.resolve(null);
        }
      }
    }
  }

  public download(
    resourceId,
    path: string,
    file: DriveFile,
    progressCallback?
  ): Promise<void> {
    let refForFile: FileManagerType = {
      direction: FileManagerDirection.DOWN, // up, down
      file,
      resourceId,
      path,
      currPart: 0,
      countPart: 0,
      progress: 0,
      started: new Date(),
      timeNeeded: "",
      done: false,
      error: false,
      requestId: null,
      speed: "",
    };

    let p = Promise.resolve(null);
    // stream file instead of blob, if can
    // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream
    if (
      this.cordovaService.isOnApp() ||
      file.size >= NanoService.FILE_STREAM_MIN_SIZE
    ) {
      p = this.getDeviceSpecificFileWriter(refForFile.file);
    }

    let now = performance.now();
    let speedWindow = 1; // how many chunks we downloaded to get speed information

    return p
      .then(
        (streamWriter: FileSystemWritableFileStreamCommonInterface | null) => {
          let requestId = this.nanoService.getFile(
            resourceId,
            path,
            streamWriter,
            (response) => {
              refForFile.done = true;

              if (!streamWriter) {
                console.log("blob");
                var a;
                a = document.createElement("a");

                try {
                  refForFile.objectURL = window.URL.createObjectURL(
                    //new Blob([response], { type: "application/octet-stream" })
                    response
                  );
                } catch (e) {}

                a.href = refForFile.objectURL;
                a.download = file.fullName;
                //document.body.appendChild(a);
                //a.style = "display: none";
                a.click();
                //a.remove();

                // remove from cache
                setTimeout(() => {
                  console.log("clean file from cache");
                  URL.revokeObjectURL(refForFile.objectURL);
                  refForFile.objectURL = null;
                }, DownloadManagerService.STORE_TIME_DOWNLOADED_DATA_IN_CACHE);
              }

              let r = this.routerHandler.getRoute();
              if (
                r.url != "download" ||
                !this.cordovaService.isAppInForeground()
              ) {
                this.cordovaService.callNotification({
                  title: marker("Download completed"),
                  message: marker("File: ") + refForFile.file.fullName,
                  callback: (data) => {
                    this.routerHandler.navigate(["/download"]);
                  },
                  callbackData: {
                    file: refForFile.file.fullName,
                  },
                });
              }
            }, // else just stream into the selected file

            (err) => {
              refForFile.error = true;
              console.error("err", err);

              let r = this.routerHandler.getRoute();
              if (
                r.url != "download" ||
                !this.cordovaService.isAppInForeground()
              ) {
                this.cordovaService.callNotification({
                  title: marker("Download fail"),
                  message: marker("File: ") + refForFile.file.fullName,
                  callback: (data) => {
                    this.routerHandler.navigate(["/download"]);
                  },
                  callbackData: {
                    file: refForFile.file.fullName,
                  },
                });
              }
            },
            (curr, max) => {
              refForFile.error = false;

              refForFile.currPart = curr;
              refForFile.countPart = max;
              refForFile.progress = Math.round((curr / max) * 100);

              if (curr != 0) {
                let now2 = performance.now();
                if (now2 - now > 1000) {
                  // show speed info in every sec
                  let speedBytePerSec =
                    (NanoService.NETWORK_FILE_CHUNK_SIZE * speedWindow) /
                    ((now2 - now) / 1000);
                  now = now2;
                  speedWindow = 1;

                  refForFile.speed = this.humanizeSpeed(speedBytePerSec);
                  refForFile.timeNeeded = this.humanizeTime(
                    Math.round(
                      ((refForFile.countPart - refForFile.currPart) *
                        NanoService.NETWORK_FILE_CHUNK_SIZE) /
                        speedBytePerSec
                    )
                  );
                } else {
                  speedWindow++;
                }
              }

              if (curr == 0) downloadStartedObserver.next(refForFile);
              progressCallback &&
                progressCallback(
                  curr,
                  max,
                  refForFile.speed,
                  refForFile.timeNeeded,
                  refForFile
                );
              downloadProgressObserver.next(refForFile);
            }
          );

          refForFile.requestId = requestId;

          this.fileList.push(refForFile);
          return;
        }
      )
      .catch((err) => {
        if (err.message != "cancel") {
          throw err;
        } else {
          console.warn("user cancel");
        }
      });
  }

  public upload(
    resourceId,
    path: string,
    file: File,
    doneCallback?: Function,
    errorCallback?: Function
  ) {
    let refForFile: FileManagerType = {
      direction: FileManagerDirection.UP, // up, down
      file: new DriveFile(file),
      resourceId,
      path,
      currPart: 0,
      countPart: 0,
      progress: 0,
      started: new Date(),
      done: false,
      error: false,
      requestId: null,
      speed: "",
      timeNeeded: "",
    };

    let startUpload = () => {
      if (this.getNotFinnishedUpload() < 8) {
        this.fileList.push(refForFile);

        let now = performance.now();
        let speedWindow = 1; // how many chunks we downloaded to get speed information

        refForFile.requestId = this.nanoService.uploadFile(
          resourceId,
          file,
          path,
          (res) => {
            console.log("upload done");
            //this.listDirectory(); TODO

            refForFile.done = true;
            doneCallback();

            this.cordovaService.callNotification({
              title: marker("Upload completed"),
              message: marker("File: ") + refForFile.file.fullName,
              callback: (data) => {
                this.routerHandler.navigate(["/download"]);
              },
              callbackData: {
                file: refForFile.file.fullName,
              },
            });
          },
          (err) => {
            console.log("error", err);
            refForFile.error = true;
            errorCallback(err);

            // repeated error handling - do not trigger annoying noti here
            /*this.cordovaService.callNotification({
              title: marker("Upload fail"),
              message: marker("File: ") + refForFile.file.fullName,
              callback: (data) => {
                this.routerHandler.navigate(["/download"]);
              },
              callbackData: {
                file: refForFile.file.fullName,
              },
            });*/
          },
          (curr, max, speed) => {
            refForFile.error = false;
            refForFile.currPart = curr;
            refForFile.countPart = max;
            refForFile.progress = Math.round((curr / max) * 100);

            if (curr != 0) {
              let now2 = performance.now();
              if (now2 - now > 1000) {
                // show speed info in every sec
                let speedBytePerSec =
                  (NanoService.NETWORK_FILE_CHUNK_SIZE * speedWindow) /
                  ((now2 - now) / 1000);
                now = now2;
                speedWindow = 1;

                refForFile.speed = this.humanizeSpeed(speedBytePerSec);
                refForFile.timeNeeded = this.humanizeTime(
                  Math.round(
                    ((refForFile.countPart - refForFile.currPart) *
                      NanoService.NETWORK_FILE_CHUNK_SIZE) /
                      speedBytePerSec
                  )
                );
              } else {
                speedWindow++;
              }
            }

            if (curr == 0) downloadStartedObserver.next(refForFile);

            downloadProgressObserver.next(refForFile);
          }
        );
      } else {
        console.log("too much upload, wait a little bit");
        setTimeout(startUpload, 1000);
      }
    };
    startUpload();
  }

  // set this limitation. We can remove this after mutation key handling refactor
  private getNotFinnishedUpload() {
    let notFinnised = 0;
    for (let i = 0; i < this.fileList.length; i++) {
      if (!this.fileList[i].done) {
        notFinnised++;
      }
    }
    return notFinnised;
  }

  public remove(file: FileManagerType) {
    if (file.direction == FileManagerDirection.UP) {
      this.nanoService.removeUploadSession(file.requestId);
    } else {
      this.nanoService.deleteRequestSession(file.requestId);
    }

    let pos = this.fileList.indexOf(file);
    if (pos > -1) {
      this.fileList.splice(pos, 1);
    }
  }

  public isUploadInProgress(): boolean {
    return this.fileList.some(
      (f) => !f.done && f.direction == FileManagerDirection.UP
    );
  }
  public isDownloadInProgress(): boolean {
    return this.fileList.some(
      (f) => !f.done && f.direction == FileManagerDirection.DOWN
    );
  }

  private humanizeSpeed(speed: number): string {
    // let sizes = ["b", "k", "M", "G"];
    // let sizeIndex = 0;
    // let humanizeSpeed = speed;
    // while (humanizeSpeed >= 1000 && sizeIndex < sizes.length - 1) {
    //   humanizeSpeed /= 1000.0;
    //   sizeIndex++;
    // }
    // if (humanizeSpeed < 10) {
    //   humanizeSpeed = Math.round(humanizeSpeed * 10) / 10;
    // } else {
    //   humanizeSpeed = Math.round(humanizeSpeed);
    // }

    return this.formatBytesPipe.transform(speed) + "/sec";
  }

  private humanizeTime(timeNeeded: number): string {
    function pad(n) {
      return n < 10 ? "0" + n : n;
    }

    var h = Math.floor(timeNeeded / 3600);
    var m = Math.floor(timeNeeded / 60) - h * 60;
    var s = Math.floor(timeNeeded - h * 3600 - m * 60);

    if (h > 0) {
      return pad(h) + ":" + pad(m) + ":" + pad(s);
    } else if (m > 0) {
      return m + ":" + pad(s);
    } else {
      return s + " sec";
    }
  }
}

let holdFileInCache = localStorage.getItem("hold-file-in-cache");
if (holdFileInCache !== undefined) {
  let limit = parseInt(holdFileInCache);
  if (limit >= 10) {
    DownloadManagerService.STORE_TIME_DOWNLOADED_DATA_IN_CACHE = limit;
  }
}
