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

package org.xbill.DNS;

import java.nio.ByteBuffer;

/**
 * A class for parsing DNS messages.
 *
 * @author Brian Wellington
 */
public class DNSInput {
  private final ByteBuffer byteBuffer;
  private final int offset;
  private final int limit;
  private int savedPos;
  private int savedEnd;

  /**
   * Creates a new DNSInput
   *
   * @param input The byte array to read from
   */
  public DNSInput(byte[] input) {
    this(ByteBuffer.wrap(input));
  }

  /**
   * Creates a new DNSInput from the given {@link ByteBuffer}
   *
   * @param byteBuffer The ByteBuffer
   */
  public DNSInput(ByteBuffer byteBuffer) {
    this.byteBuffer = byteBuffer;
    offset = byteBuffer.position();
    limit = byteBuffer.limit();
    savedPos = -1;
    savedEnd = -1;
  }

  /** Returns the current position. */
  public int current() {
    return byteBuffer.position() - offset;
  }

  /** Returns the number of bytes that can be read from this stream before reaching the end. */
  public int remaining() {
    return byteBuffer.remaining();
  }

  private void require(int n) throws WireParseException {
    if (n > remaining()) {
      throw new WireParseException("end of input");
    }
  }

  /**
   * Marks the following bytes in the stream as active.
   *
   * @param len The number of bytes in the active region.
   * @throws IllegalArgumentException The number of bytes in the active region is longer than the
   *     remainder of the input.
   */
  public void setActive(int len) {
    if (len > limit - byteBuffer.position()) {
      throw new IllegalArgumentException("cannot set active region past end of input");
    }
    byteBuffer.limit(byteBuffer.position() + len);
  }

  /**
   * Clears the active region of the string. Further operations are not restricted to part of the
   * input.
   */
  public void clearActive() {
    byteBuffer.limit(limit);
  }

  /** Returns the position of the end of the current active region. */
  public int saveActive() {
    return byteBuffer.limit() - offset;
  }

  /**
   * Restores the previously set active region. This differs from setActive() in that
   * restoreActive() takes an absolute position, and setActive takes an offset from the current
   * location.
   *
   * @param pos The end of the active region.
   */
  public void restoreActive(int pos) {
    if (pos + offset > limit) {
      throw new IllegalArgumentException("cannot set active region past end of input");
    }
    byteBuffer.limit(pos + offset);
  }

  /**
   * Resets the current position of the input stream to the specified index, and clears the active
   * region.
   *
   * @param index The position to continue parsing at.
   * @throws IllegalArgumentException The index is not within the input.
   */
  public void jump(int index) {
    if (index + offset >= limit) {
      throw new IllegalArgumentException("cannot jump past end of input");
    }
    byteBuffer.position(offset + index);
    byteBuffer.limit(limit);
  }

  /**
   * Saves the current state of the input stream. Both the current position and the end of the
   * active region are saved.
   *
   * @throws IllegalArgumentException The index is not within the input.
   */
  public void save() {
    savedPos = byteBuffer.position();
    savedEnd = byteBuffer.limit();
  }

  /** 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");
    }
    byteBuffer.position(savedPos);
    byteBuffer.limit(savedEnd);
    savedPos = -1;
    savedEnd = -1;
  }

  /**
   * Reads an unsigned 8 bit value from the stream, as an int.
   *
   * @return An unsigned 8 bit value.
   * @throws WireParseException The end of the stream was reached.
   */
  public int readU8() throws WireParseException {
    require(1);
    return byteBuffer.get() & 0xFF;
  }

  /**
   * Reads an unsigned 16 bit value from the stream, as an int.
   *
   * @return An unsigned 16 bit value.
   * @throws WireParseException The end of the stream was reached.
   */
  public int readU16() throws WireParseException {
    require(2);
    return byteBuffer.getShort() & 0xFFFF;
  }

  /**
   * Reads an unsigned 32 bit value from the stream, as a long.
   *
   * @return An unsigned 32 bit value.
   * @throws WireParseException The end of the stream was reached.
   */
  public long readU32() throws WireParseException {
    require(4);
    return byteBuffer.getInt() & 0xFFFFFFFFL;
  }

  /**
   * Reads a byte array of a specified length from the stream into an existing array.
   *
   * @param b The array to read into.
   * @param off The offset of the array to start copying data into.
   * @param len The number of bytes to copy.
   * @throws WireParseException The end of the stream was reached.
   */
  public void readByteArray(byte[] b, int off, int len) throws WireParseException {
    require(len);
    byteBuffer.get(b, off, len);
  }

  /**
   * Reads a byte array of a specified length from the stream.
   *
   * @return The byte array.
   * @throws WireParseException The end of the stream was reached.
   */
  public byte[] readByteArray(int len) throws WireParseException {
    require(len);
    byte[] out = new byte[len];
    byteBuffer.get(out, 0, len);
    return out;
  }

  /**
   * Reads a byte array consisting of the remainder of the stream (or the active region, if one is
   * set.
   *
   * @return The byte array.
   */
  public byte[] readByteArray() {
    int len = remaining();
    byte[] out = new byte[len];
    byteBuffer.get(out, 0, len);
    return out;
  }

  /**
   * Reads a counted string from the stream. A counted string is a one byte value indicating string
   * length, followed by bytes of data.
   *
   * @return A byte array containing the string.
   * @throws WireParseException The end of the stream was reached.
   */
  public byte[] readCountedString() throws WireParseException {
    int len = readU8();
    return readByteArray(len);
  }
}
