/* eslint-disable @typescript-eslint/no-unsafe-return */

// Heavily inspired by https://doc.rust-lang.org/src/std/net/parser.rs.html

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Fn = (...foo: any) => any;

export class Parser {
  private index = 0;
  private input = "";

  new(input: string): this {
    this.index = 0;
    this.input = input;
    return this;
  }

  /** Run a parser, and restore the pre-parse state if it fails. */
  readAtomically<T extends Fn>(fn: T): ReturnType<T> {
    const index = this.index;
    const result = fn();
    if (result === undefined) {
      this.index = index;
    }
    return result;
  }

  /** Run a parser, but fail if the entire input wasn't consumed. Doesn't run atomically. */
  parseWith<T extends Fn>(fn: T): ReturnType<T> | undefined {
    const result = fn();
    if (this.index !== this.input.length) {
      return undefined;
    }
    return result;
  }

  /** Peek the next character from the input */
  peekChar(): string | undefined {
    if (this.index >= this.input.length) {
      return undefined;
    }
    return this.input[this.index];
  }

  /** Read the next character from the input */
  readChar(): string | undefined {
    if (this.index >= this.input.length) {
      return undefined;
    }
    return this.input[this.index++];
  }

  /** Read the next character from the input if it matches the target. */
  readGivenChar(target: string): string | undefined {
    return this.readAtomically(() => {
      const char = this.readChar();
      if (char !== target) {
        return undefined;
      }
      return char;
    });
  }

  /**
   * Helper for reading separators in an indexed loop. Reads the separator
   * character iff index > 0, then runs the parser. When used in a loop,
   * the separator character will only be read on index > 0 (see
   * readIPv4Addr for an example)
   */
  readSeparator<T extends Fn>(sep: string, index: number, inner: T): ReturnType<T> {
    return this.readAtomically(() => {
      if (index > 0) {
        if (this.readGivenChar(sep) === undefined) {
          return undefined;
        }
      }
      return inner();
    });
  }

  /**
   * Read a number off the front of the input in the given radix, stopping
   * at the first non-digit character or eof. Fails if the number has more
   * digits than max_digits or if there is no number.
   */
  readNumber(
    radix: number,
    maxDigits: number | undefined,
    allowZeroPrefix: boolean,
    maxBytes: number
  ): number | undefined {
    return this.readAtomically(() => {
      let result = 0;
      let digitCount = 0;

      const leadingChar = this.peekChar();
      if (leadingChar === undefined) {
        return undefined;
      }
      const hasLeadingZero = leadingChar === "0";
      const maxValue = 2 ** (8 * maxBytes) - 1;

      // eslint-disable-next-line no-constant-condition
      while (true) {
        const digit = this.readAtomically(() => {
          const char = this.readChar();
          if (char === undefined) {
            return undefined;
          }
          const num = Number.parseInt(char, radix);
          if (Number.isNaN(num)) {
            return undefined;
          }
          return num;
        });
        if (digit === undefined) {
          break;
        }
        result *= radix;
        result += digit;
        if (result > maxValue) {
          return undefined;
        }
        digitCount += 1;
        if (maxDigits !== undefined) {
          if (digitCount > maxDigits) {
            return undefined;
          }
        }
      }

      if (digitCount === 0) {
        return undefined;
      } else if (!allowZeroPrefix && hasLeadingZero && digitCount > 1) {
        return undefined;
      } else {
        return result;
      }
    });
  }

  /** Read an IPv4 address. */
  readIPv4Addr(): Uint8Array | undefined {
    return this.readAtomically(() => {
      const out = new Uint8Array(4);

      for (let i = 0; i < out.length; i++) {
        const ix = this.readSeparator(".", i, () => this.readNumber(10, 3, false, 1));
        if (ix === undefined) {
          return undefined;
        }
        out[i] = ix;
      }

      return out;
    });
  }

  /** Read an IPv6 Address. */
  readIPv6Addr(): Uint8Array | undefined {
    /**
     * Read a chunk of an IPv6 address into `groups`. Returns the number
     * of groups read, along with a bool indicating if an embedded
     * trailing IPv4 address was read. Specifically, read a series of
     * colon-separated IPv6 groups (0x0000 - 0xFFFF), with an optional
     * trailing embedded IPv4 address.
     */
    const readGroups = (groups: Uint8Array): [number, boolean] => {
      for (let i = 0; i < groups.length / 2; i++) {
        const ix = i * 2;
        // Try to read a trailing embedded IPv4 address. There must be at least 4 groups left.
        if (i < groups.length - 3) {
          const ipv4 = this.readSeparator(":", i, () => this.readIPv4Addr());
          if (ipv4 !== undefined) {
            groups[ix] = ipv4[0];
            groups[ix + 1] = ipv4[1];
            groups[ix + 2] = ipv4[2];
            groups[ix + 3] = ipv4[3];

            return [ix + 4, true];
          }
        }

        const group = this.readSeparator(":", i, () => this.readNumber(16, 4, true, 2));
        if (group === undefined) {
          return [ix, false];
        }
        groups[ix] = group >> 8;
        groups[ix + 1] = group & 255;
      }
      return [groups.length, false];
    };

    return this.readAtomically(() => {
      // Read the front part of the address; either the whole thing, or up to the first ::
      const head = new Uint8Array(16);
      const [headSize, headIp4] = readGroups(head);

      if (headSize === 16) {
        return head;
      }

      // IPv4 part is not allowed before `::`
      if (headIp4) {
        return undefined;
      }

      // Read `::` if previous code parsed less than 8 groups.
      // `::` indicates one or more groups of 16 bits of zeros.
      if (this.readGivenChar(":") === undefined) {
        return undefined;
      }
      if (this.readGivenChar(":") === undefined) {
        return undefined;
      }

      // Read the back part of the address. The :: must contain at least one
      // set of zeroes, so our max length is 7.
      const tail = new Uint8Array(14);
      const limit = 16 - (headSize + 2);
      const [tailSize] = readGroups(tail.subarray(0, limit));

      // Concat the head and tail of the IP address
      head.set(tail.subarray(0, tailSize), 16 - tailSize);

      return head;
    });
  }

  /** Read an IP Address, either IPv4 or IPv6. */
  readIPAddr(): Uint8Array | undefined {
    return this.readIPv4Addr() ?? this.readIPv6Addr();
  }
}
