package org.p2c2e.zing.swing;

import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.MediaTracker;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.font.FontRenderContext;
import java.awt.font.GraphicAttribute;
import java.awt.font.ImageGraphicAttribute;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.LineMetrics;
import java.awt.font.TextAttribute;
import java.awt.font.TextHitInfo;
import java.awt.font.TextLayout;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.swing.BoxLayout;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;

import org.p2c2e.blorb.Color;
import org.p2c2e.zing.CharInputConsumer;
import org.p2c2e.zing.HyperlinkInputConsumer;
import org.p2c2e.zing.IGlk;
import org.p2c2e.zing.ITextBufferWindow;
import org.p2c2e.zing.LineInputConsumer;
import org.p2c2e.zing.ObjectCallback;
import org.p2c2e.zing.Style;
import org.p2c2e.zing.StyleHints;
import org.p2c2e.zing.TextSplitMeasurer;
import org.p2c2e.zing.types.Int;

public final class TextBufferWindow extends Window implements
		ITextBufferWindow, MouseMotionListener {
	static ObjectCallback MORE_CALLBACK;

	static final int H_MARGIN = 15;
	static final int TOP_MARGIN = 5;
	static final int HISTORY_SIZE_LIMIT = 20;

	static final char[] MEASURE_ARR = new char[] { '0' };
	static final char INLINE_IMAGE = (char) 1;
	static final char FLOW_BREAK = (char) 2;

	public static final int INLINE_UP = 1;
	public static final int INLINE_DOWN = 2;
	public static final int INLINE_CENTER = 3;
	public static final int MARGIN_LEFT = 4;
	public static final int MARGIN_RIGHT = 5;

	// Since it is illegal to print control characters to the output stream,
	// we can use those codes to represent special cases: graphics, flow
	// breaks, and so forth.
	StringBuffer buffer;
	boolean lastLineDirty = false;

	// This maps positions in the buffer to style changes.
	private TreeMap<Integer, Style> mStyles;
	boolean restyled;

	// This maps positions to graphics
	TreeMap<Integer, MarginImgStruct> mMarginGraphics;
	TreeMap<Integer, InlineImgStruct> mInlineGraphics;
	TreeMap<Integer, Integer> mHyperlinks;

	TextBufferPanel view;

	int viewWidth = 0;

	LinkedList<Paragraph> paragraphs;
	Line lastLineSeen = null;

	MediaTracker tracker;
	int imageNum = 0;

	CharInputConsumer charConsumer;
	LineInputConsumer lineConsumer;
	HyperlinkInputConsumer hyperConsumer;
	int iLineStartPos;
	int iLineCursorPos;
	int maxLineInput;
	int inputHistoryIndex;
	Style oldStyle;
	Style nonHyper;
	LinkedList<String> inputHistory;

	public TextBufferWindow(FontRenderContext context) {
		super(context);
		buffer = new StringBuffer("\n");
		mStyles = new TreeMap<Integer, Style>();
		mMarginGraphics = new TreeMap<Integer, TextBufferWindow.MarginImgStruct>();
		mInlineGraphics = new TreeMap<Integer, TextBufferWindow.InlineImgStruct>();
		mHyperlinks = new TreeMap<Integer, Integer>();
		paragraphs = new LinkedList<TextBufferWindow.Paragraph>();
		view = new TextBufferPanel();
		view.setFocusable(true);
		tracker = new MediaTracker(view);
		oldStyle = curStyle;
		mHyperlinks.put(new Integer(0), new Integer(0));
		// having the scrollbar always present makes calculating size
		// constraints
		// much easier
		panel = new JScrollPane(view, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
				JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
		((JScrollPane) panel).getVerticalScrollBar().setFocusable(false);
		((JScrollPane) panel).setBorder(null);
		view.addMouseListener(this);
		inputHistory = new LinkedList<String>();

		view.addMouseMotionListener(this);
	}

	@Override
	protected int getWindowType() {
		return TEXT_BUFFER;
	}

	@Override
	protected void restyle(boolean useHints) {
		super.restyle(useHints);

		if (nonHyper != null)
			nonHyper = (Style) hintedStyles.get(nonHyper.name);
		if (oldStyle != null) {
			Style oldRep = (Style) hintedStyles.get(oldStyle.name);
			if (oldStyle.isHyperlinked())
				oldStyle = oldRep.getHyperlinked();
			else
				oldStyle = oldRep;
		}

		Map.Entry<Integer, Style> e;
		TreeMap<Integer, Style> mNewStyles = new TreeMap<Integer, Style>();
		Iterator<Map.Entry<Integer, Style>> it = mStyles.entrySet().iterator();

		while (it.hasNext()) {
			e = it.next();
			Integer pos = e.getKey();
			Style s = e.getValue();
			Style rep = (Style) hintedStyles.get(s.name);

			mNewStyles.put(pos, ((s.isHyperlinked()) ? rep.getHyperlinked()
					: rep));
		}

		mStyles = mNewStyles;
		restyled = true;
	}

	@Override
	protected boolean isRequestingKeyboardInput() {
		return lineConsumer != null || charConsumer != null;
	}

	@Override
	public int getWindowWidth() {
		int pixWid = viewWidth - (2 * H_MARGIN);
		Style n = (Style) hintedStyles.get("normal");
		int w = (int) (new Font(n.getMap())).getStringBounds(MEASURE_ARR, 0, 1,
				frc).getWidth();
		return (pixWid / w);
	}

	@Override
	public int getWindowHeight() {
		int pixHeight = ((JScrollPane) panel).getViewport().getHeight();
		Style n = (Style) hintedStyles.get("normal");
		LineMetrics m = (new Font(n.getMap())).getLineMetrics("Hag", frc);
		return pixHeight / (int) m.getHeight();
	}

	@Override
	public void requestHyperlinkInput(HyperlinkInputConsumer hic) {
		if (hyperConsumer == null)
			hyperConsumer = hic;
	}

	@Override
	public void cancelHyperlinkInput() {
		hyperConsumer = null;
	}

	@Override
	public void setHyperlink(int val) {
		if (val != 0) {
			if (nonHyper == null) {
				Style nhs = curStyle;
				setStyle(curStyle.getHyperlinked());
				nonHyper = nhs;
			}
			mHyperlinks.put(new Integer(buffer.length()), new Integer(val));
		} else if (nonHyper != null) {
			Style nhs = nonHyper;
			nonHyper = null;
			setStyle(nhs);
			mHyperlinks.put(new Integer(buffer.length()), new Integer(val));
		}
	}

	@Override
	public void doLayout() {
		if (lastLineDirty) {
			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					doPaint(layoutLastLine(buffer.length()));
				}
			});
		} else {
			doPaint(lastLineSeen);
		}
	}

	private final void doPaint(Line l) {
		if (l != null)
			view.repaint(0L, 0, l.top - 3, view.getWidth(), view.getHeight()
					- (l.top - 3));
		else
			view.repaint();
	}

	@Override
	public void putChar(int c) {
		buffer.appendCodePoint(c);
		lastLineDirty = true;
	}

	@Override
	public void putCharUni(int c) {
		buffer.appendCodePoint(c);
		lastLineDirty = true;
	}

	@Override
	public void putString(String s) {
		if (s.length() > 0) {
			buffer.append(s);
			lastLineDirty = true;
		}
	}

	@Override
	public void clear() {
		buffer = new StringBuffer("\n");
		lastLineSeen = null;
		mMarginGraphics.clear();
		mInlineGraphics.clear();
		mHyperlinks.clear();
		mHyperlinks.put(new Integer(0), new Integer(0));
		synchronized (paragraphs) {
			paragraphs.clear();
		}
		mStyles.clear();
		setStyle(curStyle);
		view.setPreferredSize(view.preferredViewportSize);
		layout(null, null);
	}

	@Override
	public boolean measureStyle(String stName, int hint, Int b) {
		int result;
		Style unhinted = Style.getStyle(stName, TEXT_GRID);
		Style s = (Style) hintedStyles.get(stName);

		if (s != null) {
			switch (hint) {
			case IGlk.STYLEHINT_INDENTATION:
				result = (int) ((s.leftIndent * 0.03) * (viewWidth - (2 * H_MARGIN)));
				break;
			case IGlk.STYLEHINT_PARA_INDENTATION:
				result = (int) ((s.parIndent * 0.03) * (viewWidth - (2 * H_MARGIN)));
				break;
			case IGlk.STYLEHINT_JUSTIFICATION:
				result = s.justification;
				break;
			case IGlk.STYLEHINT_SIZE:
				result = s.size;
				break;
			case IGlk.STYLEHINT_WEIGHT:
				if (s.weight == TextAttribute.WEIGHT_BOLD
						|| s.weight == TextAttribute.WEIGHT_DEMIBOLD
						|| s.weight == TextAttribute.WEIGHT_EXTRABOLD
						|| s.weight == TextAttribute.WEIGHT_HEAVY)
					result = 1;
				else if (s.weight == TextAttribute.WEIGHT_REGULAR
						|| s.weight == TextAttribute.WEIGHT_SEMIBOLD
						|| s.weight == TextAttribute.WEIGHT_MEDIUM)
					result = 0;
				else
					result = -1;
				break;
			case IGlk.STYLEHINT_OBLIQUE:
				result = (s.isOblique ? 1 : 0);
				break;
			case IGlk.STYLEHINT_PROPORTIONAL:
				result = (s.isMonospace() ? 0 : 1);
				break;
			case IGlk.STYLEHINT_TEXT_COLOR:
				result = Glk.getInstance().colorToInt(s.textColor);
				break;
			case IGlk.STYLEHINT_BACK_COLOR:
				result = Glk.getInstance().colorToInt(s.backColor);
				break;
			case IGlk.STYLEHINT_REVERSE_COLOR:
				result = ((unhinted.textColor == s.backColor && unhinted.backColor == s.textColor) ? 1
						: 0);
				break;
			default:
				return false;
			}
			b.val = result;
			return true;
		}
		return false;
	}

	@Override
	public void setStyle(Style style) {
		if (nonHyper != null) {
			nonHyper = style;
			style = style.getHyperlinked();
		}

		super.setStyle(style);
		mStyles.put(new Integer(buffer.length()), style);
	}

	public boolean drawImage(Image img, int align) {
		tracker.addImage(img, imageNum++);
		try {
			tracker.waitForAll();
		} catch (InterruptedException eInterrupt) {
			System.err.println("media tracker was interrupted.");
		}
		tracker.removeImage(img);

		if (align > INLINE_CENTER) {
			int pos = buffer.length();

			// this is a margin image
			MarginImgStruct s = mMarginGraphics.get(new Integer(pos));
			if (s == null) {
				s = new MarginImgStruct();
				mMarginGraphics.put(new Integer(pos), s);
			}
			s.add(img, align);
		} else {
			InlineImgStruct s = new InlineImgStruct();
			Integer oiPos = new Integer(buffer.length());
			s.img = img;
			s.align = align;
			mInlineGraphics.put(oiPos, s);

			if (nonHyper != null)
				mStyles.put(oiPos, nonHyper);

			buffer.append(INLINE_IMAGE);

			if (nonHyper != null)
				mStyles.put(new Integer(buffer.length()), curStyle);
		}
		lastLineDirty = true;
		return true;
	}

	@Override
	public void flowBreak() {
		if (buffer.length() != 0
				&& buffer.charAt(buffer.length() - 1) != FLOW_BREAK)
			buffer.append(FLOW_BREAK);
	}

	@Override
	protected Map getStyleMap() {
		return Style.BUFFER_STYLES;
	}

	// This is a bit more complex than the grid window's version, since
	// here we actually have to worry about, for instance, indentation.
	@Override
	protected Style createHintedStyle(Style style) {
		int val;
		Color tmp;
		Style hintedStyle;
		StyleHints hints = (StyleHints) mHints.get(style.name);

		if (hints == null)
			return style;

		hintedStyle = (Style) style.clone();

		if (hints.data[StyleHints.LEFT_INDENT] != null)
			hintedStyle.leftIndent += hints.data[StyleHints.LEFT_INDENT]
					.intValue();
		if (hints.data[StyleHints.RIGHT_INDENT] != null)
			hintedStyle.rightIndent += hints.data[StyleHints.RIGHT_INDENT]
					.intValue();
		if (hints.data[StyleHints.PAR_INDENT] != null)
			hintedStyle.parIndent += hints.data[StyleHints.PAR_INDENT]
					.intValue();

		// The final result of indentation may not be negative, so we have to
		// check for this...
		if (hintedStyle.leftIndent < 0)
			hintedStyle.leftIndent = 0;
		if (hintedStyle.rightIndent < 0)
			hintedStyle.rightIndent = 0;
		if (hintedStyle.parIndent < 0)
			hintedStyle.parIndent = 0;

		if (hints.data[StyleHints.JUSTIFICATION] != null)
			hintedStyle.justification = hints.data[StyleHints.JUSTIFICATION]
					.intValue();

		if (hintedStyle.justification < Style.LEFT_FLUSH
				|| hintedStyle.justification > Style.RIGHT_FLUSH)
			hintedStyle.justification = Style.LEFT_FLUSH;

		if (hints.data[StyleHints.SIZE] != null)
			hintedStyle.size += hints.data[StyleHints.SIZE].intValue();

		if (hints.data[StyleHints.WEIGHT] != null) {
			val = hints.data[StyleHints.WEIGHT].intValue();
			if (val == 0)
				hintedStyle.weight = TextAttribute.WEIGHT_REGULAR;
			else if (val < 0)
				hintedStyle.weight = TextAttribute.WEIGHT_LIGHT;
			else
				hintedStyle.weight = TextAttribute.WEIGHT_BOLD;
		}

		if (hints.data[StyleHints.OBLIQUE] != null) {
			val = hints.data[StyleHints.OBLIQUE].intValue();
			hintedStyle.isOblique = (val == 1);
		}

		if (hints.data[StyleHints.PROPORTIONAL] != null) {
			val = hints.data[StyleHints.PROPORTIONAL].intValue();
			if (val == 1 && hintedStyle.isMonospace())
				hintedStyle.family = DEFAULT_PROPORTIONAL_FONT;
			else if (val == 0 && !hintedStyle.isMonospace())
				hintedStyle.family = DEFAULT_FIXED_FONT;
		}

		if (hints.data[StyleHints.TEXT_COLOR] != null) {
			val = hints.data[StyleHints.TEXT_COLOR].intValue();
			hintedStyle.textColor = new Color((val >> 16), (val >> 8), val);
		}
		if (hints.data[StyleHints.BACK_COLOR] != null) {
			val = hints.data[StyleHints.BACK_COLOR].intValue();
			hintedStyle.backColor = new Color((val >> 16), (val >> 8), val);
		}
		if (hints.data[StyleHints.REVERSE_COLOR] != null
				&& hints.data[StyleHints.REVERSE_COLOR].intValue() == 1) {
			tmp = hintedStyle.textColor;
			hintedStyle.textColor = hintedStyle.backColor;
			hintedStyle.backColor = tmp;
		}

		return hintedStyle;
	}

	@Override
	protected int getSplit(int numLines, int axis) {
		Style normal = (Style) hintedStyles.get("normal");

		int split = TextSplitMeasurer.getSplit(panel, numLines, axis, frc,
				normal, MEASURE_ARR);

		if (axis == BoxLayout.X_AXIS) {
			Dimension d = ((JScrollPane) panel).getVerticalScrollBar()
					.getPreferredSize();
			split += d.width;
		} else {
			split += TOP_MARGIN;
		}

		return split;
	}

	@Override
	public void rearrange(Rectangle r) {
		Dimension d;
		Insets i;

		// I *think* that basically two things need to get done here.
		// First: we have to set the preferred size. This is pretty trivial.
		// Second: we have to do a *complete* layout of the buffer. This is
		// not trivial (and is better handled in a different method).

		// The rectangle will have space for the view *and* the scroll pane.

		i = ((JScrollPane) panel).getInsets();
		d = ((JScrollPane) panel).getVerticalScrollBar().getPreferredSize();
		view.preferredViewportSize.width = r.width
				- (i.left + i.right + d.width);
		view.preferredViewportSize.height = r.height - (i.top + i.bottom);
		((JScrollPane) panel).getViewport().setPreferredSize(
				view.preferredViewportSize);
		view.setPreferredSize(view.preferredViewportSize);
		view.revalidate();

		if (view.preferredViewportSize.width != viewWidth || restyled) {
			restyled = false;
			viewWidth = view.preferredViewportSize.width;
			synchronized (paragraphs) {
				paragraphs.clear();
			}
			layout(null, null);
		}
	}

	protected synchronized void layout(Line curLine, Line lastLine) {
		char c;
		boolean isParaEnd;
		int iParaStart;
		int iStyleLen;
		int iFlowBreakPos;
		ArrayList<Integer> imgPos;
		ArrayList<Integer> breakPos;
		StringBuffer paraBuf;
		AttributedString stStyle;
		int iBuf;
		Paragraph para;

		lastLineDirty = false;

		if (curLine == null) {
			para = new Paragraph();
			iBuf = 0;
		} else {
			para = curLine.para;
			iBuf = curLine.start;
		}

		while (iBuf < buffer.length()) {
			isParaEnd = false;
			iParaStart = iBuf;
			imgPos = null;
			breakPos = null;
			iFlowBreakPos = -1;
			paraBuf = new StringBuffer();
			c = buffer.charAt(iBuf);

			// layout a paragraph at a time
			while (!isParaEnd && iBuf < buffer.length()) {
				switch (c) {
				case '\n':
					if (iBuf != iParaStart)
						isParaEnd = true;
					else
						paraBuf.append('?');
					break;
				case FLOW_BREAK:
					if (breakPos == null)
						breakPos = new ArrayList<Integer>();

					breakPos.add(new Integer(iBuf));
					if (iFlowBreakPos < 0)
						iFlowBreakPos = iBuf;
					paraBuf.append('?');
					break;
				case INLINE_IMAGE:
					if (imgPos == null)
						imgPos = new ArrayList<Integer>();

					imgPos.add(new Integer(iBuf));
					paraBuf.append('?');
					break;
				default:
					paraBuf.append(c);
				}

				if (!isParaEnd) {
					iBuf++;
					if (iBuf < buffer.length())
						c = buffer.charAt(iBuf);
				}
			}

			if (paraBuf.length() > 0) {
				// apply styles
				stStyle = new AttributedString(paraBuf.toString());
				iStyleLen = paraBuf.length();
				applyStyles(stStyle, iParaStart, iStyleLen);

				// replace images
				if (imgPos != null)
					replaceImages(stStyle, iParaStart, imgPos);

				// replace flow breaks
				if (breakPos != null)
					replaceBreaks(stStyle, iParaStart, breakPos);

				// create lines
				createLines(para, stStyle, iParaStart, iStyleLen, curLine,
						lastLine);

				// now we have to correct lines interrupted by a FLOW_BREAK
				if (iFlowBreakPos >= 0) {
					if (correctParagraph(para, stStyle, paraBuf, iParaStart,
							iStyleLen, iFlowBreakPos))
						iBuf--;
				}

				synchronized (paragraphs) {
					paragraphs.add(para);
				}
				lastLine = para.lines.getLast();
				para = new Paragraph();
				curLine = new Line();
				curLine.para = para;
				curLine.start = lastLine.end;
				curLine.top = lastLine.bottom;
			}
		}
		resizeView();
	}

	void resizeView() {
		final JViewport v;
		Dimension d;
		Paragraph p;
		final int viewportHeight;
		final Line l;
		int y;

		// must set the size of this panel to be large enough
		// FIXME: margin images may extend further than text, but we have no
		// way of determining this right now
		if (!paragraphs.isEmpty()) {
			p = paragraphs.getLast();
			l = p.lines.getLast();
			v = ((JScrollPane) panel).getViewport();
			// viewportHeight = v.getHeight();
			viewportHeight = view.preferredViewportSize.height;
			y = Math.max(l.bottom + 5, viewportHeight);
			if (view.getHeight() != y) {
				d = new Dimension(viewWidth, y);
				view.setPreferredSize(d);
				view.setSize(d);
				view.revalidate();
			}

			scrollDown(v, l.bottom + 5, viewportHeight);
		}
	}

	void scrollDown(JViewport v, int viewHeight, int portHeight) {
		if (viewHeight == 0)
			viewHeight = view.getHeight();

		boolean found = false;

		Line l = (lastLineSeen == null) ? (Line) paragraphs.getFirst().lines
				.getFirst() : lastLineSeen;
		int rectBottom = Math.min(l.top + portHeight, viewHeight);
		Point viewPos = v.getViewPosition();
		int newY = Math.max(0, rectBottom - portHeight);

		if (lastLineSeen == null || viewPos.y != newY) {
			viewPos.y = newY;
			v.setViewPosition(viewPos);
		}

		if (MORE_CALLBACK != null) {
			if (rectBottom != viewHeight)
				MORE_CALLBACK.callback(StatusPane.MORE);
			else
				MORE_CALLBACK.callback(StatusPane.BLANK);
		}

		Line l2 = l;
		synchronized (paragraphs) {
			int np = paragraphs.size();
			ListIterator<Paragraph> pit = paragraphs.listIterator(np);
			while (!found && pit.hasPrevious()) {
				Paragraph p = pit.previous();
				int nl = p.lines.size();
				ListIterator<Line> lit = p.lines.listIterator(nl);

				while (!found && lit.hasPrevious()) {
					l2 = l;
					l = lit.previous();
					if (l.bottom < rectBottom)
						found = true;
				}
			}
		}
		lastLineSeen = (lastLineSeen == l) ? l2 : l;
	}

	// return true if the very last char of para is a FLOW_BREAK
	boolean correctParagraph(Paragraph para, AttributedString s,
			StringBuffer buf, int start, int iLen, int iPos) {
		int i;
		int iNewStart;
		Line curLine = null;
		Line l = null;
		LineBreakMeasurer lbm = new LineBreakMeasurer(s.getIterator(), frc);
		int nl = para.lines.size();
		ListIterator<Line> lit = para.lines.listIterator();

		for (i = 0; i < nl; i++) {
			l = lit.next();
			if (iPos >= l.start && iPos < l.end)
				break;
		}

		if (iPos == l.end) {
			if (i == nl - 1) {
				l.end--;
				return true;
			} else {
				l = lit.next();
				i++;
			}
		}

		// is l indented because of margin images?
		if (l.leftImg != null || l.rightImg != null) {
			// these lines are now invalid
			if (i + 1 < nl)
				para.lines.subList(i + 1, nl).clear();

			// is there anything on the line before the break?
			if (iPos > l.start) {
				lbm.setPosition(l.start - start);
				layoutLine(para, start, l, lbm, iPos - start);
			}

			// the position past the flow break (FIXME: do we really want to
			// pass it?)
			iNewStart = iPos + 1;

			while (iNewStart - start < iLen) {
				curLine = new Line();
				curLine.start = iNewStart;
				curLine.top = l.getNextLineTop();

				lbm.setPosition(curLine.start - start);
				layoutLine(para, start, curLine, lbm, -1);

				l = curLine;
				curLine.para = para;
				para.lines.add(curLine);
				iNewStart = start + lbm.getPosition();
			}
		}

		return false;
	}

	void createLines(Paragraph para, AttributedString s, int start, int iLen,
			Line curLine, Line lastLine) {
		LineBreakMeasurer lbm = new LineBreakMeasurer(s.getIterator(), frc);

		if (curLine == null) {
			curLine = new Line();
			if (lastLine == null) {
				curLine.start = 0;
				curLine.top = TOP_MARGIN;
			} else {
				curLine.start = lastLine.end;
				curLine.top = lastLine.bottom;
			}
		}

		if (lastLine != null) {
			if (lastLine.leftImg != null)
				curLine.leftImg = lastLine.leftImg.getInheritance(curLine.top);
			if (lastLine.rightImg != null)
				curLine.rightImg = lastLine.rightImg
						.getInheritance(curLine.top);
		}

		while (lbm.getPosition() < iLen) {
			if (para.lines.isEmpty()) {
				// find the indentation and justification parameters for this
				// paragraph
				Style cur = mStyles.get(new Integer(start + 1));
				if (cur == null) {
					SortedMap<Integer, Style> head = mStyles
							.headMap(new Integer(start + 1));
					if (!head.isEmpty())
						cur = mStyles.get(head.lastKey());
					else
						cur = (Style) hintedStyles.get("normal");
				}
				para.style = cur;

				// Line may have its own margin images since it starts a
				// paragraph.
				// Why the "+1"? Because the first line of a paragraph always
				// starts with a newline character.
				MarginImgStruct struct = mMarginGraphics.get(new Integer(
						curLine.start + 1));
				MarginImage mimg;

				if (struct != null && struct.left != null) {
					for (int i = 0; i < struct.left.size(); i++) {
						mimg = new MarginImage();
						mimg.img = struct.left.get(i);
						mimg.inherit = curLine.leftImg;
						mimg.myTop = curLine.top;
						curLine.leftImg = mimg;
					}
				}

				if (struct != null && struct.right != null) {
					for (int i = 0; i < struct.right.size(); i++) {
						mimg = new MarginImage();
						mimg.img = struct.right.get(i);
						mimg.inherit = curLine.rightImg;
						mimg.myTop = curLine.top;
						curLine.rightImg = mimg;
					}
				}
			}
			layoutLine(para, start, curLine, lbm, -1);

			lastLine = curLine;
			curLine.para = para;
			para.lines.add(curLine);

			if (lbm.getPosition() < iLen) {
				curLine = new Line();
				curLine.start = lastLine.end;
				curLine.top = lastLine.bottom;

				if (lastLine.leftImg != null)
					curLine.leftImg = lastLine.leftImg
							.getInheritance(curLine.top);
				if (lastLine.rightImg != null)
					curLine.rightImg = lastLine.rightImg
							.getInheritance(curLine.top);
			}
		}
	}

	void layoutLine(Paragraph para, int start, Line curLine,
			LineBreakMeasurer lbm, int offsetLimit) {
		TextLayout layout;
		int indentTotal;
		int parWidthBase;
		int availWidth;

		availWidth = viewWidth - (2 * H_MARGIN);
		if (curLine.leftImg != null)
			availWidth -= (curLine.leftImg.getWidth() + 5);
		if (curLine.rightImg != null)
			availWidth -= (curLine.rightImg.getWidth() + 5);
		parWidthBase = availWidth;

		para.leftBase = para.getLeftIndent(viewWidth);
		para.rightBase = para.getRightIndent(viewWidth);

		indentTotal = para.leftBase + para.rightBase;
		if (para.lines.isEmpty())
			indentTotal += para.getParIndent(parWidthBase);

		availWidth -= indentTotal;

		// this is lame, but apparently necessary
		if (availWidth < 10)
			availWidth = 10;

		if (offsetLimit < 0)
			layout = lbm.nextLayout(availWidth);
		else
			layout = lbm.nextLayout(availWidth, offsetLimit, false);

		curLine.widthBase = availWidth;
		// if (para.style.justification == Style.LEFT_RIGHT_FLUSH)
		// layout = layout.getJustifiedLayout((float) availWidth);

		curLine.layout = layout;

		if (para.style.justification == Style.LEFT_FLUSH
				|| para.style.justification == Style.LEFT_RIGHT_FLUSH) {
			curLine.left = ((curLine.leftImg == null) ? 0 : curLine.leftImg
					.getWidth() + 5) + para.leftBase;
			if (para.lines.isEmpty())
				curLine.left += para.getParIndent(viewWidth);
		} else if (para.style.justification == Style.CENTERED) {
			curLine.left = ((viewWidth - (2 * H_MARGIN)) / 2)
					- (int) (layout.getAdvance() / 2);
		} else {
			curLine.left = viewWidth
					- (2 * H_MARGIN)
					- (((curLine.rightImg == null) ? 0 : curLine.rightImg
							.getWidth() + 5) + (int) layout.getAdvance());
		}

		curLine.end = start + lbm.getPosition();
		curLine.bottom = curLine.top
				+ (int) (layout.getAscent() + layout.getDescent()
						+ layout.getLeading() + 2);
	}

	void replaceImages(AttributedString s, int start, ArrayList<Integer> imgPos) {
		ImageGraphicAttribute iga;
		InlineImgStruct struct;
		Integer oi;
		Iterator<Integer> it = imgPos.iterator();

		while (it.hasNext()) {
			oi = it.next();
			struct = mInlineGraphics.get(oi);

			switch (struct.align) {
			case INLINE_UP:
				iga = new ImageGraphicAttribute(struct.img,
						GraphicAttribute.ROMAN_BASELINE, 0.0f,
						struct.img.getHeight(panel));
				break;
			case INLINE_DOWN:
				iga = new ImageGraphicAttribute(struct.img,
						GraphicAttribute.TOP_ALIGNMENT);
				break;
			default:
				// FIXME: probably will not work
				iga = new ImageGraphicAttribute(struct.img,
						GraphicAttribute.CENTER_BASELINE, 0.0f,
						struct.img.getHeight(panel) / 2);
			}

			s.addAttribute(TextAttribute.CHAR_REPLACEMENT, iga, oi.intValue()
					- start, (oi.intValue() + 1) - start);
		}
	}

	void replaceBreaks(AttributedString s, int start,
			ArrayList<Integer> breakPos) {
		Integer oi;
		Iterator<Integer> it = breakPos.iterator();

		while (it.hasNext()) {
			oi = it.next();
			s.addAttribute(TextAttribute.CHAR_REPLACEMENT,
					FlowBreakAttribute.SINGLETON, oi.intValue() - start,
					(oi.intValue() + 1) - start);
		}

	}

	void applyStyles(AttributedString s, int start, int iLen) {
		int iLast = 0;
		Integer oi = null;
		int iKey;
		SortedMap<Integer, Style> head = mStyles.headMap(new Integer(start));
		SortedMap<Integer, Style> sub = mStyles.subMap(new Integer(start),
				new Integer(start + iLen));
		Style cur = (Style) hintedStyles.get("normal");

		if (!head.isEmpty())
			oi = head.lastKey();

		if (oi != null)
			cur = mStyles.get(oi);

		// replace initial newline
		s.addAttribute(TextAttribute.CHAR_REPLACEMENT,
				new NewlineAttribute(cur.getMap(), frc), 0, 1);

		Iterator<Entry<Integer, Style>> it = sub.entrySet().iterator();
		while (it.hasNext()) {
			Entry<Integer, Style> e = it.next();
			oi = e.getKey();
			iKey = oi.intValue();

			if (iKey > start && iKey < start + iLen) {
				s.addAttributes(cur.getMap(), iLast, iKey - start);
			}

			iLast = iKey - start;
			cur = e.getValue();
		}

		s.addAttributes(cur.getMap(), iLast, iLen);
	}

	@Override
	public boolean isFocusStealable() {
		return (charConsumer == null && lineConsumer == null);
	}

	@Override
	public void mouseClicked(MouseEvent e) {
		if (hyperConsumer != null) {
			int x = e.getX();
			int y = e.getY();
			Line l = getHit(x, y);

			if (l != null) {
				TextLayout lay = (l.jlay == null) ? l.layout : l.jlay;
				TextHitInfo thi = lay.hitTestChar(x - l.left - H_MARGIN, y
						- l.top);
				int pos = thi.getCharIndex() + l.start;
				Integer oiPos = new Integer(pos);
				Integer oiVal = mHyperlinks.get(oiPos);

				if (oiVal == null) {
					SortedMap<Integer, Integer> m = mHyperlinks.headMap(oiPos);
					if (!m.isEmpty()) {
						oiPos = m.lastKey();
						oiVal = m.get(oiPos);
					}
				}

				if (oiVal != null && oiVal.intValue() != 0) {
					HyperlinkInputConsumer hic = hyperConsumer;
					Style nhs = nonHyper;
					nonHyper = null;
					setStyle(nhs);
					hyperConsumer = null;
					hic.consume(oiVal.intValue());
				}
			}
		}

		if (LameFocusManager.FOCUSED_WINDOW != this)
			LameFocusManager.grabFocus(this);
	}

	Line getHit(int x, int y) {
		int np = paragraphs.size();
		ListIterator<Paragraph> pit = paragraphs.listIterator(np);

		while (pit.hasPrevious()) {
			Paragraph p = pit.previous();
			int pstart = p.lines.getFirst().top;

			if (pstart <= y) {
				int nl = p.lines.size();
				ListIterator<Line> lit = p.lines.listIterator(nl);

				while (lit.hasPrevious()) {
					Line l = lit.previous();
					if (l.top <= y) {
						int ax = l.left + H_MARGIN;
						TextLayout lay = (l.jlay == null) ? l.layout : l.jlay;
						int bx = ax + (int) lay.getVisibleAdvance();
						if (l.rightImg != null)
							bx += (l.rightImg.getWidth() + 5);

						if ((x >= ax && x < bx)
								|| (l.leftImg != null && x >= H_MARGIN && x < H_MARGIN
										+ l.leftImg.getWidth()))
							return l;
						else
							return null;
					}
				}
			}
		}
		return null;
	}

	@Override
	public boolean requestCharacterInput(CharInputConsumer cic) {
		if (charConsumer != null || lineConsumer != null)
			return false;
		charConsumer = cic;
		LameFocusManager.requestFocus(this);
		iLineStartPos = iLineCursorPos = buffer.length();

		return true;
	}

	@Override
	public boolean requestLineInput(LineInputConsumer lic, String initContents,
			int max) {
		if (charConsumer != null || lineConsumer != null)
			return false;
		lineConsumer = lic;
		iLineStartPos = buffer.length();
		iLineCursorPos = iLineStartPos;
		maxLineInput = max;
		LameFocusManager.requestFocus(this);
		if (!mStyles.isEmpty())
			oldStyle = mStyles.get(mStyles.lastKey());
		setStyle((Style) hintedStyles.get("input"));
		if (initContents != null) {
			if (initContents.length() > max)
				putString(initContents.substring(0, max));
			else
				putString(initContents);
			iLineCursorPos = buffer.length();
			layoutLastLine(buffer.length());
		}

		return true;
	}

	@Override
	public void cancelCharacterInput() {
		charConsumer = null;
	}

	@Override
	public String cancelLineInput() {
		if (lineConsumer == null)
			return "";

		String s;
		LineInputConsumer lic = lineConsumer;
		lineConsumer = null;
		inputHistoryIndex = 0;
		setStyle(oldStyle);

		s = buffer.substring(iLineStartPos, buffer.length());
		lic.cancel(s);
		return s;
	}

	@Override
	protected void handleKey(KeyEvent e) {
		boolean foundNonWhite;
		JViewport v = ((JScrollPane) panel).getViewport();
		Rectangle portRect = v.getViewRect();
		int viewHeight = view.getHeight();

		if (portRect.height > 5 && viewHeight > portRect.y + portRect.height) {
			scrollDown(v, viewHeight, portRect.height);
		} else if (charConsumer != null) {
			CharInputConsumer cs = charConsumer;
			charConsumer = null;

			doPaint(lastLineSeen);

			cs.consume(mapKeyEvent(e));
		} else if (lineConsumer != null) {
			boolean needLayout = false;

			if (e.getKeyChar() == KeyEvent.CHAR_UNDEFINED) {
				switch (e.getKeyCode()) {
				case KeyEvent.VK_LEFT:
					if (e.getModifiers() == InputEvent.CTRL_MASK) {
						int i;
						foundNonWhite = false;
						for (i = iLineCursorPos; i > iLineStartPos; i--) {
							if (!foundNonWhite && i != 0
									&& buffer.charAt(i - 1) != ' ')
								foundNonWhite = true;

							if (foundNonWhite && i != 0
									&& buffer.charAt(i - 1) == ' ')
								break;
						}
						iLineCursorPos = i;
					} else if (iLineCursorPos > iLineStartPos) {
						iLineCursorPos--;
					}
					break;
				case KeyEvent.VK_RIGHT:
					if (e.getModifiers() == InputEvent.CTRL_MASK) {
						int i;
						foundNonWhite = false;
						for (i = iLineCursorPos; i < buffer.length(); i++) {
							if (!foundNonWhite && buffer.charAt(i) != ' ')
								foundNonWhite = true;

							if (foundNonWhite && buffer.charAt(i) == ' ')
								break;
						}
						iLineCursorPos = i;
					} else if (iLineCursorPos < buffer.length()
							&& iLineCursorPos < iLineStartPos + maxLineInput) {
						iLineCursorPos++;
					}
					break;
				case KeyEvent.VK_UP:
					if (inputHistoryIndex < inputHistory.size()) {
						String oldReply;

						buffer.delete(iLineStartPos, buffer.length());
						iLineCursorPos = iLineStartPos;
						oldReply = inputHistory.get(inputHistoryIndex);
						inputHistoryIndex++;
						buffer.append(oldReply);
						iLineCursorPos += oldReply.length();
						needLayout = true;
					}
					break;
				case KeyEvent.VK_DOWN:
					if (inputHistoryIndex > 0) {
						String newReply;

						buffer.delete(iLineStartPos, buffer.length());
						iLineCursorPos = iLineStartPos;
						inputHistoryIndex--;

						if (inputHistoryIndex > 0) {
							newReply = inputHistory.get(inputHistoryIndex - 1);
							buffer.append(newReply);
							iLineCursorPos += newReply.length();
						}

						needLayout = true;
					}
					break;
				case KeyEvent.VK_HOME:
					iLineCursorPos = iLineStartPos;
					break;
				case KeyEvent.VK_END:
					iLineCursorPos = buffer.length();
					break;
				}
			} else {
				char c = e.getKeyChar();
				switch (c) {
				case 1:
					iLineCursorPos = iLineStartPos;
					break;
				case 5:
					iLineCursorPos = buffer.length();
					break;
				case 8:
				case 127:
					if (e.getModifiersEx() == InputEvent.META_DOWN_MASK
							|| e.getModifiersEx() == InputEvent.ALT_DOWN_MASK)

					{
						int i;
						foundNonWhite = false;
						for (i = iLineCursorPos; i > iLineStartPos; i--) {
							if (!foundNonWhite && i != 0
									&& buffer.charAt(i - 1) != ' ')
								foundNonWhite = true;

							if (foundNonWhite && i != 0
									&& buffer.charAt(i - 1) == ' ')
								break;
						}
						if (iLineCursorPos != i) {
							buffer.delete(i, iLineCursorPos);
							iLineCursorPos = i;
							needLayout = true;
						}
					} else if (iLineCursorPos > iLineStartPos) {
						iLineCursorPos--;
						buffer.deleteCharAt(iLineCursorPos);
						needLayout = true;
					}
					break;
				case 10:
				case 13:
					String stReply;
					LineInputConsumer lic = lineConsumer;
					lineConsumer = null;
					buffer.append('\n');
					setStyle(oldStyle);

					// do the layout right HERE, before consume()
					lastLineSeen = layoutLastLine(iLineStartPos);

					stReply = buffer.substring(iLineStartPos,
							buffer.length() - 1);

					if (stReply.length() > 0) {
						inputHistory.addFirst(stReply);
						if (inputHistory.size() >= HISTORY_SIZE_LIMIT)
							inputHistory.removeLast();
					}
					inputHistoryIndex = 0;
					lic.consume(stReply);

					if (echo != null) {
						echo.putString(stReply);
						echo.putChar('\n');
					}
					break;
				default:
					if (buffer.length() < (iLineStartPos + maxLineInput)
							&& (c >= 32 && c < 127) || (c >= 160 && c < 256)) {
						buffer.insert(iLineCursorPos++, c);

						needLayout = true;
					}
				}
			}

			Line fromLine = null;
			if (needLayout)
				fromLine = layoutLastLine(iLineStartPos);

			if (fromLine == null)
				fromLine = lastLineSeen;

			doPaint(fromLine);
		}
	}

	Line layoutLastLine(int startPos) {
		Paragraph p1 = null;
		Paragraph p2 = null;
		Line l1 = null;
		Line l2 = null;
		int np = 0;
		int nl = 0;
		int i = 0;
		int j = 0;
		boolean done = false;

		synchronized (paragraphs) {
			if (!paragraphs.isEmpty()) {
				np = paragraphs.size();
				ListIterator<Paragraph> pit = paragraphs.listIterator(np);

				for (i = np - 1; !done && i >= 0; i--) {
					p1 = pit.previous();
					nl = p1.lines.size();
					ListIterator<Line> lit = p1.lines.listIterator(nl);

					for (j = nl - 1; !done && j >= 0; j--) {
						l1 = lit.previous();
						done = (l1.start < startPos);
					}
				}
				if (l1 != p1.lines.getFirst()) {
					l2 = p1.lines.get(j);
				} else if (p1 != paragraphs.getFirst()) {
					p2 = paragraphs.get(i);
					l2 = p2.lines.getLast();
				}
			}
		}

		if (l1 != null && l2 != null) {
			synchronized (paragraphs) {
				p1.lines.subList(j + 1, nl).clear();
				paragraphs.subList(i + 1, np).clear();
			}
			layout(l1, l2);
			return l1;
		} else {
			synchronized (paragraphs) {
				paragraphs.clear();
			}
			layout(null, null);
			return null;
		}
	}

	class TextBufferPanel extends JPanel implements Scrollable {
		private static final long serialVersionUID = 1L;

		Dimension preferredViewportSize = new Dimension(0, 0);
		int lSize = ((Style) hintedStyles.get("normal")).size + 2;

		@Override
		public Dimension getPreferredScrollableViewportSize() {
			return preferredViewportSize;
		}

		@Override
		public int getScrollableBlockIncrement(Rectangle visibleRect,
				int orientation, int direction) {
			return preferredViewportSize.height - lSize;
		}

		@Override
		public boolean getScrollableTracksViewportHeight() {
			return false;
		}

		@Override
		public boolean getScrollableTracksViewportWidth() {
			return true;
		}

		@Override
		public int getScrollableUnitIncrement(Rectangle visibleRect,
				int orientation, int direction) {
			return lSize;
		}

		@Override
		public synchronized void paintComponent(Graphics g) {
			Rectangle clip;
			Paragraph p;
			Shape[] carets = null;
			int translateX = 0;
			int translateY = 0;
			Line l1 = null;
			Line l2 = null;
			TextLayout layout = null;

			Style lastStyle = (Style) hintedStyles.get("normal");
			Style inputStyle = (Style) hintedStyles.get("input");

			Graphics2D g2 = (Graphics2D) g;
			clip = g2.getClipBounds();
			g2.setColor(new java.awt.Color(lastStyle.backColor.getRed(),
					lastStyle.backColor.getGreen(), lastStyle.backColor
							.getBlue()));
			g2.fillRect(clip.x, clip.y, clip.width, clip.height);

			Rectangle r = new Rectangle();
			r.x = clip.x;
			r.width = clip.width;

			synchronized (paragraphs) {
				int np = paragraphs.size();
				ListIterator<Paragraph> pit = paragraphs.listIterator();

				for (int i = 0; i < np; i++) {
					p = pit.next();
					l1 = p.lines.getFirst();
					l2 = p.lines.getLast();

					r.y = l1.top;
					r.height = l2.bottom - l1.top;

					if (r.intersects(clip)) {
						l1 = p.lines.getFirst();

						MarginImage mi = l1.leftImg;
						while (mi != null) {
							r.y = mi.myTop;
							r.height = mi.img.getHeight(this);

							if (r.intersects(clip)) {
								int x = H_MARGIN
										+ p.leftBase
										+ ((mi.inherit == null) ? 0
												: mi.inherit.getWidth());
								g2.drawImage(mi.img, x, r.y, this);
							}
							mi = mi.inherit;
						}

						mi = l1.rightImg;
						while (mi != null) {
							r.y = mi.myTop;
							r.height = mi.img.getHeight(this);

							if (r.intersects(clip)) {
								int x = getWidth()
										- (H_MARGIN + p.rightBase + mi
												.getWidth());
								g2.drawImage(mi.img, x, r.y, this);
							}
							mi = mi.inherit;
						}

						int nl = p.lines.size();
						ListIterator<Line> lit = p.lines.listIterator();

						for (int j = 0; j < nl; j++) {
							l1 = lit.next();
							r.y = l1.top;
							r.height = l1.bottom - l1.top;

							if (r.intersects(clip) && l1.layout != null) {
								lastStyle = p.style;
								if (p.style.justification == Style.LEFT_RIGHT_FLUSH
										&& j != nl - 1) {
									if (l1.jlay == null)
										l1.jlay = l1.layout
												.getJustifiedLayout(l1.widthBase);
									layout = l1.jlay;
								} else {
									layout = l1.layout;
								}

								int x = l1.left + H_MARGIN;
								int y = l1.top + Math.round(layout.getAscent());
								g2.setColor(new java.awt.Color(
										lastStyle.textColor.getRed(),
										lastStyle.textColor.getGreen(),
										lastStyle.textColor.getBlue()));
								layout.draw(g2, x, y);
							}

							if ((charConsumer != null || lineConsumer != null)
									&& ((l1.start <= iLineCursorPos && l1.end > iLineCursorPos) || (iLineCursorPos == l1.end && l1.end == buffer
											.length()))) {
								translateX = l1.left + H_MARGIN;
								translateY = Math.round(l1.top
										+ layout.getAscent());

								int curPos;
								if (charConsumer != null)
									curPos = layout.getCharacterCount();
								else
									curPos = iLineCursorPos - l1.start;

								carets = layout.getCaretShapes(curPos);
							}
						}
					}
				}
			}

			if (carets != null) {
				g2.setColor(new java.awt.Color(inputStyle.textColor.getRed(),
						inputStyle.textColor.getGreen(), inputStyle.textColor
								.getBlue()));
				g2.translate(translateX, translateY);
				g2.draw(carets[0]);
			}
		}
	}

	static class Paragraph {
		Style style;
		int leftBase = 0;
		int rightBase = 0;
		LinkedList<Line> lines = new LinkedList<Line>();

		Line getLastLine() {
			return (lines.isEmpty() ? null : lines.getLast());
		}

		int getLeftIndent(int availWidth) {
			return ((int) (availWidth * 0.03)) * style.leftIndent;
		}

		int getRightIndent(int availWidth) {
			return ((int) (availWidth * 0.03)) * style.rightIndent;
		}

		int getParIndent(int availWidth) {
			return ((int) (availWidth * 0.03)) * style.parIndent;
		}
	}

	static class Line {
		Paragraph para;
		TextLayout layout;
		TextLayout jlay;
		int widthBase;

		int top;
		int bottom;
		int start;
		int end;
		int left;

		// lazily instantiated, since most lines will not reference any margin
		// images
		MarginImage leftImg;
		MarginImage rightImg;

		int getNextLineTop() {
			int left, right;
			if (leftImg != null || rightImg != null) {
				left = (leftImg == null) ? bottom : leftImg.getFlowBreakPos();
				right = (rightImg == null) ? bottom : rightImg
						.getFlowBreakPos();
				return Math.max(bottom, Math.max(left, right));
			} else {
				return bottom;
			}
		}
	}

	class MarginImage {
		MarginImage inherit;
		Image img;
		int myTop;

		int getWidth() {
			return ((inherit == null) ? 0 : inherit.getWidth())
					+ img.getWidth(view);
		}

		int getXLimit(int y) {
			if (myTop + img.getHeight(view) >= y)
				return getWidth();
			else
				return (inherit == null) ? 0 : inherit.getXLimit(y);
		}

		MarginImage getInheritance(int y) {
			if (myTop + img.getHeight(view) >= y)
				return this;
			else
				return (inherit == null) ? null : inherit.getInheritance(y);
		}

		int getFlowBreakPos() {
			// FIXME: again the +5 for a margin
			return Math.max(myTop + img.getHeight(view) + 5,
					((inherit == null) ? 0 : inherit.getFlowBreakPos()));
		}
	}

	static class InlineImgStruct {
		Image img;
		int align;
	}

	static class MarginImgStruct {
		ArrayList<Image> left;
		ArrayList<Image> right;

		void add(Image img, int align) {
			if (align == MARGIN_LEFT) {
				if (left == null)
					left = new ArrayList<Image>(1);
				left.add(img);
			} else {
				if (right == null)
					right = new ArrayList<Image>(1);
				right.add(img);
			}
		}
	}

	static class FlowBreakAttribute extends GraphicAttribute {
		static final FlowBreakAttribute SINGLETON = new FlowBreakAttribute();

		public FlowBreakAttribute() {
			super(ROMAN_BASELINE);
		}

		@Override
		public void draw(Graphics2D g, float x, float y) {
			// is invisible
		}

		@Override
		public float getAdvance() {
			return 0.0f;
		}

		@Override
		public float getAscent() {
			return 0.0f;
		}

		@Override
		public float getDescent() {
			return 0.0f;
		}
	}

	static class NewlineAttribute extends GraphicAttribute {
		TextLayout l;

		public NewlineAttribute(Map m, FontRenderContext frc) {
			super(ROMAN_BASELINE);
			l = new TextLayout("Mfgylpq", m, frc);
		}

		@Override
		public void draw(Graphics2D g, float x, float y) {

		}

		@Override
		public float getAdvance() {
			return 0f;
		}

		@Override
		public float getAscent() {
			return l.getAscent();
		}

		@Override
		public float getDescent() {
			return l.getDescent();
		}
	}

	@Override
	public void mouseDragged(MouseEvent e) {
	}

	@Override
	public void mouseMoved(MouseEvent e) {
		if (hyperConsumer != null) {
			int x = e.getX();
			int y = e.getY();
			Line l = getHit(x, y);

			if (l != null) {
				TextLayout lay = (l.jlay == null) ? l.layout : l.jlay;
				TextHitInfo thi = lay.hitTestChar(x - l.left - H_MARGIN, y
						- l.top);
				int pos = thi.getCharIndex() + l.start;
				Integer oiPos = new Integer(pos);
				Integer oiVal = mHyperlinks.get(oiPos);

				if (oiVal == null) {
					SortedMap<Integer, Integer> m = mHyperlinks.headMap(oiPos);
					if (!m.isEmpty()) {
						oiPos = m.lastKey();
						oiVal = m.get(oiPos);
					}
				}

				if (oiVal != null && oiVal.intValue() != 0) {
					view.setCursor(new Cursor(Cursor.HAND_CURSOR));
				} else {
					view.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
				}
			} else {
				view.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
			}
		}
	}
}
