import msgpack from 'msgpack-lite';
import { blobService } from '../services/blob.service';

/**
 * For mutations requests we need a token.
 * 1 user can use max 32 tokens per nano
 * if you start a request with a token, which is under process with an earlier request. It can
 * ruin the earlier call. So lock that token in this case.
 */
export type MutationToken = {
  token: Uint8Array;
  locked: boolean;
  counter: number;
  expired: boolean;
};

export abstract class NanoRequestBase {
  protected request;
  protected payloads = [];

  /**
   * Create the valid request message. Defined by the server
   * @returns
   */
  public getRequest() {
    return this.request;
  }

  /**
   * incoming chunk
   * @param payload
   */
  public push(payload): Promise<void> {
    this.payloads.push(payload);
    return Promise.resolve();
  }

  /**
   * The message coming as parts from the server. The parts's structure are depend on the request type (getDir, getFile, etc)
   * Override this merge function to fit the actual request
   * @param payloads an array, what contains all of the msg part
   * @returns the message from the server
   */
  public abstract mergePayloads();

  public reset(): Promise<void> {
    this.payloads = [];
    return Promise.resolve();
  }

  /**
   * clean up after request done or paused request delete
   * Note: On mobile device if we put the app in the background, we can keep it alive if
   * it is still some download/upload running. After request delete we need to call a close
   * to the local webserver so it can clean up its memory. Otherwise some process will stuck
   * in it's onprogress list after pause+delete.
   */
  public close(): Promise<void> {
    return Promise.resolve();
  }

  public setMutationToken(mutationToken: MutationToken) {
    this.request['mut_key'] = mutationToken.token;
    this.request['mut_seq'] = mutationToken.counter;
  }
}

export class MutationTokenRequest extends NanoRequestBase {
  public constructor(roomId) {
    super();

    this.request = {
      type: 'MUT-CRT',
      room_id: roomId,
    };
  }

  public mergePayloads() {
    return Promise.resolve(this.payloads[0].token);
  }
}

export class MutationTokenAdminRequest extends NanoRequestBase {
  public constructor(nanoId) {
    super();

    this.request = {
      type: 'MUT-CRT',
      nano_id: nanoId,
    };
  }

  public mergePayloads() {
    return Promise.resolve(this.payloads[0].token);
  }
}

export class NanoFileSystemDeleteRequest extends NanoRequestBase {
  public constructor(resourceId: string, path: string, mutationToken?: MutationToken) {
    super();

    this.request = {
      type: 'FS-DEL',
      room_id: resourceId,
      path: path,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    return Promise.resolve(true);
  }
}

export class NanoFilesystemMkdirRequest extends NanoRequestBase {
  public constructor(resourceId: string, path: string, mutationToken?: MutationToken) {
    super();

    this.request = {
      type: 'FS-MKD',
      room_id: resourceId,
      path: path,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    let result = this.payloads[0]['norm_name'] || true;
    // Nano 0.7.8+ returns "norm_name" key in response for mkdir, put and move. Put response will only have it for offset==0 requests.
    return Promise.resolve(result);
  }
}

export class NanoFilesystemMoveRequestRequest extends NanoRequestBase {
  public constructor(
    resourceId: string,
    srcPath: string,
    newPath: string,
    mutationToken?: MutationToken
  ) {
    super();

    this.request = {
      type: 'FS-MOV',
      room_id: resourceId,
      src_path: srcPath,
      new_path: newPath,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    let result = this.payloads[0]['norm_name'] || true;
    // Nano 0.7.8+ returns "norm_name" key in response for mkdir, put and move. Put response will only have it for offset==0 requests.
    return Promise.resolve(result);
  }
}

export class NanoFilesystemPutRequest extends NanoRequestBase {
  public constructor(
    resourceId: string,
    path: string,
    size,
    mtime,
    offset,
    data,
    smac,
    overwrite,
    mutationToken?: MutationToken
  ) {
    super();

    this.request = {
      type: 'FS-PUT',
      room_id: resourceId,
      path: path,
      size: size,
      offset: offset,
      data: data,
      mtime: Math.round(mtime),
      smac: smac,
      overwrite: overwrite,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    let result = this.payloads[0]['norm_name'] || true;
    // Nano 0.7.8+ returns "norm_name" key in response for mkdir, put and move. Put response will only have it for offset==0 requests.
    return Promise.resolve(result);
  }
}

export abstract class NanoFilesystemGetRequestForFile extends NanoRequestBase {
  public constructor(resourceId: string, path: string) {
    super();
    this.request = {
      type: 'FS-GET',
      room_id: resourceId,
      path: path,
    };
  }
}

export type NanoThumbnailResponse = {
  name: string;
  mtime: number;
  data: Uint8Array;
};

export class NanoFilesystemGetRequestForThumbnail extends NanoRequestBase {
  public mergePayloads() {
    let thmbList = [];
    this.payloads.forEach((chunk) => {
      if (chunk.ts) {
        let images = msgpack.decode(chunk.ts);
        images.forEach(([name, data, mtime]) => {
          let res: NanoThumbnailResponse = { name, data, mtime };
          thmbList.push(res);
        });
      }
    });
    return Promise.resolve(thmbList);
  }

