/*
 * File    : GCStringPrinter.java
 * Created : 16 mars 2004
 * By      : Olivier
 * 
 * Azureus - a Java Bittorrent client
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details ( see the LICENSE file ).
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package org.gudy.azureus2.ui.swt.shells;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.layout.*;
import org.eclipse.swt.widgets.*;

import org.gudy.azureus2.core3.util.Debug;

import com.aelitis.azureus.ui.swt.imageloader.ImageLoader;

/**
 * @author Olivier Chalouhi
 * @author TuxPaper (rewrite)
 */
public class GCStringPrinter
{
	private static final boolean DEBUG = false;

	private static final String GOOD_STRING = "(/|,jI~`gy";

	public static final int FLAG_SKIPCLIP = 1;

	public static final int FLAG_FULLLINESONLY = 2;

	public static final int FLAG_NODRAW = 4;

	public static final int FLAG_KEEP_URL_INFO = 8;

	private static final Pattern patHREF = Pattern.compile(
			"<\\s*?a\\s.*?href\\s*?=\\s*?\"(.+?)\".*?>(.*?)<\\s*?/a\\s*?>",
			Pattern.CASE_INSENSITIVE);

	private static final Pattern patAHREF_TITLE = Pattern.compile(
			"title=\\\"([^\\\"]+)", Pattern.CASE_INSENSITIVE);

	private static final Pattern patAHREF_TARGET = Pattern.compile(
			"target=\\\"([^\\\"]+)", Pattern.CASE_INSENSITIVE);

	//private static final Pattern patOver1000 = Pattern.compile("[^\n]{1010,}");

	// Limit word/line length as OSX crashes on stringExtent on very very long words
	private static final int MAX_LINE_LEN = 4000;

	// max Word length can be same as line length since words are auto-split
	// across lines
	private static final int MAX_WORD_LEN = 4000;

	private boolean cutoff;

	private GC gc;

	private String string;

	private Rectangle printArea;

	private int swtFlags;

	private int printFlags;

	private Point size;

	private Color urlColor;

	private List listUrlInfo;

	private Image[] images;
	
	private float[] imageScales;

	private int iCurrentHeight;

	private boolean wrap;

	public static class URLInfo
	{
		public String url;

		public String text;

		public Color urlColor;
		
		public Color dropShadowColor;

		int relStartPos;

		// We could use a region, but that uses a resource that requires disposal
		public List hitAreas = null;

		int titleLength;

		public String fullString;

		public String title;

		public String target;

		public boolean urlUnderline;

		// @see java.lang.Object#toString()
		public String toString() {
			return super.toString() + ": relStart=" + relStartPos + ";url=" + url
					+ ";title=" + text + ";hit="
					+ (hitAreas == null ? 0 : hitAreas.size());
		}
	}

	private class LineInfo
	{
		public int width;

		String originalLine;

		String lineOutputed;

		int excessPos;

		public int relStartPos;
		
		public int height;
		
		public int imageIndexes[];

		public LineInfo(String originalLine, int relStartPos) {
			this.originalLine = originalLine;
			this.relStartPos = relStartPos;
		}

		// @see java.lang.Object#toString()
		public String toString() {
			return super.toString() + ": relStart=" + relStartPos + ";xcess="
					+ excessPos + ";orig=" + originalLine + ";output=" + lineOutputed;
		}
	}
	
	public static boolean printString(GC gc, String string, Rectangle printArea) {
		return printString(gc, string, printArea, false, false);
	}

	public static boolean printString(GC gc, String string, Rectangle printArea,
			boolean skipClip, boolean fullLinesOnly) {
		return printString(gc, string, printArea, skipClip, fullLinesOnly, SWT.WRAP
				| SWT.TOP);
	}

	/**
	 * 
	 * @param gc GC to print on
	 * @param string Text to print
	 * @param printArea Area of GC to print text to
	 * @param skipClip Don't set any clipping on the GC.  Text may overhang 
	 *                 printArea when this is true
	 * @param fullLinesOnly If bottom of a line will be chopped off, do not display it
	 * @param swtFlags SWT flags.  SWT.CENTER, SWT.BOTTOM, SWT.TOP, SWT.WRAP
	 * @return whether it fit
	 */
	public static boolean printString(GC gc, String string, Rectangle printArea,
			boolean skipClip, boolean fullLinesOnly, int swtFlags) {
		try {
			GCStringPrinter sp = new GCStringPrinter(gc, string, printArea, skipClip,
					fullLinesOnly, swtFlags);
			return sp.printString();
		} catch (Exception e) {
			e.printStackTrace();
		}

		return false;
	}

	private boolean _printString() {
		boolean b = false;
		try {
			boolean wasAdvanced = gc.getAdvanced();
			Rectangle clipping = null;
			// With Advanced ontext antialias in SWT.DEFAULT is not the system's default
			// Turn off Advanced while drawing text so it antialiases based on 
			// system prefs.
			if (gc.getAdvanced() && gc.getTextAntialias() == SWT.DEFAULT
					&& gc.getAlpha() == 255) {
				clipping = gc.getClipping();
				gc.setAdvanced(false);
				gc.setClipping(clipping);
			}
			b = __printString();
			if (wasAdvanced) {
				gc.setAdvanced(true);
				gc.setClipping(clipping);
			}
		} catch (Throwable t) {
			Debug.out(t);
		}

		if (DEBUG) {
			System.out.println("");
		}

		return b;
	}

