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

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

import {
  FilePayloadContainer,
  PayloadContainer,
  PayloadContainerSubscriptionResult,
  ResponsePayloadContainer,
} from "./payload-container";
import { GlobalCrypto } from "../global-crypto";
import { PartKeyHandler } from "./part-key-handler";
import { ServerError } from "../server-services/server-errors";

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;
  done: boolean;
  nextChunk: number;
  doneCallback: Function;
  errorCallback: Function;
  progressCallback: Function;
};

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;
};

@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.getData(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.getData(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);
    if (session) {
      console.log(
        "download next part",
        session.payloadParts.getLastSuccessPart()
      );
      session.nextChunk = session.payloadParts.getLastSuccessPart() + 1;

      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);
    if (session && session.pause) {
      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();
      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();
        };
        // set the start for the request
        variables["part"] = session.nextChunk;
        variables[session.initKeyType] = session.initKey; // transferKey or cacheKey = Uint8Array
      }
    } else {
      // this is a new request
      requestId = this.getNewRequestId();
      // we can init the session with the first payload only

      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(doneCallback).catch(errorCallback);
      progressCallback &&
        progressCallback(session.partCount, session.partCount);
      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);
    };

    // 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) => {

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

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

          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) {
              // this is an init or a restart message - direct requests has only 1 msg
              

              if (!session) {
                // this is an init message
                progressCallback && progressCallback(0, msg.count);

                session = {
                  query,
                  variables,
                  request,
                  useSecondarySocket,
                  encryption,
                  decryption,
                  initKey: msg[key],
                  initKeyType: key,
                  partCount: msg.count,
                  doneCallback,
                  errorCallback,
                  progressCallback,
                  payloadParts:
                    request instanceof NanoFilesystemGetRequestForFile
                      ? new FilePayloadContainer(msg.count)
                      : new ResponsePayloadContainer(msg.count),
                  nextChunk: 0,
                  pause: () => {
                    sub && sub.unsubscribe();
                  },
                  done: false,
                };

                

                if (useSession === true) {
                  this.requestSessions.set(requestId, session);
                }
              } else if (key !== null) {
                // 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.payloadParts.onPayloadPush().subscribe({
                next: onPayloadPush,
                complete: onPayloadCompleted,
                error: throwError
              })

              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 (msg.part == 1) {
                  // if we got this, we need to init the payload - this msg can be a start or a reset msg
                  if (request instanceof NanoFilesystemGetRequestForFile) {
                    // this can store and concat filechunks, plus verify them by MAC
                    session.payloadParts = new FilePayloadContainer(session.partCount);
                  } else {
                    // basic response, mostly an one size msg
                    session.payloadParts = new ResponsePayloadContainer(session.partCount);
                  }
                  session.payloadParts.onPayloadPush().subscribe({
                    next: onPayloadPush,
                    complete: onPayloadCompleted,
                    error: throwError
                  })
                  await request.reset();
                }

                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) => {
          if (useSession) {
            if (!session || !session.done) {
              console.log("<- in error", err);
              if (errorCallback) errorCallback(err);

              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
                session.pause();
                setTimeout(() => {
                  this.resumeRequest(requestId);
                }, 1000);
              }
            } 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;
            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
    );
  }

  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
    );
  }

  public adminGetInfo(
    nanoId: number,
    doneCallback,
    errorCallback?,
    progressCallback?
  ) {
    this.adminRequest(
      nanoId,
      new AdminGetInfoRequest(nanoId),
      doneCallback,
      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
    );
  }

  // 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,
            true,
            mutationToken
          ),
          (res) => {
            this.releaseToken(mutationToken);
            session.isRunning = false;

            session.progressCallback(offset, allChunk);
            if (offset == allChunk - 1) {
              session.done = true;
              session.doneCallback();
            } 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){ // 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,
    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,
    };
    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.getData(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 = localStorage.getItem("file-stream-size-limit");
if (fileStreamSizeLimit !== undefined) {
  let limit = parseInt(fileStreamSizeLimit);
  if (limit >= 0) {
    NanoService.FILE_STREAM_MIN_SIZE = limit;
  }
}
