import { Injectable } from '@angular/core';
import msgpack from 'msgpack-lite';
import { AppStorage } from 'src/app/shared/app-storage';
import { AccountSignatureConfigLoadArgs } from '../crypto/context/account_signature/args';
import { ACCOUNT_SIGNATURE_CRYPTO_CONTEXT } from '../crypto/context/account_signature/__init__';
import { NanoConfigDumpArgs, NanoConfigLoadArgs } from '../crypto/context/nano/args';
import { NANO_CRYPTO_CONTEXT } from '../crypto/context/nano/__init__';
import { NanoDataConfigLoadArgs } from '../crypto/context/nano_data/args';
import { NANO_DATA_CRYPTO_CONTEXT } from '../crypto/context/nano_data/__init__';
import { NanoOwnConfigDumpArgs, NanoOwnConfigLoadArgs } from '../crypto/context/nano_own/args';
import { NANO_OWN_CRYPTO_CONTEXT } from '../crypto/context/nano_own/__init__';
import { AbstractAccountKeyring } from '../crypto/keyring/account_base';
import { compareByteArray, concatArrayBuffer } from '../crypto/utility';
import { AccountService } from '../server-services/account.service';
import { AuthService } from '../server-services/auth.service';
import { nanoDirectRequestQuery, nanoRequestQuery } from '../server-services/querys';
import { RoomService } from '../server-services/room.service';
import { ServerRestApiService } from '../services/server-rest-api.service';

import {
  AdminAttachDriveRequest,
  AdminChangePasswordRequest,
  AdminCreateDriveRequest,
  AdminDeleteDriveRequest,
  AdminDetachDriveRequest,
  AdminEditDenyAnonymousRequest,
  AdminEditNameRequest,
  AdminEditRequireExplicitPeerTrustRequest,
  AdminGetInfoRequest,
  AdminGetListingRequest,
  AdminGetRemoteConfigRequest,
  AdminMkdirRequest,
  FileSystemWritableFileStreamCommonInterface,
  MutationToken,
  MutationTokenAdminRequest,
  MutationTokenRequest,
  NanoCreateRoomConfigBlockRequest,
  NanoFileSystemDeleteRequest,
  NanoFilesystemGetRequestForDirectory,
  NanoFilesystemGetRequestForFile,
  NanoFilesystemGetRequestForFileAsBlob,
  NanoFilesystemGetRequestForFileAsFileStream,
  NanoFilesystemGetRequestForThumbnail,
  NanoFilesystemMkdirRequest,
  NanoFilesystemMoveRequestRequest,
  NanoFilesystemPeekRequest,
  NanoFilesystemPutRequest,
  NanoRequestBase,
  NanoSearchRequest,
  NanoThumbnailResponse
} from './nano-requests';

import { AdvancedSettingsEnum } from 'src/app/components/advanced-settings/advanced-settings.component';
import { DriveFile } from 'src/app/components/resource-page/drive-window/drive-layout/drive-file';
import { GlobalCrypto } from '../global-crypto';
import { ServerError } from '../server-services/server-errors';
import { Query } from '../services/cache/cache-logic/cache-logic-interface';
import { PartKeyHandler } from './part-key-handler';
import {
  FilePayloadContainer,
  PayloadContainer,
  PayloadContainerSubscriptionResult,
  ResponsePayloadContainer
} from './payload-container';
import { ThumbnailCache } from './thumbnail-cache';

export type RequestSession = {
  // for request
  query: Query;
  variables: Object;
  request: NanoRequestBase;
  useSecondarySocket: boolean;
  // for rest
  encryption: Function;
  decryption: Function;
  // for resume
  initKey: Uint8Array;
  initKeyType: 'cacheKey' | 'transferKey';
  partCount: number;
  payloadParts: PayloadContainer;
  pause: Function;
  isPaused: boolean;
  done: boolean;
  nextChunk: number;
  doneCallback: Function;
  errorCallback: Function;
  progressCallback: Function;
  restartIfConnectionIssueResolved: boolean;
};

export type UploadSession = {
  resourceId;
  file: File;
  path: string;
  doneCallback: Function;
  errorCallback: Function;
  progressCallback: Function;
  nextChunk: number;
  smac: ArrayBuffer[];
  requestId: number;
  isPaused: boolean;
  isRunning: boolean;
  done: boolean;
  overwrite: boolean;
};

@Injectable({
  providedIn: 'root',
})
export class NanoService {
  private mutationTokens: Map<string, MutationToken[]> = new Map<string, MutationToken[]>();

  // TODO maybe we should calculate the chunk size dinamically
  public static NETWORK_FILE_CHUNK_SIZE = 500000;
  public static FILE_STREAM_MIN_SIZE = 96 * 1000000; // 96M^ - file streaming instead of blob