  public constructor(resourceId: string, path: string) {
    super();
    this.request = {
      type: 'FS-THMB',
      room_id: resourceId,
      path: path,
    };
  }
}

export class NanoFilesystemGetRequestForFileAsBlob extends NanoFilesystemGetRequestForFile {
  /**
   * fs - filesize is in the init payload
   * fc - file chunk in every part
   * @param payloads
   */
  public mergePayloads() {
    return new Promise((resolve, reject) => {
      try {
        resolve(blobService.new(this.payloads)); //  { type: "application/octet-stream" }
      } catch (e) {
        reject(e);
      }
      /*console.log("file payload", this.payloads);

      try{
        let arr = [];
        for (let i = 0; i < this.payloads.length; i++) {
          arr.push(this.payloads[i].buffer);
        }
  
        resolve(concatArrayBuffer(arr));
      }catch(e){
        reject(e);
      }*/
    });
  }

  public close(): Promise<void> {
    this.payloads = [];
    return Promise.resolve();
  }
}

export interface FileSystemWritableFileStreamCommonInterface {
  write(payload: any): Promise<void>;
  close();
  truncate(size: number): Promise<void>;
}

export interface FileSystemWritableFileStreamMobile
  extends FileSystemWritableFileStreamCommonInterface {
  getId(): string;
  getPath(): string;
}

export class NanoFilesystemGetRequestForFileAsFileStream extends NanoFilesystemGetRequestForFile {
  private fileSystemWritableFileStream: any;

  public constructor(
    resourceId: string,
    path: string,
    fileSystemWritableFileStream: FileSystemWritableFileStreamCommonInterface = null
  ) {
    super(resourceId, path);
    this.fileSystemWritableFileStream = fileSystemWritableFileStream;
  }

  public push(payload) {
    return this.fileSystemWritableFileStream.write(payload);
  }

  public mergePayloads() {
    return Promise.resolve();
  }

  public reset() {
    return this.fileSystemWritableFileStream.truncate(0);
  }

  public close(): Promise<void> {
    // known Chrome rejections:
    // DOMException: An operation that depends on state cached in an interface object was made but the state had changed since it was read from disk.
    // DOMException: Blocked by Safe Browsing.
    // The writableStream object stuck in a locked error state
    // only way to resolve this, to restart the download with a new stream
    return this.fileSystemWritableFileStream.close();
  }
}

export class NanoFilesystemGetRequestForDirectory extends NanoRequestBase {
  public constructor(resourceId: string, path: string) {
    super();
    this.request = {
      type: 'FS-GET',
      room_id: resourceId,
      path: path,
    };
  }

  /**
   * There is an "ls" attribute, what contains the file list in an array
   * @param payloads
   */
  public mergePayloads() {
    return new Promise((resolve, reject) => {
      let list = [];
      this.payloads.forEach((payload) => {
        list.splice(this.findOverlapStartIndex(list, payload.ls));
        list = list.concat(payload.ls);
      });

      resolve(list);
    });
  }

  private findOverlapStartIndex(firstArray, nextArray) {
    let overlapMaxDistanceCheck = 50;

    if (nextArray.length == 0) return firstArray.length;

    for (let i = firstArray.length - 1; i > firstArray.length - overlapMaxDistanceCheck; i--) {
      if (firstArray[i]?.name == nextArray[0]?.name) {
        return i;
      }
    }
    return firstArray.length;
  }
}

export class NanoFilesystemPeekRequest extends NanoRequestBase {
  public constructor(resourceId: string, path: string) {
    super();
    this.request = {
      type: 'FS-PEEK',
      room_id: resourceId,
      path: path,
    };
  }

  /**
   * There is an "ls" attribute, what contains the file list in an array
   * @param payloads
   */
  public mergePayloads() {
    return new Promise((resolve, reject) => {
      resolve(this.payloads[0].pk);
    });
  }
}

export class NanoSearchRequest extends NanoRequestBase {
  public constructor(resourceId: string, query: string, lang: string) {
    super();

    this.request = {
      type: 'SEARCH',
      room_id: resourceId,
      query: query,
      language: lang,
    };
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      let res = [];
      this.payloads.forEach((msg) => {
        res = res.concat(msg.docs);
      });

      resolve(res);
    });
  }
}

export class AdminGetInfoRequest extends NanoRequestBase {
  public constructor(nanoId: number) {
    super();

    this.request = {
      type: 'A-INFO',
      id: nanoId,
    };
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      resolve({
        name: this.payloads[0].name,
        version: this.payloads[0].version,
      });
    });
  }
}

export class AdminGetRemoteConfigRequest extends NanoRequestBase {
  public constructor(nanoId: number, adminPassword: string) {
    super();

    this.request = {
      admin_password: adminPassword,
      type: 'A-REM-CFG',
      id: nanoId,
    };
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      console.log('payloads', this.payloads);

      resolve(this.payloads[0]['config']);
    });
  }
}

