package morfologik.stemming;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.*;

/**
 * Stem and tag data associated with a given word.
 * 
 * Instances of this class are reused and mutable (values
 * returned from {@link #getStem()}, {@link #getWord()}
 * and other related methods change on subsequent calls to 
 * {@link DictionaryLookup} class that returned a given
 * instance of {@link WordData}.
 * 
 * If you need a copy of the
 * stem or tag data for a given word, you have to create a custom buffer
 * yourself and copy the associated data, perform {@link #clone()} or create
 * strings (they are immutable) using {@link #getStem()} and then
 * {@link CharSequence#toString()}.
 * 
 * For reasons above it makes no sense to use instances
 * of this class in associative containers or lists. In fact,
 * both {@link #equals(Object)} and {@link #hashCode()} are overridden and throw 
 * exceptions to prevent accidental damage.
 */
public final class WordData implements Cloneable {
	/**
	 * Error information if somebody puts us in a Java collection.
	 */
	private static final String COLLECTIONS_ERROR_MESSAGE = "Not suitable for use"
	        + " in Java collections framework (volatile content). Refer to documentation.";

	/** Character encoding in internal buffers. */
	private final CharsetDecoder decoder;

	/**
	 * Inflected word form data.
	 */
	private CharSequence wordCharSequence;

	/**
	 * Character sequence after converting {@link #stemBuffer} using
	 * {@link #decoder}.
	 */
	private CharBuffer stemCharSequence;

	/**
	 * Character sequence after converting {@link #tagBuffer} using
	 * {@link #decoder}.
	 */
	private CharBuffer tagCharSequence;

	/** Byte buffer holding the inflected word form data. */
	ByteBuffer wordBuffer;

	/** Byte buffer holding stem data. */
	ByteBuffer stemBuffer;

	/** Byte buffer holding tag data. */
	ByteBuffer tagBuffer;

	/**
	 * Package scope constructor.
	 */
	WordData(CharsetDecoder decoder) {
		this.decoder = decoder;

		stemBuffer = ByteBuffer.allocate(0);
		tagBuffer = ByteBuffer.allocate(0);
		stemCharSequence = CharBuffer.allocate(0);
		tagCharSequence = CharBuffer.allocate(0);
	}

	/**
	 * A constructor for tests only.
	 */
	WordData(String stem, String tag, String encoding) {
		this(Charset.forName(encoding).newDecoder());

		try {
			if (stem != null)
				stemBuffer.put(stem.getBytes(encoding));
			if (tag != null)
				tagBuffer.put(tag.getBytes(encoding));
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Copy the stem's binary data (no charset decoding) to a custom byte
	 * buffer.
	 *
	 * The buffer is cleared prior to copying and flipped for reading
	 * upon returning from this method. If the buffer is null or not large 
	 * enough to hold the result, a new buffer is allocated.
	 * 
	 * @param target
	 *            Target byte buffer to copy the stem buffer to or
	 *            <code>null</code> if a new buffer should be allocated.
	 * 
	 * @return Returns <code>target</code> or the new reallocated buffer.
	 */
	public ByteBuffer getStemBytes(ByteBuffer target) {
		target = BufferUtils.clearAndEnsureCapacity(target, stemBuffer.remaining());
		stemBuffer.mark();
		target.put(stemBuffer);
		stemBuffer.reset();
		target.flip();
		return target;
	}

	/**
	 * Copy the tag's binary data (no charset decoding) to a custom byte buffer.
	 * 
   * The buffer is cleared prior to copying and flipped for reading
   * upon returning from this method. If the buffer is null or not large 
   * enough to hold the result, a new buffer is allocated.
	 * 
	 * @param target
	 *            Target byte buffer to copy the tag buffer to or
	 *            <code>null</code> if a new buffer should be allocated.
	 * 
	 * @return Returns <code>target</code> or the new reallocated buffer.
	 */
	public ByteBuffer getTagBytes(ByteBuffer target) {
		target = BufferUtils.clearAndEnsureCapacity(target, tagBuffer.remaining());
		tagBuffer.mark();
		target.put(tagBuffer);
		tagBuffer.reset();
		target.flip();
		return target;
	}

	/**
	 * Copy the inflected word's binary data (no charset decoding) to a custom
	 * byte buffer.
	 * 
   * The buffer is cleared prior to copying and flipped for reading
   * upon returning from this method. If the buffer is null or not large 
   * enough to hold the result, a new buffer is allocated. 
	 * 
	 * @param target
	 *            Target byte buffer to copy the word buffer to or
	 *            <code>null</code> if a new buffer should be allocated.
	 * 
	 * @return Returns <code>target</code> or the new reallocated buffer.
	 */
	public ByteBuffer getWordBytes(ByteBuffer target) {
		target = BufferUtils.clearAndEnsureCapacity(target, wordBuffer.remaining());
		wordBuffer.mark();
		target.put(wordBuffer);
		wordBuffer.reset();
		target.flip();
		return target;
	}

	/**
	 * @return Return tag data decoded to a character sequence or
	 *         <code>null</code> if no associated tag data exists.
	 */
	public CharSequence getTag() {
		tagCharSequence = BufferUtils.bytesToChars(decoder, tagBuffer, tagCharSequence);
		return tagCharSequence.remaining() == 0 ? null : tagCharSequence;
	}

	/**
	 * @return Return stem data decoded to a character sequence or
	 *         <code>null</code> if no associated stem data exists.
	 */
	public CharSequence getStem() {
		stemCharSequence = BufferUtils.bytesToChars(decoder, stemBuffer, stemCharSequence);
		return stemCharSequence.remaining() == 0 ? null : stemCharSequence;
	}

	/**
	 * @return Return inflected word form data. Usually the parameter passed to
	 *         {@link DictionaryLookup#lookup(CharSequence)}.
	 */
	public CharSequence getWord() {
		return wordCharSequence;
	}

	/*
     * 
     */
	@Override
	public boolean equals(Object obj) {
		throw new UnsupportedOperationException(COLLECTIONS_ERROR_MESSAGE);
	}

	/*
     * 
     */
	@Override
	public int hashCode() {
		throw new UnsupportedOperationException(COLLECTIONS_ERROR_MESSAGE);
	}

	@Override
	public String toString() {
	    return "WordData["
	        + this.getWord() + ","
	        + this.getStem() + ","
	        + this.getTag() + "]";
	}
	
	/**
	 * Declare a covariant of {@link Object#clone()} that returns a deep copy of
	 * this object. The content of all internal buffers is copied.
	 */
	@Override
	public WordData clone() {
		final WordData clone = new WordData(this.decoder);
		clone.wordCharSequence = cloneCharSequence(wordCharSequence);
		clone.wordBuffer = getWordBytes(null);
		clone.stemBuffer = getStemBytes(null);
		clone.tagBuffer = getTagBytes(null);
		return clone;
	}

	/**
	 * Clone char sequences only if not immutable.
	 */
	private CharSequence cloneCharSequence(CharSequence chs) {
		if (chs instanceof String)
			return chs;
		return chs.toString();
	}

  void update(ByteBuffer wordBuffer, CharSequence word) {
    this.stemCharSequence.clear();
    this.tagCharSequence.clear();
    this.stemBuffer.clear();
    this.tagBuffer.clear();

    this.wordBuffer = wordBuffer;
    this.wordCharSequence = word;
  }
}