  private partKeyHandler = new PartKeyHandler();
  private requestSessions: Map<number, RequestSession> = new Map<number, RequestSession>();

  private lastRequestId = 0;
  private getNewRequestId() {
    return this.lastRequestId++;
  }

  constructor(
    private serverRestApiService: ServerRestApiService,
    private authService: AuthService,
    private roomService: RoomService,
    private accountService: AccountService
  ) {}

  // Mutation token (basic, admin) handlers ------------------------------
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  //

  private getFreeRequestToken(roomId): Promise<MutationToken> {
    return this.getFreeToken({ roomId });
  }

  private getFreeAdminRequestToken(nanoId): Promise<MutationToken> {
    return this.getFreeToken({ nanoId });
  }

  /**
   * Generate mutation token for the room.
   * @param resourceId
   */
  private getFreeToken(param): Promise<MutationToken> {
    let id = param.roomId ? 'room' + param.roomId : 'nano' + param.nanoId;

    if (!this.mutationTokens.has(id)) {
      this.mutationTokens.set(id, []);
    }

    let tokens = this.mutationTokens.get(id);

    return new Promise((resolve, reject) => {
      for (let i = 0; i < tokens.length; i++) {
        if (tokens[i].expired) {
          tokens.splice(i, 1);
          i--;
          continue;
        }
        if (!tokens[i].locked) {
          tokens[i].locked = true;
          return resolve(tokens[i]);
        }
      }

      let request: Function;
      let id;
      let mutationRequest: NanoRequestBase;
      if (param.roomId) {
        request = this.commonRequest.bind(this);
        id = param.roomId;
        mutationRequest = new MutationTokenRequest(id);
      } else {
        request = this.adminRequest.bind(this);
        id = param.nanoId;
        mutationRequest = new MutationTokenAdminRequest(id);
      }

      // there is no token, we need to get one from the curr nano
      request(
        id,
        mutationRequest,
        (incToken) => {
          if (incToken) {
            const tk: MutationToken = {
              token: incToken,
              locked: true,
              counter: 1,
              expired: false,
            };

            tokens.push(tk);

            resolve(tk);
          } else {
            reject('Response error, can not get mutation token from nano');
          }
        },
        () => {
          reject('Request error, can not get token from nano: ');
        }
      );
    });
  }

  private releaseToken(token: MutationToken) {
    if (token) {
      token.counter++;
      token.locked = false;
    }
  }

  private removeToken(token: MutationToken) {
    token.expired = true;
  }

  // Encryption - decryption for requestes -----------------------------
  //
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  //

  private encryptAdminRequest(data, extra) {
    data.request = msgpack.encode(data.request);

    return NANO_OWN_CRYPTO_CONTEXT.dump(
      new NanoOwnConfigDumpArgs(
        data.request,
        this.authService.getSelfAccountKeyring(),
        this.serverRestApiService.makeTimestamp()
      )
    ).then((dump) => {
      data.request = dump;
      return data;
    });
  }

  private encryptNanoRequest(data, extra) {
    if (this.authService.isAnonym())
      data.ephPubKey = this.authService
        .getSelfAccountKeyring()
        .get_public_key(AbstractAccountKeyring.CURVE25519);

    data.request = msgpack.encode(data.request);

    return this.roomService.getRoom(data.roomId).then((roomData) => {
      // if (!this.authService.isAnonym()){
      return this.accountService.getMe().then((me) => {
        if (me.id == roomData['ownerAccountId']) {
          // im the owner of this nano
          return NANO_OWN_CRYPTO_CONTEXT.dump(
            new NanoOwnConfigDumpArgs(
              data.request,
              this.authService.getSelfAccountKeyring(),
              this.serverRestApiService.makeTimestamp()
            )
          ).then((dump) => {
            data.request = dump;
            return data;
          });
        } else {
          // im not the owner
          return this.accountService.getPeerKeyring(roomData['ownerAccountId']).then((peer_kr) => {
            return this.roomService.getKeyring(data.roomId).then((room_kr) => {
              return NANO_CRYPTO_CONTEXT.dump(
                new NanoConfigDumpArgs(
                  data.request,
                  this.authService.getSelfAccountKeyring(),
                  peer_kr,
                  room_kr,
                  this.serverRestApiService.makeTimestamp()
                )
              ).then((dump) => {
                data.request = dump;
                return data;
              });
            });
          });
        }

        return data;
      });
      /* }
      else{
          // TODO anonym request: ephPubKey: anonym keyring public CURVE25519
          console.error('implement this TODO')
      } */
    });
  }