	/**
	 * @param gc
	 * @param string
	 * @param printArea
	 * @param printFlags
	 * @param swtFlags
	 * @return
	 *
	 * @since 3.0.4.3
	 */
	private boolean __printString() {
		size = new Point(0, 0);

		if (string == null) {
			return false;
		}

		if (printArea == null || printArea.isEmpty()) {
			return false;
		}

		ArrayList lines = new ArrayList();
		
		while (string.indexOf('\t') >= 0) {
			string = string.replace('\t', ' ');
		}

		boolean fullLinesOnly = (printFlags & FLAG_FULLLINESONLY) > 0;
		boolean skipClip = (printFlags & FLAG_SKIPCLIP) > 0;
		boolean noDraw = (printFlags & FLAG_NODRAW) > 0;
		wrap = (swtFlags & SWT.WRAP) > 0;

		if ((printFlags & FLAG_KEEP_URL_INFO) == 0) {
			Matcher htmlMatcher = patHREF.matcher(string);
			boolean hasURL = htmlMatcher.find();
			if (hasURL) {
				listUrlInfo = new ArrayList(1);

				while (hasURL) {
					URLInfo urlInfo = new URLInfo();

					// Store the full ahref string once, then use substring which doesn't
					// create real strings :)
					urlInfo.fullString = htmlMatcher.group();
					urlInfo.relStartPos = htmlMatcher.start(0);

					urlInfo.url = string.substring(htmlMatcher.start(1),
							htmlMatcher.end(1));
					urlInfo.text = string.substring(htmlMatcher.start(2),
							htmlMatcher.end(2));
					urlInfo.titleLength = urlInfo.text.length();

					Matcher matcherTitle = patAHREF_TITLE.matcher(urlInfo.fullString);
					if (matcherTitle.find()) {
						urlInfo.title = string.substring(urlInfo.relStartPos
								+ matcherTitle.start(1), urlInfo.relStartPos
								+ matcherTitle.end(1));
					}

					Matcher matcherTarget = patAHREF_TARGET.matcher(urlInfo.fullString);
					if (matcherTarget.find()) {
						urlInfo.target = string.substring(urlInfo.relStartPos
								+ matcherTarget.start(1), urlInfo.relStartPos
								+ matcherTarget.end(1));
					}

					//System.out.println("URLINFO! " + urlInfo.fullString 
					//		+ "\ntarget="
					//		+ urlInfo.target + "\ntt=" + urlInfo.title + "\nurl="
					//		+ urlInfo.url + "\ntext=" + urlInfo.text + "\n\n");

					string = htmlMatcher.replaceFirst(urlInfo.text.replaceAll("\\$",
							"\\\\\\$"));
					
					listUrlInfo.add(urlInfo);
					htmlMatcher = patHREF.matcher(string);
					hasURL = htmlMatcher.find(urlInfo.relStartPos);
				}
			}
		} else {
			Matcher htmlMatcher = patHREF.matcher(string);
			string = htmlMatcher.replaceAll("$2");
		}

		Rectangle rectDraw = new Rectangle(printArea.x, printArea.y,
				printArea.width, printArea.height);

		Rectangle oldClipping = null;
		try {
			if (!skipClip && !noDraw) {
				oldClipping = gc.getClipping();

				// Protect the GC from drawing outside the drawing area
				gc.setClipping(printArea);
			}

			// Process string line by line
			iCurrentHeight = 0;
			int currentCharPos = 0;
			
			int pos1 = string.indexOf('\n');
			int pos2 = string.indexOf('\r');
			if (pos2 == -1) {
				pos2 = pos1;
			}
			int posNewLine = Math.min(pos1, pos2);
			if (posNewLine < 0) {
				posNewLine = string.length();
			}
			int posLastNewLine = 0;
			while (posNewLine >= 0 && posLastNewLine < string.length()) {
				String sLine = string.substring(posLastNewLine, posNewLine);

				do {
					LineInfo lineInfo = new LineInfo(sLine, currentCharPos);
					lineInfo = processLine(gc, lineInfo, printArea,  fullLinesOnly,
							false);
					String sProcessedLine = (String) lineInfo.lineOutputed;

					if (sProcessedLine != null && sProcessedLine.length() > 0) {
						if (lineInfo.width == 0 || lineInfo.height == 0) {
							Point gcExtent = gc.stringExtent(sProcessedLine);
							if (lineInfo.width == 0) {
								lineInfo.width = gcExtent.x;
							}
							if (lineInfo.height == 0) {
								lineInfo.height = gcExtent.y;
							}
						}
						Point extent = new Point(lineInfo.width, lineInfo.height);
						iCurrentHeight += extent.y;
						boolean isOverY = iCurrentHeight > printArea.height;

						if (DEBUG) {
							System.out.println("Adding Line: [" + sProcessedLine + "]"
									+ sProcessedLine.length() + "; h=" + iCurrentHeight + "("
									+ printArea.height + "). fullOnly?" + fullLinesOnly
									+ ". Excess: " + lineInfo.excessPos);
						}

						if (isOverY && !fullLinesOnly) {
							//fullLinesOnly = true; // <-- don't know why we needed this
							lines.add(lineInfo);
						} else if (isOverY && fullLinesOnly && lines.size() > 0) {
							String excess = lineInfo.excessPos >= 0
									? sLine.substring(lineInfo.excessPos) : null;
							if (excess != null) {
								if (fullLinesOnly) {
									if (lines.size() > 0) {
										lineInfo = (LineInfo) lines.remove(lines.size() - 1);
										sProcessedLine = lineInfo.originalLine.length() > MAX_LINE_LEN
												? lineInfo.originalLine.substring(0, MAX_LINE_LEN)
												: lineInfo.originalLine;
										//sProcessedLine = ((LineInfo) lines.remove(lines.size() - 1)).originalLine;
										extent = gc.stringExtent(sProcessedLine);
									} else {
										if (DEBUG) {
											System.out.println("No PREV!?");
										}
										return false;
									}
								} else {
									sProcessedLine = sProcessedLine.length() > MAX_LINE_LEN
											? sProcessedLine.substring(0, MAX_LINE_LEN)
											: sProcessedLine;
								}

								if (excess.length() > MAX_LINE_LEN) {
									excess = excess.substring(0, MAX_LINE_LEN);
								}

								StringBuffer outputLine = new StringBuffer(sProcessedLine);
								lineInfo.width = extent.x;
								wrap = false;
								int newExcessPos = processWord(gc, sProcessedLine,
										" " + excess, printArea, lineInfo, outputLine,
										new StringBuffer());
								if (DEBUG) {
									System.out.println("  with word [" + excess + "] len is "
											+ lineInfo.width + "(" + printArea.width + ") w/excess "
											+ newExcessPos);
								}

								lineInfo.lineOutputed = outputLine.toString();
								lines.add(lineInfo);
								if (DEBUG) {
									System.out.println("replace prev line with: "
											+ outputLine.toString());
								}
							} else {
								if (DEBUG) {
									System.out.println("No Excess");
								}
							}
							cutoff = true;
							return false;
						} else {
							lines.add(lineInfo);
						}
						sLine = lineInfo.excessPos >= 0 && wrap
								? sLine.substring(lineInfo.excessPos) : null;
					} else {
						if (DEBUG) {
							System.out.println("Line process resulted in no text: " + sLine);
						}
						lines.add(lineInfo);
						currentCharPos++;
						break;
						//return false;
					}

					currentCharPos += lineInfo.excessPos >= 0 ? lineInfo.excessPos
							: lineInfo.lineOutputed.length();
					//System.out.println("output: " + lineInfo.lineOutputed.length() + ";" 
					//		+ lineInfo.lineOutputed + ";xc=" + lineInfo.excessPos + ";ccp=" + currentCharPos);
					//System.out.println("lineo=" + lineInfo.lineOutputed.length() + ";" + sLine.length() );
				} while (sLine != null);

				if (string.length() > posNewLine && string.charAt(posNewLine) == '\r'
						&& string.charAt(posNewLine + 1) == '\n') {
					posNewLine++;
				}
				posLastNewLine = posNewLine + 1;
				currentCharPos = posLastNewLine;

				pos1 = string.indexOf('\n', posLastNewLine);
				pos2 = string.indexOf('\r', posLastNewLine);
				if (pos2 == -1) {
					pos2 = pos1;
				}
				posNewLine = Math.min(pos1, pos2);
				if (posNewLine < 0) {
					posNewLine = string.length();
				}
			}
		} finally {
			if (lines.size() > 0) {
				// rebuild full text to get the exact y-extent of the output
				// this may be different (but shouldn't be!) than the height of each
				// line
				StringBuffer fullText = new StringBuffer(string.length() + 10);
				for (Iterator iter = lines.iterator(); iter.hasNext();) {
					LineInfo lineInfo = (LineInfo) iter.next();
					if (fullText.length() > 0) {
						fullText.append('\n');
					}
					fullText.append(lineInfo.lineOutputed);
				}

				//size = gc.textExtent(fullText.toString());

				for (Iterator iter = lines.iterator(); iter.hasNext();) {
					LineInfo lineInfo = (LineInfo) iter.next();
					size.x = Math.max(lineInfo.width, size.x);
					size.y += lineInfo.height;
				}
				
				if ((swtFlags & (SWT.BOTTOM)) != 0) {
					rectDraw.y = rectDraw.y + rectDraw.height - size.y;
				} else if ((swtFlags & SWT.TOP) == 0) {
					// center vert
					rectDraw.y = rectDraw.y + (rectDraw.height - size.y) / 2;
				}

				if (!noDraw || listUrlInfo != null) {
					for (Iterator iter = lines.iterator(); iter.hasNext();) {
						LineInfo lineInfo = (LineInfo) iter.next();
						try {
							drawLine(gc, lineInfo, swtFlags, rectDraw, noDraw);
						} catch (Throwable t) {
							t.printStackTrace();
						}
					}
				}
			}

			if (!skipClip && !noDraw) {
				gc.setClipping(oldClipping);
			}

		}
		
		cutoff |= size.y > printArea.height;
		return !cutoff;
	}

