export class Address {
  public readonly callsign: string;
  public readonly ssid: number;
  private ch: boolean; // C (Command/Response) or H (Has-Been-Repeated)
  private reserved: number; // Bits 5 and 6 (should be 0b11 normally)
  private extension: boolean; // LSB: true if last address field

  constructor(callsign: string, ssid = 0, ch = false, reserved = 0b11, extension = false) {
    if (typeof callsign !== 'string') throw new TypeError('callsign must be a string');
    const norm = callsign.toUpperCase().trim();
    if (norm.length === 0) throw new Error('callsign must not be empty');
    // callsign limited to 6 characters per AX.25 address field
    this.callsign = norm.slice(0, 6);

    if (typeof ssid !== 'number' || !Number.isFinite(ssid) || !Number.isInteger(ssid)) {
      throw new TypeError('ssid must be an integer');
    }
    if (ssid < 0 || ssid > 15) throw new RangeError('ssid must be between 0 and 15');
    this.ssid = ssid & 0x0f;
    this.ch = !!ch;
    this.reserved = reserved & 0b11;
    this.extension = !!extension;
  }

  toString(opts?: { sep?: string; showSsid?: boolean }) {
    const sep = opts?.sep ?? '-';
    const show = opts?.showSsid ?? true;
    return show ? `${this.callsign}${sep}${this.ssid}` : this.callsign;
  }

  static fromString(s: string): Address {
    if (typeof s !== 'string') throw new TypeError('argument must be a string');
    const parts = s.split('-');
    const call = (parts[0] || '').toUpperCase().trim();
    if (call.length === 0) throw new Error('callsign must not be empty');
    let ssid = 0;
    if (parts[1] !== undefined && parts[1] !== '') {
      // parse integer, reject non-integer
      if (!/^-?\d+$/.test(parts[1])) throw new Error('invalid SSID');
      ssid = Number(parts[1]);
    }
    return new Address(call, ssid);
  }

  toBytes(last = this.extension): Uint8Array {
    const buf = new Uint8Array(7);
    const cs = this.callsign.padEnd(6, ' ').slice(0, 6);
    for (let i = 0; i < 6; i++) buf[i] = cs.charCodeAt(i) << 1;
    // Compose SSID byte: bits 7 (C/H), 6-5 (reserved), 4-1 (SSID), 0 (last)
    buf[6] =
      (this.ch ? 0x80 : 0x00) |
      ((this.reserved & 0b11) << 5) |
      ((this.ssid & 0x0f) << 1);
    if (last) {
      buf[6] |= 0x01;
    }
    return buf;
  }

  static fromBytes(buf: Uint8Array): Address {
    if (buf.length < 7) throw new Error('Address.fromBytes: buffer too short');
    const chars: number[] = [];
    for (let i = 0; i < 6; i++) chars.push(buf[i] >> 1);
    const callsign = String.fromCharCode(...chars).trim();
    const ssid = (buf[6] >> 1) & 0x0f;
    const ch = (buf[6] & 0x80) !== 0;
    const reserved = (buf[6] >> 5) & 0b11;
    const extension = (buf[6] & 0x01) === 1;
    return new Address(callsign, ssid, ch, reserved, extension);
  }
  /**
   * Returns true if the extension bit (LSB) is set (this is the last address field)
   */
  getExtensionBit(): boolean {
    return this.extension;
  }

  /**
   * Set the extension bit (LSB) for this address
   */
  setExtensionBit(val: boolean): void {
    this.extension = !!val;
  }

  /**
   * Returns true if the C/H bit is set (Command/Response for src/dst, Has-Been-Repeated for repeaters)
   */
  getCH(): boolean {
    return this.ch;
  }

  /**
   * Returns the reserved bits (should be 0b11 for normal AX.25)
   */
  getReserved(): number {
    return this.reserved;
  }

  /**
   * Returns true if this is the last address field (extension bit set)
   */
  getExtension(): boolean {
    return this.extension;
  }

  /**
   * Set the extension (LSB) for this address
   */
  setExtension(val: boolean): void {
    this.extension = !!val;
  }

  /**
   * Returns true if the extension (LSB) is set in the given buffer (static helper)
   */
  static isLastAddress(buf: Uint8Array): boolean {
    if (buf.length < 7) throw new Error('Address.isLastAddress: buffer too short');
    return (buf[6] & 0x01) === 1;
  }
}
