/*
 * 02/26/2004
 *
 * SyntaxScheme.java - The set of colors and tokens used by an RSyntaxTextArea
 * to color tokens.
 * 
 * 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.Graphics2D;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import javax.swing.text.StyleContext;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;


/**
 * The set of colors and styles used by an <code>RSyntaxTextArea</code> to
 * color tokens.<p>
 * You can use this class to programmatically set the fonts and colors used in
 * an RSyntaxTextArea, but for more powerful, externalized control, consider
 * using {@link Theme}s instead.
 *
 * @author Robert Futrell
 * @version 1.0
 * @see Theme
 */
public class SyntaxScheme implements Cloneable, TokenTypes {

	private Style[] styles;

	private static final String VERSION			= "*ver1";


	/**
	 * Creates a color scheme that either has all color values set to
	 * a default value or set to <code>null</code>.
	 *
	 * @param useDefaults If <code>true</code>, all color values will
	 *        be set to default colors; if <code>false</code>, all colors
	 *        will be initially <code>null</code>.
	 */
	public SyntaxScheme(boolean useDefaults) {
		styles = new Style[DEFAULT_NUM_TOKEN_TYPES];
		if (useDefaults) {
			restoreDefaults(null);
		}
	}


	/**
	 * Creates a default color scheme.
	 *
	 * @param baseFont The base font to use.  Keywords will be a bold version
	 *        of this font, and comments will be an italicized version of this
	 *        font.
	 */
	public SyntaxScheme(Font baseFont) {
		this(baseFont, true);
	}


	/**
	 * Creates a default color scheme.
	 *
	 * @param baseFont The base font to use.  Keywords will be a bold version
	 *        of this font, and comments will be an italicized version of this
	 *        font.
	 * @param fontStyles Whether bold and italic should be used in the scheme
	 *        (vs. all tokens using a plain font).
	 */
	public SyntaxScheme(Font baseFont, boolean fontStyles) {
		styles = new Style[DEFAULT_NUM_TOKEN_TYPES];
		restoreDefaults(baseFont, fontStyles);
	}


	/**
	 * Changes the "base font" for this syntax scheme.  This is called by
	 * <code>RSyntaxTextArea</code> when its font changes via
	 * <code>setFont()</code>.  This looks for tokens that use a derivative of
	 * the text area's old font (but bolded and/or italicized) and make them
	 * use the new font with those stylings instead.  This is desirable because
	 * most programmers prefer a single font to be used in their text editor,
	 * but might want bold (say for keywords) or italics.
	 *
	 * @param old The old font of the text area.
	 * @param font The new font of the text area.
	 */
	void changeBaseFont(Font old, Font font) {
		for (int i=0; i<styles.length; i++) {
			Style style = styles[i];
			if (style!=null && style.font!=null) {
				if (style.font.getFamily().equals(old.getFamily()) &&
						style.font.getSize()==old.getSize()) {
					int s = style.font.getStyle(); // Keep bold or italic
					StyleContext sc = StyleContext.getDefaultStyleContext();
					style.font= sc.getFont(font.getFamily(), s, font.getSize());
				}
			}
		}
	}


	/**
	 * Returns a deep copy of this color scheme.
	 *
	 * @return The copy.
	 */
	@Override
	public Object clone() {
		SyntaxScheme shcs = null;
		try {
			shcs = (SyntaxScheme)super.clone();
		} catch (CloneNotSupportedException cnse) { // Never happens
			cnse.printStackTrace();
			return null;
		}
		shcs.styles = new Style[styles.length];
		for (int i=0; i<styles.length; i++) {
			Style s = styles[i];
			if (s!=null) {
				shcs.styles[i] = (Style)s.clone();
			}
		}
		return shcs;
	}


	/**
	 * Tests whether this color scheme is the same as another color scheme.
	 *
	 * @param otherScheme The color scheme to compare to.
	 * @return <code>true</code> if this color scheme and
	 *         <code>otherScheme</code> are the same scheme;
	 *         <code>false</code> otherwise.
	 */
	@Override
	public boolean equals(Object otherScheme) {

		// No need for null check; instanceof takes care of this for us,
		// i.e. "if (!(null instanceof Foo))" evaluates to "true".
		if (!(otherScheme instanceof SyntaxScheme)) {
			return false;
		}

		Style[] otherSchemes = ((SyntaxScheme)otherScheme).styles;

		int length = styles.length;
		for (int i=0; i<length; i++) {
			if (styles[i]==null) {
				if (otherSchemes[i]!=null) {
					return false;
				}
			}
			else if (!styles[i].equals(otherSchemes[i])) {
				return false;
			}
		}
		return true;

	}


