// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import * as urlSearchParams from "./url_search_params";

interface URLParts {
  protocol: string;
  username: string;
  password: string;
  hostname: string;
  port: string;
  path: string;
  query: string | null;
  hash: string;
}

const patterns = {
  protocol: "(?:([^:/?#]+):)",
  authority: "(?://([^/?#]*))",
  path: "([^?#]*)",
  query: "(\\?[^#]*)",
  hash: "(#.*)",

  authentication: "(?:([^:]*)(?::([^@]*))?@)",
  hostname: "([^:]+)",
  port: "(?::(\\d+))"
};

const urlRegExp = new RegExp(
  `^${patterns.protocol}?${patterns.authority}?${patterns.path}${
    patterns.query
  }?${patterns.hash}?`
);

const authorityRegExp = new RegExp(
  `^${patterns.authentication}?${patterns.hostname}${patterns.port}?$`
);

const searchParamsMethods: Array<keyof urlSearchParams.URLSearchParams> = [
  "append",
  "delete",
  "set"
];

function parse(url: string): URLParts | undefined {
  const urlMatch = urlRegExp.exec(url);
  if (urlMatch) {
    const [, , authority] = urlMatch;
    const authorityMatch = authority
      ? authorityRegExp.exec(authority)
      : [null, null, null, null, null];
    if (authorityMatch) {
      return {
        protocol: urlMatch[1] || "",
        username: authorityMatch[1] || "",
        password: authorityMatch[2] || "",
        hostname: authorityMatch[3] || "",
        port: authorityMatch[4] || "",
        path: urlMatch[3] || "",
        query: urlMatch[4] || "",
        hash: urlMatch[5] || ""
      };
    }
  }
  return undefined;
}

export class URL {
  private _parts: URLParts;
  private _searchParams!: urlSearchParams.URLSearchParams;

  private _updateSearchParams(): void {
    const searchParams = new urlSearchParams.URLSearchParams(this.search);

    for (const methodName of searchParamsMethods) {
      /* eslint-disable @typescript-eslint/no-explicit-any */
      const method: (...args: any[]) => any = searchParams[methodName];
      searchParams[methodName] = (...args: unknown[]): any => {
        method.apply(searchParams, args);
        this.search = searchParams.toString();
      };
      /* eslint-enable */
    }
    this._searchParams = searchParams;

    // convert to `any` that has avoided the private limit
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (this._searchParams as any).url = this;
  }

  get hash(): string {
    return this._parts.hash;
  }

  set hash(value: string) {
    value = unescape(String(value));
    if (!value) {
      this._parts.hash = "";
    } else {
      if (value.charAt(0) !== "#") {
        value = `#${value}`;
      }
      // hashes can contain % and # unescaped
      this._parts.hash = escape(value)
        .replace(/%25/g, "%")
        .replace(/%23/g, "#");
    }
  }

  get host(): string {
    return `${this.hostname}${this.port ? `:${this.port}` : ""}`;
  }

  set host(value: string) {
    value = String(value);
    const url = new URL(`http://${value}`);
    this._parts.hostname = url.hostname;
    this._parts.port = url.port;
  }

  get hostname(): string {
    return this._parts.hostname;
  }

  set hostname(value: string) {
    value = String(value);
    this._parts.hostname = encodeURIComponent(value);
  }

  get href(): string {
    const authentication =
      this.username || this.password
        ? `${this.username}${this.password ? ":" + this.password : ""}@`
        : "";

    return `${this.protocol}//${authentication}${this.host}${this.pathname}${
      this.search
    }${this.hash}`;
  }

  set href(value: string) {
    value = String(value);
    if (value !== this.href) {
      const url = new URL(value);
      this._parts = { ...url._parts };
      this._updateSearchParams();
    }
  }

  get origin(): string {
    return `${this.protocol}//${this.host}`;
  }

  get password(): string {
    return this._parts.password;
  }

  set password(value: string) {
    value = String(value);
    this._parts.password = encodeURIComponent(value);
  }

  get pathname(): string {
    return this._parts.path ? this._parts.path : "/";
  }

  set pathname(value: string) {
    value = unescape(String(value));
    if (!value || value.charAt(0) !== "/") {
      value = `/${value}`;
    }
    // paths can contain % unescaped
    this._parts.path = escape(value).replace(/%25/g, "%");
  }

  get port(): string {
    return this._parts.port;
  }

  set port(value: string) {
    const port = parseInt(String(value), 10);
    this._parts.port = isNaN(port)
      ? ""
      : Math.max(0, port % 2 ** 16).toString();
  }

  get protocol(): string {
    return `${this._parts.protocol}:`;
  }

  set protocol(value: string) {
    value = String(value);
    if (value) {
      if (value.charAt(value.length - 1) === ":") {
        value = value.slice(0, -1);
      }
      this._parts.protocol = encodeURIComponent(value);
    }
  }

  get search(): string {
    if (this._parts.query === null || this._parts.query === "") {
      return "";
    }

    return this._parts.query;
  }

  set search(value: string) {
    value = String(value);
    let query: string | null;

    if (value === "") {
      query = null;
    } else if (value.charAt(0) !== "?") {
      query = `?${value}`;
    } else {
      query = value;
    }

    this._parts.query = query;
    this._updateSearchParams();
  }

  get username(): string {
    return this._parts.username;
  }

  set username(value: string) {
    value = String(value);
    this._parts.username = encodeURIComponent(value);
  }

  get searchParams(): urlSearchParams.URLSearchParams {
    return this._searchParams;
  }

  constructor(url: string, base?: string | URL) {
    let baseParts: URLParts | undefined;
    if (base) {
      baseParts = typeof base === "string" ? parse(base) : base._parts;
      if (!baseParts) {
        throw new TypeError("Invalid base URL.");
      }
    }

    const urlParts = parse(url);
    if (!urlParts) {
      throw new TypeError("Invalid URL.");
    }

    if (urlParts.protocol) {
      this._parts = urlParts;
    } else if (baseParts) {
      this._parts = {
        protocol: baseParts.protocol,
        username: baseParts.username,
        password: baseParts.password,
        hostname: baseParts.hostname,
        port: baseParts.port,
        path: urlParts.path || baseParts.path,
        query: urlParts.query || baseParts.query,
        hash: urlParts.hash
      };
    } else {
      throw new TypeError("URL requires a base URL.");
    }
    this._updateSearchParams();
  }

  toString(): string {
    return this.href;
  }

  toJSON(): string {
    return this.href;
  }
}