	/**
	 * @param hasMoreElements 
	 * @param line
	 *
	 * @since 3.0.0.7
	 */
	private LineInfo processLine(final GC gc, final LineInfo lineInfo,
			final Rectangle printArea, final boolean fullLinesOnly,
			boolean hasMoreElements) {

		if (lineInfo.originalLine.length() == 0) {
			lineInfo.lineOutputed = "";
			lineInfo.height = gc.stringExtent(GOOD_STRING).y;
			return lineInfo;
		}
		
		StringBuffer outputLine = new StringBuffer();
		int excessPos = -1;

		if (images != null || lineInfo.originalLine.length() > MAX_LINE_LEN
				|| gc.stringExtent(lineInfo.originalLine).x > printArea.width) {
			if (DEBUG) {
				System.out.println("Line to process: " + lineInfo.originalLine);
			}
			StringBuffer space = new StringBuffer(1);

			if (!wrap && images == null) {
				if (DEBUG) {
					System.out.println("No Wrap.. doing all in one line");
				}

				String sProcessedLine = lineInfo.originalLine.length() > MAX_LINE_LEN
						? lineInfo.originalLine.substring(0, MAX_LINE_LEN)
						: lineInfo.originalLine;

				// if it weren't for the elipses, we could do:
				// outputLine.append(sProcessedLine);

				excessPos = processWord(gc, lineInfo.originalLine, sProcessedLine,
						printArea, lineInfo, outputLine, space);
			} else {
				int posLastWordStart = 0;
				int posWordStart = lineInfo.originalLine.indexOf(' ');
				while (posWordStart == 0) {
					posWordStart = lineInfo.originalLine.indexOf(' ', posWordStart + 1);
				}
				if (posWordStart < 0) {
					posWordStart = lineInfo.originalLine.length();
				}
				// Process line word by word
				int curPos = 0;
				while (posWordStart >= 0 && posLastWordStart < lineInfo.originalLine.length()) {
					String word = lineInfo.originalLine.substring(posLastWordStart, posWordStart);
					if (word.length() == 0) {
						excessPos = -1;
						outputLine.append(' ');
					}

					for (int i = 0; i < word.length(); i += MAX_WORD_LEN) {
						String subWord;
						int endPos = i + MAX_WORD_LEN;
						if (endPos > word.length()) {
							subWord = word.substring(i);
						} else {
							subWord = word.substring(i, endPos);
						}

						excessPos = processWord(gc, lineInfo.originalLine, subWord,
								printArea, lineInfo, outputLine, space);
						if (DEBUG) {
							System.out.println("  with word [" + subWord + "] len is "
									+ lineInfo.width + "(" + printArea.width + ") w/excess "
									+ excessPos);
						}
						if (excessPos >= 0) {
							excessPos += curPos;
							break;
						}
						if (endPos <= word.length()) {
							space.setLength(0);
						}
						curPos += subWord.length() + 1;
					}
					if (excessPos >= 0) {
						break;
					}
					
					posLastWordStart = posWordStart + 1;
					posWordStart = lineInfo.originalLine.indexOf(' ', posLastWordStart);
					if (posWordStart < 0) {
						posWordStart = lineInfo.originalLine.length();
					}
				}
			}
		} else {
			outputLine.append(lineInfo.originalLine);
		}

		if (!wrap && hasMoreElements && excessPos >= 0) {
			int len = outputLine.length();
			if (len > 2) {
				len -= 2;
			}
			outputLine.setLength(len);
			outputLine.append("\u2026");
			cutoff = true;
		}
		//drawLine(gc, outputLine, swtFlags, rectDraw);
		//		if (!wrap) {
		//			return hasMoreElements;
		//		}
		lineInfo.excessPos = excessPos;
		lineInfo.lineOutputed = outputLine.toString();
		return lineInfo;
	}

