"use strict";

const {
  isForbidden,
  isForbiddenResponse,
  isPrivilegedNoCORSRequest,
  isNoCORSSafelistedRequest,
  isCORSWhitelisted
} = require("./header-types");
const HeaderList = require("./header-list");

function assertName(name) {
  if (!name.match(/^[!#$%&'*+\-.^`|~\w]+$/)) {
    throw new TypeError("name is invalid");
  }
}

function assertValue(value) {
  if (value.match(/[\0\r\n]/)) {
    throw new TypeError("value is invalid");
  }
}

class HeadersImpl {
  constructor(args) {
    this.guard = "none";
    this.headersList = new HeaderList();

    if (args[0]) {
      this._fill(args[0]);
    }
  }

  _fill(init) {
    if (Array.isArray(init)) {
      for (const header of init) {
        if (header.length !== 2) {
          throw new TypeError("init is invalid");
        }
        this.append(header[0], header[1]);
      }
    } else {
      for (const key of Object.keys(init)) {
        this.append(key, init[key]);
      }
    }
  }

  has(name) {
    assertName(name);
    return this.headersList.contains(name);
  }

  get(name) {
    assertName(name);
    return this.headersList.get(name);
  }

  _removePrivilegedNoCORSHeaders() {
    this.headersList.delete("range");
  }

  append(name, value) {
    value = value.trim();
    assertName(name);
    assertValue(value);

    switch (this.guard) {
      case "immutable":
        throw new TypeError("Headers is immutable");
      case "request":
        if (isForbidden(name)) {
          return;
        }
        break;
      case "request-no-cors": {
        let temporaryValue = this.get(name);
        if (temporaryValue === null) {
          temporaryValue = value;
        } else {
          temporaryValue += `, ${value}`;
        }
        if (!isCORSWhitelisted(name, value)) {
          return;
        }
        break;
      }
      case "response":
        if (isForbiddenResponse(name)) {
          return;
        }
        break;
    }

    this.headersList.append(name, value);
    this._removePrivilegedNoCORSHeaders();
  }

  set(name, value) {
    value = value.trim();
    assertName(name);
    assertValue(value);

    switch (this.guard) {
      case "immutable":
        throw new TypeError("Headers is immutable");
      case "request":
        if (isForbidden(name)) {
          return;
        }
        break;
      case "request-no-cors": {
        if (!isCORSWhitelisted(name, value)) {
          return;
        }
        break;
      }
      case "response":
        if (isForbiddenResponse(name)) {
          return;
        }
        break;
    }
    this.headersList.set(name, value);
    this._removePrivilegedNoCORSHeaders();
  }

  delete(name) {
    assertName(name);

    switch (this.guard) {
      case "immutable":
        throw new TypeError("Headers is immutable");
      case "request":
        if (isForbidden(name)) {
          return;
        }
        break;
      case "request-no-cors": {
        if (
          !isNoCORSSafelistedRequest(name) &&
          !isPrivilegedNoCORSRequest(name)
        ) {
          return;
        }
        break;
      }
      case "response":
        if (isForbiddenResponse(name)) {
          return;
        }
        break;
    }
    this.headersList.delete(name);
    this._removePrivilegedNoCORSHeaders();
  }

  * [Symbol.iterator]() {
    for (const header of this.headersList.sortAndCombine()) {
      yield header;
    }
  }
}

exports.implementation = HeadersImpl;