  private decryptAdminRequest(data, variables, extra) {
    if (data['error']) {
      return Promise.resolve(data);
    }

    data.request = variables.request;

    return NANO_OWN_CRYPTO_CONTEXT.load(
      new NanoOwnConfigLoadArgs(
        data.response,
        this.authService.getSelfAccountKeyring(),
        this.serverRestApiService.makeTimestamp()
      )
    ).then((load) => {
      data.response = msgpack.decode(load);
      /*if ("PK" in data.response) {
        console.log("write pk");
        this.setPartKey(extra.requestId, data.response.PK);
      }*/
      return data;
    });
  }

  private decryptNanoRequest(data, variables, extra) {
    if (!data.error) {
      if (data.part > 0) {
        return this.partKeyHandler.getPartKey(extra.requestId).then((partKey) => {
          return NANO_DATA_CRYPTO_CONTEXT.load(
            new NanoDataConfigLoadArgs(data.response, partKey)
          ).then((load) => {
            data.response = msgpack.decode(load);
            return data;
          });
        });
      } else {
        data.request = variables.request;

        return this.roomService.getRoom(variables.roomId).then((roomData) => {
          // if (!this.authService.isAnonym()){
          return this.accountService.getMe().then((me) => {
            if (me.id == roomData['ownerAccountId']) {
              // im the owner of this nano

              return NANO_OWN_CRYPTO_CONTEXT.load(
                new NanoOwnConfigLoadArgs(
                  data.response,
                  this.authService.getSelfAccountKeyring(),
                  this.serverRestApiService.makeTimestamp()
                )
              ).then((load) => {
                data.response = msgpack.decode(load);
                if ('PK' in data.response) {
                  this.partKeyHandler.setPartKey(extra.requestId, data.response.PK);
                }
                return data;
              });
            } else {
              return this.accountService
                .getPeerKeyring(roomData['ownerAccountId'])
                .then((peer_kr) => {
                  return this.roomService.getKeyring(variables.roomId).then((room_kr) => {
                    return NANO_CRYPTO_CONTEXT.load(
                      new NanoConfigLoadArgs(
                        data.response,
                        this.authService.getSelfAccountKeyring(),
                        peer_kr,
                        room_kr,
                        this.serverRestApiService.makeTimestamp()
                      )
                    ).then((load) => {
                      data.response = msgpack.decode(load);
                      if ('PK' in data.response) {
                        this.partKeyHandler.setPartKey(extra.requestId, data.response.PK);
                      }
                      return data;
                    });
                  });
                });
            }
          });
          /* }
          else{
              // TODO anonym request: ephPubKey: anonym keyring public CURVE25519
              console.error('implement this TODO')
          } */
        });
      }
    } else return Promise.resolve(data);
  }

  // Core for requestes -----------------------------------------
  //
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  //

  private commonRequest(
    roomId,
    request: NanoRequestBase,
    doneCallback: Function,
    errorCallback?: Function,
    progressCallback?: Function,
    useSecondarySocket = false,
    createSession = false
  ): number {
    return this.nanoRequest(
      nanoRequestQuery,
      { roomId },
      request,
      doneCallback,
      errorCallback,
      progressCallback,
      useSecondarySocket,
      this.encryptNanoRequest,
      this.decryptNanoRequest,
      createSession
    );
  }

  private commonMutationRequest(
    resourceId,
    request: NanoRequestBase,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.getFreeRequestToken(resourceId)
      .then((token) => {
        request.setMutationToken(token);
        this.commonRequest(
          resourceId,
          request,
          () => {
            this.releaseToken(token);
            doneCallback();
          },
          (err) => {
            if (err.error == ServerError.NANO_REQUEST_ERROR_MUTATION_ID) {
              this.removeToken(token);
              console.log('token expired, try again with a new one');
              this.commonMutationRequest(
                resourceId,
                request,
                doneCallback,
                errorCallback,
                progressCallback
              );
            } else {
              errorCallback && errorCallback(err);
            }
          },
          progressCallback
        );
      })
      .catch(errorCallback);
  }

  private adminRequest(
    nanoId,
    request: NanoRequestBase,
    doneCallback: Function,
    errorCallback?: Function,
    progressCallback?: Function,
    useSecondarySocket = false
  ) {
    this.nanoRequest(
      nanoDirectRequestQuery,
      { nanoId },
      request,
      doneCallback,
      errorCallback,
      progressCallback,
      useSecondarySocket,
      this.encryptAdminRequest,
      this.decryptAdminRequest
    );
  }

  private adminMutationRequest(
    nanoId,
    request: NanoRequestBase,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.getFreeAdminRequestToken(nanoId)
      .then((token) => {
        request.setMutationToken(token);
        this.adminRequest(
          nanoId,
          request,
          () => {
            this.releaseToken(token);
            doneCallback();
          },
          (err) => {
            if (err.error == ServerError.NANO_REQUEST_ERROR_MUTATION_ID) {
              this.removeToken(token);
              console.log('token expired, try again with a new one');
              this.adminMutationRequest(
                nanoId,
                request,
                doneCallback,
                errorCallback,
                progressCallback
              );
            } else {
              errorCallback && errorCallback(err);
            }
          },
          progressCallback
        );
      })
      .catch(errorCallback);
  }

