// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

/**
 * A class for rendering DNS messages.
 *
 * @author Brian Wellington
 */
public class DNSOutput {

  private byte[] array;
  private int pos;
  private int savedPos;

  /**
   * Create a new DNSOutput with a specified size.
   *
   * @param size The initial size
   */
  public DNSOutput(int size) {
    array = new byte[size];
    pos = 0;
    savedPos = -1;
  }

  /** Create a new DNSOutput */
  public DNSOutput() {
    this(32);
  }

  /** Returns the current position. */
  public int current() {
    return pos;
  }

  private void check(long val, int bits) {
    long max = 1;
    max <<= bits;
    if (val < 0 || val > max) {
      throw new IllegalArgumentException(val + " out of range for " + bits + " bit value");
    }
  }

  private void need(int n) {
    if (array.length - pos >= n) {
      return;
    }
    int newsize = array.length * 2;
    if (newsize < pos + n) {
      newsize = pos + n;
    }
    byte[] newarray = new byte[newsize];
    System.arraycopy(array, 0, newarray, 0, pos);
    array = newarray;
  }

  /**
   * Resets the current position of the output stream to the specified index.
   *
   * @param index The new current position.
   * @throws IllegalArgumentException The index is not within the output.
   */
  public void jump(int index) {
    if (index > pos) {
      throw new IllegalArgumentException("cannot jump past end of data");
    }
    pos = index;
  }

  /**
   * Saves the current state of the output stream.
   *
   * @throws IllegalArgumentException The index is not within the output.
   */
  public void save() {
    savedPos = pos;
  }

  /** Restores the input stream to its state before the call to {@link #save}. */
  public void restore() {
    if (savedPos < 0) {
      throw new IllegalStateException("no previous state");
    }
    pos = savedPos;
    savedPos = -1;
  }

  /**
   * Writes an unsigned 8 bit value to the stream.
   *
   * @param val The value to be written
   */
  public void writeU8(int val) {
    check(val, 8);
    need(1);
    array[pos++] = (byte) (val & 0xFF);
  }

  /**
   * Writes an unsigned 16 bit value to the stream.
   *
   * @param val The value to be written
   */
  public void writeU16(int val) {
    check(val, 16);
    need(2);
    array[pos++] = (byte) ((val >>> 8) & 0xFF);
    array[pos++] = (byte) (val & 0xFF);
  }

  /**
   * Writes an unsigned 16 bit value to the specified position in the stream.
   *
   * @param val The value to be written
   * @param where The position to write the value.
   */
  public void writeU16At(int val, int where) {
    check(val, 16);
    if (where > pos - 2) {
      throw new IllegalArgumentException("cannot write past end of data");
    }
    array[where++] = (byte) ((val >>> 8) & 0xFF);
    array[where] = (byte) (val & 0xFF);
  }

  /**
   * Writes an unsigned 32 bit value to the stream.
   *
   * @param val The value to be written
   */
  public void writeU32(long val) {
    check(val, 32);
    need(4);
    array[pos++] = (byte) ((val >>> 24) & 0xFF);
    array[pos++] = (byte) ((val >>> 16) & 0xFF);
    array[pos++] = (byte) ((val >>> 8) & 0xFF);
    array[pos++] = (byte) (val & 0xFF);
  }

  /**
   * Writes a byte array to the stream.
   *
   * @param b The array to write.
   * @param off The offset of the array to start copying data from.
   * @param len The number of bytes to write.
   */
  public void writeByteArray(byte[] b, int off, int len) {
    need(len);
    System.arraycopy(b, off, array, pos, len);
    pos += len;
  }

  /**
   * Writes a byte array to the stream.
   *
   * @param b The array to write.
   */
  public void writeByteArray(byte[] b) {
    writeByteArray(b, 0, b.length);
  }

  /**
   * Writes a counted string from the stream. A counted string is a one byte value indicating string
   * length, followed by bytes of data.
   *
   * @param s The string to write.
   */
  public void writeCountedString(byte[] s) {
    if (s.length > 0xFF) {
      throw new IllegalArgumentException("Invalid counted string");
    }
    need(1 + s.length);
    array[pos++] = (byte) (s.length & 0xFF);
    writeByteArray(s, 0, s.length);
  }

  /** Returns a byte array containing the current contents of the stream. */
  public byte[] toByteArray() {
    byte[] out = new byte[pos];
    System.arraycopy(array, 0, out, 0, pos);
    return out;
  }

  static byte[] toU16(int val) {
    return new byte[] {(byte) ((val >>> 8) & 0xFF), (byte) (val & 0xFF)};
  }
}