	/**
	 * @param int Position of part of word that didn't fit
	 *
	 * @since 3.0.0.7
	 */
	private int processWord(final GC gc, final String sLine, String word,
			final Rectangle printArea, final LineInfo lineInfo,
			StringBuffer outputLine, final StringBuffer space) {

		if (word.length() == 0) {
			space.append(' ');
			return -1;
		}
		
		//System.out.println("PW: " + word);
		if (images != null && word.length() >= 2 && word.charAt(0) == '%') {
			int imgIdx = word.charAt(1) - '0';
			if (images.length > imgIdx && imgIdx >= 0 && images[imgIdx] != null) {
				Image img = images[imgIdx];
				Rectangle bounds = img.getBounds();
				if (imageScales != null && imageScales.length > imgIdx) {
					bounds.width = (int) (bounds.width * imageScales[imgIdx]);
					bounds.height = (int) (bounds.height * imageScales[imgIdx]);
				}
				
				Point spaceExtent = gc.stringExtent(space.toString());
				int newWidth = lineInfo.width + bounds.width + spaceExtent.x;


				if (newWidth > printArea.width) {
					if (bounds.width + spaceExtent.x < printArea.width || lineInfo.width > 0) {
						//outputLine.append(space);
						//outputLine.append(word, 0, 2);
						//System.out.println("w1 = " + lineInfo.width + ";h=" + lineInfo.height);
						return 0;
					}
				}
				
				if (lineInfo.imageIndexes == null) {
					lineInfo.imageIndexes = new int[] { imgIdx };
				}
				
				
				int targetWidth = lineInfo.width + newWidth;
				
				lineInfo.width = newWidth;
				lineInfo.height = Math.max(bounds.height, lineInfo.height);

				Point ptWordSize = gc.stringExtent(word.substring(2) + " ");
				if (lineInfo.width + ptWordSize.x > printArea.width) {
					outputLine.append(space);
					outputLine.append(word.substring(0,2));
					//System.out.println("w8 = " + lineInfo.width + ";h=" + lineInfo.height);
					return 2;
				}
				
				outputLine.append(space);
				space.setLength(0);
				outputLine.append(word.substring(0, 2));
				word = word.substring(2);
				//outputLine.append(word);
				//if (space.length() > 0) {
				//	space.delete(0, space.length());
				//}
				//space.append(' ');
				
				//System.out.println("w2 = " + lineInfo.width + ";h=" + lineInfo.height);
				//return -1;
			}
		}

		Point ptLineAndWordSize = gc.stringExtent(outputLine + word + " ");
		//System.out.println(ptLineAndWordSize + ";" + outputLine  + "::WordComp " + (ptLineAndWordSize.x - lineInfo.width));
		if (ptLineAndWordSize.x > printArea.width) {
			// word is longer than space avail, split

			Point ptWordSize2 = gc.stringExtent(word + " ");
			boolean bWordLargerThanWidth = ptWordSize2.x > printArea.width;
			// This will split put a word that is longer than a full line onto a new
			// line (when the existing line has text).
			if (bWordLargerThanWidth && lineInfo.width > 0) {
				//System.out.println("w3 = " + lineInfo.width + ";h=" + lineInfo.height);
				return 0;
			}

			int endIndex = word.length();
			long diff = endIndex;

			while (ptLineAndWordSize.x != printArea.width) {
				diff = (diff >> 1) + (diff % 2);

				if (diff <= 0) {
					diff = 1;
				}

				//System.out.println("diff=" + diff + ";e=" + endIndex + ";tw=" + targetWidth + ";paw= " + printArea.width);
				if (ptLineAndWordSize.x > printArea.width) {
					endIndex -= diff;
					if (endIndex < 1) {
						endIndex = 1;
					}
				} else {
					endIndex += diff;
					if (endIndex > word.length()) {
						endIndex = word.length();
					}
				}

				ptLineAndWordSize = gc.stringExtent(outputLine + word.substring(0, endIndex) + " ");

				if (diff <= 1) {
					break;
				}
			}
			boolean nothingFit = endIndex == 0;
			if (nothingFit) {
				endIndex = 1;
			}
			if (ptLineAndWordSize.x > printArea.width && endIndex > 1) {
				endIndex--;
				ptLineAndWordSize = gc.stringExtent(outputLine + word.substring(0, endIndex) + " ");
			}

			if (DEBUG) {
				System.out.println("excess starts at " + endIndex + " of "
						+ word.length() + ". "
						+ "wrap?" + wrap);
			}
			if (wrap && (printFlags & FLAG_FULLLINESONLY) > 0) {
				int nextLineHeight = gc.stringExtent(GOOD_STRING).y;
				if (iCurrentHeight + ptLineAndWordSize.y + nextLineHeight > printArea.height) {
					if (DEBUG) {
						System.out.println("turn off wrap");
					}
					wrap = false;
				}
			}

			if (endIndex > 0 && outputLine.length() > 0 && !nothingFit) {
				outputLine.append(space);
			}

			int w = ptLineAndWordSize.x - lineInfo.width;
			if (wrap && !nothingFit && !bWordLargerThanWidth) {
				// whole word is excess
				return 0;
			}

			outputLine.append(word.substring(0, endIndex));
			if (!wrap) {
				int len = outputLine.length();
				if (len == 0) {
					if (word.length() > 0) {
						outputLine.append(word.charAt(0));
					} else if (sLine.length() > 0) {
						outputLine.append(sLine.charAt(0));
					}
				} else {
					if (len > 2) {
						len -= 2;
					}
					outputLine.setLength(len);
					outputLine.append("\u2026");
					cutoff = true;
				}
			}
			//drawLine(gc, outputLine, swtFlags, rectDraw);
			if (DEBUG) {
				System.out.println("excess " + word.substring(endIndex));
			}
			//System.out.println("w9 = " + lineInfo.width + ";h=" + lineInfo.height);
			return endIndex;
		}

		lineInfo.width = ptLineAndWordSize.x;
		if (lineInfo.width > printArea.width) {
			if (space.length() > 0) {
				space.delete(0, space.length());
			}

			if (!wrap) {
				int len = outputLine.length();
				if (len == 0) {
					if (word.length() > 0) {
						outputLine.append(word.charAt(0));
					} else if (sLine.length() > 0) {
						outputLine.append(sLine.charAt(0));
					}
				} else {
					if (len > 2) {
						len -= 2;
					}
					outputLine.setLength(len);
					outputLine.append("\u2026");
					cutoff = true;
				}
				//System.out.println("w5 = " + lineInfo.width + ";h=" + lineInfo.height);
				return -1;
			} else {
				//System.out.println("w6 = " + lineInfo.width + ";h=" + lineInfo.height);
				return 0;
			}
			//drawLine(gc, outputLine, swtFlags, rectDraw);
		}

		if (outputLine.length() > 0) {
			outputLine.append(space);
		}
		outputLine.append(word);
		if (space.length() > 0) {
			space.delete(0, space.length());
		}
		space.append(' ');

		//System.out.println("w4 = " + lineInfo.width + ";h=" + lineInfo.height);
		return -1;
	}