  public resumeRequest(requestId) {
    let session = this.requestSessions.get(requestId);
    session.restartIfConnectionIssueResolved = true;
    if (session) {
      if (session.isPaused) {
        session.nextChunk = session?.payloadParts?.getLastSuccessPart()
          ? session.payloadParts.getLastSuccessPart() + 1
          : 1;
        session.isPaused = false;

        this.nanoRequest(
          session.query,
          session.variables,
          session.request,
          session.doneCallback,
          session.errorCallback,
          session.progressCallback,
          session.useSecondarySocket,
          session.encryption,
          session.decryption,
          requestId
        );
      }
    } else {
      session.errorCallback && session.errorCallback('Can not find session');
    }
  }

  public pauseRequest(requestId): boolean {
    let session = this.requestSessions.get(requestId);
    session.restartIfConnectionIssueResolved = false;
    if (session && session.pause && !session.isPaused) {
      session.pause();
      return true;
    } else {
      throw 'Can not find session';
    }
  }

  public deleteRequestSession(requestId) {
    let session = this.requestSessions.get(requestId);

    if (session) {
      session.pause();
      session.payloadParts.reset();
      session.request.close();
      this.requestSessions.delete(requestId);
    }
  }

  /**
   * Start a request and merge it's payload
   * @param requestId
   * @param request nanoRequestBase
   * @param doneCallback cb(merged Message by passed requestHandler)
   * @param errorCallback cb('error msg as string')
   * @param progressCallback cb(current, max, speed)
   * @param useSecondarySocket secondary socket is for large file upload/download
   * @param useSession true/false: register a session for the event; requestId: continue the request
   * @return requestId
   */
  private nanoRequest(
    query: Query,
    variables: Object,
    request: NanoRequestBase,
    doneCallback: Function,
    errorCallback: Function,
    progressCallback: Function,
    useSecondarySocket: boolean = false,
    encryption: Function,
    decryption: Function,
    useSession: boolean | number = false
  ): number {
    let sub; // subscription of the request
    let error = false; // stop, if error happened
    // bubble error up
    let throwError = (msg, err?) => {
      console.error('request error', msg, err);
      errorCallback && errorCallback(msg, err);

      error = true; // do not start to parse the async other messages
      sub && sub.unsubscribe(); // unsub the subscription, if it is still alive (client error)
    };
    let requestId;

    // we need the request as binary+encrypted form, the nano will open it
    variables['request'] = request.getRequest();

    // the whole request logic
    let session: RequestSession;

    // init or resume a download session request
    if (useSession !== false && useSession !== true) {
      // this is a continue request
      requestId = useSession;
      session = this.requestSessions.get(useSession);

      if (!session) {
        throwError('Request session not found');
      } else {
        // register the next pause event for this subscription scope
        session.pause = () => {
          sub && sub.unsubscribe();
          session.isPaused = true;
        };
        // set the start for the request
        if (session.initKeyType) {
          variables['part'] = session.nextChunk || 1;
          variables[session.initKeyType] = session.initKey; // transferKey or cacheKey = Uint8Array
        } // else init a new stream-line
      }
    } else {
      // this is a new request
      requestId = this.getNewRequestId();

      // we can init the keys with the first payload only
      session = {
        query,
        variables,
        request,
        useSecondarySocket,
        encryption,
        decryption,
        initKey: null,
        initKeyType: null,
        partCount: 1,
        doneCallback,
        errorCallback,
        progressCallback,
        payloadParts: null,
        nextChunk: 0,
        restartIfConnectionIssueResolved: true,
        pause: () => {
          sub && sub.unsubscribe();
          session.isPaused = true;
        },
        isPaused: false,
        done: false,
      };

      if (useSession === true) {
        this.requestSessions.set(requestId, session);
      }

      if (this.authService.isAnonym()) {
        // on anonym call we need the curve key
        variables['ephPubKey'] = this.authService
          .getSelfAccountKeyring()
          .get_public_key(AbstractAccountKeyring.CURVE25519);
      }
    }

    let onPayloadPush = (result: PayloadContainerSubscriptionResult) => {
      // bubble up the checked n ready chunks for blob concat or stream write
      let promises = [];
      result.chunks.forEach((c) => {
        promises.push(request.push(c));
      });

      Promise.all(promises)
        .then(() => {
          result.done();
        })
        .catch((err) => {
          throwError('could not push data into the file sink', err);
        });
    };

    let onPayloadCompleted = () => {
      // this is the last message
      session.done = true;
      request
        .mergePayloads()
        .then((result) => {
          return request.close().then(() => {
            doneCallback(result);
          });
        })
        .catch(errorCallback);
      progressCallback && progressCallback(session.partCount, session.partCount, session);
      sub && sub.unsubscribe(); // end the subscription
      //console.log('cleared request', session)
      //session.variables['request'] = null; // Release it from memory. It can hold upload payload
      //session.request = null;
      //console.log('cleared request', session)
    };

    let pushMessage = (msg, dataMAC?) => {
      session.payloadParts.push(msg.part, msg.response, dataMAC);
    };

    if (!session.isPaused) {
      // let's rock - start the request
      sub = this.serverRestApiService
        .subscribe({
          query,
          variables,
          useSecondarySocket,
          extra: {
            requestId, // we need this to store the partKey under this session (nanoRequest only)
          },
          encrypt: encryption.bind(this),
          decrypt: decryption.bind(this),
        })
        .subscribe({
          next: async (msg: any) => {
            //console.log('nano res', msg);

            if (msg.error) {
              errorCallback(msg);
              sub.unsubscribe();
              return;
            }

            if (msg.part != 0) {
              progressCallback && progressCallback(msg.part, session.partCount, session);
            }

            if (msg.error) error = true;

            if (!error) {
              // here comes a part of the msg, it can be init or x-th part
              const key = this.getKeyFromInitMessage(msg);

              if (key !== null) {
                // init, restart, or restarted continue (the first payload always the info about the file)
                if (!session.initKey) {
                  // this is an init or a restart message - direct requests has only 1 msg
                  progressCallback && progressCallback(0, msg.count, session);
                  session.initKeyType = key;
                  // the server re-sent the init payload, it can be a restart or a continue payload
                  session.initKey = msg[key];
                  session.partCount = msg.count;
                  session.nextChunk = 0;
                  session.payloadParts =
                    request instanceof NanoFilesystemGetRequestForFile
                      ? new FilePayloadContainer(msg.count)
                      : new ResponsePayloadContainer(msg.count);
                  session.payloadParts.onPayloadPush().subscribe({
                    next: onPayloadPush,
                    complete: onPayloadCompleted,
                    error: throwError,
                  });
                  await request.reset();
                }

                this.verifyInitMessage(session.initKey, msg)
                  .then(() => {
                    if (msg.response.C == session.partCount) {
                      pushMessage(msg);
                    } else {
                      throwError('part count mismatch in init message');
                    }
                  })
                  .catch((err) => {
                    throwError('init msg verify error', err);
                  });
              } else {
                // this is an x-th part message

                if (msg.response.P == msg.part) {
                  // check the correct part
                  if ('MAC' in msg.response) {
                    // at random positions we can get MAC during file download. Concat all the payloads after the prev MAC and test it
                    this.checkMAC(msg.response.MAC, msg.response.MACS, variables['roomId'])
                      .then(() => {
                        pushMessage(msg, msgpack.decode(msg.response.MAC));
                      })
                      .catch((err) => {
                        throwError('MAC/MACS check failed', err);
                      });
                  } else {
                    pushMessage(msg);
                  }
                } else {
                  throwError('part count mismatch in part message');
                }
              }
            } else {
              throwError(msg);
            }
          },
          error: (err) => {
            console.log('<- in error', err, Object.assign({}, session));
            // console.trace();
            if (useSession) {
              if (!session || !session.done) {
                // check connection status
                if (
                  (useSecondarySocket && this.serverRestApiService.isSecondaryWebsocketOpen()) ||
                  (!useSecondarySocket && this.serverRestApiService.isPrimaryWebsocketOpen())
                ) {
                  // status is open, but problem happened, we could not fix this, so throw error
                  if (errorCallback) errorCallback(err);
                } else {
                  // it is a connection problem, the websocket will solve it later, so just wait a little bit
                  if (useSecondarySocket) {
                    // we need to resume it manually otherwise the wsClient will resend the original request
                    // which is the first or the latest paused part
                    if (session) {
                      if (!session.isPaused) {
                        session.pause();
                        session.restartIfConnectionIssueResolved = true;
                        setTimeout(() => {
                          if (session.restartIfConnectionIssueResolved) {
                            this.resumeRequest(requestId);
                          }
                        }, 2000);
                      }
                    } else {
                      if (!session.isPaused) {
                        setTimeout(() => {
                          if (!session.isPaused) {
                            this.nanoRequest(
                              query,
                              variables,
                              request,
                              doneCallback,
                              errorCallback,
                              progressCallback,
                              useSecondarySocket,
                              encryption,
                              decryption,
                              requestId
                            );
                          }
                        }, 2000);
                      }
                    }
                  }
                }
              } else {
                console.error('<- error registered but download is already done', err);
              }
            } else {
              //session.variables['request'] = null; // it can hold the upload payload in the memory
              //session.request = null;
              console.log('why am i here', useSession);
              if (errorCallback) errorCallback(err);
            }
          },
        });
    }

    return requestId;
  }