	/**
	 * Returns a hex string representing an RGB color, of the form
	 * <code>"$rrggbb"</code>.
	 *
	 * @param c The color.
	 * @return The string representation of the color.
	 */
	private static final String getHexString(Color c) {
		return "$" + Integer.toHexString((c.getRGB() & 0xffffff)+0x1000000).
									substring(1);
	}


	/**
	 * Returns the specified style.
	 *
	 * @param index The index of the style.
	 * @return The style.
	 * @see #setStyle(int, Style)
	 * @see #getStyleCount()
	 */
	public Style getStyle(int index) {
		return styles[index];
	}


	/**
	 * Returns the number of styles.
	 *
	 * @return The number of styles.
	 * @see #getStyle(int)
	 */
	public int getStyleCount() {
		return styles.length;
	}


	/**
	 * Used by third party implementors e.g. SquirreL SQL. Most applications do
	 * not need to call this method.
	 * <p>
	 * Note that the returned array is not a copy of the style data; editing the
	 * array will modify the styles used by any <code>RSyntaxTextArea</code>
	 * using this scheme.
	 *
	 * @return The style array.
	 * @see #setStyles(Style[])
	 */
	public Style[] getStyles() {
		return styles;
	}


	/**
	 * This is implemented to be consistent with {@link #equals(Object)}.
	 * This is a requirement to keep FindBugs happy.
	 *
	 * @return The hash code for this object.
	 */
	@Override
	public int hashCode() {
		// Keep me fast.  Iterating over *all* syntax schemes contained is
		// probably much slower than a "bad" hash code here.
		int hashCode = 0;
		int count = styles.length;
		for (int i=0; i<count; i++) {
			if (styles[i]!=null) {
				hashCode ^= styles[i].hashCode();
				break;
			}
		}
		return hashCode;
	}


	/**
	 * Loads a syntax scheme from an input stream.<p>
	 * 
	 * Consider using the {@link Theme} class for saving and loading RSTA
	 * styles rather than using this API.
	 *
	 * @param baseFont The font to use as the "base" for the syntax scheme.
	 *        If this is <code>null</code>, a default monospaced font is used.
	 * @param in The stream to load from.  It is up to the caller to close this
	 *        stream when they are done.
	 * @return The syntax scheme.
	 * @throws IOException If an IO error occurs.
	 */
	public static SyntaxScheme load(Font baseFont, InputStream in)
									throws IOException {
		if (baseFont==null) {
			baseFont = RSyntaxTextArea.getDefaultFont();
		}
		return SyntaxSchemeLoader.load(baseFont, in);
	}


	/**
	 * Loads a syntax highlighting color scheme from a string created from
	 * <code>toCommaSeparatedString</code>.  This method is useful for saving
	 * and restoring color schemes.<p>
	 * 
	 * Consider using the {@link Theme} class for saving and loading RSTA
	 * styles rather than using this API.
	 *
	 * @param string A string generated from {@link #toCommaSeparatedString()}.
	 * @return A color scheme.
	 * @see #toCommaSeparatedString()
	 */
	public static SyntaxScheme loadFromString(String string) {
		return loadFromString(string, DEFAULT_NUM_TOKEN_TYPES);
	}


