import { Injectable } from '@angular/core';
import { marker } from '@biesbjerg/ngx-translate-extract-marker';
import { Subject } from 'rxjs';
import { AdvancedSettingsEnum } from 'src/app/components/advanced-settings/advanced-settings.component';
import { AppStorage } from 'src/app/shared/app-storage';
import { DriveFile } from '../../components/resource-page/drive-window/drive-layout/drive-file';
import {
  FileSystemWritableFileStreamCommonInterface,
  FileSystemWritableFileStreamMobile,
} from '../nano/nano-requests';
import { NanoService } from '../nano/nano.service';
import { FormatBytesPipe } from '../pipes/format-bytes.pipe';
import { BlobService } from './blob.service';
import { CordovaService } from './cordova.service';
import { DialogService } from './dialog.service';
import { RouterHandler } from './router-handler.service';
import { SnackBarService } from './snackbar.service';

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

export type FileManagerType = {
  direction: string; // up, down
  file: DriveFile;
  resourceId: string;
  path: string;
  currPart: number;
  countPart: number;
  speed: string;
  timeNeeded: string;
  started: Date;
  progress: number;
  paused: boolean;
  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; // createObjectURL
  mobileLocalPath?: string;
};

export type CollectionObject = {
  file: any; // FileRow
  writable: FileSystemWritableFileStreamCommonInterface;
  path: string;
  status: 'pending' | 'inprogress' | 'done' | 'error';
  requestId?: number;

  speed?: number;
  humanizedSpeed?: string;
  // rawTimeNeeded?: number;
  timeNeeded?: string;
  currPart?: number;
  countPart?: number;
  progress?: number;
};

/**
 * Used to manage collections (batched files like folder)
 */
export type CollectionManagerType = {
  direction: string; // up, down
  file?: { fullName?: string }; // only for progress-bar component to handle the merged observers
  path: string;
  parts: CollectionObject[];
  isPaused: boolean;
  inProgressParts: CollectionObject[];
  resourceId: string;
  currPart: number;
  countPart: number;
  currSize: number;
  maxSize: number;
  speed: string;
  timeNeeded: string;
  started: Date;
  progress: number;
  done: boolean;
  verifying: boolean;
  error: boolean;
  errorFixStarted: boolean;
  errorParts: CollectionObject[];
  collectionId: string;
  doneCallback?: Function;
  progressCallback?: Function;
  errorCallback?: Function;
  dirHandler: any; // directory handler (important on error handling, to keep the handler reference of the directory we download in)
};

export let downloadProgressObserver = new Subject<FileManagerType>();
export let downloadStartedObserver = new Subject<FileManagerType>();
export let downloadFinishedObserver = new Subject<FileManagerType>();
export let downloadErrorObserver = new Subject<{ error: any; file: FileManagerType }>();

export let uploadProgressObserver = new Subject<FileManagerType>();
export let uploadStartedObserver = new Subject<FileManagerType>();
export let uploadFinishedObserver = new Subject<FileManagerType>();
export let uploadErrorObserver = new Subject<{ error: any; file: FileManagerType }>();

export let fileRemovedObserver = new Subject<FileManagerType>();

export let downloadCollectionProgressObserver = new Subject<CollectionManagerType>();
export let downloadCollectionStartedObserver = new Subject<CollectionManagerType>();
export let downloadCollectionFinishedObserver = new Subject<CollectionManagerType>();
export let downloadCollectionErrorObserver = new Subject<CollectionManagerType>();

// enum QueuedCallbackStatus {
//   queued,
//   inprogress,
//   done,
//   error,
// }

// interface QueuedCallback {
//   callback: Function;
//   status: QueuedCallbackStatus;
// }

@Injectable({
  providedIn: 'root',
})
export class DownloadManagerService {
  /**
   * number of parts processed at the same time in a collection
   */
  public MAX_PROCESSED_PARTS = 4;

  private fileList: FileManagerType[] = [];
  private collectionList: CollectionManagerType[] = [];

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

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

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