	/**
	 * printArea is updated to the position of the next row
	 * 
	 * @param gc
	 * @param outputLine
	 * @param swtFlags
	 * @param printArea
	 * @param noDraw 
	 */
	private void drawLine(GC gc, LineInfo lineInfo, int swtFlags,
			Rectangle printArea, boolean noDraw) {
		String text = lineInfo.lineOutputed;
		// TODO: ensure width and height have values
		if (lineInfo.width == 0 || lineInfo.height == 0) {
			Point gcExtent = gc.stringExtent(text);;
			if (lineInfo.width == 0) {
				lineInfo.width = gcExtent.x;
			}
			if (lineInfo.height == 0) {
				lineInfo.height = gcExtent.y;
			}
		}
		Point drawSize = new Point(lineInfo.width, lineInfo.height);
		
		int x0;
		if ((swtFlags & SWT.RIGHT) > 0) {
			x0 = printArea.x + printArea.width - drawSize.x;
		} else if ((swtFlags & SWT.CENTER) > 0) {
			x0 = printArea.x + (printArea.width - drawSize.x) / 2;
		} else {
			x0 = printArea.x;
		}

		int y0 = printArea.y;

		int lineInfoRelEndPos = lineInfo.relStartPos
				+ lineInfo.lineOutputed.length();
		int relStartPos = lineInfo.relStartPos;
		int lineStartPos = 0;

		URLInfo urlInfo = null;
		boolean drawURL = hasHitUrl();

		if (drawURL) {
			URLInfo[] hitUrlInfo = getHitUrlInfo();
			int nextHitUrlInfoPos = 0;

			while (drawURL) {
				drawURL = false;
				for (int i = nextHitUrlInfoPos; i < hitUrlInfo.length; i++) {
					urlInfo = hitUrlInfo[i];

					drawURL = (urlInfo.relStartPos < lineInfoRelEndPos)
							&& (urlInfo.relStartPos + urlInfo.titleLength > relStartPos)
							&& (relStartPos >= lineInfo.relStartPos)
							&& (relStartPos < lineInfoRelEndPos);
					if (drawURL) {
						nextHitUrlInfoPos = i + 1;
						break;
					}
				}

				if (!drawURL) {
					break;
				}

				//int numHitUrlsAlready = urlInfo.hitAreas == null ? 0 : urlInfo.hitAreas.size();

				// draw text before url
				int i = lineStartPos + urlInfo.relStartPos - relStartPos;
				//System.out.println("numHitUrlsAlready = " + numHitUrlsAlready + ";i=" + i);
				if (i > 0 && i > lineStartPos && i <= text.length()) {
					String s = text.substring(lineStartPos, i);
					//gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_RED));
					x0 += drawText(gc, s, x0, y0, lineInfo.height, null, noDraw).x;

					relStartPos += (i - lineStartPos);
					lineStartPos += (i - lineStartPos);
					//System.out.println("|" + s + "|" + textExtent.x);
				}

				// draw url text
				int end = i + urlInfo.titleLength;
				if (i < 0) {
					i = 0;
				}
				//System.out.println("end=" + end + ";" + text.length() + ";titlelen=" + urlInfo.titleLength);
				if (end > text.length()) {
					end = text.length();
				}
				String s = text.substring(i, end);
				relStartPos += (end - i);
				lineStartPos += (end - i);
				Point pt = null;
				//System.out.println("|" + s + "|");
				Color fgColor = null;
				if (!noDraw) {
					fgColor = gc.getForeground();

					if (urlInfo.dropShadowColor != null) {
						gc.setForeground(urlInfo.dropShadowColor);
						drawText(gc, s, x0 + 1, y0 + 1, lineInfo.height, null, noDraw);
					}

					if (urlInfo.urlColor != null) {
						gc.setForeground(urlInfo.urlColor);
					} else if (urlColor != null) {
						gc.setForeground(urlColor);
					}
				}
				if (urlInfo.hitAreas == null) {
					urlInfo.hitAreas = new ArrayList(1);
				}
				pt = drawText(gc, s, x0, y0, lineInfo.height, urlInfo.hitAreas, noDraw);
				if (!noDraw) {
					if (urlInfo.urlUnderline) {
						gc.drawLine(x0, y0 + pt.y - 1, x0 + pt.x - 1, y0 + pt.y - 1); 
					}
					gc.setForeground(fgColor);
				}

				if (urlInfo.hitAreas == null) {
					urlInfo.hitAreas = new ArrayList(1);
				}
				//gc.drawRectangle(new Rectangle(x0, y0, pt.x, lineInfo.height));

				x0 += pt.x;
			}
		}

		// draw text after url
		if (lineStartPos < text.length()) {
			String s = text.substring(lineStartPos);
			if (!noDraw) {
				drawText(gc, s, x0, y0, lineInfo.height, null, noDraw);
			}
		}
		printArea.y += drawSize.y;
	}
	