	/**
	 * Loads a syntax highlighting color scheme from a string created from
	 * <code>toCommaSeparatedString</code>.  This method is useful for saving
	 * and restoring color schemes.<p>
	 * 
	 * Consider using the {@link Theme} class for saving and loading RSTA
	 * styles rather than using this API.
	 *
	 * @param string A string generated from {@link #toCommaSeparatedString()}.
	 * @param tokenTypeCount The number of token types saved in this string.
	 *        This should be the number of token types saved by your custom
	 *        SyntaxScheme subclass,
	 *        or {@link TokenTypes#DEFAULT_NUM_TOKEN_TYPES} if you used the
	 *        standard implementation (which most people will).
	 * @return A color scheme.
	 * @see #loadFromString(String)
	 * @see #toCommaSeparatedString()
	 */
	public static SyntaxScheme loadFromString(String string,
			int tokenTypeCount) {

		SyntaxScheme scheme = new SyntaxScheme(true);

		try {

			if (string!=null) {

				String[] tokens = string.split(",", -1);

				// Check the version string, use defaults if incompatible
				if (tokens.length==0 || !VERSION.equals(tokens[0])) {
					return scheme; // Still set to defaults
				}

				int tokenCount = tokenTypeCount*7 + 1; // Version string
				if (tokens.length!=tokenCount) {
					throw new Exception(
						"Not enough tokens in packed color scheme: expected " +
						tokenCount + ", found " + tokens.length);
				}

				// Use StyleContext to create fonts to get composite fonts for
				// Asian glyphs.
				StyleContext sc = StyleContext.getDefaultStyleContext();

				// Loop through each token style.  Format:
				// "index,(fg|-),(bg|-),(t|f),((font,style,size)|(-,,))"
				for (int i=0; i<tokenTypeCount; i++) {

					int pos = i*7 + 1;
					int integer = Integer.parseInt(tokens[pos]); // == i
					if (integer!=i)
						throw new Exception("Expected " + i + ", found " +
											integer);

					Color fg = null; String temp = tokens[pos+1];
					if (!"-".equals(temp)) { // "-" => keep fg as null
						fg = stringToColor(temp);
					}
					Color bg = null; temp = tokens[pos+2];
					if (!"-".equals(temp)) { // "-" => keep bg as null
						bg = stringToColor(temp);
					}

					// Check for "true" or "false" since we don't want to
					// accidentally suck in an int representing the next
					// packed color, and any string != "true" means false.
					temp = tokens[pos+3];
					if (!"t".equals(temp) && !"f".equals(temp))
						throw new Exception("Expected 't' or 'f', found " + temp);
					boolean underline = "t".equals(temp);

					Font font = null;
					String family = tokens[pos+4];
					if (!"-".equals(family)) {
						font = sc.getFont(family,
							Integer.parseInt(tokens[pos+5]),	// style
							Integer.parseInt(tokens[pos+6]));	// size
					}
					scheme.styles[i] = new Style(fg, bg, font, underline);

				}

			}

		} catch (Exception e) {
			e.printStackTrace();
		}

		return scheme;

	}


	void refreshFontMetrics(Graphics2D g2d) {
		// It is assumed that any rendering hints are already applied to g2d.
		for (int i=0; i<styles.length; i++) {
			Style s = styles[i];
			if (s!=null) {
				s.fontMetrics = s.font==null ? null :
								g2d.getFontMetrics(s.font);
			}
		}
	}


	/**
	 * Restores all colors and fonts to their default values.
	 *
	 * @param baseFont The base font to use when creating this scheme.  If
	 *        this is <code>null</code>, then a default monospaced font is
	 *        used.
	 */
	public void restoreDefaults(Font baseFont) {
		restoreDefaults(baseFont, true);
	}