  // direct api calls ------------------------------------------------------------------------
  //
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  //

  public listDir(
    resourceId: string,
    path: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.commonRequest(
      resourceId,
      new NanoFilesystemGetRequestForDirectory(resourceId, path),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public peekFile(resourceId: string, path: string, doneCallback, errorCallback?) {
    this.commonRequest(
      resourceId,
      new NanoFilesystemPeekRequest(resourceId, path),
      doneCallback,
      errorCallback,
      () => {}
    );
  }

  /**
   * Download a file, return with the requestId so you can pause/resume
   * @param resourceId
   * @param path
   * @param doneCallback
   * @param errorCallback
   * @param progressCallback
   * @returns requestId
   */
  public getFile(
    resourceId: string,
    path: string,
    fileSystemWritableFileStream: FileSystemWritableFileStreamCommonInterface | null,
    doneCallback,
    errorCallback?,
    progressCallback?
  ): number {
    return this.commonRequest(
      resourceId,
      fileSystemWritableFileStream
        ? new NanoFilesystemGetRequestForFileAsFileStream(
            resourceId,
            path,
            fileSystemWritableFileStream
          )
        : new NanoFilesystemGetRequestForFileAsBlob(resourceId, path),
      doneCallback,
      errorCallback,
      progressCallback,
      true,
      true
    );
  }

  private thumbnailCache = new ThumbnailCache();

  public getFileThumbnail(
    roomId: string,
    path: string,
    files: DriveFile[],
    doneCallback,
    errorCallback,
    progressCallback?
  ) {
    let cachedThumbnails = this.thumbnailCache.getCachedThumbnails(roomId, path, files);
    if (cachedThumbnails !== null) {
      doneCallback(cachedThumbnails);
    } else {
      return this.commonRequest(
        roomId,
        new NanoFilesystemGetRequestForThumbnail(roomId, path),
        (thumbnails: NanoThumbnailResponse[]) => {
          this.thumbnailCache.setCachedThumbnails(roomId, path, files, thumbnails);

          doneCallback(thumbnails);
        },
        errorCallback,
        progressCallback
      );
    }
  }

  public search(
    resourceId: string,
    query: string,
    lang: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.commonRequest(
      resourceId,
      new NanoSearchRequest(resourceId, query, lang),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public delete(resourceId: string, path: string, doneCallback, errorCallback?, progressCallback?) {
    this.commonMutationRequest(
      resourceId,
      new NanoFileSystemDeleteRequest(resourceId, path),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public makeDir(
    resourceId: string,
    path: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.commonMutationRequest(
      resourceId,
      new NanoFilesystemMkdirRequest(resourceId, path),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public moveFile(
    resourceId: string,
    srcPath: string,
    newPath: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.commonMutationRequest(
      resourceId,
      new NanoFilesystemMoveRequestRequest(resourceId, srcPath, newPath),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public nanoCreateRoomConfigBlock(
    resourceId,
    memberFlags: Object,
    groupFlags: number,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.commonMutationRequest(
      resourceId,
      new NanoCreateRoomConfigBlockRequest(resourceId, memberFlags, groupFlags),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  /**
   * Keeps information (like name, version) for nanoslots on rooms
   */
  public nanoSlotDetails: { [key: string]: { detail: any } } = {};
  public adminGetInfo(
    nanoId: number,
    doneCallback,
    errorCallback?,
    progressCallback?,
    alwaysFetch = true
  ) {
    if (!alwaysFetch && nanoId in this.nanoSlotDetails) {
      doneCallback(this.nanoSlotDetails[nanoId]);
    } else {
      const mergedDoneCallback = (res) => {
        this.nanoSlotDetails[nanoId] = { detail: res };
        doneCallback(res);
      };
      this.adminRequest(
        nanoId,
        new AdminGetInfoRequest(nanoId),
        mergedDoneCallback,
        errorCallback,
        progressCallback
      );
    }
  }

  public adminGetRemoteConfig(
    nanoId: number,
    adminPassword: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.adminRequest(
      nanoId,
      new AdminGetRemoteConfigRequest(nanoId, adminPassword),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public adminChangePasswordRequest(
    nanoId: number,
    oldPassword: string,
    newPassword: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.adminMutationRequest(
      nanoId,
      new AdminChangePasswordRequest(nanoId, oldPassword, newPassword),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public adminGetListing(
    nanoId,
    path: string,
    adminPassword: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.adminRequest(
      nanoId,
      new AdminGetListingRequest(nanoId, path, adminPassword),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public adminMakeDir(
    nanoId,
    path: string,
    name: string,
    adminPassword: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.adminMutationRequest(
      nanoId,
      new AdminMkdirRequest(nanoId, path, name, adminPassword),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public adminCreateDrive(
    nanoId,
    path: string,
    adminPassword: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.adminMutationRequest(
      nanoId,
      new AdminCreateDriveRequest(nanoId, path, adminPassword),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public adminDeleteDrive(
    nanoId,
    path: string,
    adminPassword: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.adminMutationRequest(
      nanoId,
      new AdminDeleteDriveRequest(nanoId, path, adminPassword),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public adminAttachDrive(
    nanoId,
    path: string,
    roomId: string,
    roomBlocks: string[],
    adminPassword: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.adminMutationRequest(
      nanoId,
      new AdminAttachDriveRequest(nanoId, path, roomId, roomBlocks, adminPassword),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public adminDetachDrive(
    nanoId,
    path: string,
    roomId: string,
    adminPassword: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.adminMutationRequest(
      nanoId,
      new AdminDetachDriveRequest(nanoId, path, roomId, adminPassword),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public adminEditNameRequest(
    nanoId,
    name: string,
    adminPassword: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.adminMutationRequest(
      nanoId,
      new AdminEditNameRequest(nanoId, name, adminPassword),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public adminEditDenyAnonymous(
    nanoId,
    value: boolean,
    adminPassword: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.adminMutationRequest(
      nanoId,
      new AdminEditDenyAnonymousRequest(nanoId, value, adminPassword),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  public adminEditRequireExplicitPeerTrust(
    nanoId,
    value: boolean,
    adminPassword: string,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.adminMutationRequest(
      nanoId,
      new AdminEditRequireExplicitPeerTrustRequest(nanoId, value, adminPassword),
      doneCallback,
      errorCallback,
      progressCallback
    );
  }

  // Upload ------------------------------------------------------
  //
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  //

  private uploadSession: { [key: number]: UploadSession } = {};

  pauseUploadSession(sessionId) {
    let session = this.uploadSession[sessionId];
    if (session && !session.done) {
      session.isPaused = true;
    }
  }

  resumeUploadSession(sessionId) {
    let session = this.uploadSession[sessionId];

    if (session && !session.done) {
      session.isPaused = false;
      if (!session.isRunning) {
        this.startChunkUpload(session);
      }
    }
  }

  removeUploadSession(sessionId) {
    let session = this.uploadSession[sessionId];

    if (session) {
      session.isPaused = true;
      delete this.uploadSession[sessionId];
    }
  }

  public async startChunkUpload(session: UploadSession) {
    if (!session.isPaused) {
      const offset = session.nextChunk;

      const size = session.file.size;
      const mtime = session.file.lastModified;
      const allChunk = Math.ceil(size / NanoService.NETWORK_FILE_CHUNK_SIZE);
      let chunk;
      try {
        chunk = await this.getFileChunkByOffset(session.file, offset);
      } catch (err) {
        session.errorCallback && session.errorCallback(err);
        session.isRunning = false;
        this.pauseUploadSession(session.requestId);
        console.error('Error happened while slicing the file', err, session.file, offset);
      }

      session.smac[offset] = await this.generateSMAC(chunk);

      let finalSMAC = null;
      if (offset == allChunk - 1) {
        finalSMAC = await this.generateSMAC(concatArrayBuffer(session.smac));
      }

      this.getFreeRequestToken(session.resourceId).then((mutationToken) => {
        session.isRunning = true;

        this.commonRequest(
          session.resourceId,
          new NanoFilesystemPutRequest(
            session.resourceId,
            session.path,
            size,
            mtime,
            session.nextChunk * NanoService.NETWORK_FILE_CHUNK_SIZE,
            chunk,
            finalSMAC,
            session.overwrite,
            mutationToken
          ),
          (res) => {
            this.releaseToken(mutationToken);
            session.isRunning = false;

            session.progressCallback(offset, allChunk);
            if (offset == allChunk - 1) {
              session.done = true;
              session.doneCallback(res); // norm_name
            } else {
              session.nextChunk++;
              if (!session.isPaused) {
                this.startChunkUpload(session);
              }
            }
          },
          (err) => {
            if (!session.done) {
              session.errorCallback && session.errorCallback(err);

              session.isRunning = false;
              this.pauseUploadSession(session.requestId);
              if (err.error != 4216 && err.error != 4218) {
                // permission problem
                setTimeout(() => {
                  this.resumeUploadSession(session.requestId);
                }, 1000);
              } else {
                console.error('permission problem during upload', err);
              }
            } else {
              console.error('<- error registered but upload is already done', err);
            }
          },
          () => {},
          true
        );
      });
    }
  }

  /**
   *
   * @param resourceId
   * @param file
   * @param path
   * @param doneCallback
   * @param errorCallback
   * @param progressCallback
   * @returns requestId for pause/resume
   */
  public uploadFile(
    resourceId: string,
    file: File,
    path: string,
    overwrite: boolean,
    doneCallback,
    errorCallback,
    progressCallback
  ): number {
    let requestId = this.getNewRequestId();
    let session: UploadSession = {
      requestId,
      resourceId,
      file,
      path,
      doneCallback,
      errorCallback,
      progressCallback,
      nextChunk: 0,
      smac: [],
      isPaused: false,
      isRunning: false,
      done: false,
      overwrite,
    };
    this.uploadSession[requestId] = session;
    this.startChunkUpload(session);
    return requestId;
  }

  // Helper functions ------------------------------------------------------
  //
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  ////
  //
  //
  //
  //
  //

  /**
   *
   * @param initMessage initMessage inside the graphqlResponse['data']['nanoRequest']
   * @returns "cacheKey" or "transferKey" string if its an init message, null otherwise
   */
  private getKeyFromInitMessage(initMessage): 'cacheKey' | 'transferKey' {
    if ('cacheKey' in initMessage && initMessage.cacheKey !== null) {
      return 'cacheKey';
    } else if ('transferKey' in initMessage && initMessage.transferKey !== null) {
      return 'transferKey';
    } else return null;
  }

  private verifyInitMessage(initKey, msg): Promise<void> {
    return this.generateRAH(initKey, msg.request)
      .then((RAH) => {
        if (compareByteArray(RAH, msg.response.RAH)) {
          return true;
        } else {
          return Promise.reject('RAH mismatch');
        }
      })
      .catch((err) => {
        console.error('RAH error', err);
        return Promise.reject('Can not generate RAH from msg');
      });
  }

  /**
   * Recreate the RAH from the message: (CacheKey or TransferKey + request first 128 byte) -> sha256() -> first 16 byte
   * @param key CacheKey or TransferKey
   * @param request nanoRequestBase.getRequest, binary data for graphql nanoRequest subscription request param
   * @returns Promise<RAH>
   */
  private generateRAH(key: Uint8Array, request: Uint8Array): any {
    const arr = new Uint8Array(key.length + Math.min(128, request.length));
    arr.set(key, 0);
    arr.set(request.subarray(0, 128), key.length);

    return GlobalCrypto.SHA256(arr).then((result) => {
      return new Uint8Array(result.slice(0, 16));
    });
  }

  // future encode check
  // nano owner account peerkeyring signature crypto context
  // cipher: MACS, peer_kr: owner, plain: MAC
  private checkMAC(mac, macs, resourceId) {
    return this.roomService.getRoom(resourceId).then((roomData) => {
      return this.accountService.getPeerKeyring(roomData['ownerAccountId']).then((peer_kr) => {
        return ACCOUNT_SIGNATURE_CRYPTO_CONTEXT.load(
          new AccountSignatureConfigLoadArgs(macs, peer_kr, mac)
        );
      });
    });
  }

  /**
   * For upload file: all chunk need to be encoded, then concat them and encode again
   * @param data
   * @returns
   */
  private generateSMAC(data: ArrayBuffer) {
    return GlobalCrypto.SHA256(data).then((result) => {
      return result.slice(0, 16);
    });
  }

  /**
   * We want to check if a file has changed during the upload.
   * There is no supported option in browser yet. But there is a trick.
   * If you start reading a changed file via FileReader, it will throw an error.
   * However you should abort the file reading immediatly after it started, because
   * it can load the entire file into the memory, and we dont want to do that right?
   *
   * @param {File} file - the file which need to be checked
   * @param errorCallback we can register only on the errorCallback, the behaviour is depends
   * on the browser. Firefox throw an AbortError, but chrome doesnt. But on NotReadableError
   * you can register using a callback
   */
  private isFileOk(file: File, errorCallback) {
    const reader = new FileReader();
    reader.onprogress = function (res) {
      console.log(res);
      if (res.loaded > 0) reader.abort();
    };
    reader.onerror = function (err) {
      if (err.target.error?.name != 'AbortError') {
        // it is tipically "NotReadableError"
        errorCallback(err.target.error.message);
      }
    };

    reader.readAsArrayBuffer(file);
  }

  private getFileChunkByOffset(file: File, offset: number) {
    const start = offset * NanoService.NETWORK_FILE_CHUNK_SIZE;
    let end = start + NanoService.NETWORK_FILE_CHUNK_SIZE;
    if (end > file.size) {
      end = file.size;
    }

    return file.slice(start, end).arrayBuffer();
  }
}

let fileStreamSizeLimit = AppStorage.getItem(AdvancedSettingsEnum.FILE_STREAM_SIZE_LIMIT);
if (fileStreamSizeLimit !== undefined) {
  let limit = parseInt(fileStreamSizeLimit);
  if (limit >= 0) {
    NanoService.FILE_STREAM_MIN_SIZE = limit;
  }
}
