import * as ba from "base64-arraybuffer";
import {
  DEFAULT_BLOB_PROPS,
  DEFAULT_CONTENT_TYPE,
} from "./FileSystemConstants";
import { dataUrlToBase64 } from "./FileSystemUtil";

const CHUNK_SIZE = 96 * 1024;

function decode(str: string) {
  return ba.decode(str);
}

function encode(buffer: ArrayBuffer) {
  return ba.encode(buffer);
}

export function isBlob(value: unknown): value is Blob {
  return value instanceof Blob || toString.call(value) === "[object Blob]";
}

export function isUint8Array(value: unknown): value is Uint8Array {
  return (
    value instanceof Uint8Array ||
    toString.call(value) === "[object Uint8Array]"
  );
}

export function isBuffer(value: any): value is Buffer {
  /* eslint-disable */
  return (
    typeof value?.constructor?.isBuffer === "function" &&
    value.constructor.isBuffer(value)
  );
  /* eslint-enable */
}

function concatArrayBuffers(chunks: ArrayBuffer[], byteLength: number) {
  const u8 = new Uint8Array(byteLength);
  let pos = 0;
  for (const chunk of chunks) {
    u8.set(new Uint8Array(chunk), pos);
    pos += chunk.byteLength;
  }
  return u8.buffer;
}

async function blobToArrayBufferUsingReadAsArrayBuffer(blob: Blob) {
  if (blob.size === 0) {
    return new ArrayBuffer(0);
  }

  let byteLength = 0;
  const chunks: ArrayBuffer[] = [];
  for (let start = 0, end = blob.size; start < end; start += CHUNK_SIZE) {
    const blobChunk = blob.slice(start, start + CHUNK_SIZE);
    const chunk = await new Promise<ArrayBuffer>((resolve, reject) => {
      const reader = new FileReader();
      reader.onerror = (ev) => {
        reject(reader.error || ev);
      };
      reader.onload = () => {
        const chunk = reader.result as ArrayBuffer;
        byteLength += chunk.byteLength;
        resolve(chunk);
      };
      reader.readAsArrayBuffer(blobChunk);
    });
    chunks.push(chunk);
  }

  return concatArrayBuffers(chunks, byteLength);
}

async function blobToArrayBufferUsingReadAsDataUrl(blob: Blob) {
  const base64 = await blobToBase64(blob);
  if (!base64) {
    return new ArrayBuffer(0);
  }

  return base64ToArrayBuffer(base64);
}

async function blobToBuffer(blob: Blob) {
  const arrayBuffer = await blobToArrayBuffer(blob);
  return Buffer.from(arrayBuffer);
}

async function blobToArrayBuffer(blob: Blob) {
  if (blob.size === 0) {
    return new ArrayBuffer(0);
  }

  let buffer: ArrayBuffer;
  if (navigator && navigator.product === "ReactNative") {
    buffer = await blobToArrayBufferUsingReadAsDataUrl(blob);
  } else {
    buffer = await blobToArrayBufferUsingReadAsArrayBuffer(blob);
  }
  return buffer;
}

function base64ToBuf(base64: string) {
  return Buffer.from(base64, "base64");
}

function base64ToBuffer(base64: string) {
  const chunks: Buffer[] = [];
  let byteLength = 0;
  for (let start = 0, end = base64.length; start < end; start += CHUNK_SIZE) {
    const base64chunk = base64.substr(start, CHUNK_SIZE);
    const chunk = base64ToBuf(base64chunk);
    byteLength += chunk.byteLength;
    chunks.push(chunk);
  }

  const buffer = Buffer.alloc(byteLength);
  let pos = 0;
  for (const chunk of chunks) {
    buffer.set(chunk, pos);
    pos += chunk.byteLength;
  }
  return buffer;
}

function base64ToArrayBuffer(base64: string) {
  let byteLength = 0;
  const chunks: ArrayBuffer[] = [];
  for (let start = 0, end = base64.length; start < end; start += CHUNK_SIZE) {
    const base64chunk = base64.substr(start, CHUNK_SIZE);
    const chunk = decode(base64chunk);
    byteLength += chunk.byteLength;
    chunks.push(chunk);
  }
  return concatArrayBuffers(chunks, byteLength);
}

function uint8ArrayToArrayBuffer(view: Uint8Array) {
  const viewLength = view.length;
  const buffer = view.buffer;
  if (viewLength === buffer.byteLength) {
    return buffer;
  }

  const newBuffer = new ArrayBuffer(viewLength);
  const newView = new Uint8Array(newBuffer);
  for (let i = 0; i < viewLength; i++) {
    newView[i] = view[i];
  }
  return newBuffer;
}

