/*
 * 07/28/2008
 *
 * RtfGenerator.java - Generates RTF via a simple Java API.
 * 
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package com.fr.design.gui.syntax.ui.rsyntaxtextarea;

import java.awt.Color;
import java.awt.Font;
import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.List;

import com.fr.design.gui.syntax.ui.rtextarea.RTextArea;


/**
 * Generates RTF text via a simple Java API.<p>
 *
 * The following RTF features are supported:
 * <ul>
 *    <li>Fonts
 *    <li>Font sizes
 *    <li>Foreground and background colors
 *    <li>Bold, italic, and underline
 * </ul>
 *
 * The RTF generated isn't really "optimized," but it will do, especially for
 * small amounts of text, such as what's common when copy-and-pasting.  It
 * tries to be sufficient for the use case of copying syntax highlighted
 * code:
 * <ul>
 *    <li>It assumes that tokens changing foreground color often is fairly
 *        common.
 *    <li>It assumes that background highlighting is fairly uncommon.
 * </ul>
 *
 * @author Robert Futrell
 * @version 1.0
 */
public class RtfGenerator {

	private List<Font> fontList;
	private List<Color> colorList;
	private StringBuilder document;
	private boolean lastWasControlWord;
	private int lastFontIndex;
	private int lastFGIndex;
	private boolean lastBold;
	private boolean lastItalic;
	private int lastFontSize;

	/**
	 * Java2D assumes a 72 dpi screen resolution, but on Windows the screen
	 * resolution is either 96 dpi or 120 dpi, depending on your font display
	 * settings.  This is an attempt to make the RTF generated match the
	 * size of what's displayed in the RSyntaxTextArea.
	 */
	private int screenRes;

	/**
	 * The default font size for RTF.  This is point size, in half
	 * points.
	 */
	private static final int DEFAULT_FONT_SIZE = 12;//24;


	/**
	 * Constructor.
	 */
	public RtfGenerator() {
		fontList = new ArrayList<Font>(1); // Usually only 1.
		colorList = new ArrayList<Color>(1); // Usually only 1.
		document = new StringBuilder();
		reset();
	}


	/**
	 * Adds a newline to the RTF document.
	 *
	 * @see #appendToDoc(String, Font, Color, Color)
	 */
	public void appendNewline() {
		document.append("\\par");
		document.append('\n'); // Just for ease of reading RTF.
		lastWasControlWord = false;
	}


	/**
	 * Appends styled text to the RTF document being generated.
	 *
	 * @param text The text to append.
	 * @param f The font of the text.  If this is <code>null</code>, the
	 *        default font is used.
	 * @param fg The foreground of the text.  If this is <code>null</code>,
	 *        the default foreground color is used.
	 * @param bg The background color of the text.  If this is
	 *        <code>null</code>, the default background color is used.
	 * @see #appendNewline()
	 */
	public void appendToDoc(String text, Font f, Color fg, Color bg) {
		appendToDoc(text, f, fg, bg, false);
	}


	/**
	 * Appends styled text to the RTF document being generated.
	 *
	 * @param text The text to append.
	 * @param f The font of the text.  If this is <code>null</code>, the
	 *        default font is used.
	 * @param bg The background color of the text.  If this is
	 *        <code>null</code>, the default background color is used.
	 * @param underline Whether the text should be underlined.
	 * @see #appendNewline()
	 */
	public void appendToDocNoFG(String text, Font f, Color bg,
							boolean underline) {
		appendToDoc(text, f, null, bg, underline, false);
	}


	/**
	 * Appends styled text to the RTF document being generated.
	 *
	 * @param text The text to append.
	 * @param f The font of the text.  If this is <code>null</code>, the
	 *        default font is used.
	 * @param fg The foreground of the text.  If this is <code>null</code>,
	 *        the default foreground color is used.
	 * @param bg The background color of the text.  If this is
	 *        <code>null</code>, the default background color is used.
	 * @param underline Whether the text should be underlined.
	 * @see #appendNewline()
	 */
	public void appendToDoc(String text, Font f, Color fg, Color bg,
							boolean underline) {
		appendToDoc(text, f, fg, bg, underline, true);
	}


