import { animate, style, transition, trigger } from '@angular/animations';
import {
  AfterViewInit,
  Component,
  ElementRef,
  EventEmitter,
  HostListener,
  Input,
  OnChanges,
  OnDestroy,
  OnInit,
  Output,
  SimpleChanges,
  ViewChild,
} from '@angular/core';
import { MatBottomSheet } from '@angular/material/bottom-sheet';
import { MatDialog } from '@angular/material/dialog';
import { MatMenuTrigger } from '@angular/material/menu';
import { ActivatedRoute } from '@angular/router';
import { marker } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core';
import { DeviceDetectorService } from 'ngx-device-detector';
import { base64 } from 'rfc4648';
import { Observable, Subject, Subscription } from 'rxjs';
import { AppStorage } from 'src/app/shared/app-storage';
import { SheetAction } from 'src/app/shared/consts/enums';
import { AbstractRoomKeyring } from 'src/app/shared/crypto/keyring/room_base';
import { DeleteFileDialogComponent } from 'src/app/shared/dialogs/delete-file-dialog/delete-file-dialog.component';
import { FilePropertiesDialogComponent } from 'src/app/shared/dialogs/file-properties-dialog/file-properties-dialog.component';
import { FolderDownloadConfirmDialogComponent } from 'src/app/shared/dialogs/folder-download-confirm-dialog/folder-download-confirm-dialog.component';
import { MoveFileDialogComponent } from 'src/app/shared/dialogs/move-file-dialog/move-file-dialog.component';
import { NewFolderDialogComponent } from 'src/app/shared/dialogs/new-folder-dialog/new-folder-dialog.component';
import { RenameExistingFileDialogComponent } from 'src/app/shared/dialogs/rename-existing-file-dialog/rename-existing-file-dialog.component';
import { RenameFileDialogComponent } from 'src/app/shared/dialogs/rename-file-dialog/rename-file-dialog.component';
import { isNanoFeatureSupported, NanoFeature } from 'src/app/shared/drive-version';
import { getFileFromClipboard } from 'src/app/shared/file-reader';
import { MediaViewerService } from 'src/app/shared/media-viewer/media-viewer.service';
import { NanoThumbnailResponse } from 'src/app/shared/nano/nano-requests';
import { NanoService } from 'src/app/shared/nano/nano.service';
import { ObjectSorter } from 'src/app/shared/object-sorter';
import { AccountService } from 'src/app/shared/server-services/account.service';
import { AuthService } from 'src/app/shared/server-services/auth.service';
import { NanoSlotRecord } from 'src/app/shared/server-services/query-records/nano-records';
import { RoomWithData } from 'src/app/shared/server-services/query-records/room-records';
import { WorkspaceSubscriptionRoomAccountPermissionEventRecord } from 'src/app/shared/server-services/query-records/workspace-records';
import { getNanoSlotsQuery } from 'src/app/shared/server-services/querys';
import { RoomService } from 'src/app/shared/server-services/room.service';
import { ServerError } from 'src/app/shared/server-services/server-errors';
import {
  SubscriptionServiceEvent,
  SubscriptionServiceEventType,
} from 'src/app/shared/server-services/subscription-event';
import { SubscriptionService } from 'src/app/shared/server-services/subscription.service';
import { BlobService } from 'src/app/shared/services/blob.service';
import { ClipboardService } from 'src/app/shared/services/clipboard.service';
import { CordovaService } from 'src/app/shared/services/cordova.service';
import { DialogService } from 'src/app/shared/services/dialog.service';
import {
  CollectionObject,
  DownloadManagerService,
  FileManagerDirection,
} from 'src/app/shared/services/download-manager-service';
import { NanoActionQueueService } from 'src/app/shared/services/nano-action-queue.service';
import { RouterHandler } from 'src/app/shared/services/router-handler.service';
import { ServerRestApiService } from 'src/app/shared/services/server-rest-api.service';
import { SnackBarService } from 'src/app/shared/services/snackbar.service';
import { environment } from 'src/environments/environment';
import { BottomSheetMenuComponent } from './bottom-sheet-menu/bottom-sheet-menu.component';
import { DriveFile } from './drive-file';

enum ViewMode {
  TILE = 0,
  LIST = 1,
}

type FileRowSelectedEnum = 'selected' | 'indeterminate' | 'empty';
interface FileRow {
  file?: DriveFile;
  name: string;
  path: string;
  parent?: FileRow;
  children?: FileRow[];
  selected: FileRowSelectedEnum;
  status: 'not-started' | 'loading' | 'done' | 'error-while-loading' | 'error';
  type: 'file' | 'folder';
}

export let scrollToFileObserver = new Subject<string>();

