package brn.swans.route;

import jist.swans.Constants;
import jist.swans.misc.Message;
import jist.swans.net.NetAddress;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

/**
 * A McExOR message.
 *
 * @author Zubow
 */
public class RouteMcExORMsg implements Message, Cloneable {
  // ////////////////////////////////////////////////
  // constants
  //

  // Option types

  /**
   * Type of a Route Request option.
   */
  public static final byte OPT_ROUTE_REQUEST = 2;

  /**
   * Type of a Route Reply option.
   */
  public static final byte OPT_ROUTE_REPLY = 1;

  /**
   * Type of a Route Error option.
   */
  public static final byte OPT_ROUTE_ERROR = 3;

  /**
   * Type of a Source Route option.
   */
  public static final byte OPT_SOURCE_ROUTED = 96;

  /**
   * Type of a Data with no source route option.
   */
  public static final byte OPT_DATA_NO_ROUTE = (byte) 224;

  /**
   * Type of a Flooding option.
   */
  public static final byte OPT_FLOODING = 4;

  /**
   * Type of a PadN option.
   */
  public static final byte OPT_PADN = 0;

  // Error types

  /**
   * Error code for "unreachable node".
   */
  public static final byte ERROR_NODE_UNREACHABLE = 1;

  /**
   * Error code for "flow state not supported".
   */
  public static final byte ERROR_FLOW_STATE_NOT_SUPPORTED = 2;

  /**
   * Error code for "option not supported".
   */
  public static final byte ERROR_OPTION_NOT_SUPPORTED = 3;

  // Ways of dealing with unrecognized options

  /**
   * Code for ignoring unrecognized options.
   */
  public static final int UNRECOGNIZED_OPT_IGNORE = 0;

  /**
   * Code for removing unrecognized options.
   */
  public static final int UNRECOGNIZED_OPT_REMOVE = 1;

  /**
   * Code for marking unrecognized options.
   */
  public static final int UNRECOGNIZED_OPT_MARK = 2;

  /**
   * Code for dropping unrecognized options.
   */
  public static final int UNRECOGNIZED_OPT_DROP = 3;

  // ////////////////////////////////////////////////
  // locals
  //

  /**
   * The payload of the McExOR message.
   */
  private Message content;

  /**
   * The list of options contained in the McExOR header.
   */
  private ArrayList options;

  /**
   * The protocol number of the message contained in this McExOR header.
   */
  private short nextHeaderType = Constants.NET_PROTOCOL_NO_NEXT_HEADER;

  // ////////////////////////////////////////////////
  // initialization
  //

  /**
   * Creates a new <code>RouteMcExORMsg</code> with the given payload.
   *
   * @param content the payload of the McExOR message. This can be <code>null</code>.
   */
  public RouteMcExORMsg(Message content) {
    this.content = content;
    this.options = new ArrayList();
  }

  // ////////////////////////////////////////////////
  // helpers
  //

  /**
   * Returns the options contained in the McExOR header.
   *
   * @return <code>List</code> of options.
   */
  public List getOptions() {
    return options;
  }

  /**
   * Returns the the content of the McExOR message.
   *
   * @return the content of the McExOR message, or <code>null</code> if none.
   */
  public Message getContent() {
    return content;
  }

  /**
   * Returns the network protocol number of the content of the McExOR message.
   *
   * @return the protocol number of the content of the McExOR message. This is
   *         equal to IPv6's No Next Header protocol number if the McExOR header
   *         has no content.
   */
  public short getNextHeaderType() {
    return nextHeaderType;
  }

  /**
   * Sets the protocol number of the content of the McExOR message.
   *
   * @param nextHeaderType the protocol number of the content of the McExOR message.
   */
  public void setNextHeaderType(short nextHeaderType) {
    this.nextHeaderType = nextHeaderType;
  }

