/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive.ad;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

/**
 * Class to represent an active directory SID. Provides conversion from binary to string and vice versa.
 *
 * @author  Middleware Services
 */
public final class SecurityIdentifier
{


  /** Default constructor. */
  private SecurityIdentifier() {}


  /**
   * Converts the supplied SID to its string format.
   *
   * @param  sid  to convert
   *
   * @return  string format of the SID
   */
  public static String toString(final byte[] sid)
  {
    // CheckStyle:MagicNumber OFF

    // format of SID: S-R-X-Y1-Y2...-Yn
    // S: static 'S', indicating string
    // R: revision
    // X: authority
    // Yn: sub-authority

    // create a byte buffer for reading the sid
    final ByteBuffer sidBuffer = ByteBuffer.wrap(sid);

    // string identifier
    final StringBuilder sb = new StringBuilder("S");

    // byte[0] is the revision
    sb.append("-").append(sidBuffer.get() & 0xFF);

    // byte[1] is the count of sub-authorities
    final int countSubAuth = sidBuffer.get() & 0xFF;

    // byte[2] - byte[7] is the authority (48 bits)
    sidBuffer.limit(8);
    sb.append("-").append(getLong(sidBuffer, true));

    // byte[8] - ? is the sub-authorities,
    // (32 bits per authority, little endian)
    for (int i = 0; i < countSubAuth; i++) {
      // values are unsigned, so get 4 bytes as a long
      sidBuffer.limit(sidBuffer.position() + 4);
      sb.append("-").append(getLong(sidBuffer, false));
    }

    return sb.toString();
    // CheckStyle:MagicNumber ON
  }


  /**
   * Converts the supplied SID to its binary format.
   *
   * @param  sid  to convert
   *
   * @return  binary format of the SID
   */
  public static byte[] toBytes(final String sid)
  {
    // CheckStyle:MagicNumber OFF

    // format of SID: S-R-X-Y1-Y2...-Yn
    // S: static 'S', indicating string
    // R: revision
    // X: authority
    // Yn: sub-authority

    final StringTokenizer st = new StringTokenizer(sid, "-");
    // first token is the 'S'
    st.nextToken();

    // second token is the revision
    final int revision = Integer.valueOf(st.nextToken());
    // third token is the authority
    final long authority = Long.valueOf(st.nextToken());
    // remaining token are the sub authorities
    final List<String> subAuthorities = new ArrayList<>();
    while (st.hasMoreTokens()) {
      subAuthorities.add(st.nextToken());
    }

    // revision is 1 byte
    // sub-authorities count is 1 byte
    // authority is 6 bytes
    // 4 bytes for each sub-authority
    final int size = 8 + (4 * subAuthorities.size());
    final ByteBuffer sidBuffer = ByteBuffer.allocate(size);
    sidBuffer.put((byte) (revision & 0xFF));
    sidBuffer.put((byte) (subAuthorities.size() & 0xFF));
    sidBuffer.limit(8);
    putLong(sidBuffer, authority, true);
    for (String subAuthority : subAuthorities) {
      sidBuffer.limit(sidBuffer.position() + 4);
      putLong(sidBuffer, Long.valueOf(subAuthority), false);
    }

    return sidBuffer.array();
    // CheckStyle:MagicNumber ON
  }


  /**
   * Reads a long from the supplied byte buffer. The byte buffer limit must be set appropriately by the caller.
   *
   * @param  buffer  to read long from
   * @param  bigEndian  whether to read the bytes as big endian
   *
   * @return  long value
   */
  private static long getLong(final ByteBuffer buffer, final boolean bigEndian)
  {
    // CheckStyle:MagicNumber OFF
    long value = buffer.get() & 0xFF;
    if (bigEndian) {
      // shift the value to the right, or with the next byte
      while (buffer.hasRemaining()) {
        value <<= Byte.SIZE;
        value |= buffer.get() & 0xFF;
      }
    } else {
      // shift the next byte to the right, or with the value
      int offset = Byte.SIZE;
      while (buffer.hasRemaining()) {
        value |= (buffer.get() & 0xFF) << offset;
        offset += Byte.SIZE;
      }
    }
    return value & 0xFFFFFFFFL;
    // CheckStyle:MagicNumber ON
  }


  /**
   * Writes a long into the supplied byte buffer. The byte buffer limit must be set appropriately by the caller.
   *
   * @param  buffer  to write long to
   * @param  value  to write
   * @param  bigEndian  whether to write the bytes as big endian
   */
  private static void putLong(final ByteBuffer buffer, final long value, final boolean bigEndian)
  {
    // CheckStyle:MagicNumber OFF
    if (bigEndian) {
      int offset = Byte.SIZE * (buffer.limit() - buffer.position() - 1);
      while (buffer.hasRemaining()) {
        // get the high bits and decrement down
        buffer.put((byte) ((value >> offset) & 0xFF));
        offset -= Byte.SIZE;
      }
    } else {
      int offset = 0;
      while (buffer.hasRemaining()) {
        // get the low bits and increment up
        buffer.put((byte) ((value >> offset) & 0xFF));
        offset += Byte.SIZE;
      }
    }
    // CheckStyle:MagicNumber ON
  }
}