	/**
	 * Appends styled text to the RTF document being generated.
	 *
	 * @param text The text to append.
	 * @param f The font of the text.  If this is <code>null</code>, the
	 *        default font is used.
	 * @param fg The foreground of the text.  If this is <code>null</code>,
	 *        the default foreground color is used.
	 * @param bg The background color of the text.  If this is
	 *        <code>null</code>, the default background color is used.
	 * @param underline Whether the text should be underlined.
	 * @param setFG Whether the foreground specified by <code>fg</code> should
	 *        be honored (if it is non-<code>null</code>).
	 * @see #appendNewline()
	 */
	public void appendToDoc(String text, Font f, Color fg, Color bg,
							boolean underline, boolean setFG) {

		if (text!=null) {

			// Set font to use, if different from last addition.
			int fontIndex = f==null ? 0 : (getFontIndex(fontList, f)+1);
			if (fontIndex!=lastFontIndex) {
				document.append("\\f").append(fontIndex);
				lastFontIndex = fontIndex;
				lastWasControlWord = true;
			}

			// Set styles to use.
			if (f!=null) {
				int fontSize = fixFontSize(f.getSize2D()*2); // Half points!
				if (fontSize!=lastFontSize) {
					document.append("\\fs").append(fontSize);
					lastFontSize = fontSize;
					lastWasControlWord = true;
				}
				if (f.isBold()!=lastBold) {
					document.append(lastBold ? "\\b0" : "\\b");
					lastBold = !lastBold;
					lastWasControlWord = true;
				}
				if (f.isItalic()!=lastItalic) {
					document.append(lastItalic ? "\\i0" : "\\i");
					lastItalic = !lastItalic;
					lastWasControlWord = true;
				}
			}
			else { // No font specified - assume neither bold nor italic.
				if (lastFontSize!=DEFAULT_FONT_SIZE) {
					document.append("\\fs").append(DEFAULT_FONT_SIZE);
					lastFontSize = DEFAULT_FONT_SIZE;
					lastWasControlWord = true;
				}
				if (lastBold) {
					document.append("\\b0");
					lastBold = false;
					lastWasControlWord = true;
				}
				if (lastItalic) {
					document.append("\\i0");
					lastItalic = false;
					lastWasControlWord = true;
				}
			}
			if (underline) {
				document.append("\\ul");
				lastWasControlWord = true;
			}

			// Set the foreground color.
			if (setFG) {
				int fgIndex = 0;
				if (fg!=null) { // null => fg color index 0
					fgIndex = getColorIndex(colorList, fg)+1;
				}
				if (fgIndex!=lastFGIndex) {
					document.append("\\cf").append(fgIndex);
					lastFGIndex = fgIndex;
					lastWasControlWord = true;
				}
			}

			// Set the background color.
			if (bg!=null) {
				int pos = getColorIndex(colorList, bg);
				document.append("\\highlight").append(pos+1);
				lastWasControlWord = true;
			}

			if (lastWasControlWord) {
				document.append(' '); // Delimiter
				lastWasControlWord = false;
			}
			escapeAndAdd(document, text);

			// Reset everything that was set for this text fragment.
			if (bg!=null) {
				document.append("\\highlight0");
				lastWasControlWord = true;
			}
			if (underline) {
				document.append("\\ul0");
				lastWasControlWord = true;
			}

		}

	}


	/**
	 * Appends some text to a buffer, with special care taken for special
	 * characters as defined by the RTF spec:
	 *
	 * <ul>
	 *   <li>All tab characters are replaced with the string
	 *       "<code>\tab</code>"
	 *   <li>'\', '{' and '}' are changed to "\\", "\{" and "\}"
	 * </ul>
	 *
	 * @param text The text to append (with tab chars substituted).
	 * @param sb The buffer to append to.
	 */
	private final void escapeAndAdd(StringBuilder sb, String text) {
		int count = text.length();
		for (int i=0; i<count; i++) {
			char ch = text.charAt(i);
			switch (ch) {
				case '\t':
					// Micro-optimization: for syntax highlighting with
					// tab indentation, there are often multiple tabs
					// back-to-back at the start of lines, so don't put
					// spaces between each "\tab".
					sb.append("\\tab");
					while ((++i<count) && text.charAt(i)=='\t') {
						sb.append("\\tab");
					}
					sb.append(' ');
					i--; // We read one too far.
					break;
				case '\\':
				case '{':
				case '}':
					sb.append('\\').append(ch);
					break;
				default:
					sb.append(ch);
					break;
			}
		}
	}


	/**
	 * Returns a font point size, adjusted for the current screen resolution.<p>
	 * 
	 * Java2D assumes 72 dpi.  On systems with larger dpi (Windows, GTK, etc.),
	 * font rendering will appear too small if we simply return a Java "Font"
	 * object's getSize() value.  We need to adjust it for the screen
	 * resolution.
	 *
	 * @param pointSize A Java Font's point size, as returned from
	 *        <code>getSize2D()</code>.
	 * @return The font point size, adjusted for the current screen resolution.
	 *         This will allow other applications to render fonts the same
	 *         size as they appear in the Java application.
	 */
	private int fixFontSize(float pointSize) {
		if (screenRes!=72) { // Java2D assumes 72 dpi
			pointSize = Math.round(pointSize*72f/screenRes);
		}
		return (int)pointSize;
	}