	/**
	 * Restores all colors and fonts to their default values.
	 *
	 * @param baseFont The base font to use when creating this scheme.  If
	 *        this is <code>null</code>, then a default monospaced font is
	 *        used.
	 * @param fontStyles Whether bold and italic should be used in the scheme
	 *        (vs. all tokens using a plain font).
	 */
	public void restoreDefaults(Font baseFont, boolean fontStyles) {

		// Colors used by tokens.
		Color comment			= new Color(0,128,0);
		Color docComment		= new Color(164,0,0);
		Color markupComment		= new Color(0, 96, 0);
		Color keyword			= Color.BLUE;
		Color dataType			= new Color(0,128,128);
		Color function			= new Color(173,128,0);
		Color preprocessor		= new Color(128,128,128);
		Color operator			= new Color(128, 64, 64);
		Color regex				= new Color(0,128,164);
		Color variable			= new Color(255,153,0);
		Color literalNumber		= new Color(100,0,200);
		Color literalString		= new Color(220,0,156);
		Color error			= new Color(148,148,0);

		// (Possible) special font styles for keywords and comments.
		if (baseFont==null) {
			baseFont = RSyntaxTextArea.getDefaultFont();
		}
		Font commentFont = baseFont;
		Font keywordFont = baseFont;
		if (fontStyles) {
			// WORKAROUND for Sun JRE bug 6282887 (Asian font bug in 1.4/1.5)
			// That bug seems to be hidden now, see 6289072 instead.
			StyleContext sc = StyleContext.getDefaultStyleContext();
			Font boldFont = sc.getFont(baseFont.getFamily(), Font.BOLD,
					baseFont.getSize());
			Font italicFont = sc.getFont(baseFont.getFamily(), Font.ITALIC,
					baseFont.getSize());
			commentFont = italicFont;//baseFont.deriveFont(Font.ITALIC);
			keywordFont = boldFont;//baseFont.deriveFont(Font.BOLD);
		}

		styles[COMMENT_EOL]				= new Style(comment, null, commentFont);
		styles[COMMENT_MULTILINE]			= new Style(comment, null, commentFont);
		styles[COMMENT_DOCUMENTATION]		= new Style(docComment, null, commentFont);
		styles[COMMENT_KEYWORD]			= new Style(new Color(255,152,0), null, commentFont);
		styles[COMMENT_MARKUP]			= new Style(Color.gray, null, commentFont);
		styles[RESERVED_WORD]				= new Style(keyword, null, keywordFont);
		styles[RESERVED_WORD_2]			= new Style(keyword, null, keywordFont);
		styles[FUNCTION]					= new Style(function);
		styles[LITERAL_BOOLEAN]			= new Style(literalNumber);
		styles[LITERAL_NUMBER_DECIMAL_INT]	= new Style(literalNumber);
		styles[LITERAL_NUMBER_FLOAT]		= new Style(literalNumber);
		styles[LITERAL_NUMBER_HEXADECIMAL]	= new Style(literalNumber);
		styles[LITERAL_STRING_DOUBLE_QUOTE]	= new Style(literalString);
		styles[LITERAL_CHAR]				= new Style(literalString);
		styles[LITERAL_BACKQUOTE]			= new Style(literalString);
		styles[DATA_TYPE]				= new Style(dataType, null, keywordFont);
		styles[VARIABLE]					= new Style(variable);
		styles[REGEX]						= new Style(regex);
		styles[ANNOTATION]				= new Style(Color.gray);
		styles[IDENTIFIER]				= new Style(null);
		styles[WHITESPACE]				= new Style(Color.gray);
		styles[SEPARATOR]				= new Style(Color.RED);
		styles[OPERATOR]					= new Style(operator);
		styles[PREPROCESSOR]				= new Style(preprocessor);
		styles[MARKUP_TAG_DELIMITER]		= new Style(Color.RED);
		styles[MARKUP_TAG_NAME]			= new Style(Color.BLUE);
		styles[MARKUP_TAG_ATTRIBUTE]		= new Style(new Color(63,127,127));
		styles[MARKUP_TAG_ATTRIBUTE_VALUE]= new Style(literalString);
		styles[MARKUP_COMMENT]              = new Style(markupComment, null, commentFont);
		styles[MARKUP_DTD]              = new Style(function);
		styles[MARKUP_PROCESSING_INSTRUCTION] = new Style(preprocessor);
		styles[MARKUP_CDATA]				= new Style(new Color(0xcc6600));
		styles[MARKUP_CDATA_DELIMITER]		= new Style(new Color(0x008080));
		styles[MARKUP_ENTITY_REFERENCE]		= new Style(dataType);
		styles[ERROR_IDENTIFIER]			= new Style(error);
		styles[ERROR_NUMBER_FORMAT]		= new Style(error);
		styles[ERROR_STRING_DOUBLE]		= new Style(error);
		styles[ERROR_CHAR]				= new Style(error);

	}


	/**
	 * Sets a style to use when rendering a token type.
	 *
	 * @param type The token type.
	 * @param style The style for the token type.
	 * @see #getStyle(int)
	 */
	public void setStyle(int type, Style style) {
		styles[type] = style;
	}


	/**
	 * Used by third party implementors e.g. SquirreL SQL. Most applications do
	 * not need to call this method; individual styles can be set via
	 * {@link #setStyle(int, Style)}.
	 *
	 * @param styles The new array of styles to use.  Note that this should
	 *        have length of at least
	 *        {@link TokenTypes#DEFAULT_NUM_TOKEN_TYPES}.
	 * @see #setStyle(int, Style)
	 * @see #getStyles()
	 */
	public void setStyles(Style[] styles) {
		this.styles = styles;
	}


	/**
	 * Returns the color represented by a string.  If the first char in the
	 * string is '<code>$</code>', it is assumed to be in hex, otherwise it is
	 * assumed to be decimal.  So, for example, both of these:
	 * <pre>
	 * "$00ff00"
	 * "65280"
	 * </pre>
	 * will return <code>new Color(0, 255, 0)</code>.
	 *
	 * @param s The string to evaluate.
	 * @return The color.
	 */
	private static final Color stringToColor(String s) {
		// Check for decimal as well as hex, for backward
		// compatibility (fix from GwynEvans on forums)
		char ch = s.charAt(0);
		return new Color((ch=='$' || ch=='#') ?
				Integer.parseInt(s.substring(1),16) :
				Integer.parseInt(s));
	}


