package morfologik.stemming;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.Arrays;

public final class BufferUtils {
  /**
   * No instances.
   */
  private BufferUtils() {
    // empty
  }

  /**
   * Ensure the buffer's capacity is large enough to hold a given number
   * of elements. If the input buffer is not large enough, a new buffer is allocated
   * and returned.
   * 
   * @param elements The required number of elements to be appended to the buffer.
   * 
   * @param buffer
   *          The buffer to check or <code>null</code> if a new buffer should be
   *          allocated.
   *
   * @return Returns the same buffer or a new buffer with the given capacity. 
   */
  public static ByteBuffer clearAndEnsureCapacity(ByteBuffer buffer, int elements) {
    if (buffer == null || buffer.capacity() < elements) {
      buffer = ByteBuffer.allocate(elements);
    } else {
      buffer.clear();
    }
    return buffer;
  }

  /**
   * Ensure the buffer's capacity is large enough to hold a given number
   * of elements. If the input buffer is not large enough, a new buffer is allocated
   * and returned.
   * 
   * @param elements The required number of elements to be appended to the buffer.
   * 
   * @param buffer
   *          The buffer to check or <code>null</code> if a new buffer should be
   *          allocated.
   *
   * @return Returns the same buffer or a new buffer with the given capacity. 
   */
  public static CharBuffer clearAndEnsureCapacity(CharBuffer buffer, int elements) {
    if (buffer == null || buffer.capacity() < elements) {
      buffer = CharBuffer.allocate(elements);
    } else {
      buffer.clear();
    }
    return buffer;
  }

  /**
   * @param buffer The buffer to convert to a string.
   * @param charset The charset to use when converting bytes to characters.
   * @return A string representation of buffer's content.
   */
  public static String toString(ByteBuffer buffer, Charset charset) {
    buffer = buffer.slice();
    byte [] buf = new byte [buffer.remaining()];
    buffer.get(buf);
    return new String(buf, charset);
  }

  public static String toString(CharBuffer buffer) {
    buffer = buffer.slice();
    char [] buf = new char [buffer.remaining()];
    buffer.get(buf);
    return new String(buf);
  }

  /**
   * @param buffer The buffer to read from.
   * @return Returns the remaining bytes from the buffer copied to an array.
   */
  public static byte[] toArray(ByteBuffer buffer) {
    byte [] dst = new byte [buffer.remaining()];
    buffer.mark();
    buffer.get(dst);
    buffer.reset();
    return dst;
  }

  /**
   * Compute the length of the shared prefix between two byte sequences.
   */
  static int sharedPrefixLength(ByteBuffer a, int aStart, ByteBuffer b, int bStart) {
    int i = 0;
    final int max = Math.min(a.remaining() - aStart, b.remaining() - bStart);
    aStart += a.position();
    bStart += b.position();
    while (i < max && a.get(aStart++) == b.get(bStart++)) {
      i++;
    }
    return i;
  }

  /**
   * Compute the length of the shared prefix between two byte sequences.
   */
  static int sharedPrefixLength(ByteBuffer a, ByteBuffer b) {
    return sharedPrefixLength(a, 0, b, 0);
  }

  /**
   * Convert byte buffer's content into characters. The input buffer's bytes are not
   * consumed (mark is set and reset).
   */
  public static CharBuffer bytesToChars(CharsetDecoder decoder, ByteBuffer bytes, CharBuffer chars) {
    assert decoder.malformedInputAction() == CodingErrorAction.REPORT;

    chars = clearAndEnsureCapacity(chars, (int) (bytes.remaining() * decoder.maxCharsPerByte()));

    bytes.mark();
    decoder.reset();
    CoderResult cr = decoder.decode(bytes, chars, true);
    if (cr.isError()) {
      bytes.reset();
      try {
        cr.throwException();
      } catch (CharacterCodingException e) {
        throw new RuntimeException("Input cannot be mapped to bytes using encoding "
            + decoder.charset().name() + ": " + Arrays.toString(toArray(bytes)), e);
      }
    }

    assert cr.isUnderflow();  // This should be guaranteed by ensuring max. capacity.
    cr = decoder.flush(chars);
    assert cr.isUnderflow();

    chars.flip();
    bytes.reset();

    return chars;
  }

  /**
   * Convert chars into bytes.
   */
  public static ByteBuffer charsToBytes(CharsetEncoder encoder, CharBuffer chars, ByteBuffer bytes) 
      throws UnmappableInputException {
    assert encoder.malformedInputAction() == CodingErrorAction.REPORT;

    bytes = clearAndEnsureCapacity(bytes, (int) (chars.remaining() * encoder.maxBytesPerChar()));

    chars.mark();
    encoder.reset();

    CoderResult cr = encoder.encode(chars, bytes, true);
    if (cr.isError()) {
      chars.reset();
      try {
        cr.throwException();
      } catch (CharacterCodingException e) {
        throw new UnmappableInputException("Input cannot be mapped to characters using encoding "
            + encoder.charset().name() + ": " + Arrays.toString(toArray(bytes)), e);
      }
    }

    assert cr.isUnderflow();  // This should be guaranteed by ensuring max. capacity.
    cr = encoder.flush(bytes);
    assert cr.isUnderflow();

    bytes.flip();
    chars.reset();

    return bytes;
  }
}
