/*******************************************************************************
 * Copyright (c) 2010, 2011 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 ******************************************************************************/
package org.eclipse.equinox.bidi.internal;

import java.lang.ref.SoftReference;
import org.eclipse.equinox.bidi.StructuredTextTypeHandlerFactory;

/**
 * Records strings which contain structured text. Several static
 * methods in this class allow to record such strings in a pool, and to find if
 * a given string is member of the pool.
 * <p>
 * Instances of this class are the records which are members of the pool.
 * </p><p>
 * The pool is managed as a cyclic list. When the pool is full,
 * each new element overrides the oldest element in the list.
 * </p><p>
 * A string may be itself entirely a structured text, or it may contain
 * segments each of which is a structured text of a given type. Each such
 * segment is identified by its starting and ending offsets within the
 * string, and by the handler which is appropriate to handle it.
 */
public class StructuredTextStringRecord {
	/**
	 * Number of entries in the pool of recorded strings
	 */
	public static final int POOLSIZE = 100;

	// maximum index allowed
	private static final int MAXINDEX = POOLSIZE - 1;

	// index of the last entered record
	private static int last = -1;

	// flag indicating that the pool has wrapped around
	private static boolean wrapAround;

	// the pool
	@SuppressWarnings("unchecked")
	private static SoftReference<StructuredTextStringRecord>[] recordRefs = new SoftReference[POOLSIZE];

	// hash code of the recorded strings
	private static int[] hashArray = new int[POOLSIZE];

	// total number of segments in the record
	private int totalSegmentCount;

	// number of used segments in the record
	private int usedSegmentCount;

	// reference to the recorded string
	private String string;

	// reference to the handlers of the STT segments in the recorded string
	private String[] handlers;

	// reference to the boundaries of the STT segments in the recorded string
	// (entries 0, 2, 4, ... are start offsets; entries 1, 3, 5, ... are
	// ending offsets)
	private short[] boundaries;

	/**
	 * Constructor
	 */
	private StructuredTextStringRecord() {
		// inhibit creation of new instances by customers
	}

	/**
	 * Records a string in the pool. The caller must specify the number
	 * of segments in the record (at least 1), and the handler, starting
	 * and ending offsets for the first segment.
	 *
	 * @param  string the string to record.
	 * @param  segmentCount number of segments allowed in this string.
	 *         This number must be >= 1.
	 * @param  handlerID identifier for the handler appropriate to handle 
	 *         the type of structured text present in the first segment.
	 *         It may be one of the predefined identifiers in 
	 *         {@link StructuredTextTypeHandlerFactory}, or it may be an identifier
	 *         for a type handler created by a plug-in or by the application.
	 * @param  start offset in the string of the starting character of the first
	 *         segment. It must be >= 0 and less than the length of the string.
	 * @param  limit offset of the character following the first segment. It
	 *         must be greater than the <code>start</code> argument and
	 *         not greater than the length of the string.
	 *
	 * @return an instance of StructuredTextRecordString which represents this record.
	 *         This instance may be used to specify additional segments with
	 *         {@link #addSegment addSegment}.
	 *
	 * @throws IllegalArgumentException if <code>string</code> is null or
	 *         if <code>segmentCount</code> is less than 1.
	 * @throws IllegalArgumentException if <code>handlerID</code> is null,
	 *         or if <code>start</code> or <code>limit</code> have invalid
	 *         values.
	 * @throws IllegalStateException if the current segment exceeds the
	 *         number of segments specified by <code>segmentCount</code>
	 *         in the call to {@link #addRecord addRecord} which created
	 *         the StructuredTextStringRecord instance.
	 */
	public static StructuredTextStringRecord addRecord(String string, int segmentCount, String handlerID, int start, int limit) {
		if (string == null)
			throw new IllegalArgumentException("The string argument must not be null!"); //$NON-NLS-1$
		if (segmentCount < 1)
			throw new IllegalArgumentException("The segment count must be at least 1!"); //$NON-NLS-1$
		synchronized (recordRefs) {
			if (last < MAXINDEX)
				last++;
			else {
				wrapAround = true;
				last = 0;
			}
		}
		StructuredTextStringRecord record = null;
		if (recordRefs[last] != null)
			record = recordRefs[last].get();
		if (record == null) {
			record = new StructuredTextStringRecord();
			recordRefs[last] = new SoftReference<>(record);
		}
		hashArray[last] = string.hashCode();
		for (int i = 0; i < record.usedSegmentCount; i++)
			record.handlers[i] = null;
		if (segmentCount > record.totalSegmentCount) {
			record.handlers = new String[segmentCount];
			record.boundaries = new short[segmentCount * 2];
			record.totalSegmentCount = segmentCount;
		}
		record.usedSegmentCount = 0;
		record.string = string;
		record.addSegment(handlerID, start, limit);
		return record;
	}