@Component({
  selector: 'app-drive-layout',
  templateUrl: './drive-layout.component.html',
  styleUrls: ['./drive-layout.component.scss'],
  animations: [
    trigger('inOutAnimation', [
      transition(':enter', [
        style({ bottom: -60, opacity: 0.5 }),
        animate('0.2s ease-out', style({ bottom: 0, opacity: 1 })),
      ]),
      transition(':leave', [
        style({ bottom: 0, opacity: 1 }),
        animate('0.2s ease-in', style({ bottom: -60, opacity: 0.5 })),
        style({ display: 'none' }),
      ]),
    ]),
  ],
})
export class DriveLayoutComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
  @Input() resourceId;
  @Input() path;
  @Output() readonly navigateEvent: EventEmitter<{ path: string; from?: string }> =
    new EventEmitter<{ path: string; from?: string }>();

  @ViewChild('hiddenClipboard', { read: ElementRef })
  hiddenClipboard: ElementRef;

  @ViewChild('hiddenFileInput', { read: ElementRef })
  hiddenFileInput: ElementRef;

  @ViewChild('resourceContainer', { read: ElementRef })
  resourceContainerRef: ElementRef;
  @ViewChild(MatMenuTrigger) context_trigger: MatMenuTrigger;
  public contextMenuPosition = { x: '0px', y: '0px' };

  private routeSubscription;
  private driveViewCacheKey = 'DriveViewMode';

  public folderStack = [];
  public isContentLoading = false;

  public showBackArrow = false;

  public currOrderBy = 'name';
  public currOrderDirection = 1; // 1 - asc, -1 desc

  public viewMode: ViewMode = ViewMode.LIST;
  public viewModeEnum = ViewMode;

  public files: DriveFile[] = [];
  public selectedFiles: DriveFile[] = [];
  public lastInteracted: DriveFile;
  public isSelectingMode: boolean = false;

  private elementInARow = 1;
  private ownRoom: boolean = false;
  public allowAnonym: boolean = false;

  public loadError = false;
  public pathError = false;
  public pinnedKeyError = false;
  public peerNotTrustedError = false;
  public initLoadDone = false;
  public competingError = false;

  public competingClientNames: string;

  public isReadOnly: boolean = false;
  public canUpload: boolean = false;
  public navigatedFile: DriveFile;

  public showDropArea: boolean = false;

  public nanoDetailsName: string = '';
  public isMobile: boolean = false;

  private backFolderNavigated = false; // flag for handling file focusing from !back url fragment - (on folder jumps)

  private onFragmentChangeSubscription = null;

  private scrollToFileSubscription: Subscription = null;

  public hasDirectoryPicker = window.hasOwnProperty('showDirectoryPicker');

  constructor(
    private nanoService: NanoService,
    private routerHandler: RouterHandler,
    private activatedRoute: ActivatedRoute,
    private el: ElementRef,
    public dialog: MatDialog,
    private downloadManagerService: DownloadManagerService,
    private nanoActionQueueService: NanoActionQueueService,
    private accountService: AccountService,
    private roomService: RoomService,
    private dialogService: DialogService,
    private authService: AuthService,
    private subscriptionService: SubscriptionService,
    private serverRestApi: ServerRestApiService,
    private clipboardService: ClipboardService,
    private mediaViewerService: MediaViewerService,
    private blobService: BlobService,
    private cordovaService: CordovaService,
    private snackbarService: SnackBarService,
    private bottomSheet: MatBottomSheet,
    private translate: TranslateService,
    private deviceDetector: DeviceDetectorService
  ) {
    // the enum casted localstorage will be string, not number,
    // template side the eqeqeq (===) won't work, so assign the value like this:
    this.viewMode =
      <ViewMode>(<unknown>AppStorage.getItem(this.driveViewCacheKey)) == ViewMode.LIST
        ? ViewMode.LIST
        : ViewMode.TILE;

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_ACCOUNT_PERMISSION_EVENT,
      SubscriptionServiceEventType.ALL,
      this.onRoomAccountPermissionChange.bind(this)
    );

    this.onFragmentChangeSubscription = this.activatedRoute.fragment.subscribe(
      this.onFragmentChange
    );

    this.scrollToFileSubscription = scrollToFileObserver.subscribe({ next: this.scrollToFile });

    this.isMobile = !this.deviceDetector.isDesktop();
  }

  public scrollToFile = (fullName: string) => {
    const selectedFile = this.files.find((f) => f?.fullName === fullName);
    if (selectedFile) {
      this.selectFile(selectedFile);
    }

    if (document.getElementById('f-' + fullName)) {
      document
        .getElementById('f-' + fullName)
        .querySelectorAll('.file-name')
        .forEach((el) => {
          el.scrollIntoView({
            block: 'center',
          });
        });
    }
  };

  ngOnDestroy() {
    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_ACCOUNT_PERMISSION_EVENT,
      SubscriptionServiceEventType.ALL,
      this.onRoomAccountPermissionChange.bind(this)
    );
    if (this.onFragmentChangeSubscription) {
      this.onFragmentChangeSubscription.unsubscribe();
    }

    this.scrollToFileSubscription.unsubscribe();
  }

  private onRoomAccountPermissionChange(
    event: WorkspaceSubscriptionRoomAccountPermissionEventRecord
  ) {
    if (this.resourceId == event.id) {
      this.checkReadOnly();
    }
  }

  @HostListener('window:keydown', ['$event']) onKeyDown(e) {
    if (this.dialog.openDialogs.length > 0) return;

    if (e.ctrlKey) {
      this.hiddenClipboard.nativeElement.focus();
    }

    if (e.ctrlKey && e.code == 'KeyA') {
      this.selectedFiles = this.files.slice(0, this.files.length);
      this.lastInteracted = this.selectedFiles?.[0] || null;
      e.preventDefault();
    } else if (e.shiftKey && this.lastInteracted) {
      let index;

      if (e.code == 'ArrowUp') {
        index = Math.max(this.files.indexOf(this.lastInteracted) - this.elementInARow, 0);
      } else if (e.code == 'ArrowDown') {
        index = Math.min(
          this.files.indexOf(this.lastInteracted) + this.elementInARow,
          this.files.length - 1
        );
      } else if (e.code == 'ArrowLeft') {
        index = Math.max(this.files.indexOf(this.lastInteracted) - 1, 0);
      } else if (e.code == 'ArrowRight') {
        index = Math.min(this.files.indexOf(this.lastInteracted) + 1, this.files.length - 1);
      }

      if (index != undefined) {
        if (this.selectedFiles.indexOf(this.files[index]) == -1) {
          this.selectedFiles.push(this.files[index]);
        }

        this.lastInteracted = this.files[index];
        e.preventDefault();
      }
    } else if (e.code == 'ArrowUp' && this.lastInteracted) {
      this.lastInteracted =
        this.files[Math.max(0, this.files.indexOf(this.lastInteracted) - this.elementInARow)];
      e.preventDefault();
    } else if (e.code == 'ArrowDown' && this.lastInteracted) {
      this.lastInteracted =
        this.files[
          Math.min(
            this.files.length - 1,
            this.files.indexOf(this.lastInteracted) + this.elementInARow
          )
        ];
      e.preventDefault();
    } else if (e.code == 'ArrowLeft' && this.lastInteracted) {
      this.lastInteracted = this.files[Math.max(0, this.files.indexOf(this.lastInteracted) - 1)];
      e.preventDefault();
    } else if (e.code == 'ArrowRight' && this.lastInteracted) {
      this.lastInteracted =
        this.files[Math.min(this.files.length - 1, this.files.indexOf(this.lastInteracted) + 1)];
      e.preventDefault();
    } else if (e.code == 'Delete' || e.code == 'F8') {
      if (this.isReadOnly || (this.selectedFiles.length === 0 && !this.lastInteracted)) {
        // disabled
      } else {
        this.openDeleteDialog();
      }
    } else if (e.code == 'F7') {
      if (this.isReadOnly) {
        // disabled
      } else {
        this.openCreateFolderDialog();
      }
    }
  }

  @HostListener('window:resize') onResize() {
    this.elementInARow = this.calcElementInARow();
  }

  ngOnInit() {
    Promise.all([this.accountService.getMe(), this.roomService.getRoom(this.resourceId)])
      .then(([me, room]) => {
        this.ownRoom = me.id == room.ownerAccountId;
        this.getNanoSlots(room);
      })
      .catch((error) => {
        console.error('Error during me or room query', error);
      });

    this.checkReadOnly();
    this.loadThumbnails();
  }

  ngOnChanges(change: SimpleChanges) {
    if (change['path'] && change['path'].currentValue !== change['path'].previousValue) {
      if (change['path'].currentValue && change['path'].currentValue.length > 0) {
        this.folderStack = change['path'].currentValue.split('/');
      } else {
        this.folderStack = [];
      }
    }

    if (change['resourceId']) {
      this.checkReadOnly();
    }

    this.listDirectory(() => {
      let fragment = this.routerHandler.getRoute().fragment;
      this.updateFocusedFile(fragment);

      this.loadThumbnails();
    });
  }

  ngAfterViewInit() {
    this.elementInARow = this.calcElementInARow();
  }

  private checkReadOnly() {
    if (this.authService.isAnonym()) {
      this.isReadOnly = true;
    } else {
      console.log('get my room perm', this.resourceId);
      this.roomService
        .getMyRoomPermissions(this.resourceId)
        .then((myPerm) => {
          this.isReadOnly =
            !myPerm.account ||
            !(myPerm.ownerAccountId === myPerm.account?.id || myPerm.account.driveWrite);
          this.canUpload = !this.isReadOnly || myPerm.account?.driveUpload;
          this.allowAnonym = myPerm.allowAnonymous;
        })
        .catch(() => {
          this.isReadOnly = true;
          this.canUpload = false;
        });
    }
  }

  private loadThumbnails(): void {
    this.roomService.getNanoSession(this.resourceId).then((nanoSession) => {
      if (
        nanoSession &&
        isNanoFeatureSupported(nanoSession.version, NanoFeature.THUMBNAIL) &&
        this.files.length != 0
      ) {
        this.nanoService.getFileThumbnail(
          this.resourceId,
          this.getCurrentFolderPath(),
          this.files,
          (thumbnails: NanoThumbnailResponse[]) => {
            thumbnails.forEach((thmb) => {
              let file = this.files.find((f) => f.fullName === thmb.name);
              if (file) {
                file.thumbnail = this.blobService.new([thmb.data.buffer]); // {type: "image/jpeg"}
              }
            });
          },
          (err) => {
            console.error('Thumbnail load error', err);
          },
          () => {}
        );
      }
    });
  }

  public openCreateFolderDialog() {
    if (!this.canUpload) {
      this.snackbarService.showSnackbar('Permission required');
      return;
    }

    this.roomService
      .getNanoSession(this.resourceId)
      .then((nanoSession) => {
        let canUse = nanoSession && isNanoFeatureSupported(nanoSession.version, NanoFeature.TRUST);
        if (!canUse) {
          this.dialogService.openAlertDialog(
            marker('Nano client outdated'),
            marker(
              "This feature is not supported by the Drive's Nano client. Please notify the Drive owner to update the Nano client."
            )
          );
        } else {
          const dialogRef = this.dialog.open(NewFolderDialogComponent, {
            data: { name: this.translate.instant(marker('Folder')) },
          });

          dialogRef.afterClosed().subscribe((name) => {
            if (name && name.length > 0) {
              this.createFolder(name);
            }
          });
        }
      })
      .catch((err) => {
        console.error('Could not query nano session', err);
        throw 'Could not query nano session';
      });
  }

  private createFolder(name) {
    let fullPath = name;
    if (this.folderStack.length > 0) {
      fullPath = this.getCurrentFolderPath() + '/' + name;
    }
    this.nanoActionQueueService.makeDir(
      this.resourceId,
      fullPath,
      () => {
        this.listDirectory(() => {
          for (let i = 0; i < this.files.length; i++) {
            /*console.log(
              "s",
              this.files[i].fullName,
              name,
              this.files[i].fullName == name
            );*/
            if (this.files[i].fullName == name) {
              this.lastInteracted = this.files[i];
              break;
            }
          }
        });
      },
      (err) => {
        console.error('error', err);
        this.dialogService.openAlertDialog(
          marker('Error'),
          marker('Error happened during the operation')
        );
      }
    );
  }

  public openMoveDialog() {
    if (this.isReadOnly) {
      this.snackbarService.showSnackbar('Permission required');
      return;
    }

    let files = this.selectedFiles.length > 0 ? this.selectedFiles : [this.lastInteracted];
    let dialogRef = this.dialog.open(MoveFileDialogComponent, {
      data: { path: this.getCurrentFolderPath(), files: files, resourceId: this.resourceId },
    });

    dialogRef.afterClosed().subscribe((newPath: string) => {
      if (newPath === null) return;

      let doneCount = 0;

      let moveDoneCallback = () => {
        doneCount++;
        if (doneCount == files.length) this.refresh();
      };

      for (let file of files) {
        let currentFile = this.getCurrentFolderPath() + '/' + file.fullName;
        let newFile = newPath + '/' + file.fullName;

        if (currentFile.startsWith('/')) currentFile = currentFile.substring(1, currentFile.length);
        if (newFile.startsWith('/')) newFile = newFile.substring(1, newFile.length);

        if (currentFile == newFile) {
          moveDoneCallback();
          return;
        }

        this.nanoActionQueueService.move(
          this.resourceId,
          currentFile,
          newFile,
          () => moveDoneCallback(),
          () => moveDoneCallback()
        );
      }
    });
  }

  public openEditDialog() {
    if (this.isReadOnly) {
      this.snackbarService.showSnackbar('Permission required');
      return;
    }

    let editFile = this.lastInteracted;

    console.log(editFile);

    const dialogRef = this.dialog.open(RenameFileDialogComponent, {
      data: { name: editFile.fullName, isFolder: editFile.isDir() },
    });

    dialogRef.afterClosed().subscribe((newName) => {
      if (newName && newName.length > 0) {
        const trimmedResult = newName.trim();
        if (trimmedResult && editFile.fullName != trimmedResult) {
          let path = '';
          if (this.folderStack.length > 0) {
            path = this.getCurrentFolderPath() + '/' + path;
          }

          this.nanoService.moveFile(
            this.resourceId,
            path + editFile.fullName,
            path + trimmedResult,
            () => {
              this.refresh();
            },
            () => {
              this.dialogService.openAlertDialog(
                marker('Error'),
                marker('Error happened during the operation')
              );
            }
          );
        }
      }
    });
  }

  public copyPublicFileUrl() {
    this.roomService.getKeyring(this.resourceId).then((r) => {
      this.clipboardService
        .copy(
          environment.site_base +
            '/room/' +
            this.resourceId +
            '/drive#' +
            this.routerHandler.fragmentToRaw({
              path: this.path,
              file: this.lastInteracted.fullName,
              roomkey: base64.stringify(r['_keys'][AbstractRoomKeyring.PINNED]),
            })
        )
        .then(() => {
          this.snackbarService.showSnackbar(
            marker('File URL copied!') + ' (' + this.lastInteracted.fullName + ')'
          );
        });
    });
  }

  public copyFileUrl() {
    this.clipboardService
      .copy(
        environment.site_base +
          '/room/' +
          this.resourceId +
          '/drive#' +
          this.routerHandler.fragmentToRaw({
            path: this.path,
            file: this.lastInteracted.fullName,
          })
      )
      .then(() => {
        this.snackbarService.showSnackbar(
          marker('File URL copied!') + ' (' + this.lastInteracted.fullName + ')'
        );
      });
  }

  public openDeleteDialog() {
    if (this.isReadOnly) {
      this.snackbarService.showSnackbar('Permission required');
      return;
    }

    let forDelete = this.selectedFiles.length > 0 ? this.selectedFiles : [this.lastInteracted];

    const dialogRef = this.dialog.open(DeleteFileDialogComponent, {
      data: forDelete,
    });

    dialogRef.afterClosed().subscribe((acceptedForDelete) => {
      if (acceptedForDelete && acceptedForDelete.length > 0) {
        this.deleteFiles(acceptedForDelete);
      }
    });
  }

  public openDescriptionDialog() {
    const dialogRef = this.dialog.open(FilePropertiesDialogComponent, {
      data: { file: this.lastInteracted },
      hasBackdrop: true,
      disableClose: false,
    });
    // dialogRef.afterClosed().subscribe((acceptedForDelete) => {
    //   if (acceptedForDelete && acceptedForDelete.length > 0) {
    //     this.deleteFiles(acceptedForDelete);
    //   }
    // });
  }

  public deleteFiles(passedFiles: DriveFile[]) {
    let done = 0;

    passedFiles.forEach((f) => {
      let path = f.fullName;
      if (this.folderStack.length > 0) {
        path = this.getCurrentFolderPath() + '/' + path;
      }

      this.nanoActionQueueService.delete(
        this.resourceId,
        path,
        () => {
          done++;
          if (done == passedFiles.length) {
            this.stopSelectingMode();
            this.refresh();
          }
        },
        (err) => {
          done++;
          if (done == passedFiles.length) {
            this.stopSelectingMode();
            this.refresh();
          }
          console.error('error delete', err);
        }
      );
    });
  }

  private calcElementInARow() {
    let container = this.el.nativeElement.querySelector('#file-tile-table');

    if (container) {
      let el = container.querySelector('app-file-block-view:first-of-type');
      if (el) {
        return Math.floor(
          container.getBoundingClientRect().width / el.getBoundingClientRect().width
        );
      }
    } else {
      return 1;
    }
  }

  private getCurrentFolderPath() {
    let path = '';
    this.folderStack.forEach((dir) => {
      if (path.length > 0) {
        path += '/';
      }
      path += dir;
    });
    return path;
  }

  public switchViewMode(mode: ViewMode) {
    this.viewMode = mode;
    AppStorage.setItem(this.driveViewCacheKey, mode.toString());

    setTimeout(() => {
      // call this async or register the viewMode on the ngOnChanges
      this.elementInARow = this.calcElementInARow();
    }, 10);
  }

  public toggleViewMode(): void {
    if (this.viewMode == ViewMode.LIST) this.switchViewMode(ViewMode.TILE);
    else this.switchViewMode(ViewMode.LIST);
  }

  public selectAll(): void {
    if (this.selectedFiles.length != this.files.length) this.selectedFiles = this.files;
    else this.selectedFiles = [];
  }

  private updateFocusedFile = (fragment) => {
    const backFolder = fragment?.['from'];

    let navigatedToFileName;
    if (fragment && fragment['file']) {
      navigatedToFileName = fragment['file'];
    }
    const selectedFile = this.files.find((f) => f.fullName === navigatedToFileName);
    if (selectedFile) {
      this.selectFile(selectedFile);
    }

    if (!selectedFile && backFolder && !this.backFolderNavigated) {
      let focusedFile = this.files.find((f) => f.fullName === backFolder);
      if (focusedFile) {
        this.lastInteracted = focusedFile;
        this.backFolderNavigated = true;
      }
    }
  };

  public listDirectory(cb?: Function) {
    this.isContentLoading = true;
    this.nanoActionQueueService.listDir(
      this.resourceId,
      this.getCurrentFolderPath(),
      (data) => {
        this.initLoadDone = true;
        this.loadError = false;
        this.pathError = false;
        this.pinnedKeyError = false;

        this.files = [];

        // let navigatedToFileName;
        // let fragment = this.routerHandler.getRoute().fragment;
        // if (fragment && fragment['file']) {
        //   navigatedToFileName = fragment['file'];
        // }

        data
          .filter((d) => (d.unresolved ? !d.unresolved : true))
          .forEach((file) => {
            let driveFile = new DriveFile(file);
            this.files.push(driveFile);
            // if (navigatedToFileName && driveFile.fullName == navigatedToFileName) {
            //   this.selectFile(driveFile);
            // }
          });

        this.reorder();
        this.isContentLoading = false;
        this.showBackArrow = this.folderStack.length > 0;

        if (cb) cb();
      },
      (err) => {
        this.pathError = false;
        this.pinnedKeyError = false;
        this.loadError = true;
        this.isContentLoading = false;

        if (err.error == ServerError.NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST) {
          this.pathError = true;
        } else if (err.error == ServerError.NANO_REQUEST_ERROR_PEER_NOT_TRUSTED) {
          this.peerNotTrustedError = true;
        } else if (err.message == 'Nano sessions are competing for target!') {
          //TODO get custom error ID
          this.getCompetingNanos();
        } else if (err.message == 'MissingKey Pinned key is missing') {
          this.pinnedKeyError = true;
        }

        console.error('can not load drive', err);
      },
      (curr, max) => {
        //console.log('progress '+curr+'/'+max)
      }
    );
  }

  public openImageViewer(file: DriveFile) {
    let path = file.fullName;
    if (this.folderStack.length > 0) {
      path = this.getCurrentFolderPath() + '/' + file.fullName;
    }

    this.mediaViewerService.showImage(this.resourceId, path, file);
  }

  /*
  public openPdfViewer(file: DriveFile) {
    let path = file.fullName;
    if (this.folderStack.length > 0) {
      path = this.getCurrentFolderPath() + "/" + file.fullName;
    }

    this.mediaViewerService.showPdf(this.resourceId, path, file);
  }
  */

  public onPress(f: DriveFile): void {
    if (this.isSelectingMode) {
      return;
    }

    this.toggleSelection(f);
    this.isSelectingMode = true;
    this.lastInteracted = f;
  }

  public openMenuSheet(): void {
    var sheetRef = this.bottomSheet.open(BottomSheetMenuComponent, {
      data: {
        selectedFiles: this.selectedFiles,
        lastInteracted: this.lastInteracted,
        allowAnonym: this.allowAnonym,
      },
    });

    sheetRef.afterDismissed().subscribe((ret: SheetAction) => {
      switch (ret) {
        case SheetAction.DELETE: {
          this.openDeleteDialog();
          break;
        }
        case SheetAction.DOWNLOAD: {
          this.downloadFile(this.lastInteracted);
          break;
        }
        case SheetAction.GETLINK: {
          this.copyFileUrl();
          break;
        }
        case SheetAction.GETPUBLICLINK: {
          this.copyPublicFileUrl();
          break;
        }
        case SheetAction.LISTVIEW: {
          this.switchViewMode(this.viewModeEnum.LIST);
          break;
        }
        case SheetAction.TILEVIEW: {
          this.switchViewMode(this.viewModeEnum.TILE);
          break;
        }
        case SheetAction.NEWFOLDER: {
          this.openCreateFolderDialog();
          break;
        }
        case SheetAction.RENAME: {
          this.openEditDialog();
          break;
        }
        case SheetAction.UPLOAD: {
          this.hiddenFileInput.nativeElement.value = '';
          this.hiddenFileInput.nativeElement.click();
          break;
        }
        case SheetAction.MOVE: {
          this.openMoveDialog();
          break;
        }
        case SheetAction.PROPERTIES: {
          this.openDescriptionDialog();
          break;
        }
        case SheetAction.FOLDERDOWNLOAD: {
          this.downloadThisFolderHandler();
          break;
        }
        case SheetAction.DRIVEDOWNLOAD: {
          this.downloadDrive();
          break;
        }
      }
    });
  }

  public stopSelectingMode(): void {
    this.isSelectingMode = false;
    this.deselectFiles();
  }

  public toggleSelection(f: DriveFile): void {
    let index = this.selectedFiles.indexOf(f);
    if (index > -1) {
      this.selectedFiles.splice(index, 1);
    } else {
      this.selectedFiles.push(f);
    }

    if (this.isSelectingMode && this.selectedFiles.length == 0 && !this.isMobile)
      this.isSelectingMode = false;
  }

  /**
   * Returns file- and folder create promises
   *
   * Recursively calls itself on sub-folders
   */
  private createFileHierarchy = (
    promises,
    dirHandler,
    data: { excludedFiles: string[]; files: FileRow[]; success: boolean }
  ) => {
    for (const file of data.files) {
      // do the thing
      const isExcludedIndex = data.excludedFiles.findIndex((exc) => exc === file.path);
      if (isExcludedIndex > -1) {
        delete data.excludedFiles[isExcludedIndex];
        continue;
      }
      if (file.type === 'folder') {
        promises.push(
          dirHandler.getDirectoryHandle(file.name, { create: true }).then(
            (subDirHandler) => {
              if (file?.children?.length) {
                this.createFileHierarchy(promises, subDirHandler, {
                  excludedFiles: data.excludedFiles,
                  files: file?.children,
                  success: true,
                });
              }
              return {
                file: file,
              };
            },
            (err) => {
              console.error('error while folder', err);
            }
          )
        );
      }
      if (file.type === 'file') {
        promises.push(
          dirHandler.getFileHandle(file.name, { create: true }).then(
            (fileHandle) => {
              const collectionObject: CollectionObject = {
                writable: fileHandle.createWritable(),
                file: file,
                path: file.path,
                status: 'pending', // default 'pending'
              };
              return collectionObject;
            },
            (err) => {
              console.error('error while file', err);
            }
          )
        );
      }
    }
    return promises;
  };

  public openFolderDownloadDialog(
    path: string,
    fileNames: string[],
    resourceId: string
  ): Observable<boolean> {
    var ref = this.dialog.open(FolderDownloadConfirmDialogComponent, {
      data: { path, fileNames, resourceId },
    });

    return ref.afterClosed();
  }

  public downloadDrive = async () => {
    await this.downloadFolderByPath('');
  };

  public downloadThisFolderHandler = async () => {
    const currentPath = this.getCurrentFolderPath();
    await this.downloadFolderByPath(currentPath);
  };

  public downloadFolderByPath = async (path: string) => {
    if (this.hasDirectoryPicker) {
      await this.downloadFolders(path, []);
    }
  };

  public downloadFile = async (file: DriveFile) => {
    if (this.selectedFiles.length > 1) {
      const hasFolderInSelectedFiles = this.selectedFiles.find((f) => f.ext == 'dir');
      const currentPath = this.getCurrentFolderPath();
      if (hasFolderInSelectedFiles && this.hasDirectoryPicker) {
        await this.downloadFolders(
          currentPath,
          this.selectedFiles.map((f) => f.fullName)
        );
      } else {
        this.downloadMultipleFiles();
      }

      return;
    }

    if (file.ext == 'dir') {
      if (
        this.hasDirectoryPicker //|| // desktop chrome/opera
        //this.cordovaService.isOnApp() // cordova app
      ) {
        // file picker dialog
        const currentPath = this.getCurrentFolderPath();
        await this.downloadFolders(currentPath, file.fullName);
      } else {
        this.snackbarService.showSnackbar(marker('Operation is not supported.'));
      }
      return;
    }

    var fileList = this.downloadManagerService.getFileList();

    if (
      fileList
        .filter((file) => !file.done && file.direction == FileManagerDirection.DOWN)
        .some(
          (inProgressFile) =>
            inProgressFile.file == file && inProgressFile.resourceId == this.resourceId
        )
    ) {
      this.snackbarService.showSnackbar(marker('Download already started!'));
      return;
    }

    let path = file.fullName;
    if (this.folderStack.length > 0) {
      path = this.getCurrentFolderPath() + '/' + file.fullName;
    }

    this.downloadManagerService
      .download(this.resourceId, path, file)
      .then(() => {})
      .catch((e) => {
        console.error('download error', e);
        this.dialogService.openAlertDialog(
          marker('Error'),
          marker('Error happened during the file download'),
          e.message ? e.message : e
        );
      });
  };

  private downloadFolders = async (currentPath: string, fileNames: string | string[]) => {
    let fileNameArray: string[] = [];
    if (typeof fileNames === 'string') {
      fileNameArray.push(fileNames);
    } else {
      fileNameArray = fileNames;
    }

    await this.openFolderDownloadDialog(currentPath, fileNameArray, this.resourceId).subscribe({
      next: (
        response: any & {
          excludedFiles: string[];
          files: FileRow[];
          resourceId: string;
          success: boolean;
        }
      ) => {
        if (response && response?.success) {
          // select folder to download things
          const promises = [];
          if (this.hasDirectoryPicker) {
            const castedWindow: Window & typeof globalThis & { showDirectoryPicker?: any } = window;
            castedWindow.showDirectoryPicker().then(async (dirHandler) => {
              // creates folders and temporary files
              await this.createFileHierarchy(promises, dirHandler, response);

              this.snackbarService.showSnackbar(marker('Preparing downloads')); // @todo translate 'Letöltés előkészítése folyamatban'

              let downloadTitle =
                '/' + currentPath + '/' + (fileNameArray?.[0] ? fileNameArray[0] : '');
              if (fileNameArray.length > 1) {
                downloadTitle = currentPath ? '/' + currentPath : '/';
              }
              await this.prepareDownloads(promises, downloadTitle, dirHandler);
            });
          } else {
            // isOnApp
            // app download handling
            console.log('app download');
          }
        }
      },
    });
  };

  // its like Promise.all but working
  private prepareDownloads = async (
    promises: Promise<any>[],
    currentPath: string,
    dirHandler: any
  ) => {
    console.log('prepareDownloads started', currentPath);
    let i = 0;
    const files = [];
    for (const singlePromise of promises) {
      // dont await in for
      const result = await singlePromise.then(
        (res) => {
          if (res?.file?.type === 'file') {
            files.push(res);
          }

          return res;
        },
        (err) => {
          console.error('err', err);
        }
      );
      //console.log('result', result);
      i += 1;
    }
    await this.downloadManagerService.downloadCollection(
      this.resourceId,
      currentPath,
      files,
      dirHandler
    );

    console.log('prepareDownloads done');
  };

  public downloadMultipleFiles(): void {
    console.log('Downloading multiple files start');
    let toDownloadFiles = this.selectedFiles.filter((selectedFile) => !selectedFile.isDir());
    let fileList = this.downloadManagerService.getFileList();
    //get those files which are already being downloaded
    let alreadyDownloadingFiles = fileList.filter(
      (inProgressFile) =>
        !inProgressFile.done &&
        inProgressFile.direction == FileManagerDirection.DOWN &&
        inProgressFile.resourceId == this.resourceId &&
        toDownloadFiles.some((toDownloadFile) => toDownloadFile.shallowEquals(inProgressFile.file))
    );

    //filter out those files which area already being downloaded
    toDownloadFiles = toDownloadFiles.filter((toDownloadFile) =>
      alreadyDownloadingFiles.every(
        (inProgressFile) => !toDownloadFile.shallowEquals(inProgressFile.file)
      )
    );

    if (alreadyDownloadingFiles.length > 0) {
      this.snackbarService.showSnackbar(marker('Some of the files are already being downloaded!'));
    }

    this.downloadManagerService
      .downloadMultiple(this.resourceId, this.getCurrentFolderPath(), toDownloadFiles)
      .catch((err) => {
        if (err.message == 'The user aborted a request.') {
          this.snackbarService.showSnackbar(marker('You aborted the multi-file download.'));
        } else {
          this.dialogService.openAlertDialog(
            marker('Error'),
            marker('Error happened during the file download'),
            err.message ? err.message : err
          );
        }
      });
  }

  public openSelectFileDialog(event) {
    let files = event?.target?.files;

    if (files) {
      for (var i = 0; i < files.length; i++) {
        if (files[i].size > 0) this.callUpload(files[i]);
      }
    }

    /*selectLocalFile(true).then((fileList) => {
      for (var i = 0; i < fileList.length; i++) {
        this.callUpload(fileList[i]);
      }
    });*/
  }

  public callUpload(file: File, overwrite: boolean = false, alias: string = null) {
    if (!this.canUpload) {
      this.snackbarService.showSnackbar('Permission required');
      return;
    }

    var fileList = this.downloadManagerService.getFileList();

    let path = '';
    if (this.folderStack.length > 0) {
      path = this.getCurrentFolderPath() + '/';
    }

    if (
      fileList
        .filter((file) => !file.done && file.direction == FileManagerDirection.UP)
        .some(
          (inProgressFile) =>
            inProgressFile.path == path + file.name && inProgressFile.resourceId == this.resourceId
        )
    ) {
      this.snackbarService.showSnackbar('Upload already started!');
      return;
    }

    path = alias || file.name;
    if (this.folderStack.length > 0) {
      path = this.getCurrentFolderPath() + '/' + path;
    }

    this.downloadManagerService.upload(
      this.resourceId,
      path,
      file,
      overwrite,
      alias,
      (resp) => {
        // norm_name
        console.log('upload', resp);
        this.refresh();
      },
      (err) => {
        console.log('upload error', err);
        if (err.error == 4218) {
          this.handleFileNameExistsError(file);
        }
      }
    );
  }

  private handleFileNameExistsError(file: File): void {
    console.log(file);
    const dialogRef = this.dialog.open(RenameExistingFileDialogComponent, {
      data: { name: file.name },
    });

    dialogRef.afterClosed().subscribe((name) => {
      if (name && name.length > 0) {
        const overwrite = file.name === name;
        this.callUpload(file, overwrite, name);
      }
    });
  }

  public singleClick(event: MouseEvent, f: DriveFile) {
    if (this.isContentLoading) return;

    if (event.button == 2) {
      //right click
      if (this.selectedFiles.some((selectedFile) => selectedFile == f)) {
        //right clicked file is within the currently selected files
        return;
      }
    }

    if (event.ctrlKey || this.isSelectingMode) {
      this.toggleSelection(f);
    } else if (event.shiftKey && this.lastInteracted) {
      let index = this.files.indexOf(f);
      let lastIndex = this.files.indexOf(this.lastInteracted);
      let direction = Math.sign(index - lastIndex);

      for (let i = lastIndex; i != index; i = i + direction) {
        if (this.selectedFiles.indexOf(this.files[i]) == -1) {
          this.selectedFiles.push(this.files[i]);
        }
      }

      if (this.selectedFiles.indexOf(f) == -1) {
        this.selectedFiles.push(f);
      }
    } else {
      this.selectedFiles = [];
    }

    this.lastInteracted = f;
  }

  public navigateToSubFolder(folder: DriveFile) {
    if (folder.ext == 'dir') {
      this.folderStack.push(folder.name);
      this.navigateEvent.emit({ path: this.folderStack.join('/') });
    }
  }

  public doubleClick(file: DriveFile) {
    if (this.isContentLoading || this.isSelectingMode) return;

    if (this.selectedFiles.length > 1) {
      this.downloadMultipleFiles();
      return;
    }

    this.selectedFiles = [];

    if (file.ext == 'dir') {
      this.folderStack.push(file.name);
      this.navigateEvent.emit({ path: this.folderStack.join('/') });
    } else {
      if (file.mime.startsWith('image/')) {
        this.openImageViewer(file);
      } /* else if (file.mime == "application/pdf") {
        this.openPdfViewer(file);
      }*/ else {
        this.downloadFile(file);
      }
    }
  }

  /**
   * You have a structured folder stack 0 - root, 1 - first folder level, 2 - go on...
   * If you jump to the 0, you will relocate to the root folder
   * If you jump to the 1, you will relocate to the first subfolder of the root, "root/folder"
   *
   * This is for the navigation bar
   * @param level
   */
  public jumpInFolderStack(level) {
    this.navigateEvent.emit({
      path: this.folderStack.slice(0, level).join('/'),
      from: this.folderStack?.[level],
    });
  }

  public backInFolderStack() {
    if (!this.isContentLoading) {
      this.jumpInFolderStack(this.folderStack.length - 1);
    }
  }

  public refresh() {
    this.listDirectory(() => {
      this.loadThumbnails();
    });
  }

  public reorder() {
    this.reorderBy(
      this.currOrderBy,
      this.currOrderDirection,
      ['name', 'ext'].includes(this.currOrderBy)
    );
  }

  public setReorder(attr) {
    if (this.currOrderBy == attr) {
      // if click on the same attr, just change the direction
      this.currOrderDirection *= -1;
    } else {
      this.currOrderBy = attr;
      this.currOrderDirection = 1;
    }
    this.reorder();
  }

  /**
   *
   * @param {string} attr order by an attribute
   * @param {number} direction 1 - asc, -1 - desc
   * @param {boolean} convertToLowerCase in case of string, you should do this (true/false)
   */
  public reorderBy(attr, direction = 1, convertToLowerCase = false) {
    this.files.sort((a, b) => {
      if (a.ext == 'dir' && b.ext != 'dir') return -1;
      if (a.ext != 'dir' && b.ext == 'dir') return 1;
      if (a.ext == 'dir' && b.ext == 'dir' && attr == 'size') {
        return a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase() ? -1 : 1;
      }

      return ObjectSorter.getCollator().compare(a[attr], b[attr]) * direction;
    });
  }

  public onDragEnter(event) {
    if (event?.dataTransfer?.items) {
      let items = event.dataTransfer.items;
      for (let i = 0; i < items.length; i++) {
        if (items[i].kind == 'file') {
          this.showDropArea = true;
          return;
        }
      }
    }
  }

  public onDragLeave(event) {
    this.showDropArea = false;
  }

  public onDragDrop(event) {
    event.preventDefault();
    event.stopPropagation();
    this.askBeforeUpload(getFileFromClipboard(event.dataTransfer));

    this.showDropArea = false;
  }

  public onPaste(event: ClipboardEvent) {
    this.askBeforeUpload(getFileFromClipboard(event.clipboardData));
  }

  private askBeforeUpload(files: File[]) {
    if (!this.canUpload) {
      this.snackbarService.showSnackbar('Permission required');
      return;
    }
    if (files.length > 0) {
      this.dialogService.openFileUploadConfirmDialog(files).subscribe((response) => {
        if (response) {
          files.forEach((f) => {
            this.callUpload(f);
          });
        }
      });
    } else {
      this.snackbarService.showSnackbar(
        marker('This operation is not supported by your current browser')
      );
    }
  }

  private async getCompetingNanos() {
    var clients = <Array<NanoSlotRecord>>(
      await this.serverRestApi.query({ query: getNanoSlotsQuery })
    );
    var roomData = await this.roomService.getRoom(this.resourceId);

    var driveSessionIds: string[] = [];

    for (let sessionId in roomData.nanoSessions) {
      //if (roomData.nanoSessions[sessionId] == 1)
      driveSessionIds.push(sessionId);
    }

    var competingClients = clients.filter((c) =>
      driveSessionIds.some((dsId) => dsId == c.sessionId)
    );

    var counter = 0;
    for (let cc of competingClients) {
      this.nanoService.adminGetInfo(
        cc.id,
        (res) => {
          console.log(res);
          counter++;
          cc.detail = res;

          if (counter == competingClients.length) {
            this.competingError = true;
            this.competingClientNames = competingClients.map((cc) => cc.detail.name).join(', ');
          }
        },
        (err) => {
          console.error(err);
          counter++;

          if (counter == competingClients.length) {
            this.competingError = true;
          }
        }
      );
    }
  }

  private selectFile(driveFile: DriveFile): void {
    this.selectedFiles = [driveFile];
    this.lastInteracted = driveFile;
  }

  private deselectFiles(): void {
    this.selectedFiles = [];
    this.lastInteracted = null;
  }

  private onFragmentChange = (fragment: string) => {
    // routerHandler not working: rawFragment changes but fragment object does not change
    // let fileFragment = this.routerHandler.getRoute().fragment["file"];
    // console.log("file fragment", this.routerHandler.getRoute(), fileFragment);

    if (!fragment) this.deselectFiles();

    //if files all zero, it's our first load so it's not a problem if we dont handle fragment change
    if (this.files.length == 0) return;

    let fileFragment = fragment?.split('file!')[1];
    if (fileFragment) {
      const decodedFragment = this.routerHandler.decodeFragmentValue(fileFragment);
      let driveFile = this.files.find((f) => f.fullName === decodedFragment);
      if (driveFile) {
        this.selectFile(driveFile);
      }
    }
  };

  @HostListener('document:click', ['$event'])
  documentClick(event: MouseEvent) {
    if (!this.isMobile && this.shouldStopSelect(event.target as HTMLElement))
      this.stopSelectingMode();

    if (this.context_trigger.menuOpen) this.context_trigger.closeMenu();
  }

  @HostListener('document:contextmenu', ['$event'])
  contextMenu(event: MouseEvent) {
    if (this.isMobile) return;

    var containerElement: HTMLElement = this.resourceContainerRef.nativeElement;
    var targetElement: HTMLElement = event.target as HTMLElement;

    //if we right clicked inside the drive layout
    if (containerElement.contains(targetElement)) {
      event.preventDefault();

      if (this.context_trigger.menuOpen) {
        this.context_trigger.closeMenu();
        var sub = this.context_trigger.menuClosed.subscribe(() => {
          let parent = event.target as HTMLElement;
          if (this.shouldStopSelect(parent)) {
            this.stopSelectingMode();
          }

          this.openContextMenu(event);

          sub.unsubscribe();
        });
      } else {
        this.openContextMenu(event);
      }
    }
  }

  private shouldStopSelect(element: HTMLElement): boolean {
    while (element.parentNode) {
      if (
        element.nodeName.toUpperCase() === 'APP-FILE-BLOCK-VIEW' ||
        element.nodeName.toUpperCase() === 'APP-FILE-LIST-VIEW' ||
        element.nodeName.toUpperCase() === 'BUTTON' ||
        element.nodeName.toUpperCase() === 'MAT-DIALOG-CONTAINER' ||
        element.classList.contains('cdk-overlay-container')
      ) {
        return false;
      }

      element = element.parentNode as HTMLElement;
    }

    return true;
  }

  private openContextMenu(event: MouseEvent) {
    this.contextMenuPosition.x = event.clientX + 'px';
    this.contextMenuPosition.y = event.clientY + 'px';
    this.context_trigger.openMenu();
  }

  private async getNanoSlots(room: RoomWithData) {
    if (this.ownRoom) {
      const roomId = room.id;
      const sessionId = Object.keys(room?.nanoSessions || {})?.[0];
      console.log('session id for getNanoSlots: ', sessionId);

      const nanoSlots = <Array<NanoSlotRecord>>await this.serverRestApi.query({
        query: getNanoSlotsQuery,
        variables: {
          roomId: roomId,
        },
      });
      const ownNano = nanoSlots.find((slot) => slot.sessionId === sessionId);
      if (Object.prototype.hasOwnProperty.call(ownNano, ['id'])) {
        this.nanoService.adminGetInfo(
          ownNano.id,
          (res) => {
            this.nanoDetailsName = res?.detail?.name ?? res?.name;
          },
          (err) => {
            console.error(err);
          },
          null,
          false
        );
      }
    }
  }
}
