// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.

((window) => {
  const { Blob } = window.__bootstrap.blob;
  const { ReadableStream, isReadableStreamDisturbed } =
    window.__bootstrap.streams;
  const { Buffer } = window.__bootstrap.buffer;
  const {
    getHeaderValueParams,
    hasHeaderValueOf,
    isTypedArray,
  } = window.__bootstrap.webUtil;
  const { MultipartParser } = window.__bootstrap.multipart;

  function validateBodyType(owner, bodySource) {
    if (isTypedArray(bodySource)) {
      return true;
    } else if (bodySource instanceof ArrayBuffer) {
      return true;
    } else if (typeof bodySource === "string") {
      return true;
    } else if (bodySource instanceof ReadableStream) {
      return true;
    } else if (bodySource instanceof FormData) {
      return true;
    } else if (bodySource instanceof URLSearchParams) {
      return true;
    } else if (!bodySource) {
      return true; // null body is fine
    }
    throw new Error(
      `Bad ${owner.constructor.name} body type: ${bodySource.constructor.name}`,
    );
  }

  async function bufferFromStream(
    stream,
    size,
  ) {
    const encoder = new TextEncoder();
    const buffer = new Buffer();

    if (size) {
      // grow to avoid unnecessary allocations & copies
      buffer.grow(size);
    }

    while (true) {
      const { done, value } = await stream.read();

      if (done) break;

      if (typeof value === "string") {
        buffer.writeSync(encoder.encode(value));
      } else if (value instanceof ArrayBuffer) {
        buffer.writeSync(new Uint8Array(value));
      } else if (value instanceof Uint8Array) {
        buffer.writeSync(value);
      } else if (!value) {
        // noop for undefined
      } else {
        throw new Error("unhandled type on stream read");
      }
    }

    return buffer.bytes().buffer;
  }

  const BodyUsedError =
    "Failed to execute 'clone' on 'Body': body is already used";

  class Body {
    #contentType = "";
    #size = undefined;

    constructor(_bodySource, meta) {
      validateBodyType(this, _bodySource);
      this._bodySource = _bodySource;
      this.#contentType = meta.contentType;
      this.#size = meta.size;
      this._stream = null;
    }

    get body() {
      if (this._stream) {
        return this._stream;
      }

      if (this._bodySource instanceof ReadableStream) {
        this._stream = this._bodySource;
      }
      if (typeof this._bodySource === "string") {
        const bodySource = this._bodySource;
        this._stream = new ReadableStream({
          start(controller) {
            controller.enqueue(bodySource);
            controller.close();
          },
        });
      }
      return this._stream;
    }

    get bodyUsed() {
      if (this.body && isReadableStreamDisturbed(this.body)) {
        return true;
      }
      return false;
    }

    async blob() {
      return new Blob([await this.arrayBuffer()], {
        type: this.#contentType,
      });
    }

    // ref: https://fetch.spec.whatwg.org/#body-mixin
    async formData() {
      const formData = new FormData();
      if (hasHeaderValueOf(this.#contentType, "multipart/form-data")) {
        const params = getHeaderValueParams(this.#contentType);

        // ref: https://tools.ietf.org/html/rfc2046#section-5.1
        const boundary = params.get("boundary");
        const body = new Uint8Array(await this.arrayBuffer());
        const multipartParser = new MultipartParser(body, boundary);

        return multipartParser.parse();
      } else if (
        hasHeaderValueOf(this.#contentType, "application/x-www-form-urlencoded")
      ) {
        // From https://github.com/github/fetch/blob/master/fetch.js
        // Copyright (c) 2014-2016 GitHub, Inc. MIT License
        const body = await this.text();
        try {
          body
            .trim()
            .split("&")
            .forEach((bytes) => {
              if (bytes) {
                const split = bytes.split("=");
                const name = split.shift().replace(/\+/g, " ");
                const value = split.join("=").replace(/\+/g, " ");
                formData.append(
                  decodeURIComponent(name),
                  decodeURIComponent(value),
                );
              }
            });
        } catch (e) {
          throw new TypeError("Invalid form urlencoded format");
        }
        return formData;
      } else {
        throw new TypeError("Invalid form data");
      }
    }

    async text() {
      if (typeof this._bodySource === "string") {
        return this._bodySource;
      }

      const ab = await this.arrayBuffer();
      const decoder = new TextDecoder("utf-8");
      return decoder.decode(ab);
    }

    async json() {
      const raw = await this.text();
      return JSON.parse(raw);
    }

    arrayBuffer() {
      if (isTypedArray(this._bodySource)) {
        return Promise.resolve(this._bodySource.buffer);
      } else if (this._bodySource instanceof ArrayBuffer) {
        return Promise.resolve(this._bodySource);
      } else if (typeof this._bodySource === "string") {
        const enc = new TextEncoder();
        return Promise.resolve(
          enc.encode(this._bodySource).buffer,
        );
      } else if (this._bodySource instanceof ReadableStream) {
        return bufferFromStream(this._bodySource.getReader(), this.#size);
      } else if (
        this._bodySource instanceof FormData ||
        this._bodySource instanceof URLSearchParams
      ) {
        const enc = new TextEncoder();
        return Promise.resolve(
          enc.encode(this._bodySource.toString()).buffer,
        );
      } else if (!this._bodySource) {
        return Promise.resolve(new ArrayBuffer(0));
      }
      throw new Error(
        `Body type not yet implemented: ${this._bodySource.constructor.name}`,
      );
    }
  }

  window.__bootstrap.body = {
    Body,
    BodyUsedError,
  };
})(this);