	private Point drawText(GC gc, String s, int x, int y, int height,
			List hitAreas, boolean nodraw) {
		Point textExtent;

		if (images != null) {
  		int pctPos = s.indexOf('%');
  		int lastPos = 0;
  		int w = 0;
  		int h = 0;
  		while (pctPos >= 0) {
    		if (pctPos >= 0 && s.length() > pctPos + 1) {
    			int imgIdx = s.charAt(pctPos + 1) - '0';
    			
    			if (imgIdx >= images.length || imgIdx < 0 || images[imgIdx] == null) {
      			String sStart = s.substring(lastPos, pctPos + 1);
    				textExtent = gc.textExtent(sStart);
    				int centerY = y + (height / 2 - textExtent.y / 2); 
        		if (hitAreas != null) {
        			hitAreas.add(new Rectangle(x, centerY, textExtent.x, textExtent.y));
        		}
      			if (!nodraw) {
      				gc.drawText(sStart, x, centerY, true);
      			}
      			x += textExtent.x;
      			w += textExtent.x;
      			h = Math.max(h, textExtent.y);

      			lastPos = pctPos + 1;
        		pctPos = s.indexOf('%', pctPos + 1);
    				continue;
    			}
    			
    			String sStart = s.substring(lastPos, pctPos);
    			textExtent = gc.textExtent(sStart);
  				int centerY = y + (height / 2 - textExtent.y / 2); 
    			if (!nodraw) {
    				gc.drawText(sStart, x, centerY, true);
    			}
    			x += textExtent.x;
    			w += textExtent.x;
    			h = Math.max(h, textExtent.y);
    			if (hitAreas != null) {
    				hitAreas.add(new Rectangle(x, centerY, textExtent.x, textExtent.y));
    			}

    			//System.out.println("drawimage: " + x + "x" + y + ";idx=" + imgIdx);
    			Rectangle imgBounds = images[imgIdx].getBounds();
    			float scale = 1.0f;
  				if (imageScales != null && imageScales.length > imgIdx) {
  					scale = imageScales[imgIdx];
  				}
  				int scaleImageWidth = (int) (imgBounds.width * scale);
					int scaleImageHeight = (int) (imgBounds.height * scale);

    			
  				centerY = y + (height / 2 - scaleImageHeight / 2); 
    			if (hitAreas != null) {
    				hitAreas.add(new Rectangle(x, centerY, scaleImageWidth, scaleImageHeight));
    			}
    			if (!nodraw) {
    				//gc.drawImage(images[imgIdx], x, centerY);
    				gc.drawImage(images[imgIdx], 0, 0, imgBounds.width,
								imgBounds.height, x, centerY, scaleImageWidth, scaleImageHeight);
    			}
    			x += scaleImageWidth;
    			w += scaleImageWidth;
    			
    			h = Math.max(h, scaleImageHeight);
    		}
    		lastPos = pctPos + 2;
    		pctPos = s.indexOf('%', lastPos);
  		}

  		if (s.length() >= lastPos) {
    		String sEnd = s.substring(lastPos);
  			textExtent = gc.textExtent(sEnd);
				int centerY = y + (height / 2 - textExtent.y / 2); 
  			if (hitAreas != null) {
  				hitAreas.add(new Rectangle(x, centerY, textExtent.x, textExtent.y));
  			}
  			if (!nodraw) {
  				gc.drawText(sEnd, x, centerY, true);
  			}
  			x += textExtent.x;
  			w += textExtent.x;
  			h = Math.max(h, textExtent.y);
  		}
  		return new Point(w, h);
		}


		if (!nodraw) {
			gc.drawText(s, x, y, true);
		}
		textExtent = gc.textExtent(s);
		if (hitAreas != null) {
			hitAreas.add(new Rectangle(x, y, textExtent.x, textExtent.y));
		}
		return textExtent;
	}