	/**
	 * Adds a second or further segment to a record.
	 *
	 * @param  handlerID identifier for the handler appropriate to handle 
	 *         the type of structured text present in the first segment.
	 *         It may be one of the predefined identifiers in 
	 *         {@link StructuredTextTypeHandlerFactory}, or it may be an identifier
	 *         for a type handler created by a plug-in or by the application.
	 * @param  start offset in the string of the starting character of the
	 *         segment. It must be >= 0 and less than the length of the string.
	 * @param  limit offset of the character following the segment. It must be
	 *         greater than the <code>start</code> argument and not greater
	 *         than the length of the string.
	 *
	 * @throws IllegalArgumentException if <code>handlerID</code> is null,
	 *         or if <code>start</code> or <code>limit</code> have invalid
	 *         values.
	 * @throws IllegalStateException if the current segment exceeds the
	 *         number of segments specified by <code>segmentCount</code>
	 *         in the call to {@link #addRecord addRecord} which created
	 *         the StructuredTextStringRecord instance.
	 */
	public void addSegment(String handlerID, int start, int limit) {
		if (handlerID == null)
			throw new IllegalArgumentException("The handlerID argument must not be null!"); //$NON-NLS-1$
		if (start < 0 || start >= string.length())
			throw new IllegalArgumentException("The start position must be at least 0 and less than the length of the string!"); //$NON-NLS-1$
		if (limit <= start || limit > string.length())
			throw new IllegalArgumentException("The limit position must be greater than the start position but no greater than the length of the string!"); //$NON-NLS-1$
		if (usedSegmentCount >= totalSegmentCount)
			throw new IllegalStateException("All segments of the record are already used!"); //$NON-NLS-1$
		handlers[usedSegmentCount] = handlerID;
		boundaries[usedSegmentCount * 2] = (short) start;
		boundaries[usedSegmentCount * 2 + 1] = (short) limit;
		usedSegmentCount++;
	}

	/**
	 * Checks if a string is recorded and retrieves its record.
	 *
	 * @param  string the string to check.
	 *
	 * @return <code>null</code> if the string is not recorded in the pool;
	 *         otherwise, return the <code>StructuredTextStringRecord</code> instance 
	 *         which records this string.<br>
	 *         Once a record has been found, the number of its segments can
	 *         be retrieved using {@link #getSegmentCount getSegmentCount},
	 *         its handler ID can
	 *         be retrieved using {@link #getHandler getHandler},
	 *         its starting offset can
	 *         be retrieved using {@link #getStart getStart},
	 *         its ending offset can
	 *         be retrieved using {@link #getLimit getLimit},
	 */
	public static StructuredTextStringRecord getRecord(String string) {
		if (last < 0) // no records at all
			return null;
		if (string == null || string.length() < 1)
			return null;
		StructuredTextStringRecord record;
		int myLast = last;
		int hash = string.hashCode();
		for (int i = myLast; i >= 0; i--) {
			if (hash != hashArray[i])
				continue;
			record = recordRefs[i].get();
			if (record == null)
				continue;
			if (string.equals(record.string))
				return record;
		}
		if (!wrapAround) // never recorded past myLast
			return null;
		for (int i = MAXINDEX; i > myLast; i--) {
			if (hash != hashArray[i])
				continue;
			record = recordRefs[i].get();
			if (record == null)
				continue;
			if (string.equals(record.string))
				return record;
		}
		return null;
	}

	/**
	 * Retrieves the number of segments in a record.
	 * 
	 * @return the number of segments in the current record
	 */
	public int getSegmentCount() {
		return usedSegmentCount;
	}

	private void checkSegmentNumber(int segmentNumber) {
		if (segmentNumber >= usedSegmentCount)
			throw new IllegalArgumentException("The segment number " + segmentNumber + " is greater than the total number of segments = " + usedSegmentCount + "!"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}

	/**
	 * Retrieves the handler ID of a given segment.
	 *
	 * @param  segmentNumber number of the segment about which information
	 *         is required. It must be >= 0 and less than the number of
	 *         segments returned  by {@link #getSegmentCount}.
	 *
	 * @return the handler ID of the handler appropriate to 
	 *         process the structured text in the segment
	 *         specified by <code>segmentNumber</code>.
	 *
	 * @throws IllegalArgumentException if <code>segmentNumber</code>
	 *         has an invalid value.
	 */
	public String getHandler(int segmentNumber) {
		checkSegmentNumber(segmentNumber);
		return handlers[segmentNumber];
	}

	/**
	 * Retrieves the starting offset of a given segment.
	 *
	 * @param  segmentNumber number of the segment about which information
	 *         is required. It must be >= 0 and less than the number of
	 *         segments returned  by {@link #getSegmentCount}.
	 *
	 * @return the starting offset within the string of the segment
	 *         specified by <code>segmentNumber</code>.
	 *
	 * @throws IllegalArgumentException if <code>segmentNumber</code>
	 *         has an invalid value.
	 */
	public int getStart(int segmentNumber) {
		checkSegmentNumber(segmentNumber);
		return boundaries[segmentNumber * 2];
	}

	/**
	 * Retrieves the ending offset of a given segment.
	 *
	 * @param  segmentNumber number of the segment about which information
	 *         is required. It must be >= 0 and less than the number of
	 *         segments returned  by {@link #getSegmentCount}.
	 *
	 * @return the offset of the position following the segment
	 *         specified by <code>segmentNumber</code>.
	 *
	 * @throws IllegalArgumentException if <code>segmentNumber</code>
	 *         has an invalid value.
	 */
	public int getLimit(int segmentNumber) {
		checkSegmentNumber(segmentNumber);
		return boundaries[segmentNumber * 2 + 1];
	}

	/**
	 * Clears the pool. All elements of the pool are erased and any associated
	 * memory is freed.
	 */
	public static synchronized void clear() {
		for (int i = 0; i <= MAXINDEX; i++) {
			hashArray[i] = 0;
			SoftReference<StructuredTextStringRecord> softRef = recordRefs[i];
			if (softRef == null)
				continue;
			StructuredTextStringRecord record = softRef.get();
			if (record == null)
				continue;
			record.boundaries = null;
			record.handlers = null;
			record.totalSegmentCount = 0;
			record.usedSegmentCount = 0;
			recordRefs[i].clear();
		}
		last = -1;
		wrapAround = false;
	}

}