export class AdminChangePasswordRequest extends NanoRequestBase {
  public constructor(
    nanoId: number,
    oldPassword: string,
    newPassword: string,
    mutationToken?: MutationToken
  ) {
    super();

    this.request = {
      admin_password: oldPassword,
      type: 'A-CH-PWD',
      id: nanoId,
      new_password: newPassword,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      console.log('payloads', this.payloads);

      resolve(true); // there is no payloads for this
    });
  }
}

export class AdminGetListingRequest extends NanoRequestBase {
  public constructor(nanoId: number, path: string, adminPassword: string) {
    super();

    this.request = {
      type: 'A-LISTING',
      id: nanoId,
      path,
      admin_password: adminPassword,
    };
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      console.log('payloads', this.payloads);

      resolve(this.payloads[0].ls);
    });
  }
}

export class AdminMkdirRequest extends NanoRequestBase {
  public constructor(
    nanoId: number,
    path: string,
    name: string,

    adminPassword: string,
    mutationToken?: MutationToken
  ) {
    super();

    this.request = {
      type: 'A-MKD',
      id: nanoId,
      path,
      name,
      admin_password: adminPassword,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      console.log('payloads', this.payloads);
      resolve(true);
    });
  }
}

export class AdminCreateDriveRequest extends NanoRequestBase {
  public constructor(
    nanoId: number,
    path: string,
    adminPassword: string,
    mutationToken?: MutationToken
  ) {
    super();

    this.request = {
      type: 'A-D-CREATE',
      id: nanoId,
      path,
      admin_password: adminPassword,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      console.log('payloads', this.payloads);
      resolve(true);
    });
  }
}

export class AdminDeleteDriveRequest extends NanoRequestBase {
  public constructor(
    nanoId: number,
    path: string,
    adminPassword: string,
    mutationToken?: MutationToken
  ) {
    super();

    this.request = {
      type: 'A-D-DELETE',
      id: nanoId,
      path,
      admin_password: adminPassword,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      console.log('payloads', this.payloads);
      resolve(true);
    });
  }
}

export class AdminAttachDriveRequest extends NanoRequestBase {
  public constructor(
    nanoId: number,
    path: string,
    roomId: string,
    roomBlocks: string[],
    adminPassword: string,
    mutationToken?: MutationToken
  ) {
    super();

    this.request = {
      type: 'A-D-ATTACH',
      id: nanoId,
      path,
      target_room_id: roomId,
      room_blocks: roomBlocks,
      admin_password: adminPassword,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      console.log('payloads', this.payloads);

      resolve(true);
    });
  }
}

export class AdminDetachDriveRequest extends NanoRequestBase {
  public constructor(
    nanoId: number,
    path: string,
    roomId: string,
    adminPassword: string,
    mutationToken?: MutationToken
  ) {
    super();

    this.request = {
      type: 'A-D-DETACH',
      id: nanoId,
      path,
      target_room_id: roomId,
      admin_password: adminPassword,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      console.log('payloads', this.payloads);

      resolve(true);
    });
  }
}

export class AdminEditNameRequest extends NanoRequestBase {
  public constructor(
    nanoId: number,
    name: string,
    adminPassword: string,
    mutationToken?: MutationToken
  ) {
    super();

    this.request = {
      type: 'A-EDIT-N',
      id: nanoId,
      name,
      admin_password: adminPassword,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      console.log('payloads', this.payloads);

      resolve(true);
    });
  }
}

export class AdminEditDenyAnonymousRequest extends NanoRequestBase {
  public constructor(
    nanoId: number,
    value: boolean,
    adminPassword: string,
    mutationToken?: MutationToken
  ) {
    super();

    this.request = {
      type: 'A-EDIT-DENY-ANON',
      id: nanoId,
      value,
      admin_password: adminPassword,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      console.log('payloads', this.payloads);

      resolve(true);
    });
  }
}

export class AdminEditRequireExplicitPeerTrustRequest extends NanoRequestBase {
  public constructor(
    nanoId: number,
    value: boolean,
    adminPassword: string,
    mutationToken?: MutationToken
  ) {
    super();

    this.request = {
      type: 'A-EDIT-REQ-EXPLICIT-PEER-TRUST',
      id: nanoId,
      value,
      admin_password: adminPassword,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      console.log('payloads', this.payloads);

      resolve(true);
    });
  }
}

export class NanoCreateRoomConfigBlockRequest extends NanoRequestBase {
  public constructor(
    resourceId: number,
    memberFlags: Object,
    groupFlags: number,
    mutationToken?: MutationToken
  ) {
    super();

    this.request = {
      type: 'ROOM-CFG',
      room_id: resourceId,
      member_flags: memberFlags,
      group_flags: groupFlags,
    };

    if (mutationToken) {
      this.setMutationToken(mutationToken);
    }
  }

  public mergePayloads() {
    return new Promise((resolve, reject) => {
      console.log('payloads', this.payloads);

      resolve(this.payloads[0]['block_id']);
    });
  }
}
