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

import { contentType, Status } from "./deps.ts";
import { Request } from "./request.ts";
import { isHtml, isRedirectStatus } from "./util.ts";

interface ServerResponse {
  status?: number;
  headers?: Headers;
  body?: Uint8Array | Deno.Reader;
}

export const REDIRECT_BACK = Symbol("redirect backwards");

const BODY_TYPES = ["string", "number", "bigint", "boolean", "symbol"];

const encoder = new TextEncoder();

/** Guard for `Deno.Reader`. */
function isReader(value: any): value is Deno.Reader {
  return typeof value === "object" && "read" in value &&
    typeof value.read === "function";
}

export class Response {
  #request: Request;
  #writable = true;

  #getBody = (): Uint8Array | Deno.Reader | undefined => {
    const typeofBody = typeof this.body;
    let result: Uint8Array | Deno.Reader | undefined;
    this.#writable = false;
    if (BODY_TYPES.includes(typeofBody)) {
      const bodyText = String(this.body);
      result = encoder.encode(bodyText);
      this.type = this.type || (isHtml(bodyText) ? "html" : "text/plain");
    } else if (this.body instanceof Uint8Array || isReader(this.body)) {
      result = this.body;
    } else if (typeofBody === "object" && this.body !== null) {
      result = encoder.encode(JSON.stringify(this.body));
      this.type = this.type || "json";
    }
    return result;
  };

  #setContentType = (): void => {
    if (this.type) {
      const contentTypeString = contentType(this.type);
      if (contentTypeString && !this.headers.has("Content-Type")) {
        this.headers.append("Content-Type", contentTypeString);
      }
    }
  };

  /** The body of the response */
  body?: any;

  /** Headers that will be returned in the response */
  headers = new Headers();

  /** The HTTP status of the response */
  status?: Status;

  /** The media type, or extension of the response */
  type?: string;

  get writable(): boolean {
    return this.#writable;
  }

  constructor(request: Request) {
    this.#request = request;
  }

  /** Sets the response to redirect to the supplied `url`.
   * 
   * If the `.status` is not currently a redirect status, the status will be set
   * to `302 Found`.
   * 
   * The body will be set to a message indicating the redirection is occurring.
   */
  redirect(url: string | URL): void;
  /** Sets the response to redirect back to the referrer if available, with an
   * optional `alt` URL if there is no referrer header on the request.  If there
   * is no referrer header, nor an `alt` parameter, the redirect is set to `/`.
   * 
   * If the `.status` is not currently a redirect status, the status will be set
   * to `302 Found`.
   * 
   * The body will be set to a message indicating the redirection is occurring.
   */
  redirect(url: typeof REDIRECT_BACK, alt?: string | URL): void;
  redirect(
    url: string | URL | typeof REDIRECT_BACK,
    alt: string | URL = "/",
  ): void {
    if (url === REDIRECT_BACK) {
      url = this.#request.headers.get("Referrer") ?? String(alt);
    } else if (typeof url === "object") {
      url = String(url);
    }
    this.headers.set("Location", encodeURI(url));
    if (!this.status || !isRedirectStatus(this.status)) {
      this.status = Status.Found;
    }

    if (this.#request.accepts("html")) {
      url = encodeURI(url);
      this.type = "text/html; charset=utf-8";
      this.body = `Redirecting to <a href="${url}">${url}</a>.`;
      return;
    }
    this.type = "text/plain; charset=utf-8";
    this.body = `Redirecting to ${url}.`;
  }

  /** Take this response and convert it to the response used by the Deno net
   * server. */
  toServerResponse(): ServerResponse {
    // Process the body
    const body = this.#getBody();

    // If there is a response type, set the content type header
    this.#setContentType();

    const { headers, status } = this;

    // If there is no body and no content type and no set length, then set the
    // content length to 0
    if (
      !(
        body ||
        headers.has("Content-Type") ||
        headers.has("Content-Length")
      )
    ) {
      headers.append("Content-Length", "0");
    }

    return {
      status: status ?? (body ? Status.OK : Status.NotFound),
      body,
      headers,
    };
  }
}