// Copyright 1996, Marimba Inc. All Rights Reserved.


// Confidential and Proprietary Information of Marimba, Inc.


// @(#)Text.java, 1.20, 12/15/96





package marimba.text;





import java.io.*;


import java.awt.*;


import java.util.*;





import marimba.io.*;


import marimba.util.*;





/**


 * This class implements a text object.  This stores information about


 * the actual bytes/chars in the text as well as any style information.


 * TextView provides a view onto this text, and, of course, there can


 * be multiple views onto a single text widget.


 *


 * @author	Jonathan Payne


 * @version 1.20, 12/15/96


 */


public class Text {


    static byte charMap[] = new byte[256];


    static final int UPPER = 1;


    static final int LOWER = 2;


    static final int LETTER_MASK = 3;


    static final int DIGIT = 4;


    static final int WORDMISC = 8;


    static final int WORD_MASK = 0xF;


    static final int OPENP = 16;


    static final int CLOSEP = 32;


    static final int WHITE = 64;


    static {


	int i;





	for (i = 'a'; i <= 'z'; i++)


	    charMap[i] = (byte) LOWER;


	for (i = 'A'; i <= 'Z'; i++)


	    charMap[i] = (byte) UPPER;


	for (i = '0'; i <= '9'; i++)


	    charMap[i] = (byte) DIGIT;


	charMap['_'] = (byte) WORDMISC;


	charMap['('] = charMap['{'] = charMap['['] = (byte) OPENP;


	charMap[')'] = charMap['}'] = charMap[']'] = (byte) CLOSEP;


	charMap[' '] = charMap['\t'] = (byte) WHITE;


    }





    public static boolean isLetter(int c) {


	return (charMap[c & 0xFF] & LETTER_MASK) != 0;


    }





    public static boolean isDigit(int c) {


	return (charMap[c & 0xFF] & DIGIT) != 0;


    }





    public static boolean isWordChar(int c) {


	return (charMap[c & 0xFF] & WORD_MASK) != 0;


    }





    /** The actual characters. */


    protected byte data[];





    /** Number of characters in this buffer. */


    protected int count;





    /** The style references in this document. */


    protected StylePool styles;





    public Text(int size) {


	data = new byte[size];


	styles = new StylePool();


    }





    public Text() {


	this(16);


    }





    public StylePool getStyles() {


	return styles;


    }





    TextView views[];


    int nviews = 0;





    /** Adds a new text view to views. */


    protected void addView(TextView view) {


	for (int i = nviews; --i >= 0; )


	    if (views[i] == view)


		return;


	if (views == null || nviews == views.length) {


	    TextView vw[] = new TextView[nviews + 1];


	    if (views != null)


		System.arraycopy(views, 0, vw, 0, nviews);


	    views = vw;


	}


	views[nviews++] = view;


    }





    /** Removes a text view from views. */


    protected void removeView(TextView view) {


	for (int i = nviews; --i >= 0; ) {


	    if (views[i] == view) {


		if (i == nviews - 1)


		    views[i] = null;


		else


		    System.arraycopy(views, i + 1, views, i, nviews - i - 1);


		nviews -= 1;


	    }


	}


    }





    /** Notifies views of an insertion. */


    protected void notifyInsert(int pos, int count) {


	styles.notifyInsert(pos, count);


	for (int i = nviews; --i >= 0; )


	    views[i].notifyInsert(pos, count);


    }





    /** Notifies views of a deletion just before it occurs. */


    protected void notifyDelete(int pos, int count) {


	styles.notifyDelete(pos, count);


	for (int i = nviews; --i >= 0; )


	    views[i].notifyDelete(pos, count);


    }





    /** Notifies views of a deletion after the fact. */


    protected void notifyDeleteAfter(int pos, int count) {


    }





    /**


     * Notifies views that a region of characters have changed (due to


     * a style change, most likely).


     */


    protected void notifyDirtyRegion(int pos0, int pos1) {


	for (int i = nviews; --i >= 0; )


	    views[i].notifyDirtyRegion(pos0, pos1);


    }





    /**


     * Makes sure there's room for 'bytes' new bytes, at the specified


     * position.


     */


