// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

import Exception from "./exception.js";
import * as reader from "../stream/reader.js";
import * as common from "./common.js";

export const LOOPBACK = 0x00;
export const IPV4 = 0x01;
export const IPV6 = 0x02;
export const HOSTNAME = 0x03;

export const MAX_ADDR_LEN = 0x3f;

export class Address {
  /**
   * Read builds an Address from data readed from the reader
   *
   * @param {reader.Reader} rd The reader
   *
   * @returns {Address} The Address
   *
   * @throws {Exception} when address type is invalid
   */
  static async read(rd) {
    let readed = await reader.readN(rd, 3),
      portNum = 0,
      addrType = LOOPBACK,
      addrData = null;

    portNum |= readed[0];
    portNum <<= 8;
    portNum |= readed[1];

    addrType = readed[2] >> 6;

    switch (addrType) {
      case LOOPBACK:
        break;

      case IPV4:
        addrData = await reader.readN(rd, 4);
        break;

      case IPV6:
        addrData = await reader.readN(rd, 16);
        break;

      case HOSTNAME:
        addrData = await reader.readN(rd, 0x3f & readed[2]);
        break;

      default:
        throw new Exception("Unknown address type");
    }

    return new Address(addrType, addrData, portNum);
  }

  /**
   * constructor
   *
   * @param {number} type Type of the address
   * @param {Uint8Array} address Address data
   * @param {number} port port number of the address
   *
   */
  constructor(type, address, port) {
    this.addrType = type;
    this.addrData = address;
    this.addrPort = port;
  }

  /**
   * Return the address type
   *
   */
  type() {
    return this.addrType;
  }

  /**
   * Return the address data
   *
   */
  address() {
    return this.addrData;
  }

  /**
   * Return the port data
   *
   */
  port() {
    return this.addrPort;
  }

  /**
   * Buffer returns the marshalled address
   *
   * @returns {Uint8Array} Marshalled address
   *
   * @throws {Exception} When address data is invalid
   *
   */
  buffer() {
    switch (this.type()) {
      case LOOPBACK:
        return new Uint8Array([
          this.addrPort >> 8,
          this.addrPort & 0xff,
          LOOPBACK << 6
        ]);

      case IPV4:
        if (this.addrData.length != 4) {
          throw new Exception("Invalid address length");
        }

        return new Uint8Array([
          this.addrPort >> 8,
          this.addrPort & 0xff,
          IPV4 << 6,
          this.addrData[0],
          this.addrData[1],
          this.addrData[2],
          this.addrData[3]
        ]);

      case IPV6:
        if (this.addrData.length != 16) {
          throw new Exception("Invalid address length");
        }

        return new Uint8Array([
          this.addrPort >> 8,
          this.addrPort & 0xff,
          IPV6 << 6,
          this.addrData[0],
          this.addrData[1],
          this.addrData[2],
          this.addrData[3],
          this.addrData[4],
          this.addrData[5],
          this.addrData[6],
          this.addrData[7],
          this.addrData[8],
          this.addrData[9],
          this.addrData[10],
          this.addrData[11],
          this.addrData[12],
          this.addrData[13],
          this.addrData[14],
          this.addrData[15]
        ]);

      case HOSTNAME:
        if (this.addrData.length > MAX_ADDR_LEN) {
          throw new Exception("Host name cannot longer than " + MAX_ADDR_LEN);
        }

        let dataBuf = new Uint8Array(this.addrData.length + 3);

        dataBuf[0] = (this.addrPort >> 8) & 0xff;
        dataBuf[1] = this.addrPort & 0xff;
        dataBuf[2] = HOSTNAME << 6;
        dataBuf[2] |= this.addrData.length;

        dataBuf.set(this.addrData, 3);

        return dataBuf;

      default:
        throw new Exception("Unknown address type");
    }
  }
}

/**
 * Get address data
 *
 * @param {string} s Address string
 * @param {number} defaultPort Default port number
 *
 * @returns {object} result
 *
 * @throws {Exception} when the address is invalid
 */
export function parseHostPort(s, defaultPort) {
  let d = common.splitHostPort(s, defaultPort),
    t = HOSTNAME;

  switch (d.type) {
    case "IPv4":
      t = IPV4;
      break;

    case "IPv6":
      t = IPV6;
      break;

    case "Hostname":
      break;

    default:
      throw new Exception("Invalid address type");
  }

  return {
    type: t,
    address: d.addr,
    port: d.port
  };
}