  /**
   * {@inheritDoc}
   */
  public void getBytes(byte[] buf, int offset) {
    // Copy the fixed part of the McExOR Options header
    if (buf.length - offset > 0) {
      buf[offset++] = (byte) nextHeaderType;
    }

    if (buf.length - offset > 0) {
      buf[offset++] = 0;
    }

    if (buf.length - offset > 0) {
      buf[offset++] = (byte) ((getOptionsSize() >> 8) & 0xFF);
    }

    if (buf.length - offset > 0) {
      buf[offset++] = (byte) (getOptionsSize() & 0xFF);
    }

    // Copy the options
    for (int i = 0; i < options.size(); i++) {
      byte[] option = (byte[]) options.get(i);
      int bytesToCopy = Math.min(buf.length - offset, option.length);

      System.arraycopy(buf, offset, option, 0, bytesToCopy);
      offset += bytesToCopy;
    }

    // Copy the remainder of the message
    if (content != null) {
      content.getBytes(buf, offset);
    }
  }

  /**
   * {@inheritDoc}
   */
  public int getSize() {
    int headerSize = 4 + getOptionsSize();

    return headerSize + (content == null ? 0 : content.getSize());
  }

  /**
   * Adds a new option to the McExOR message.
   *
   * @param opt the option to add
   */
  public void addOption(byte[] opt) {
    options.add(opt);
  }

  /**
   * Determines whether the McExOR header contains an option of the given type.
   *
   * @param optType the type of option to search for
   * @return whether the McExOR header contains an option of type
   *         <code>optType</code>.
   */
  public boolean hasOption(byte optType) {
    Iterator iter = options.iterator();

    while (iter.hasNext()) {
      byte[] opt = (byte[]) iter.next();
      if (opt[0] == optType)
        return true;
    }

    return false;
  }

  /**
   * Determines the size in bytes of the options contained in this McExOR
   * message.
   *
   * @return the size in bytes of the options contained in this McExOR message.
   *         This value does not include the size of the fixed portion of the
   *         McExOR header or the payload of the message.
   */
  private int getOptionsSize() {
    int optionsSize = 0;

    for (int i = 0; i < options.size(); i++) {
      optionsSize += ((byte[]) options.get(i)).length;
    }

    return optionsSize;
  }

  /**
   * {@inheritDoc}
   */
  public Object clone() {
    // Note that this clone is fairly shallow -- it doesn't copy the
    // content of the McExOR message, and the individual options are
    // not cloned either.
    RouteMcExORMsg copy = new RouteMcExORMsg(content);
    copy.options = (ArrayList) options.clone();
    copy.nextHeaderType = nextHeaderType;
    return copy;
  }

  /**
   * Extract the sequence number of the packet, if available.
   *
   * @return the sequence number (equal/bigger than 0), or -1 if not found.
   */
  public int getSeqNr() {
    Iterator iter = this.getOptions().iterator();
    RouteMcExORMsg.OptionDataNoRoute data = null;

    while (iter.hasNext()) {
      byte[] optBuf = (byte[]) iter.next();
      RouteMcExORMsg.Option opt = RouteMcExORMsg.Option.create(optBuf, 0);

      if (opt == null) {
        // This should never happen in the simulation
        throw new RuntimeException("Unrecognized ExOR Option");
      }

      if (opt.getType() == RouteMcExORMsg.OPT_DATA_NO_ROUTE) { // data packets
        // are routed
        // opportunistic
        data = (RouteMcExORMsg.OptionDataNoRoute) opt;
        return data.getSeqNum();
      }
    }
    return -1;
  }

  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    final RouteMcExORMsg that = (RouteMcExORMsg) o;

    if (nextHeaderType != that.nextHeaderType) return false;
    if (content != null ? !content.equals(that.content) : that.content != null) return false;
    if (options != null ? !options.equals(that.options) : that.options != null) return false;

