/* See LICENSE for licensing and NOTICE for copyright. */
package org.cryptacular.x509.dn;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import javax.security.auth.x500.X500Principal;
import org.cryptacular.codec.HexEncoder;

/**
 * Produces a string representation of an X.500 distinguished name using the process described in section 2 of RFC 2253,
 * LADPv3 Distinguished Names.
 *
 * @author  Middleware Services
 */
public class LdapNameFormatter implements NameFormatter
{

  /** Separator character between RDN components. */
  public static final char RDN_SEPARATOR = ',';

  /** Separator character between ATV components in the same RDN element. */
  public static final char ATV_SEPARATOR = '+';

  /** Escape character. */
  public static final char ESCAPE_CHAR = '\\';

  /** String of characters that need to be escaped. */
  public static final String RESERVED_CHARS = ",+\"\\<>;";

  /** Handles hex encoding. */
  private static final HexEncoder ENCODER = new HexEncoder();


  @Override
  public String format(final X500Principal dn)
  {
    final StringBuilder builder = new StringBuilder();
    final RDNSequence sequence = NameReader.readX500Principal(dn);
    int i = 0;
    for (RDN rdn : sequence.backward()) {
      if (i++ > 0) {
        builder.append(RDN_SEPARATOR);
      }

      int j = 0;
      for (Attribute attr : rdn.getAttributes()) {
        if (j++ > 0) {
          builder.append(ATV_SEPARATOR);
        }
        builder.append(attr.getType()).append('=');

        final AttributeType type = attr.getType();
        if (type instanceof StandardAttributeType) {
          escape(attr.getValue(), builder);
        } else {
          encode(attr.getValue(), builder);
        }
      }
    }
    return builder.toString();
  }


  /**
   * Appends the given value to the output with proper character escaping.
   *
   * @param  value  Value to escape.
   * @param  output  String builder where escaped value is written.
   */
  private static void escape(final String value, final StringBuilder output)
  {
    char c = value.charAt(0);
    if (c == ' ' || c == '#') {
      output.append(ESCAPE_CHAR);
    }
    output.append(c);

    final int nmax = value.length() - 1;
    for (int n = 1; n < nmax; n++) {
      c = value.charAt(n);
      if (RESERVED_CHARS.indexOf(c) > -1) {
        output.append(ESCAPE_CHAR);
      }
      output.append(c);
    }
    c = value.charAt(nmax);
    if (c == ' ') {
      output.append(ESCAPE_CHAR);
    }
    output.append(c);
  }


  /**
   * Appends the given value to the output using the HEX encoding method described in section 2.4.
   *
   * @param  value  Value to encode.
   * @param  output  String builder where encoded value is written.
   */
  private static void encode(final String value, final StringBuilder output)
  {
    output.append('#');

    final byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
    final CharBuffer out = CharBuffer.allocate(bytes.length * 2);
    ENCODER.encode(ByteBuffer.wrap(bytes), out);
    output.append(out.flip());
  }
}