export async function toBuffer(
  content: Blob | BufferSource | string
): Promise<Buffer> {
  if (!content) {
    return Buffer.from([]);
  }

  let buffer: Buffer;
  if (typeof content === "string") {
    buffer = base64ToBuffer(content);
  } else if (isBlob(content)) {
    buffer = await blobToBuffer(content);
  } else if (isBuffer(content)) {
    buffer = content;
  } else if (ArrayBuffer.isView(content)) {
    buffer = Buffer.from(
      content.buffer,
      content.byteOffset,
      content.byteLength
    );
  } else {
    buffer = Buffer.from(content);
  }
  return buffer;
}

export async function toArrayBuffer(
  content: Blob | BufferSource | string
): Promise<ArrayBuffer> {
  if (!content) {
    return new ArrayBuffer(0);
  }

  let buffer: ArrayBuffer;
  if (typeof content === "string") {
    buffer = base64ToArrayBuffer(content);
  } else if (isBlob(content)) {
    buffer = await blobToArrayBuffer(content);
  } else if (isBuffer(content)) {
    buffer = content.buffer.slice(
      content.byteOffset,
      content.byteOffset + content.byteLength
    );
  } else if (ArrayBuffer.isView(content)) {
    buffer = uint8ArrayToArrayBuffer(content as Uint8Array);
  } else {
    buffer = content;
  }
  return buffer;
}

function base64ToBlob(base64: string, type = DEFAULT_CONTENT_TYPE): Blob {
  try {
    const buffer = base64ToArrayBuffer(base64);
    return new Blob([buffer], { type });
  } catch (e) {
    console.warn(e, base64);
    return new Blob([], DEFAULT_BLOB_PROPS);
  }
}

export function toBlob(content: Blob | BufferSource | string): Blob {
  if (!content) {
    return new Blob([], DEFAULT_BLOB_PROPS);
  }

  let blob: Blob;
  if (typeof content === "string") {
    blob = base64ToBlob(content);
  } else if (isBlob(content)) {
    blob = content;
  } else {
    blob = new Blob([content]);
  }
  return blob;
}

async function blobToBase64(blob: Blob): Promise<string> {
  if (blob.size === 0) {
    return "";
  }

  const chunks: string[] = [];
  for (let start = 0, end = blob.size; start < end; start += CHUNK_SIZE) {
    const blobChunk = blob.slice(start, start + CHUNK_SIZE);
    const chunk = await new Promise<string>((resolve, reject) => {
      const reader = new FileReader();
      reader.onerror = function (ev) {
        reject(reader.error || ev);
      };
      reader.onload = function () {
        const base64 = dataUrlToBase64(reader.result as string);
        resolve(base64);
      };
      reader.readAsDataURL(blobChunk);
    });
    chunks.push(chunk);
  }
  return chunks.join("");
}

function arrayBufferToBase64(ab: ArrayBuffer): string {
  return encode(ab);
}

function uint8ArrayToBase64(u8: Uint8Array): string {
  const chunks: string[] = [];
  for (let start = 0, end = u8.byteLength; start < end; start += CHUNK_SIZE) {
    const u8Chunk = u8.slice(start, start + CHUNK_SIZE);
    const abChunk = uint8ArrayToArrayBuffer(u8Chunk);
    const chunk = arrayBufferToBase64(abChunk);
    chunks.push(chunk);
  }
  const base64 = chunks.join("");
  return base64;
}

function bufferToBase64(buffer: Buffer) {
  const chunks: string[] = [];
  for (
    let start = 0, end = buffer.byteLength;
    start < end;
    start += CHUNK_SIZE
  ) {
    const bufferChunk = buffer.slice(start, start + CHUNK_SIZE);
    const chunk = bufferChunk.toString("base64");
    chunks.push(chunk);
  }
  const base64 = chunks.join("");
  return base64;
}

export async function toBase64(
  content: Blob | BufferSource | string
): Promise<string> {
  if (!content) {
    return "";
  }

  let base64: string;
  if (typeof content === "string") {
    base64 = content;
  } else if (isBlob(content)) {
    base64 = await blobToBase64(content);
  } else if (isBuffer(content)) {
    base64 = bufferToBase64(content);
  } else if (ArrayBuffer.isView(content)) {
    base64 = uint8ArrayToBase64(content as Uint8Array);
  } else {
    base64 = uint8ArrayToBase64(new Uint8Array(content));
  }
  return base64;
}