	public static void main(String[] args) {

		//String s = "this is $1.00";
		//String s2 = "$1";
		//String s3 = s2.replaceAll("\\$", "\\\\\\$");
		//System.out.println(s3);
		//s.replaceAll("h", s3);
		//System.out.println(s);
		//if (true) {
		//	return;
		//}
		
		final Display display = Display.getDefault();
		final Shell shell = new Shell(display, SWT.SHELL_TRIM);

		ImageLoader imageLoader = ImageLoader.getInstance();
		
		final Image[] images = {
			imageLoader.getImage("azureus32"),
			imageLoader.getImage("azureus64"),
			imageLoader.getImage("azureus"),
			imageLoader.getImage("azureus128"),
		};

		//final String text = "Opil Wrir, Na Poys Iysk, Yann Only. test of the string printer averlongwordthisisyesindeed";
		final String text = "Apple <A HREF=\"aa\">Banana</a>, Cow <A HREF=\"ss\">Dug Ergo</a>, Flip Only. test of the string printer averlongwordthisisyesindeed";
		//final String text = "Apple, Cow sfjkhsd %1 f, Flip Only. test of %0 the string printer averlongwordthisisyesindeed";

		shell.setSize(500, 500);

		GridLayout gridLayout = new GridLayout(2, false);
		shell.setLayout(gridLayout);

		Composite cButtons = new Composite(shell, SWT.NONE);
		GridData gridData = new GridData(SWT.NONE, SWT.FILL, false, true);
		cButtons.setLayoutData(gridData);
		final Canvas cPaint = new Canvas(shell, SWT.DOUBLE_BUFFERED);
		gridData = new GridData(SWT.FILL, SWT.NONE, true, false);
		gridData.heightHint = 40;
		cPaint.setLayoutData(gridData);

		cButtons.setLayout(new RowLayout(SWT.VERTICAL));

		Listener l = new Listener() {
			public void handleEvent(Event event) {
				cPaint.redraw();
			}
		};

		final Text txtText = new Text(cButtons, SWT.WRAP | SWT.MULTI | SWT.BORDER);
		txtText.setText(text);
		txtText.addListener(SWT.Modify, l);
		txtText.setLayoutData(new RowData(100, 200));
		txtText.addKeyListener(new KeyListener() {
			public void keyReleased(KeyEvent e) {
			}

			public void keyPressed(KeyEvent e) {
				if (e.keyCode == 'a' && e.stateMask == SWT.CONTROL) {
					txtText.selectAll();
				}
			}
		});

		final Button btnSkipClip = new Button(cButtons, SWT.CHECK);
		btnSkipClip.setText("Skip Clip");
		btnSkipClip.setSelection(true);
		btnSkipClip.addListener(SWT.Selection, l);

		final Button btnFullOnly = new Button(cButtons, SWT.CHECK);
		btnFullOnly.setText("Full Lines Only");
		btnFullOnly.setSelection(true);
		btnFullOnly.addListener(SWT.Selection, l);

		final Combo cboVAlign = new Combo(cButtons, SWT.READ_ONLY);
		cboVAlign.add("Top");
		cboVAlign.add("Bottom");
		cboVAlign.add("None");
		cboVAlign.addListener(SWT.Selection, l);
		cboVAlign.select(0);

		final Combo cboHAlign = new Combo(cButtons, SWT.READ_ONLY);
		cboHAlign.add("Left");
		cboHAlign.add("Center");
		cboHAlign.add("Right");
		cboHAlign.add("None");
		cboHAlign.addListener(SWT.Selection, l);
		cboHAlign.select(0);

		final Button btnWrap = new Button(cButtons, SWT.CHECK);
		btnWrap.setText("Wrap");
		btnWrap.setSelection(true);
		btnWrap.addListener(SWT.Selection, l);
		
		final Label lblInfo = new Label(shell, SWT.WRAP);
		lblInfo.setText("Welcome");
		

		Listener l2 = new Listener() {
			URLInfo lastHitInfo = null;

			public void handleEvent(Event event) {
				GC gc = event.gc;
				//System.out.println("HE" + event.type);
				boolean ourGC = gc == null;
				if (ourGC) {
					gc = new GC(cPaint);
				}
				try {
					GCStringPrinter sp = buildSP(gc);
					Color colorURL = gc.getDevice().getSystemColor(SWT.COLOR_RED);
					Color colorURL2 = gc.getDevice().getSystemColor(
							SWT.COLOR_DARK_MAGENTA);

					if (event.type == SWT.MouseMove) {
						Point pt = cPaint.toControl(display.getCursorLocation());
						URLInfo hitUrl = sp.getHitUrl(pt.x, pt.y);
						String url1 = hitUrl == null || hitUrl.url == null ? ""
								: hitUrl.url;
						String url2 = lastHitInfo == null || lastHitInfo.url == null ? ""
								: lastHitInfo.url;

						if (url1.equals(url2)) {
							return;
						}
						cPaint.redraw();
						lastHitInfo = hitUrl;
						return;
					}

					Rectangle bounds = cPaint.getClientArea();

					Color colorBox = gc.getDevice().getSystemColor(SWT.COLOR_YELLOW);
					Color colorText = gc.getDevice().getSystemColor(SWT.COLOR_BLACK);

					gc.setForeground(colorText);

					Point pt = cPaint.toControl(display.getCursorLocation());
					sp.setUrlColor(colorURL);
					URLInfo hitUrl = sp.getHitUrl(pt.x, pt.y);
					if (hitUrl != null) {
						shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_HAND));
						hitUrl.urlColor = colorURL2;
					} else {
						shell.setCursor(null);
					}
					boolean fit = sp.printString();
					
					lblInfo.setText(fit ? "fit" : "no fit");

					bounds.width--;
					bounds.height--;

					gc.setForeground(colorBox);
					gc.drawRectangle(bounds);

					//System.out.println("-         " + System.currentTimeMillis());

				} catch (Throwable t) {
					t.printStackTrace();

				} finally {
					if (ourGC) {
						gc.dispose();
					}
				}
			}