    private synchronized void roomFor(int bytes, int at) {


	if (count + bytes > data.length) {


	    byte d[] = new byte[(int) ((count + bytes) * 1.5)];





	    System.arraycopy(data, 0, d, 0, at);


	    System.arraycopy(data, at, d, at + bytes, count - at);


	    data = d;


	} else if (count > at) {


	    System.arraycopy(data, at, data, at + bytes, count - at);


	}


    }





    /** Inserts string S at POS. */


    public synchronized void insert(String s, int pos) {


	insert(s, pos, s.length());


    }





    /** Inserts LENGTH characters of string S at POS. */


    public synchronized void insert(String s, int pos, int length) {


	roomFor(length, pos);


	s.getBytes(0, length, data, pos);


	count += length;


	notifyInsert(pos, length);


    }





    /**


     * Inserts LENGTH bytes from BYTES at offset OFF into this text


     * buffer at POS.


     */


    public synchronized void insert(byte bytes[], int off, int length, int pos) {


	roomFor(length, pos);


	System.arraycopy(bytes, off, data, pos, length);


	count += length;


	notifyInsert(pos, length);


    }





    /** Inserts character C at POS into this text. */


    public synchronized void insert(int c, int pos) {


	roomFor(1, pos);


	data[pos] = (byte) c;


	count += 1;


	notifyInsert(pos, 1);


    }





    /** Inserts stream IN at POS, until EOF is reached. */


    public synchronized void insert(InputStream in, int pos) throws IOException {


	byte newdata[] = new byte[1024];


	int n;


	int nbytes = 0;


	int pos0 = pos;





	if ((n = in.available()) > 0) {


	    /* if it's a file, we'll make the thing big enough


	       straight off the bat. */


	    roomFor(n, pos);


	}


	while ((n = in.read(newdata, 0, newdata.length)) >= 0) {


	    roomFor(n, pos);


	    System.arraycopy(newdata, 0, data, pos, n);


	    count += n;


	    pos += n;


	    nbytes += n;


	}


	notifyInsert(pos0, nbytes);


    }





    /** Deletes N bytes starting at POS0. */


    public synchronized void delete(int pos0, int n) {


	int pos1;





	if (n < 0) {


	    pos1 = pos0;


	    pos0 += n;


	    n = -n;


	} else


	    pos1 = pos0 + n;


	if (pos0 < 0 || pos1 < pos0 || pos0 + n > count)


	    throw new ArrayIndexOutOfBoundsException("delete(" +


						     pos0 + ", " + pos1


						     + ")");


	notifyDelete(pos0, n);


	System.arraycopy(data, pos1, data, pos0, count - pos1);


	count -= n;


	notifyDeleteAfter(pos0, n);


    }





    /** Deletes all the text in the buffer. */


    public void clear() {


	delete(0, count);


    }





    /** Returns contents of the text from POS0 to POS1 as a string. */


    public synchronized String substring(int pos0, int pos1) {


	return new String(data, 0, pos0, pos1 - pos0);


    }





    /** Returns contents of the text from POS0 to the end as a string. */


    public synchronized String substring(int pos0) {


	return substring(pos0, count);


    }





    /** Returns the entire text object as a String. */


    public synchronized String toString() {


	return substring(0, count);


    }





    /** Returns the byte array. */


    public final byte[] getBytes() {


	return data;


    }





    /**


     * Searches for the first instance of the specified character C,


     * starting from POS, in the specified direction.  If DIRECTION is


     * > 0 , the search is forward; otherwise it's backward.


     *


     * Returns -1 if there's no such character.  When the direction is


     * backward, the scan starts at the character before POS.  When


     * the direction is forward, the scan starts at POS.


     */


    public synchronized final int scan(int c, int pos, int direction) {


	byte data[] = this.data;


	byte ch = (byte) c;





	if (direction > 0) {


	    int limit = count;


	    while (pos < limit) {


		if (data[pos] == ch) {


		    return pos;


		}


		pos += 1;


	    }


	} else if (pos <= count) {


	    while (--pos >= 0) {


		if (data[pos] == ch) {


		    return pos;


		}


	    }


	}


	return -1;


    }