  private getDeviceSpecificFileWriter(
    file: DriveFile
  ): Promise<FileSystemWritableFileStreamCommonInterface | null> {
    if (this.cordovaService.isOnApp()) {
      return this.cordovaService.getFileWriter(file.fullName).catch((e) => {
        this.dialogService.openAlertDialog(
          marker('Permission required'),
          marker(
            'File Storage permission is required for this operation. Please make sure to give Nano the requested permission.'
          )
        );
        return Promise.reject(e);
      });
    } 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 (AppStorage.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) {
                AppStorage.setItem('big-file-download-warning', '1');
                return null;
              } else {
                throw new Error('cancel');
              }
            });
        } else {
          return Promise.resolve(null);
        }
      }
    }
  }

  public startCollection = async (
    collection: CollectionManagerType,
    batchSize: number = 1,
    progressCallback?: Function
  ) => {
    if (collection && batchSize) {
      const availableParts = collection.parts
        .filter((part) => part.status === 'pending')
        .slice(0, batchSize);

      for (let part of availableParts) {
        const { file, writable, path } = part;

        // pushes itself to inprogress parts
        collection.inProgressParts.push(part);
        const awaitedWritable = await writable;
        part.status = 'inprogress';

        let now = performance.now();
        let speedWindow = 1;

        part.requestId = this.nanoService.getFile(
          collection.resourceId,
          path,
          awaitedWritable,
          (response) => {
            part.status = 'done';

            const index = collection.inProgressParts.findIndex(
              (p) => p.requestId === part.requestId
            );
            if (index > -1) {
              collection.inProgressParts.splice(index, 1);
            }

            if (progressCallback) {
              progressCallback(0, 1, false); // we dont want to add the size, the progressCallback handles that
            }

            const statuses = collection.parts.map((part) => part.status);

            if (statuses.includes('pending')) {
              // still has pending parts
              // resolve them
              if (!collection.isPaused) {
                this.startCollection(collection, 1, progressCallback);
              }
            } else if (
              // !statuses.includes('pending') &&  // previous statement guarantees this
              !statuses.includes('inprogress')
            ) {
              // has no pending and inprogress parts
              // done or error
              if (statuses.includes('error')) {
                // send error
                if (!collection.errorFixStarted) {
                  console.log('error happened, fixing initiated');
                  collection.errorFixStarted = true;
                  this.resumeCollectionWithError(collection);
                } else {
                  // let it run i guess
                  console.log('Error happened but fixing is in progress');
                  return null;
                }

                // try to resolve errors
                console.log('has error');
              } else {
                collection.errorFixStarted = false;
                // everything is done
                console.log('everything is done');
                collection.doneCallback();
              }
            } else {
              // still in progress
            }
          },
          (err) => {
            part.status = 'error';
            collection.error = true;

            const splicedPart = collection.inProgressParts.splice(
              collection.inProgressParts.findIndex((p) => p.requestId === part.requestId),
              1
            );
            if (splicedPart?.[0]) {
              collection.errorParts.push(splicedPart[0]); // ha hiba van fájl letöltése közben, akkor az adott sessiont pauseolja
            }

            this.startCollection(collection, 1, progressCallback);

            console.log('err', err, 'part', part);
          },
          (curr, max, speed) => {
            part.currPart = curr;
            part.countPart = max;
            part.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;

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

            let progressedSize: number;

            if (curr == 0 || curr == max) {
              progressedSize = 0;
            } else if (curr < max - 1) {
              progressedSize = NanoService.NETWORK_FILE_CHUNK_SIZE;
            } else {
              // last part
              progressedSize = file.file.size % NanoService.NETWORK_FILE_CHUNK_SIZE;
              if (progressedSize == 0) {
                progressedSize = NanoService.NETWORK_FILE_CHUNK_SIZE;
              }
            }

            // progressing one chunk
            if (progressCallback) {
              progressCallback(progressedSize, 0, false);
            }
            downloadCollectionProgressObserver.next(collection);
          }
        );
      }

      if (!availableParts.length && !collection.inProgressParts.length) {
        // edge case on folder copy without files
        collection.doneCallback();
      }
    } else {
      // throw error
      console.error('start collection called with invalid args: ', collection, batchSize);
    }
  };

  public async resumeCollection(collection: CollectionManagerType) {
    const isOnApp = this.cordovaService.isOnApp();
    for (const part of collection.inProgressParts) {
      // app
      if (isOnApp) {
        this.cordovaService.registerRunningProcess(part.requestId);
      }
      if (collection.direction === FileManagerDirection.UP) {
        this.nanoService.resumeUploadSession(part.requestId);
      } else {
        try {
          if (part.status === 'inprogress') {
            this.nanoService.resumeRequest(part.requestId);
          }
        } catch (err) {
          console.error('Error while resuming part', part, err);
          const splicedPart = collection.inProgressParts.splice(
            collection.inProgressParts.findIndex((p) => p.requestId === part.requestId),
            1
          );
          if (splicedPart?.[0]) {
            collection.errorParts.push(splicedPart[0]); // ha hiba van fájl letöltése közben, akkor az adott sessiont pauseolja
          }
        }
      }
    }
    this.startCollection(
      collection,
      this.MAX_PROCESSED_PARTS - collection.inProgressParts.length,
      collection.progressCallback
    );
  }

  public resumeCollectionWithError(
    collection: CollectionManagerType,
    targetedPart?: CollectionObject
  ) {
    console.log('resumeCollectionWithError started ', collection);
    // try to finish paused parts with errors
    collection.error = false;
    for (const part of collection.errorParts) {
      if (targetedPart && part.requestId !== targetedPart.requestId) {
        return;
      }
      if (!part) {
        return;
      }
      try {
        if (part?.writable?.close) {
          // enclose with trycatch?
          // can throw error
          console.log('close writable stream');
          part.writable.close();
        }
        const path = part.path[0] === '/' ? part.path.slice(1) : part.path;
        const pathArray = path.split('/');
        const reDownloadFileCallback = (dirhandler) => {
          dirhandler.getFileHandle(part.file.name, { create: true }).then(
            (fileHandle) => {
              const collectionObject: CollectionObject = {
                writable: fileHandle.createWritable(),
                file: part.file,
                path: part.file.path,
                status: 'pending',
              };
              part.writable = collectionObject.writable;
              part.file = collectionObject.file;
              part.path = collectionObject.file.path;
              part.status = collectionObject.status;

              const splicableIndex = collection.errorParts.findIndex(
                (p) => p.requestId === part.requestId
              );

              if (splicableIndex >= 0) {
                const splicedPart = collection.errorParts.splice(
                  collection.errorParts.findIndex((p) => p.requestId === part.requestId),
                  1
                );
                /*if (splicedPart?.[0]) {
                  collection.inProgressParts.push(splicedPart[0]);
                }*/
              }

              if (collection.done || collection.isPaused) {
                this.resumeCollection(collection);
              }
              this.startCollection(collection, 1, collection.progressCallback);
            },
            (err) => {
              console.error('error while fixing file', err);
              // throw err ?
            }
          );
        };

        const success = this.getFolderRecursively(
          collection.dirHandler,
          pathArray,
          0,
          reDownloadFileCallback
        );
        console.log('success?', success, collection.inProgressParts.length);
      } catch (err) {
        console.error('Error while resuming part', part);
        part.status = 'error';
        collection.error = true;
      }
    }
  }

  /**
   * Get the direhandler for the file
   * @param dirHandler
   * @param pathArray
   * @param index
   * @param cb
   * @returns dirhandler
   */
  private getFolderRecursively = (dirHandler, pathArray, index, cb) => {
    if (index + 1 >= pathArray.length) {
      return cb(dirHandler);
    }
    const path = pathArray[index];

    return dirHandler.getDirectoryHandle(path, { create: true }).then((subDirHandle) => {
      return this.getFolderRecursively(subDirHandle, pathArray, index + 1, cb);
    });
  };

  public async pauseCollection(collection: CollectionManagerType) {
    if (!collection.isPaused) {
      collection.isPaused = true;

      const isOnApp = this.cordovaService.isOnApp();
      for (const part of collection.inProgressParts) {
        // app
        if (isOnApp) {
          this.cordovaService.removeRunningProcess(part.requestId);
        }

        if (collection.direction === FileManagerDirection.UP) {
          this.nanoService.pauseUploadSession(part.requestId);
        } else {
          console.log('pausing: ', part);
          try {
            this.nanoService.pauseRequest(part.requestId);
            console.log('paused: ', part);
          } catch (err) {
            console.error('pause error on requestId: ', part.requestId, err);
          }
        }
      }
    }
  }

  public async downloadCollection(
    resourceId: string,
    path: string,
    collection: CollectionObject[],
    dirHandler: any
  ) {
    console.log('downloadCollection started', resourceId, path, collection);
    console.log('DIRHANDLER', dirHandler);

    const now = new Date();
    const collectionId = `collection_${resourceId}_${now.toISOString()}`;

    const collectionManagerType: CollectionManagerType = {
      direction: FileManagerDirection.DOWN,
      parts: collection,
      inProgressParts: [],
      errorParts: [],
      path: path,
      isPaused: false,
      resourceId: resourceId,
      currPart: 0,
      countPart: collection?.length ?? 0,
      currSize: 0,
      maxSize: collection.reduce((sum, curr) => sum + curr.file.file.size, 0),
      speed: '', // 0? string?
      timeNeeded: '', //0? string
      started: now,
      progress: 0, // number
      done: false,
      verifying: false,
      error: false,
      errorFixStarted: false,
      collectionId: collectionId,
      doneCallback: (result) => {
        //console.log('doneCallback', result);
        collectionManagerType.done = true;
        console.log('collection done');
        //console.log('Verifying started');
        //this.verifyCollection(collectionManagerType);
        // clearInterval(this.callbackQueueInterval);

        downloadCollectionFinishedObserver.next(collectionManagerType);
      },
      progressCallback: (progressedSize, progressedPart, error) => {
        if (progressedSize) {
          const newSize = collectionManagerType.currSize + progressedSize;

          collectionManagerType.currSize = newSize;
          collectionManagerType.progress = (newSize / collectionManagerType.maxSize) * 100;
        }
        if (progressedPart) {
          collectionManagerType.currPart += progressedPart;
        }
        if (error) {
          collectionManagerType.error = true;
        }
      },
      errorCallback: (err) => {
        console.error('err on coll and part', err, collectionManagerType);
        collectionManagerType.error = true;
        this.pauseCollection(collectionManagerType);
        // downloadCollectionErrorObserver.next(collectionManagerType)
      },
      dirHandler: dirHandler,
    };

    this.collectionList.push(collectionManagerType);
    downloadCollectionStartedObserver.next(collectionManagerType);

    console.log('START COLLECTION WITH ', collectionManagerType);
    this.startCollection(
      collectionManagerType,
      this.MAX_PROCESSED_PARTS,
      collectionManagerType.progressCallback
    );
    // this.callbackQueue.push({
    //   callback: async (doneCb) => {
    //     await this.startCollection(
    //       collectionManagerType,
    //       this.MAX_PROCESSED_PARTS,
    //       collectionManagerType.progressCallback
    //     );
    //     doneCb();
    //   },
    //   status: QueuedCallbackStatus.queued,
    // });

    // this.processCallbackQueue();
  }

  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,
      paused: 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
            | FileSystemWritableFileStreamMobile
            | null
        ) => {
          let requestId = this.nanoService.getFile(
            resourceId,
            path,
            streamWriter,
            (response) => {
              refForFile.done = true;
              downloadFinishedObserver.next(refForFile);
              if (this.cordovaService.isOnApp()) {
                let mobileWriter = streamWriter as FileSystemWritableFileStreamMobile;
                this.cordovaService.removeRunningProcess(refForFile.requestId);
                refForFile.mobileLocalPath = mobileWriter.getPath();
              }

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

                try {
                  refForFile.objectURL = this.blobService.createObjectURL(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');
                  this.blobService.revokeObjectURL(refForFile.objectURL);
                  refForFile.objectURL = null;
                }, DownloadManagerService.STORE_TIME_DOWNLOADED_DATA_IN_CACHE);
              }
            }, // else just stream into the selected file

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

              downloadErrorObserver.next({ error: err, file: refForFile });
            },
            (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);
              else if (curr != max) {
                downloadProgressObserver.next(refForFile);
                progressCallback &&
                  progressCallback(curr, max, refForFile.speed, refForFile.timeNeeded, refForFile);
              }
            }
          );
          this.cordovaService.isOnApp() && this.cordovaService.registerRunningProcess(requestId);

          refForFile.requestId = requestId;

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

  public async downloadMultiple(
    resourceId: string,
    folderPath: string,
    files: DriveFile[]
  ): Promise<void> {
    if (!window['showDirectoryPicker']) {
      return this.legacyDownloadMultiple(resourceId, folderPath, files);
    }

    let fsDirectoryHandle = await window['showDirectoryPicker']();
    let now = performance.now();
    let speedWindow = 1;
    let existingFileNames: Array<string> = [];

    for await (let handle of fsDirectoryHandle.values()) {
      existingFileNames.push(handle.name);
    }

    let startError: string[] = [];

    let maximumFileDownloadChunk = 8;
    let nextDownload = 0;

    let downloadFile = async (file) => {
      try {
        let newFileName = file.fullName;

        if (existingFileNames.some((existingFileName) => existingFileName == file.fullName))
          newFileName = this.createUnusedFileName(existingFileNames, file);

        let newFilePath = folderPath.length == 0 ? newFileName : folderPath + '/' + newFileName;
        let originalFilePath =
          folderPath.length == 0 ? file.fullName : folderPath + '/' + file.fullName;
        let fileHandle = await fsDirectoryHandle.getFileHandle(newFileName, { create: true });
        let streamWriter = await fileHandle.createWritable();

        let fileReference = this.createDefaultFileManagerType(file, resourceId, newFilePath);

        let requestId = this.nanoService.getFile(
          resourceId,
          originalFilePath,
          streamWriter,
          () => {
            this.handleMultipleDownloadDone(fileReference);
            let nextIndex = nextDownload++;

            if (nextIndex < files.length) {
              //console.log('start next', nextIndex, files.length);
              downloadFile(files[nextIndex]);
            }
          },
          (err) => {
            this.handleMultipleDownloadError(err, fileReference);
          },
          (curr, max) => {
            this.handleMultipleDownloadProgress(curr, max, fileReference, speedWindow, now);
          }
        );

        fileReference.requestId = requestId;
        this.fileList.push(fileReference);
      } catch (err) {
        console.error('error during file download', file, err);
        startError.push(file.fullName + ': ' + err);
      }
    };

    for (let i = 0; i < files.length && i < maximumFileDownloadChunk; i++) {
      let f = files[i];
      nextDownload++;
      downloadFile(f);

      //console.log('first start', i, files.length);
    }

    if (startError.length == 0) {
      return Promise.resolve();
    } else {
      return Promise.reject(startError.join('\r\n'));
    }
  }

  private async legacyDownloadMultiple(
    resourceId: string,
    folderPath: string,
    files: DriveFile[]
  ): Promise<void> {
    let confirm = await this.dialogService
      .openConfirmDialog(
        marker('Multiple file download'),
        marker('You are about to download multiple files! Are you sure you want to continue?')
      )
      .toPromise();
    if (!confirm) return;

    for (let file of files) {
      let newFilePath = folderPath.length == 0 ? file.fullName : folderPath + '/' + file.fullName;
      this.download(resourceId, newFilePath, file, () => {});
    }
  }

  private handleMultipleDownloadDone(fileReference: FileManagerType): void {
    //console.log('File download done during multiple file download.');
    fileReference.done = true;

    if (!this.routerHandler.isOnPage('download')) {
      this.snackbarService.showSnackbar(marker('Download done: ' + fileReference.file.fullName));
    }
  }

  private handleMultipleDownloadProgress(
    curr,
    max,
    fileReference: FileManagerType,
    speedWindow: number,
    now: number
  ): void {
    fileReference.error = false;
    fileReference.currPart = curr;
    fileReference.countPart = max;
    fileReference.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;

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

    if (curr == 0) downloadStartedObserver.next(fileReference);
    downloadProgressObserver.next(fileReference);
  }

  private handleMultipleDownloadError(err, fileReference: FileManagerType): void {
    console.error('Error happened during the multiple-file download: ', err);
    fileReference.error = true;
  }

  private createUnusedFileName(existingNames: string[], file: DriveFile): string {
    let existingFileName = existingNames.find((existingName) => existingName == file.fullName);
    let matches = existingFileName.match(/\((\d*)\)[^(]*$/);
    let newFileName: string = '';
    let index: number = 1;
    let toReplace: string = `.${file.ext}`;

    if (matches && matches.length > 1) {
      //filename ends with numbers between parentheses
      toReplace = matches[0]; //last part with parentheses. Eg.: (3).png
    }

    newFileName = file.fullName.replace(toReplace, ` (${index}).${file.ext}`);

    //check names until we find an unused one
    while (existingNames.some((existingName) => existingName == newFileName)) {
      newFileName = file.fullName.replace(toReplace, ` (${index}).${file.ext}`);
      index++;
    }

    return newFileName;
  }

  public createDefaultFileManagerType(
    file: DriveFile,
    resourceId: string,
    path: string
  ): FileManagerType {
    return {
      direction: FileManagerDirection.DOWN,
      file,
      resourceId,
      path,
      currPart: 0,
      countPart: 0,
      progress: 0,
      started: new Date(),
      timeNeeded: '',
      done: false,
      paused: false,
      error: false,
      requestId: null,
      speed: '',
    };
  }

  public upload(
    resourceId,
    path: string,
    file: File,
    overwrite: boolean,
    alias?: string,
    doneCallback?: Function,
    errorCallback?: Function
  ) {
    let driveFile = new DriveFile(file);
    if (alias) {
      driveFile.updateName(alias);
    }

    let refForFile: FileManagerType = {
      direction: FileManagerDirection.UP, // up, down
      file: driveFile,
      resourceId,
      path,
      currPart: 0,
      countPart: 0,
      progress: 0,
      started: new Date(),
      done: false,
      paused: 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,
          overwrite,
          (res) => {
            //this.listDirectory(); TODO
            refForFile.done = true;
            uploadFinishedObserver.next(refForFile);
            this.cordovaService.isOnApp() &&
              this.cordovaService.removeRunningProcess(refForFile.requestId);
            if (typeof res == 'string' && res.length > 0) {
              refForFile.file.fullName = res;
            }
            doneCallback(res); // norm_name
          },
          (err) => {
            console.log('error', err);
            refForFile.error = true;
            uploadErrorObserver.next({ error: err, file: refForFile });
            errorCallback(err);

            if (err.error === 4218) {
              this.remove(refForFile);
            }
          },
          (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) uploadStartedObserver.next(refForFile);
            else if (curr != 100) uploadProgressObserver.next(refForFile);
          }
        );
      } else {
        console.log('too much upload, wait a little bit');
        setTimeout(startUpload, 1000);
      }
    };
    startUpload();
    this.cordovaService.isOnApp() &&
      this.cordovaService.registerRunningProcess(refForFile.requestId);
  }

  // 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);
    }
    this.cordovaService.isOnApp() && this.cordovaService.removeRunningProcess(file.requestId);

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

    fileRemovedObserver.next(file);
  }

  public removeCollection(collection: CollectionManagerType) {
    const isOnApp = this.cordovaService.isOnApp();
    let index = 0;
    for (const part of collection.parts) {
      try {
        if (part.status !== 'done' && part.requestId >= 0) {
          if (collection.direction === FileManagerDirection.UP) {
            this.nanoService.removeUploadSession(part.requestId);
          } else {
            console.log('delete requestId of ', part);
            this.nanoService.deleteRequestSession(part.requestId);
          }
          if (isOnApp) {
            this.cordovaService.removeRunningProcess(part.requestId);
          }
        }
        index++;
      } catch (err) {
        console.error('err', err);
      }
    }

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

  /**
   * In Chrome there is a bug, with small chance the filewriter can not close the file, so it is not
   * flush the last part. In this case we can not use the corrupted filewriter, need to create a new one and
   * redownload the file
   * @param collection
   * @returns
   */
  verifyCollection = (collection: CollectionManagerType) => {
    collection.verifying = true;

    const dirHandlerMap = { [collection.path]: collection.dirHandler };

    let partCounter = 0;
    let verifyError = false;
    for (const part of collection.parts) {
      console.log('starting with part: ', partCounter, part);
      if (!part) {
        return;
      }
      if (verifyError) {
        // go out if error
        break;
      }
      partCounter += 1;
      if (part.file?.parent) {
        const path = part.path[0] === '/' ? part.path.slice(1) : part.path;
        const pathArray = path.split('/');
        if (part.file.parent.path in dirHandlerMap) {
          // folder's dirHandler is already used and stored in dirHandlerMap
          dirHandlerMap[part.file.parent.path]
            .getDirectoryHandle(pathArray[pathArray.length - 2], { create: true })
            .then((dirHandler) => {
              const lastIndex = path.lastIndexOf('/');
              const slicedFolderPath = path.slice(0, lastIndex < 0 ? path.length : lastIndex);
              dirHandlerMap[slicedFolderPath] = dirHandler;
              dirHandler.getFileHandle(part.file.name, { create: true }).then((fileHandle) => {
                verifyError = true;
                this.verifySingleFile(fileHandle, part, collection);
              });
            });
        } else {
          this.getFolderRecursively(collection.dirHandler, pathArray, 0, (foundDirHandler) => {
            const lastIndex = path.lastIndexOf('/');
            const slicedFolderPath = path.slice(0, lastIndex < 0 ? path.length : lastIndex);
            dirHandlerMap[slicedFolderPath] = foundDirHandler;
            foundDirHandler.getFileHandle(part.file.name, { create: true }).then((fileHandle) => {
              verifyError = true;
              this.verifySingleFile(fileHandle, part, collection);
            });
          });
        }
      } else {
        const path = part.path[0] === '/' ? part.path.slice(1) : part.path;
        collection.dirHandler.getDirectoryHandle(path, { create: true }).then((subDirHandler) => {
          console.log('subdirHandler in root', subDirHandler);
          // ha nincs parent, akkor gyökérben vagyunk, ez csak a főmappánál lehetséges
        });
      }
    }
  };

  private verifySingleFile = (fileHandle, part, collection) => {
    fileHandle.getFile().then((file) => {
      if (file.size !== part.file.size) {
        console.error('Error while verifying file: ', file, part);
        // ezeket összegyűjteni promisokként és a végén lefuttatni. Ha mind megvan, akkor resumeCollectionWithError
        part.status = 'error';
        collection.error = true;
        collection.errorParts.push(part);
        this.resumeCollectionWithError(collection, part);
      }
    });
  };

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

  private humanizeSpeed(speed: number): string {
    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';
    }
  }

  public clear(): void {
    while (this.fileList.length > 0) {
      this.remove(this.fileList[0]);
    }

    while (this.collectionList.length > 0) {
      this.removeCollection(this.collectionList[0]);
    }
  }
}

let holdFileInCache = AppStorage.getItem(AdvancedSettingsEnum.HOLD_FILE_IN_CACHE);
if (holdFileInCache !== undefined) {
  let limit = parseInt(holdFileInCache);
  if (limit >= 10) {
    DownloadManagerService.STORE_TIME_DOWNLOADED_DATA_IN_CACHE = limit;
  }
}