	/**
	 * Returns this syntax highlighting scheme as a comma-separated list of
	 * values as follows:
	 * <ul>
	 *   <li>If a color is non-null, it is added as a 24-bit integer
	 *      of the form <code>((r<<16) | (g<<8) | (b))</code>; if it is
	 *       <code>null</code>, it is added as "<i>-,</i>".
	 *   <li>The font and style (bold/italic) is added as an integer like so:
	 *       "<i>family,</i> <i>style,</i> <i>size</i>".
	 *   <li>The entire syntax highlighting scheme is thus one long string of
	 *       color schemes of the format "<i>i,[fg],[bg],uline,[style]</i>,
	 *       where:
	 *       <ul>
	 *          <li><code>i</code> is the index of the syntax scheme.
	 *          <li><i>fg</i> and <i>bg</i> are the foreground and background
	 *              colors for the scheme, and may be null (represented by 
	 *              <code>-</code>).
	 *          <li><code>uline</code> is whether or not the font should be
	 *              underlined, and is either <code>t</code> or <code>f</code>.
	 *          <li><code>style</code> is the <code>family,style,size</code>
	 *              triplet described above.
	 *       </ul>
	 * </ul>
	 *
	 * @return A string representing the rgb values of the colors.
	 * @see #loadFromString(String)
	 */
	public String toCommaSeparatedString() {

		StringBuilder sb = new StringBuilder(VERSION);
		sb.append(',');

		for (int i=0; i<styles.length; i++) {

			sb.append(i).append(',');

			Style ss = styles[i];
			if (ss==null) { // Only true for i==0 (NULL token)
				sb.append("-,-,f,-,,,");
				continue;
			}

			Color c = ss.foreground;
			sb.append(c!=null ? (getHexString(c) + ",") : "-,");
			c = ss.background;
			sb.append(c!=null ? (getHexString(c) + ",") : "-,");

			sb.append(ss.underline ? "t," : "f,");

			Font font = ss.font;
			if (font!=null) {
				sb.append(font.getFamily()).append(',').
					append(font.getStyle()).append(',').
						append(font.getSize()).append(',');
			}
			else {
				sb.append("-,,,");
			}

		}

		return sb.substring(0,sb.length()-1); // Take off final ','.

	}

	/**
	 * Loads a <code>SyntaxScheme</code> from an XML file.
	 */
	private static class SyntaxSchemeLoader extends DefaultHandler {

		private Font baseFont;
		private SyntaxScheme scheme;

		public SyntaxSchemeLoader(Font baseFont) {
			scheme = new SyntaxScheme(baseFont);
		}

		public static SyntaxScheme load(Font baseFont, InputStream in)
				throws IOException {
			SyntaxSchemeLoader parser = null;
			try {
				XMLReader reader = XMLReaderFactory.createXMLReader();
				parser = new SyntaxSchemeLoader(baseFont);
				parser.baseFont = baseFont;
				reader.setContentHandler(parser);
				InputSource is = new InputSource(in);
				is.setEncoding("UTF-8");
				reader.parse(is);
			} catch (SAXException se) {
				throw new IOException(se.toString());
			}
			return parser.scheme;
		}

		@Override
		public void startElement(String uri, String localName, String qName,
								Attributes attrs) {

			if ("style".equals(qName)) {

				String type = attrs.getValue("token");
				Field field = null;
				try {
					field = Token.class.getField(type);
				} catch (RuntimeException re) {
					throw re; // FindBugs
				} catch (Exception e) {
					System.err.println("Invalid token type: " + type);
					return;
				}

				if (field.getType()==int.class) {

					int index = 0;
					try {
						index = field.getInt(scheme);
					} catch (IllegalArgumentException e) {
						e.printStackTrace();
						return;
					} catch (IllegalAccessException e) {
						e.printStackTrace();
						return;
					}

					String fgStr = attrs.getValue("fg");
					if (fgStr!=null) {
						Color fg = stringToColor(fgStr);
						scheme.styles[index].foreground = fg;
					}

					String bgStr = attrs.getValue("bg");
					if (bgStr!=null) {
						Color bg = stringToColor(bgStr);
						scheme.styles[index].background = bg;
					}

					boolean styleSpecified = false;
					boolean bold = false;
					boolean italic = false;
					String boldStr = attrs.getValue("bold");
					if (boldStr!=null) {
						bold = Boolean.valueOf(boldStr).booleanValue();
						styleSpecified = true;
					}
					String italicStr = attrs.getValue("italic");
					if (italicStr!=null) {
						italic = Boolean.valueOf(italicStr).booleanValue();
						styleSpecified = true;
					}
					if (styleSpecified) {
						int style = 0;
						if (bold) { style |= Font.BOLD; }
						if (italic) { style |= Font.ITALIC; }
						scheme.styles[index].font = baseFont.deriveFont(style);
					}

					String ulineStr = attrs.getValue("underline");
					if (ulineStr!=null) {
						boolean uline= Boolean.valueOf(ulineStr).booleanValue();
						scheme.styles[index].underline = uline;
					}

				}

			}

		}

	}


}