const DEFAULT_OPTIONS = {
  origin: true,
  methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
  allowedHeaders: [],
  exposedHeaders: [],
};
class CORS {
  options;
  constructor(options) {
    // Merge user options with default options
    this.options = Object.assign({}, DEFAULT_OPTIONS, options);
  }
  async exec(request, response) {
    let isPreflight = request.method.toLowerCase() === "options";
    await this.configureOrigin(response.headers, request);
    this.configureCredentials(response.headers);
    this.configureExposedHeaders(response.headers);
    if (isPreflight) {
      this.configureMethods(response.headers);
      this.configureAllowedHeaders(response.headers, request);
      this.configureMaxAge(response.headers);
      // waiting for the body
      if (response.status === 204) response.headers.set("Content-Length", "0");
    }
    return response;
  }
  async resolveOrigin(request) {
    let { origin } = this.options;
    if (typeof origin === "function") {
      return await origin(request.headers.get("origin") ?? "");
    }
    return origin;
  }
  configureMaxAge(headers) {
    var { maxAge } = this.options;
    if (!this.isNumber(maxAge)) return headers;
    headers.append("Access-Control-Max-Age", maxAge.toString());
    return headers;
  }
  configureExposedHeaders(headers) {
    let exposedHeaders = this.options.exposedHeaders?.join(",");
    if (!this.isString(exposedHeaders) || exposedHeaders === "") return headers;
    headers.append("Access-Control-Expose-Headers", exposedHeaders);
    return null;
  }
  configureAllowedHeaders(headers, request) {
    let allowedHeaders = this.options.allowedHeaders?.join(",");
    if (!allowedHeaders) {
      // headers wasn't specified, so reflect the request headers
      let requestHeaders = request.headers.get(
        "Access-Control-Request-Headers"
      );
      if (this.isString(requestHeaders)) allowedHeaders = requestHeaders;
      headers.append("Vary", "Access-Control-Request-Headers");
    }
    if (allowedHeaders && allowedHeaders !== "") {
      headers.append("Access-Control-Allow-Headers", allowedHeaders);
    }
    return headers;
  }
  configureCredentials(headers) {
    if (this.options.credentials === true) {
      headers.append("Access-Control-Allow-Credentials", "true");
    }
    return headers;
  }
  configureMethods(headers) {
    let methods = this.options.methods?.join(",");
    if (!this.isString(methods)) return headers;
    headers.append("Access-Control-Allow-Methods", methods);
    return headers;
  }
  async configureOrigin(headers, request) {
    let origin = await this.resolveOrigin(request);
    let requestOrigin = request.headers.get("origin");
    if (!requestOrigin || origin === false) return headers;
    if (origin === undefined || origin === "*") {
      // allow any origin
      headers.append("Access-Control-Allow-Origin", "*");
      return headers;
    }
    if (this.isString(origin)) {
      // fixed origin
      headers.append("Access-Control-Allow-Origin", origin);
      headers.append("Vary", "Origin");
      return headers;
    }
    if (!this.isOriginAllowed(requestOrigin, origin)) return headers;
    // reflect origin
    headers.append("Access-Control-Allow-Origin", requestOrigin);
    headers.append("Vary", "Origin");
    return headers;
  }
  isOriginAllowed(origin, allowedOrigin) {
    if (Array.isArray(allowedOrigin)) {
      for (let element of allowedOrigin) {
        if (this.isOriginAllowed(origin, element)) return true;
      }
      return false;
    }
    if (this.isString(allowedOrigin)) {
      return origin === allowedOrigin;
    }
    if (allowedOrigin instanceof RegExp) {
      return allowedOrigin.test(origin);
    }
    return !!allowedOrigin;
  }
  isString(value) {
    return typeof value === "string" || value instanceof String;
  }
  isNumber(value) {
    return typeof value === "number" || value instanceof Number;
  }
}
/**
 * Setup CORS for a giving Request and Response objects pair using the specified
 * options.
 *
 * The default options are:
 * - origin: true
 * - methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"]
 * - allowedHeaders: []
 * - exposedHeaders: []
 * - credentials: false
 * - maxAge: 0
 *
 * @param request The Request object
 * @param response The Response object
 * @param options Optional configuration for CORS
 * @returns The same Response object mutated
 *
 * @example
 * // Create a response, then setup CORS for it
 * export async function loader({ request }: LoaderArgs) {
 *   let data = await getData(request);
 *   let response = json<LoaderData>(data);
 *   return await cors(request, response);
 * }
 * @example
 * // Create response and setup CORS in a single line
 * export async function loader({ request }: LoaderArgs) {
 *   let data = await getData(request);
 *   return await cors(request, json<LoaderData>(data));
 * }
 * @example
 * // Setup for any data request
 * export let handleDataRequest: HandleDataRequestFunction = async (
 *   response,
 *   { request }
 * ) => {
 *   return await cors(request, response);
 * };
 * @example
 * // Pass a configuration object to setup CORS
 * export async function loader({ request }: LoaderArgs) {
 *   let data = await getData(request);
 *   return await cors(request, json<LoaderData>(data), {
 *     origin: "https://example.com"
 *   });
 * }
 * @example
 * // Mutate response and then return it
 * export async function loader({ request }: LoaderArgs) {
 *   let data = await getData(request);
 *   let response = json<LoaderData>(data);
 *   await cors(request, response); // this mutates the Response object
 *   return response;
 * }
 */
export async function cors(request, response, options = DEFAULT_OPTIONS) {
  return new CORS(options).exec(request, response);
}