	/**
	 * Returns the index of the specified item in a list.  If the item
	 * is not in the list, it is added, and its new index is returned.
	 *
	 * @param list The list (possibly) containing the item.
	 * @param item The item to get the index of.
	 * @return The index of the item.
	 */
	private static int getColorIndex(List<Color> list, Color item) {
		int pos = list.indexOf(item);
		if (pos==-1) {
			list.add(item);
			pos = list.size()-1;
		}
		return pos;
	}


	private String getColorTableRtf() {

		// Example:
		// "{\\colortbl ;\\red255\\green0\\blue0;\\red0\\green0\\blue255; }"

		StringBuilder sb = new StringBuilder();

		sb.append("{\\colortbl ;");
		for (Color c : colorList) {
			sb.append("\\red").append(c.getRed());
			sb.append("\\green").append(c.getGreen());
			sb.append("\\blue").append(c.getBlue());
			sb.append(';');
		}
		sb.append("}");

		return sb.toString();

	}


	/**
	 * Returns the index of the specified font in a list of fonts.  This
	 * method only checks for a font by its family name; its attributes such
	 * as bold and italic are ignored.<p>
	 *
	 * If the font is not in the list, it is added, and its new index is
	 * returned.
	 *
	 * @param list The list (possibly) containing the font.
	 * @param font The font to get the index of.
	 * @return The index of the font.
	 */
	private static int getFontIndex(List<Font> list, Font font) {
		String fontName = font.getFamily();
		for (int i=0; i<list.size(); i++) {
			Font font2 = list.get(i);
			if (font2.getFamily().equals(fontName)) {
				return i;
			}
		}
		list.add(font);
		return list.size()-1;
	}


	private String getFontTableRtf() {

		// Example:
		// "{\\fonttbl{\\f0\\fmodern\\fcharset0 Courier;}}"

		StringBuilder sb = new StringBuilder();

		// Workaround for text areas using the Java logical font "Monospaced"
		// by default.  There's no way to know what it's mapped to, so we
		// just search for a monospaced font on the system.
		String monoFamilyName = getMonospacedFontFamily();

		sb.append("{\\fonttbl{\\f0\\fnil\\fcharset0 " + monoFamilyName + ";}");
		for (int i=0; i<fontList.size(); i++) {
			Font f = fontList.get(i);
			String familyName = f.getFamily();
			if (familyName.equals("Monospaced")) {
				familyName = monoFamilyName;
			}
			sb.append("{\\f").append(i+1).append("\\fnil\\fcharset0 ");
			sb.append(familyName).append(";}");
		}
		sb.append('}');

		return sb.toString();

	}


	/**
	 * Returns a good "default" monospaced font to use when Java's logical
	 * font "Monospaced" is found.
	 *
	 * @return The monospaced font family to use.
	 */
	private static final String getMonospacedFontFamily() {
		String family = RTextArea.getDefaultFont().getFamily();
		if ("Monospaced".equals(family)) {
			family = "Courier";
		}
		return family;
	}


	/**
	 * Returns the RTF document created by this generator.
	 *
	 * @return The RTF document, as a <code>String</code>.
	 */
	public String getRtf() {

		StringBuilder sb = new StringBuilder();
		sb.append("{");

		// Header
		sb.append("\\rtf1\\ansi\\ansicpg1252");
		sb.append("\\deff0"); // First font in font table is the default
		sb.append("\\deflang1033");
		sb.append("\\viewkind4");		// "Normal" view
		sb.append("\\uc\\pard\\f0");
		sb.append("\\fs20");			// Font size in half-points (default 24)
		sb.append(getFontTableRtf()).append('\n');
		sb.append(getColorTableRtf()).append('\n');

		// Content
		sb.append(document);

		sb.append("}");

		//System.err.println("*** " + sb.length());
		return sb.toString();

	}


	/**
	 * Resets this generator.  All document information and content is
	 * cleared.
	 */
	public void reset() {
		fontList.clear();
		colorList.clear();
		document.setLength(0);
		lastWasControlWord = false;
		lastFontIndex = 0;
		lastFGIndex = 0;
		lastBold = false;
		lastItalic = false;
		lastFontSize = DEFAULT_FONT_SIZE;
		screenRes = Toolkit.getDefaultToolkit().getScreenResolution();
	}


}