    /**


     * Searches for string S starting at POS moving forward if


     * DIRECTION is > 0, backwards otherwise.  Returns -1 if no such


     * string is found.


     */


    public synchronized final int search(String s, int pos, int direction) {


	int c = s.charAt(0);


	int limit = count;


	byte data[] = this.data;


	int slimit = s.length();





	while ((pos = scan(c, pos, direction)) >= 0) {


	    int i = pos + 1;


	    int si = 1;


	    if (pos + slimit > limit) {


		break;


	    }


	    while (i < limit && si < slimit && data[i] == s.charAt(si++)) {


		i += 1;


	    }


	    if (si == slimit) {


		return pos;


	    }


	    pos += 1;


	}


	return -1;


    }





    /** Returns the length of the text. */


    public final int length() {


	return count;


    }





    /** Returns the byte at POS. */


    public synchronized final int byteAt(int pos) {


	if (pos >= count) {


	    throw new ArrayIndexOutOfBoundsException(pos + " >= " + count);


	}


	return data[pos] & 0xFF;


    }





    public synchronized void applyStyleChange(StyleChange sc, int pos0, int pos1) {


	styles.applyStyle(sc, pos0, pos1);


	notifyDirtyRegion(pos0, pos1);


    }





    /** Applies a style change to the range from POS0 to POS1. */


    public synchronized boolean applyStyleChangeCheck(StyleChange sc, int pos0, int pos1) {


	boolean changed = styles.applyStyle(sc, pos0, pos1);


	if (changed) {


	    notifyDirtyRegion(pos0, pos1);


	}


	return changed;


    }





    /** Deletes all styles in the range from POS0 to POS1. */


    public synchronized void deleteStyles(int pos0, int pos1) {


	styles.deleteStyles(this, pos0, pos1);


    }





    /** Returns a "rich text" representation of the text. */


    public synchronized String getRichText() {


	try {


	    FastOutputStream out = new FastOutputStream();


	    styles.writeStyles(data, 0, count, out);


	    return out.toString();


	} catch (IOException e) {


	    return null;


	}


    }





    /** Sets the value of the text from a "rich text" representation. */


    public synchronized void setRichText(String s) {


	try {


	    clear();


	    ByteString bs = new ByteString();


	    StringBufferInputStream in = new StringBufferInputStream(s);


	    styles.readStyles(bs, in);


	    data = bs.data;


	    count = bs.length;


	    notifyDirtyRegion(0, bs.length);


	} catch (IOException e) {}


    }





/* Basic support for typical editor commands. */





    /**


     * Returns the position that is the beginning of the line


     * containing pos.


     */


    public synchronized int getBeginningOfLine(int pos) {


	return scan('\n', pos, -1) + 1;


    }





    /** Returns the position that is the end of the line containing pos. */


    public synchronized int getEndOfLine(int pos) {


	pos = scan('\n', pos, 1);


	if (pos == -1) {


	    return count;


	}


	return pos;


    }





    /**


     * Returns the position of the beginning of the next line.


     * If pos is on the last line, returns -1.


     */


    public synchronized int getNextLine(int pos) {


	if (pos == count) {


	    return -1;


	}


	pos = getEndOfLine(pos) + 1;


	if (pos > count) {


	    pos = count;


	}


	return pos;


    }





    /**


     * Returns the position of the beginning of the nth line from the


     * specified pos.


     */


    public synchronized int getLinePos(int pos, int n) {


	while (--n >= 0) {


	    pos = scan('\n', pos, 1);


	    if (pos == -1) {


		return count;


	    } else {


		pos += 1;


	    }


	}


	return pos;


    }





    /**


     * Return the position of the Nth line in the buffer.


     */


    public int getLinePos(int n) {


	return getLinePos(0, n);


    }





    /**


     * Returns the position that is the end (or beginning if direction


     * < 0) of the word that POS is part of.  If POS is not part of a


     * word, the position is unchanged.


     */


    public synchronized int getWordEnd(int pos, int direction) {


	byte data[] = this.data;





	if (direction > 0) {


	    int limit = count;





	    if (pos == 0 || isWordChar(data[pos - 1])) {


		while (pos < limit && isWordChar(data[pos])) {


		    pos += 1;


		}


	    }


	} else {


	    if (pos < count && isWordChar(data[pos])) {


		while (pos > 0 && isWordChar(data[pos - 1])) {


		    pos -= 1;


		}


	    }


	}


	return pos;


    }