    return true;
  }

  public int hashCode() {
    int result;
    result = (content != null ? content.hashCode() : 0);
    result = 29 * result + (options != null ? options.hashCode() : 0);
    result = 29 * result + (int) nextHeaderType;
    return result;
  }

  // ////////////////////////////////////////////////
  // internal classes
  //

  /**
   * The base class for all McExOR header options.
   */
  public abstract static class Option {
    /**
     * The raw bytes of the option.
     */
    protected byte[] optBuf;

    /**
     * The offset into <code>optbuf</code> where the option encoding begins.
     */
    protected int optBufOffset;

    /**
     * Creates a new option from the given byte array, starting at the given
     * offset.
     *
     * @param buf    the buffer containing the option
     * @param offset the offset into <code>buf</code> where the option begins
     */
    public Option(byte[] buf, int offset) {
      optBuf = buf;
      optBufOffset = offset;
    }

    /**
     * Returns the McExOR type code for this kind of option.
     *
     * @return the McExOR type code for this kind of option.
     */
    public abstract byte getType();

    /**
     * Returns the size in bytes of this option.
     *
     * @return the size in bytes of this option.
     */
    public abstract int getSize();

    /**
     * Returns the McExOR type code for the given option.
     *
     * @param buf a McExOR option in raw byte form
     * @return the McExOR type code of <code>buf</code>.
     */
    public static byte getType(byte[] buf) {
      return buf[0];
    }

    /**
     * Retrieves the actual bytes of the option. Copies up to the end of the
     * buffer or the end of the option, whichever comes first.
     *
     * @param buf    the buffer to put the option bytes in
     * @param offset the offset into the buffer to start writing at
     * @return the number of bytes written.
     */
    public int getBytes(byte[] buf, int offset) {
      int numBytesToCopy = Math.min(getSize(), buf.length - offset);
      System.arraycopy(optBuf, optBufOffset, buf, offset, Math.min(getSize(),
              numBytesToCopy));
      return numBytesToCopy;
    }

    /**
     * {@inheritDoc}
     */
    public boolean equals(Object o) {
      if (!(o instanceof Option)) {
        return false;
      }

      Option opt = (Option) o;
      return optBufOffset == opt.optBufOffset
              && Arrays.equals(optBuf, opt.optBuf);
    }

    /**
     * {@inheritDoc}
     */
    public int hashCode() {
      // I don't think this method ever gets used
      return optBuf.hashCode();
    }

    /**
     * Creates a new <code>Option</code> from the given byte array, starting
     * at the given offset.
     *
     * @param buf    the McExOR option in raw byte form
     * @param offset the offset into <code>buf</code> where the option begins
     * @return the McExOR option encoded in <code>buf</code>, or
     *         <code>null</code> if <code>buf</code> is not a recognized
     *         option.
     */
    public static Option create(byte[] buf, int offset) {
      switch (buf[0]) {
        case OPT_ROUTE_REQUEST:
          return new OptionRouteRequest(buf, offset);

        case OPT_ROUTE_REPLY:
          return new OptionRouteReply(buf, offset);

        case OPT_ROUTE_ERROR:
          return new OptionRouteError(buf, offset);

        case OPT_SOURCE_ROUTED:
          return new OptionSourceRouted(buf, offset);

        case OPT_DATA_NO_ROUTE:
          return new OptionDataNoRoute(buf, offset);

        case OPT_PADN:
          return new OptionPadN(buf, offset);

        default:
          return null;
      }
    }
  }

  /**
   * A Route Request option.
   */
  public static class OptionRouteRequest extends Option {
    /**
     * {@inheritDoc}
     */
    public OptionRouteRequest(byte[] buf, int offset) {
      super(buf, offset);
    }

    /**
     * {@inheritDoc}
     */
    public byte getType() {
      return OPT_ROUTE_REQUEST;
    }

    /**
     * {@inheritDoc}
     */
    public int getSize() {
      // Defeat sign-extension and add 2 for the Option Type and Opt Data Len
      // fields
      return (optBuf[optBufOffset + 1] & 0x000000FF) + 2;
    }

    /**
     * Returns the identification number of this route request.
     *
     * @return the identification number of this route request.
     */
    public short getId() {
      // Why doesn't Java have unsigned integer types?
      short highOrderByte = (short) (optBuf[optBufOffset + 2] & 0x000000FF);
      short lowOrderByte = (short) (optBuf[optBufOffset + 3] & 0x000000FF);
      return (short) ((highOrderByte << 8) | lowOrderByte);
    }

    /**
     * Returns the target address of this route request.
     *
     * @return the target address of this route request.
     */
    public NetAddress getTargetAddress() {
      return new NetAddress(new byte[]{optBuf[optBufOffset + 4],
              optBuf[optBufOffset + 5], optBuf[optBufOffset + 6],
              optBuf[optBufOffset + 7]});
    }

    /**
     * Returns the number of addresses listed in the route request option (not
     * includingd the target address).
     *
     * @return the number of addresses listed in the route request option (not
     *         including the target address).
     */
    public int getNumAddresses() {
      return (getSize() - 8) / 5;
    }

    /**
     * Returns the nth route entry listed in the route request option, counting from
     * zero.
     *
     * @param n the index into the Route Request
     * @return the address indexed by <code>n</code>.
     */
    public RouteMcExOR.RouteEntry getAddress(int n) {
      if (n >= getNumAddresses())
        throw new IndexOutOfBoundsException();

      int addressOffset = optBufOffset + 8 + 5 * n;

      NetAddress addr = new NetAddress(new byte[]{optBuf[addressOffset],
              optBuf[addressOffset + 1], optBuf[addressOffset + 2],
              optBuf[addressOffset + 3]});

      return new RouteMcExOR.RouteEntry(addr, optBuf[addressOffset + 4]);
    }

    /**
     * Creates a new Route Request option.
     *
     * @param id     the identification number of the route request
     * @param target the address of the node being searched for
     * @param addrs  the addresses of the nodes that have forwarded this request
     * @return the byte array corresponding to the desired option.
     */
    public static byte[] create(short id, NetAddress target, RouteMcExOR.RouteEntry[] addrs) {
      byte[] opt = new byte[8 + (4 + 1) * addrs.length]; // ip + metric

      // Set the Option Type and Option Data Length fields
      opt[0] = OPT_ROUTE_REQUEST;
      opt[1] = (byte) (opt.length - 2);

      // Set the identification field
      opt[2] = (byte) (id >>> 8);
      opt[3] = (byte) (id & 0xFF);

      // Set the target and intermediate addresses
      System.arraycopy(target.toIP().getAddress(), 0, opt, 4, 4);

      for (int i = 0; i < addrs.length; i++) {
        System.arraycopy(addrs[i].addr.toIP().getAddress(), 0, opt, 5 * (i + 2) - 2, 4);
        opt[5 * (i + 2) + 2] = addrs[i].metric;
      }

      return opt;
    }
  }

  /**
   * A Route Reply option.
   */
  public static class OptionRouteReply extends Option {
    /**
     * {@inheritDoc}
     */
    public OptionRouteReply(byte[] buf, int offset) {
      super(buf, offset);
    }

    /**
     * {@inheritDoc}
     */
    public byte getType() {
      return OPT_ROUTE_REPLY;
    }

    /**
     * {@inheritDoc}
     */
    public int getSize() {
      // Defeat sign-extension and add 2 for the Option Type and Opt Data Len
      // fields
      return (optBuf[optBufOffset + 1] & 0x000000FF) + 2;
    }

    /**
     * Determines whether the last hop of the path is external.
     *
     * @return whether the last hop of this route is external.
     */
    public boolean isLastHopExternal() {
      return (optBuf[optBufOffset + 2] & 0x80) != 0;
    }

    /**
     * Returns the number of addresses listed in the Route Reply option.
     *
     * @return the number of addresses listed in the Route Reply option.
     */
    public int getNumAddresses() {
      return (getSize() - 3) / 5;
    }

    /**
     * Returns the nth address listed in the route request option, counting from
     * zero.
     *
     * @param n the index into the route
     * @return the address indexed by <code>n</code>.
     */
    public RouteMcExOR.RouteEntry getAddress(int n) {
      if (n >= getNumAddresses())
        throw new IndexOutOfBoundsException();

      int addressOffset = optBufOffset + 3 + 5 * n;
      NetAddress addr = new NetAddress(new byte[]{optBuf[addressOffset],
              optBuf[addressOffset + 1], optBuf[addressOffset + 2],
              optBuf[addressOffset + 3]});

      return new RouteMcExOR.RouteEntry(addr, optBuf[addressOffset + 4]);
    }

    /**
     * Creates a new Route Reply option.
     *
     * @param lastHopExternal whether the last hop in the route is external
     * @param addrs           the addresses of the nodes in the route
     * @return the byte array corresponding to the desired option.
     */
    public static byte[] create(boolean lastHopExternal, RouteMcExOR.RouteEntry[] addrs) {
      byte[] opt = new byte[3 + (4 + 1) * addrs.length]; // ip + metric

      // Set the Option Type and Option Data Length fields
      opt[0] = OPT_ROUTE_REPLY;
      opt[1] = (byte) (opt.length - 2);

      // Set the Last Hop External and Reserved fields
      opt[3] = lastHopExternal ? (byte) 0x80 : 0x00;

      // Set the route addresses
      for (int i = 0; i < addrs.length; i++) {
        System.arraycopy(addrs[i].addr.toIP().getAddress(), 0, opt, 3 + 5 * i, 4);
        opt[3 + 5 * i + 4] = addrs[i].metric;
      }

      return opt;
    }

    /**
     * Creates a new Route Reply option with the Last Hop External field set to
     * false.
     *
     * @param addrs the addresses of the nodes in the route
     * @return the byte array corresponding to the desired option.
     */
    public static byte[] create(RouteMcExOR.RouteEntry[] addrs) {
      return create(false, addrs);
    }
  }

  /**
   * A Route Error option.
   */
  public static class OptionRouteError extends Option {
    /**
     * {@inheritDoc}
     */
    public OptionRouteError(byte[] buf, int offset) {
      super(buf, offset);
    }

    /**
     * {@inheritDoc}
     */
    public byte getType() {
      return OPT_ROUTE_ERROR;
    }

    /**
     * {@inheritDoc}
     */
    public int getSize() {
      // Defeat sign-extension and add 2 for the Option Type and Opt Data Len
      // fields
      return (optBuf[optBufOffset + 1] & 0x000000FF) + 2;
    }

    /**
     * Returns the error code contained in this Route Error.
     *
     * @return the error code contained in this Route Error.
     */
    public int getErrorType() {
      return optBuf[optBufOffset + 2] & 0xFF;
    }

    /**
     * Returns the salvage count of this Route Error.
     *
     * @return the salvage count of this Route Error.
     */
    public int getSalvageCount() {
      return optBuf[optBufOffset + 3] & 0xF;
    }

    /**
     * Returns the source of this Route Error.
     *
     * @return the source of this Route Error.
     */
    public NetAddress getSourceAddress() {
      return new NetAddress(new byte[]{optBuf[optBufOffset + 4],
              optBuf[optBufOffset + 5], optBuf[optBufOffset + 6],
              optBuf[optBufOffset + 7]});
    }

    /**
     * Returns the destination of this Route Error.
     *
     * @return the destination of this Route Error.
     */
    public NetAddress getDestAddress() {
      return new NetAddress(new byte[]{optBuf[optBufOffset + 8],
              optBuf[optBufOffset + 9], optBuf[optBufOffset + 10],
              optBuf[optBufOffset + 11]});
    }

    /**
     * Returns the size in bytes of any type-specific information contained in
     * this Route Error.
     *
     * @return the size in bytes of the type-specific information.
     */
    public int getTypeSpecificInfoSize() {
      return getSize() - 12;
    }

    /**
     * Returns the type-specific information contained in this Route Error.
     *
     * @param buf    the byte array to copy the type-specific information into
     * @param offset the offset into <code>buf</code> where copying should begin
     * @return the number of bytes copied into <code>buf</code>.
     */
    public int getTypeSpecificInfoBytes(byte[] buf, int offset) {
      int numBytesToCopy = Math.min(getTypeSpecificInfoSize(), buf.length
              - offset);
      System.arraycopy(optBuf, optBufOffset + 12, buf, offset, Math.min(
              getSize(), numBytesToCopy));
      return numBytesToCopy;
    }

    /**
     * Creates a new Route Error option.
     *
     * @param type    the code corresponding to the type of error being reported
     * @param salvage the salvage count
     * @param src     the source address of the error
     * @param dest    the destination address of the error
     * @param tsi     any type-specific information for this error
     * @return the byte array corresponding to the desired option.
     */
    public static byte[] create(byte type, int salvage, NetAddress src,
                                NetAddress dest, byte[] tsi) {
      byte[] opt = new byte[12 + tsi.length];

      // Set the Option Type, Option Data Length, and Error Type fields
      opt[0] = OPT_ROUTE_ERROR;
      opt[1] = (byte) (opt.length - 2);
      opt[2] = type;

      // Set the Salvage and Reserved fields.
      if (salvage < 0 || salvage > 0xF)
        throw new IllegalArgumentException("Salvage count too high");

      opt[3] = (byte) salvage;

      // Set the source and destination fields
      System.arraycopy(src.toIP().getAddress(), 0, opt, 4, 4);
      System.arraycopy(dest.toIP().getAddress(), 0, opt, 8, 4);

      // Set the Type-Specific Information field
      System.arraycopy(tsi, 0, opt, 12, tsi.length);

      return opt;
    }
  }

  /**
   * A source routed option; used by route reply messages.
   */
  public static class OptionSourceRouted extends Option {
    /**
     * {@inheritDoc}
     */
    public OptionSourceRouted(byte[] buf, int offset) {
      super(buf, offset);
    }

    /**
     * {@inheritDoc}
     */
    public byte getType() {
      return OPT_SOURCE_ROUTED;
    }

    /**
     * {@inheritDoc}
     */
    public int getSize() {
      return (optBuf[optBufOffset + 1] & 0x000000FF) + 2;
    }

    /**
     * Indicates whether the first hop on the route is external to the McExOR
     * network.
     *
     * @return whether the first hop is external.
     */
    public boolean isFirstHopExternal() {
      return (optBuf[optBufOffset + 2] & 0x80) != 0;
    }

    /**
     * Indicates whether the last hop on the route is external to the McExOR
     * network.
     *
     * @return whether the last hop is external.
     */
    public boolean isLastHopExternal() {
      return (optBuf[optBufOffset + 2] & 0x40) != 0;
    }

    /**
     * Returns the salvage count of this option.
     *
     * @return the salvage count of this option.
     */
    public int getSalvageCount() {
      // The salvage count overlaps the third and fourth byte
      int b3 = optBuf[optBufOffset + 2] & 0xFF;
      int b4 = optBuf[optBufOffset + 3] & 0xFF;
      return ((b3 & 0x3) << 2) | ((b4 & 0xC0) >> 6);
    }

    /**
     * Returns the number of segments on the source route that have yet to be
     * traversed.
     *
     * @return the number of segments on the route left to go.
     */
    public int getNumSegmentsLeft() {
      return optBuf[optBufOffset + 3] & 0x3F;
    }

    /**
     * Returns the number of addresses in the route, not including the source
     * and destination addresses.
     *
     * @return the number of addresses in the route.
     */
    public int getNumAddresses() {
      return (getSize() - 4) / 5;
    }

    /**
     * Returns the nth address on the route, counting the first hop after the
     * source as node zero.
     *
     * @param n the index into the route
     * @return the address in the route indexed by <code>n</code>.
     */
    public RouteMcExOR.RouteEntry getAddress(int n) {
      if (n >= getNumAddresses())
        throw new IndexOutOfBoundsException();

      int addressOffset = optBufOffset + 4 + 5 * n;
      NetAddress addr = new NetAddress(new byte[]{optBuf[addressOffset],
              optBuf[addressOffset + 1], optBuf[addressOffset + 2],
              optBuf[addressOffset + 3]});

      return new RouteMcExOR.RouteEntry(addr, optBuf[addressOffset + 4]);
    }

    /**
     * Creates a new Source Route option.
     *
     * @param firstHopExternal whether the first hop on the route is external
     * @param lastHopExternal  whether the last hop on the route is external
     * @param salvage          the salvage count
     * @param segsLeft         the number of explicitly listed intermediate nodes still to be
     *                         visited before reaching the final destination
     * @param addrs            the addresses of the nodes along the route
     * @return The byte array corresponding to the desired option.
     */
    public static byte[] create(boolean firstHopExternal,
                                boolean lastHopExternal, int salvage, int segsLeft, RouteMcExOR.RouteEntry[] addrs) {
      byte[] opt = new byte[4 + (4 + 1) * addrs.length];

      // Set the Option Type and Option Data Length fields
      opt[0] = OPT_SOURCE_ROUTED;
      opt[1] = (byte) (opt.length - 2);

      // Set the First Hop External, Last Hop External, and Reserved fields,
      // along
      // with the upper two bits of the Salvage field
      if (salvage < 0 || salvage > 0xF)
        throw new IllegalArgumentException("Salvage count too high");

      opt[2] = 0;
      if (firstHopExternal)
        opt[2] |= 0x80;
      if (lastHopExternal)
        opt[2] |= 0x40;
      opt[2] |= (byte) (salvage >> 2);

      // Set the rest of the Salvage field and the Segments Left field
      if (segsLeft < 0 || segsLeft > 0x3F)
        throw new IllegalArgumentException("Segments Left count too high");

      opt[3] = 0;
      opt[3] |= (byte) ((salvage << 6) & 0xC0);
      opt[3] |= segsLeft;

      // Set the addresses
      for (int i = 0; i < addrs.length; i++) {
        System.arraycopy(addrs[i].addr.toIP().getAddress(), 0, opt, 4 + 5 * i, 4);
        opt[4 + 5 * i + 4] = addrs[i].metric;
      }

      return opt;
    }

    /**
     * Creates a new Source Route option with the First Hop External and the
     * Last Hop External fields set to false.
     *
     * @param salvage  the salvage count
     * @param segsLeft the number of explicitly listed intermediate nodes still to be
     *                 visited before reaching the final destination
     * @param addrs    the addresses of the nodes along the route
     * @return the byte array corresponding to the desired option.
     */
    public static byte[] create(int salvage, int segsLeft, RouteMcExOR.RouteEntry[] addrs) {
      return create(false, false, salvage, segsLeft, addrs);
    }
  }

  /**
   * A Pad1 option. This represents one byte of padding in the McExOR header.
   */
  public static class OptionDataNoRoute extends Option {

    /**
     * Packet sequence number limit.
     */
    public static final int MAX_SEQ = Integer.MAX_VALUE;

    /**
     * {@inheritDoc}
     */
    public OptionDataNoRoute(byte[] buf, int offset) {
      super(buf, offset);
    }

    /**
     * {@inheritDoc}
     */
    public byte getType() {
      return OPT_DATA_NO_ROUTE;
    }

    /**
     * {@inheritDoc}
     */
    public int getSize() {
      // Pad1 option is always one byte long
      return optBuf.length;
    }

    public int getSeqNum() {
      ByteBuffer bb = ByteBuffer.wrap(optBuf);
      return bb.getInt(optBufOffset + 1);
    }

    public byte[] getUsedChannels() {

      if (optBuf.length <= /*Integer.SIZE*/32 / 8 + 1)
        return null;

      int len = optBuf[/*Integer.SIZE*/32 / 8 + 1];
      byte[] usedChannels = new byte[len];

      for (int i = 0; i < len; i++) {
        usedChannels[i] = optBuf[/*Integer.SIZE*/32 / 8 + 2 + i];
      }
      return usedChannels;
    }

    public static byte[] create(int seq) {
      return create(seq, null);
    }

    /**
     * Creates a new Pad1 option.
     *
     * @return a byte array corresponding to the desired option.
     */
    public static byte[] create(int seq, byte[] usedChannels) {
      int chLen = (usedChannels != null) ? usedChannels.length + 1 : 1;
      byte[] opt = new byte[1 + /*Integer.SIZE*/32 / 8 + chLen];

      // Set the Option Type and Option Data Length fields
      opt[0] = OPT_DATA_NO_ROUTE;

      byte[] buf = new byte[/*Integer.SIZE*/32 / 8];
      ByteBuffer bb = ByteBuffer.wrap(buf);
      bb.putInt(seq);

      System.arraycopy(buf, 0, opt, 1, buf.length);

      if (usedChannels != null) {
        opt[buf.length + 1] = (byte) usedChannels.length; // channel list length

        System.arraycopy(usedChannels, 0, opt, 2 + buf.length, usedChannels.length);
      }

      return opt;
    }
  }

  /**
   * A PadN option. This represents N bytes of padding in the McExOR header.
   */
  public static class OptionPadN extends Option {
    /**
     * {@inheritDoc}
     */
    public OptionPadN(byte[] buf, int offset) {
      super(buf, offset);
    }

    /**
     * {@inheritDoc}
     */
    public byte getType() {
      return OPT_PADN;
    }

    /**
     * {@inheritDoc}
     */
    public int getSize() {
      return (optBuf[optBufOffset + 1] & 0x000000FF) + 2;
    }

    /**
     * Creates a new PadN option.
     *
     * @param len the length of the option in bytes, not including the Option Type
     *            or Option Data Length fields
     * @return a byte array corresponding to the desired option.
     */
    public static byte[] create(byte len) {
      byte[] opt = new byte[len + 2];

      // Set the Option Type and Option Data Length fields
      opt[0] = OPT_PADN;
      opt[1] = len;

      // The rest of the option is already set to zero, thanks to Java
      return opt;
    }
  }
}
