/*
 *  This Source Code Form is subject to the terms of the Mozilla Public
 *  License, v. 2.0. If a copy of the MPL was not distributed with this
 *  file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

interface ICIDRBlockArgs { addr: number; prefix: number; }

export class CIDRBlock {
  /**
   * Construct a [[CIDRBlock]] object from four IP component numbers (eg: a.b.c.d) and a prefix. The network
   * address is sanitized by zeroing all the bits after the prefix.
   * @throws If any IP component number is invalid or if the prefix is invalid.
   */
  public static fromNumbers(a: number, b: number, c: number, d: number, prefix: number): CIDRBlock {
    const { addr, prefix: netPrefix } = CIDRBlock.fromNumbersInner(a, b, c, d, prefix);

    return new CIDRBlock(addr, netPrefix);
  }

  /**
   * Parse a string into a CIDR block.
   * @throws If the stirng is not a valid CIDR block
   */
  public static fromString(cidr: string): CIDRBlock {
    const { addr, prefix } = CIDRBlock.fromStringInner(cidr);
    return new CIDRBlock(addr, prefix);
  }

  protected static fromNumbersInner(a: number, b: number, c: number, d: number, prefix: number): ICIDRBlockArgs {
    if (![a, b, c, d].every((n) => CIDRBlock.checkIPBoundaries(n))) {
      throw new Error(`Invalid network address for ${a}.${b}.${c}.${d}/${prefix}`);
    }

    if (!CIDRBlock.checkPrefix(prefix)) {
      throw new Error(`Invalid prefix address for ${a}.${b}.${c}.${d}/${prefix}`);
    }

    let addr = ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;

    // Sanitize the network address
    const sanitizer = (0xFFFFFFFF << (32 - prefix) >>> 0);
    addr = (addr & sanitizer) >>> 0;

    return {
      addr,
      prefix,
    };
  }

  protected static fromStringInner(cidr: string): ICIDRBlockArgs {
    const splittedCidr = cidr.split("/");

    if (splittedCidr.length !== 2) {
      throw new Error(`"${cidr} is not a valid CIDR`);
    }

    const prefix = Number.parseInt(splittedCidr[1], 10);
    if (prefix === null || !CIDRBlock.checkPrefix(prefix)) {
      throw new Error(`"${prefix}" is not a valid prefix in CIDR "${cidr}"`);
    }

    const splittedNetworkAddr = splittedCidr[0].split(".")
      .map((i) => Number.parseInt(i, 10))
      .filter((n: number) => !Number.isNaN(n));

    if (splittedNetworkAddr.length !== 4) {
      throw new Error(`"${splittedCidr[0]}" is not a valid network address in CIDR "${cidr}"`);
    }

    return CIDRBlock.fromNumbersInner(
      splittedNetworkAddr[0],
      splittedNetworkAddr[1],
      splittedNetworkAddr[2],
      splittedNetworkAddr[3],
      prefix);
  }

  private static checkIPBoundaries(n: number): boolean {
    return Number.isInteger(n) && n >= 0 && n <= 255;
  }

  private static checkPrefix(prefix: number): boolean {
    return Number.isInteger(prefix) && prefix >= 0 && prefix <= 32;
  }

  private static networkAddressToString(netAddr: number): string {
    const a = netAddr >> 24;
    const b = netAddr >> 16 & 0xFF;
    const c = netAddr >> 8 & 0xFF;
    const d = netAddr & 0xFF;

    const networkAddr = new Uint8Array([a, b, c, d]);

    return `${networkAddr[0]}.${networkAddr[1]}.${networkAddr[2]}.${networkAddr[3]}`;
  }

  public readonly networkPrefix: number;

  private innerNetworkAddr: number;

  protected constructor(networkAddr: number, prefix: number) {
    this.innerNetworkAddr = networkAddr;
    this.networkPrefix = prefix;
  }

  public get networkAddress(): string {
    return CIDRBlock.networkAddressToString(this.innerNetworkAddr);
  }

  /**
   * Return the broadcast address of this CIDR block.
   */
  public get broadcastAddress(): string {
    const broadcastAddress = this.innerNetworkAddr | (0xFFFFFFFF >>> this.networkPrefix);
    return CIDRBlock.networkAddressToString(broadcastAddress);
  }

  /**
   * Return a generator yielding all the IP address this block can contain. It doesn't yield the network address and
   * the broadcast address.
   */
  public * ipAddress(): IterableIterator<string> {
    for (let i = 1; i < this.maxAddressIndex; i++) {
      yield this.getAddress(i);
    }
  }

  /**
   * Return the CIDR notation of this block
   */
  public toString(): string {
    return `${this.networkAddress}/${this.networkPrefix}`;
  }

  /**
   * Split the CIDR block into at least `requiredSubnetAmount`.
   * @returns The new CIDR blocks generated from the split
   * @throws If the block can't be divided by at least `requiredSubnetAmount` or if `requiredSubnetAmount` is negative
   */
  public split(requiredSubnetAmount: number): CIDRBlock[] {
    if (requiredSubnetAmount <= 0) {
      throw new Error("The required subnet amount must be positive");
    }

    const prefixDiff = Math.ceil(Math.log2(requiredSubnetAmount));
    const newPrefix = this.networkPrefix + prefixDiff;
    if (!CIDRBlock.checkPrefix(newPrefix)) {
      throw new Error(`Can't divide "${this.toString()}" into ${requiredSubnetAmount}`);
    }

    const resultLength = Math.pow(2, prefixDiff);

    const result = [];
    for (let i = 0; i < resultLength; i++) {
      const networkAddr = this.innerNetworkAddr + (i << (32 - newPrefix));
      result.push(new CIDRBlock(networkAddr, newPrefix));
    }

    return result;
  }

  /**
   * Return the maximum address index usable with {@link CIDRBlock.getAddress}
   */
  public get maxAddressIndex(): number {
    return 0xFFFFFFFF >>> this.networkPrefix;
  }

  /**
   * Return the i-th address inside this CIDR Block.
   *
   * Note that `getAddress(0)` is equivalent [[CIDRBlock.networkAddress]],
   * `this.getAddress(this.maxAddressIndex)` is equivaent to [[CIDRBlock.broadcastAddress]]
   *
   * @throws if `i` is not an integer between 0 and [[CIDRBlock.maxAddressIndex]]
   */
  public getAddress(i: number) {
    if (!Number.isInteger(i) || i < 0 || i > this.maxAddressIndex) {
      throw new Error(`${i} is not a valid address index for "${this.toString()}"`);
    }

    const addr = this.innerNetworkAddr + i;
    return CIDRBlock.networkAddressToString(addr);
  }
}