    /**


     * Moves forward or backward a word (Emacs style).  Returns the


     * new position.


     */


    public synchronized int getNextWord(int pos, int direction) {


	byte data[] = this.data;





	if (direction > 0) {


	    int limit = count;





	    while (pos < limit && !isWordChar(data[pos])) {


		pos += 1;


	    }


	    while (pos < limit && isWordChar(data[pos])) {


		pos += 1;


	    }


	} else {


	    while (pos > 0 && !isWordChar(data[pos - 1])) {


		pos -= 1;


	    }


	    while (pos > 0 && isWordChar(data[pos - 1])) {


		pos -= 1;


	    }


	}


	return pos;


    }





    public synchronized int getNextWordStart(int pos) {


	int limit = count;


	while (pos < limit && !isWordChar(data[pos])) {


	    pos += 1;


	}


	while (pos < limit && isWordChar(data[pos])) {


	    pos += 1;


	}


	while (pos < limit && !isWordChar(data[pos]) && data[pos] != '\n') {


	    pos += 1;


	}


	return pos;


    }





    /**


     * Returns the indent of a line, in character positions, for the


     * specified POS.


     */


    public synchronized int getColumnFor(int pos) {


	int pos0 = getBeginningOfLine(pos);


	int col = 0;


	byte data[] = this.data;





	while (pos0 < pos) {


	    int c;





	    switch (c = (data[pos0] & 0xFF)) {


	      case '\t':


		col = (col + 8) & ~7;


		break;





	      default:


		if (c >= ' ')


		    col += 1;


		break;


	    }


	    pos0 += 1;


	}


	return col;


    }





    /**


     * Returns the buffer position that hits the specified column on


     * the line containing POS.


     */


    public synchronized int getPosFor(int column, int pos) {


	pos = getBeginningOfLine(pos);


	int limit = count;


	int col = 0;


	byte data[] = this.data;





      loop:


	while (pos < limit) {


	    int c;





	    switch (c = (data[pos] & 0xFF)) {


	      case '\t':


		col = (col + 8) & ~7;


		break;





	      case '\n':


	      case '\r':


		break loop;





	      default:


		if (c >= ' ')


		    col += 1;


		break;


	    }


	    if (col > column)


		break;


	    pos += 1;


	}


	return (pos > limit) ? limit : pos;


    }





    /** Skips over white space. */


    public synchronized int skipWhiteSpace(int pos, int direction) {


	int limit = count;


	byte charMap[] = this.charMap;





	if (direction > 0)


	    while (pos < limit && (charMap[data[pos] & 0xFF] & WHITE) != 0)


		pos += 1;


	else


	    while (pos > 0 && (charMap[data[pos - 1] & 0xFF] & WHITE) != 0)


		pos -= 1;


	return pos;


    }





    /**


     * Moves to the indent (first non white space character) in line


     * containing POS.


     */


    public synchronized int getIndent(int pos) {


	pos = getBeginningOfLine(pos);


	return skipWhiteSpace(pos, 1);


    }





    /**


     * Deletes all the white space around POS.  Returns the resulting


     * buffer position.


     */


    public synchronized int deleteWhiteSpace(int pos) {


	byte charMap[] = this.charMap;


	int pos0 = skipWhiteSpace(pos, -1);





	pos = skipWhiteSpace(pos, 1);


	delete(pos0, pos - pos0);





	return pos0;


    }





    /**


     * Indent to the specified destination column, starting at the


     * specified position, using the smallest combination of Tabs and


     * Spaces as possible.


     */


    public synchronized int indentTo(int pos, int destcol) {


	pos = deleteWhiteSpace(pos);


	int col = getColumnFor(pos);





	while (col < destcol) {


	    int newcol = (col + 8) & ~7;





	    if (newcol <= destcol) {


		insert('\t', pos++);


		col = newcol;


	    } else


		break;


	}


	insert("        ", pos, destcol - col);


	return pos + destcol - col;


    }


}