			private GCStringPrinter buildSP(GC gc) {
				//gc.setFont(Utils.getFontWithHeight(shell.getFont(), gc, 15));
				//gc.setTextAntialias(SWT.ON);
				Rectangle bounds = cPaint.getClientArea();

				int style = btnWrap.getSelection() ? SWT.WRAP : 0;
				if (cboVAlign.getSelectionIndex() == 0) {
					style |= SWT.TOP;
				} else if (cboVAlign.getSelectionIndex() == 1) {
					style |= SWT.BOTTOM;
				}

				if (cboHAlign.getSelectionIndex() == 0) {
					style |= SWT.LEFT;
				} else if (cboHAlign.getSelectionIndex() == 1) {
					style |= SWT.CENTER;
				} else if (cboHAlign.getSelectionIndex() == 2) {
					style |= SWT.RIGHT;
				}

				String text = txtText.getText();
				text = text.replaceAll("\r\n", "\n");
				GCStringPrinter sp = new GCStringPrinter(gc, text, bounds,
						btnSkipClip.getSelection(), btnFullOnly.getSelection(), style);
				sp.setImages(images);
				sp.calculateMetrics();

				return sp;
			}
		};
		cPaint.addListener(SWT.Paint, l2);
		cPaint.addListener(SWT.MouseMove, l2);

		shell.open();

		while (!shell.isDisposed()) {
			if (!display.readAndDispatch()) {
				display.sleep();
			}
		}
	}

	/**
	 * 
	 */
	public GCStringPrinter(GC gc, String string, Rectangle printArea,
			boolean skipClip, boolean fullLinesOnly, int swtFlags) {
		this.gc = gc;
		this.string = string;
		this.printArea = printArea;
		this.swtFlags = swtFlags;

		printFlags = 0;
		if (skipClip) {
			printFlags |= FLAG_SKIPCLIP;
		}
		if (fullLinesOnly) {
			printFlags |= FLAG_FULLLINESONLY;
		}
	}

	public GCStringPrinter(GC gc, String string, Rectangle printArea,
			int printFlags, int swtFlags) {
		this.gc = gc;
		this.string = string;
		this.printArea = printArea;
		this.swtFlags = swtFlags;
		this.printFlags = printFlags;
	}

	public boolean printString() {
		return _printString();
	}

	public boolean printString(int printFlags) {
		int oldPrintFlags = this.printFlags;
		printFlags |= printFlags;
		boolean b = _printString();
		this.printFlags = oldPrintFlags;
		return b;
	}

	public void calculateMetrics() {
		int oldPrintFlags = printFlags;
		printFlags |= FLAG_NODRAW;
		_printString();
		printFlags = oldPrintFlags;
	}

	/**
	 * @param rectangle
	 *
	 * @since 3.0.4.3
	 */
	public void printString(GC gc, Rectangle rectangle, int swtFlags) {
		this.gc = gc;
		int printFlags = this.printFlags;
		if (printArea.width == rectangle.width) {
			printFlags |= FLAG_KEEP_URL_INFO;
		}
		printArea = rectangle;
		this.swtFlags = swtFlags;
		printString(printFlags);
	}

	public Point getCalculatedSize() {
		return size;
	}

	public Color getUrlColor() {
		return urlColor;
	}

	public void setUrlColor(Color urlColor) {
		this.urlColor = urlColor;
	}

	public URLInfo getHitUrl(int x, int y) {
		if (listUrlInfo == null || listUrlInfo.size() == 0) {
			return null;
		}
		for (Iterator iter = listUrlInfo.iterator(); iter.hasNext();) {
			URLInfo urlInfo = (URLInfo) iter.next();
			if (urlInfo.hitAreas != null) {
				for (Iterator iter2 = urlInfo.hitAreas.iterator(); iter2.hasNext();) {
					Rectangle r = (Rectangle) iter2.next();
					if (r.contains(x, y)) {
						return urlInfo;
					}
				}
			}
		}
		return null;
	}

	public URLInfo[] getHitUrlInfo() {
		if (listUrlInfo == null) {
			return new URLInfo[0];
		}
		return (URLInfo[]) listUrlInfo.toArray(new URLInfo[0]);
	}

	public boolean hasHitUrl() {
		return listUrlInfo != null && listUrlInfo.size() > 0;
	}

	public boolean isCutoff() {
		return cutoff;
	}
	
	public void setImages(Image[] images) {
		this.images = images;
	}

	public float[] getImageScales() {
		return imageScales;
	}

	public void setImageScales(float[] imageScales) {
		this.imageScales = imageScales;
	}

	/**
	 * @return
	 *
	 * @since 4.0.0.1
	 */
	public String getText() {
		return string;
	}

	/*
	private Point stringExtent(GC gc, String s) {
		Matcher m = patOver1000.matcher(s);
		//if (s.length() > MAX_LINE_LEN) {
		if (m.find()) {
			System.out.println(s.length() + "\n" + Debug.getStackTrace(false, false));
			System.out.println(s);
		}
		return gc.stringExtent(s);
	}

	private Point textExtent(GC gc, String s) {
		Matcher m = patOver1000.matcher(s);
		//if (s.length() > MAX_LINE_LEN) {
		if (m.find()) {
			System.out.println(s.length() + "\n" + Debug.getStackTrace(false, false));
			System.out.println(s);
		}
		return gc.textExtent(s);
	}
	*/
}
