/*******************************************************************************
 * Copyright (c) 2007, 2011 David Green and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     David Green - initial API and implementation
 *******************************************************************************/
package org.eclipse.mylyn.internal.wikitext.ui.editor.syntax;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.TextAttribute;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.ITokenScanner;
import org.eclipse.mylyn.internal.wikitext.core.util.css.CssParser;
import org.eclipse.mylyn.internal.wikitext.core.util.css.CssRule;
import org.eclipse.mylyn.internal.wikitext.ui.WikiTextUiPlugin;
import org.eclipse.mylyn.internal.wikitext.ui.editor.preferences.Preferences;
import org.eclipse.mylyn.internal.wikitext.ui.editor.syntax.FastMarkupPartitioner.MarkupPartition;
import org.eclipse.mylyn.internal.wikitext.ui.viewer.CssStyleManager;
import org.eclipse.mylyn.internal.wikitext.ui.viewer.FontState;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.graphics.Font;

/**
 * A token scanner that uses the results of the {@link FastMarkupPartitioner} to identify tokens.
 * 
 * @author David Green
 */
public class MarkupTokenScanner implements ITokenScanner {

	private Token currentToken = null;

	private Iterator<Token> tokenIt = null;

	private CssStyleManager styleManager;

	private FontState defaultState;

	private Preferences preferences;

	private final CssParser cssParser = new CssParser();

	public MarkupTokenScanner(Font defaultFont, Font defaultMonospaceFont) {
		initialize(defaultFont, defaultMonospaceFont);
		reloadPreferences();
	}

	/**
	 * Reset the fonts used by this token scanner.
	 * 
	 * @param defaultFont
	 *            the default font, must not be null.
	 * @param defaultMonospaceFont
	 *            the default monospace font, or null if a suitable default should be selected
	 */
	public void resetFonts(Font defaultFont, Font defaultMonospaceFont) {
		if (defaultFont == null) {
			throw new IllegalArgumentException();
		}
		initialize(defaultFont, defaultMonospaceFont);
	}

	private void initialize(Font defaultFont, Font defaultMonospaceFont) {
		styleManager = new CssStyleManager(defaultFont, defaultMonospaceFont);
		defaultState = styleManager.createDefaultFontState();
	}

	public void reloadPreferences() {
		preferences = WikiTextUiPlugin.getDefault().getPreferences();
	}

	public int getTokenLength() {
		return currentToken == null ? -1 : currentToken.getLength();
	}

	public int getTokenOffset() {
		return currentToken == null ? -1 : currentToken.getOffset();
	}

	public IToken nextToken() {
		if (tokenIt != null && tokenIt.hasNext()) {
			currentToken = tokenIt.next();
		} else {
			currentToken = null;
			tokenIt = null;
			return org.eclipse.jface.text.rules.Token.EOF;
		}
		return currentToken;
	}

	public void setRange(IDocument document, int offset, int length) {
		IDocumentPartitioner partitioner = document.getDocumentPartitioner();
		List<Token> tokens = null;

		if (partitioner instanceof FastMarkupPartitioner) {
			FastMarkupPartitioner fastMarkupPartitioner = (FastMarkupPartitioner) partitioner;
			ITypedRegion[] partitioning = partitioner.computePartitioning(offset, length);
			if (partitioning != null) {
				tokens = new ArrayList<Token>();

				ITypedRegion[] partitions = ((FastMarkupPartitioner) partitioner).getScanner().computePartitions(
						document, offset, length);
				int lastEnd = offset;

				Token defaultToken;
				{
					StyleRange styleRange = styleManager.createStyleRange(defaultState, 0, 1);
					TextAttribute textAttribute = createTextAttribute(styleRange);
					defaultToken = new Token(defaultState, textAttribute, offset, length);
				}
				if (partitions != null) {
					for (ITypedRegion region : partitions) {
						if (region.getOffset() >= (offset + length)) {
							break;
						}
						if ((region.getOffset() + region.getLength()) < offset) {
							continue;
						}
						if (region instanceof MarkupPartition) {
							MarkupPartition partition = (MarkupPartition) region;

							if (lastEnd < partition.getOffset()) {
								Token blockBridgeToken = new Token(defaultToken.fontState, defaultToken.getData(),
										lastEnd, partition.getOffset() - lastEnd);
								addToken(tokens, blockBridgeToken);
							}

							// a token that spans the whole block
							Token blockToken = createToken(partition);
							if (blockToken == null) {
								blockToken = defaultToken;
							}
							if (!partition.getBlock().isSpansComputed()) {
								fastMarkupPartitioner.reparse(document, partition.getBlock());
							}
							List<Span> spans = partition.getSpans();
							if (spans != null) {
								for (Span span : spans) {
									if (span.getOffset() < lastEnd) {
										continue;
									}
									Token spanToken = createToken(blockToken.getFontState(), span);
									if (spanToken != null) {
										int blockTokenStartOffset = lastEnd < offset ? offset : lastEnd;
										if (blockTokenStartOffset < spanToken.getOffset()) {
											int blockTokenLength = spanToken.getOffset() - blockTokenStartOffset;
											final Token blockBridgeToken = new Token(blockToken.fontState,
													blockToken.getData(), blockTokenStartOffset, blockTokenLength);
											addToken(tokens, blockBridgeToken);
										}

										Token[] spanTokens = null;
										if (!span.getChildren().isEmpty()) {
											spanTokens = splitSpan(spanToken, span, defaultToken);
										}
										if (spanTokens != null) {
											for (Token spanSplitToken : spanTokens) {
												addToken(tokens, spanSplitToken);
											}
										} else {
											addToken(tokens, spanToken);
										}
										lastEnd = spanToken.offset + spanToken.length;
										if (lastEnd > partition.getOffset() + partition.getLength()) {
											throw new IllegalStateException();
										}
									}
								}
							}
							final int partitionEnd = partition.getOffset() + partition.getLength();
							if (lastEnd < partitionEnd) {
								final int realLastEnd = Math.max(lastEnd, partition.getOffset());
								int diff = (partitionEnd) - realLastEnd;
								if (diff > 0) {
									int blockTokenStartOffset = realLastEnd;
									int blockTokenLength = diff;
									final Token blockBridgeToken = new Token(blockToken.fontState,
											blockToken.getData(), blockTokenStartOffset, blockTokenLength);
									addToken(tokens, blockBridgeToken);
									lastEnd = blockTokenStartOffset + blockTokenLength;
									if (lastEnd > partition.getOffset() + partition.getLength()) {
										throw new IllegalStateException();
									}
								}
							}
						}
					}
				}
				if (lastEnd < (offset + length)) {
					addToken(tokens, new Token(defaultToken.fontState, defaultToken.getData(), lastEnd, length
							- (lastEnd - offset)));
				}
			}
		}

		currentToken = null;
		if (tokens == null || tokens.isEmpty()) {
			tokenIt = null;
		} else {
			Iterator<Token> it = tokens.iterator();
			while (it.hasNext()) {
				Token next = it.next();
				if (next.getOffset() < offset) {
					it.remove();
				} else if (next.getOffset() + next.getLength() > (offset + length)) {
					it.remove();
				}
			}
			tokenIt = tokens.iterator();
		}

	}

	protected TextAttribute createTextAttribute(StyleRange styleRange) {
		int fontStyle = styleRange.fontStyle;
		if (styleRange.strikeout) {
			fontStyle |= TextAttribute.STRIKETHROUGH;
		}
		if (styleRange.underline) {
			fontStyle |= TextAttribute.UNDERLINE;
		}
		return new TextAttribute(styleRange.foreground, styleRange.background, fontStyle, styleRange.font);
	}

	/**
	 * handle nested spans: given a token for a specific span, split it into one or more tokens based on analyzing its
	 * children
	 * 
	 * @return an array of tokens that contiguously cover the region represented by the original span.
	 */
	private Token[] splitSpan(Token spanToken, Span span, Token defaultToken) {
		List<Token> tokens = new ArrayList<Token>(span.getChildren().size() + 1);
		int previousEnd = spanToken.offset;
		for (Span child : span.getChildren().asList()) {
			if (child.getOffset() > previousEnd) {
				tokens.add(new Token(spanToken.fontState, spanToken.getData(), previousEnd, child.getOffset()
						- previousEnd));
			}
			Token childToken = createToken(spanToken.fontState, child);
			if (childToken == null) {
				StyleRange styleRange = styleManager.createStyleRange(spanToken.fontState, 0, 1);
				TextAttribute textAttribute = createTextAttribute(styleRange);
				childToken = new Token(spanToken.fontState, textAttribute, child.getOffset(), child.getLength());
			}
			if (child.getChildren().isEmpty()) {
				tokens.add(childToken);
			} else {
				// recursively apply to children
				for (Token t : splitSpan(childToken, child, defaultToken)) {
					tokens.add(t);
				}
			}
			previousEnd = child.getEndOffset();
		}
		if (previousEnd < span.getEndOffset()) {
			tokens.add(new Token(spanToken.fontState, spanToken.getData(), previousEnd, span.getEndOffset()
					- previousEnd));
		}
		return tokens.toArray(new Token[tokens.size()]);
	}

	private void addToken(List<Token> tokens, Token newToken) {
		checkAddToken(tokens, newToken);
		tokens.add(newToken);
	}

	private void checkAddToken(List<Token> tokens, Token newToken) {
		if (newToken.getLength() <= 0) {
			throw new IllegalStateException(NLS.bind(Messages.MarkupTokenScanner_badTokenLength,
					new Object[] { newToken.getLength() }));
		}
		if (newToken.getOffset() < 0) {
			throw new IllegalStateException(NLS.bind(Messages.MarkupTokenScanner_badTokenOffset,
					new Object[] { newToken.getOffset() }));
		}
		if (!tokens.isEmpty()) {
			Token previous = tokens.get(tokens.size() - 1);
			if (previous.getOffset() >= newToken.getOffset()) {
				throw new IllegalStateException(Messages.MarkupTokenScanner_2);
			} else if (previous.getOffset() + previous.getLength() > newToken.getOffset()) {
				throw new IllegalStateException(Messages.MarkupTokenScanner_3);
			}
		}
	}

	private Token createToken(FontState parentState, Span span) {
		if (span.getLength() == 0) {
			return null;
		}
		String cssStyles = null;
		String key = null;
		switch (span.getType()) {
		case BOLD:
			key = Preferences.PHRASE_BOLD;
			break;
		case CITATION:
			key = Preferences.PHRASE_CITATION;
			break;
		case CODE:
			key = Preferences.PHRASE_CODE;
			break;
		case DELETED:
			key = Preferences.PHRASE_DELETED_TEXT;
			break;
		case EMPHASIS:
			key = Preferences.PHRASE_EMPHASIS;
			break;
		case INSERTED:
			key = Preferences.PHRASE_INSERTED_TEXT;
			break;
		case ITALIC:
			key = Preferences.PHRASE_ITALIC;
			break;
		case MONOSPACE:
			key = Preferences.PHRASE_MONOSPACE;
			break;
		case QUOTE:
			key = Preferences.PHRASE_QUOTE;
			break;
		case SPAN:
			key = Preferences.PHRASE_SPAN;
			break;
		case STRONG:
			key = Preferences.PHRASE_STRONG;
			break;
		case SUBSCRIPT:
			key = Preferences.PHRASE_SUBSCRIPT;
			break;
		case SUPERSCRIPT:
			key = Preferences.PHRASE_SUPERSCRIPT;
			break;
		case UNDERLINED:
			key = Preferences.PHRASE_UNDERLINED;
			break;
		}
		cssStyles = preferences.getCssByPhraseModifierType().get(key);
		if (cssStyles == null && span.getAttributes().getCssStyle() == null && span.getChildren().isEmpty()) {
			return null;
		}
		FontState fontState = new FontState(parentState);
		if (cssStyles != null) {
			processCssStyles(fontState, parentState, cssStyles);
		}
		if (span.getAttributes().getCssStyle() != null) {
			processCssStyles(fontState, parentState, span.getAttributes().getCssStyle());
		}
		StyleRange styleRange = styleManager.createStyleRange(fontState, 0, 1);

		TextAttribute textAttribute = createTextAttribute(styleRange);
		return new Token(fontState, textAttribute, span.getOffset(), span.getLength());
	}

	private Token createToken(MarkupPartition partition) {
		if (partition.getLength() == 0) {
			return null;
		}
		FontState fontState = new FontState(defaultState);
		boolean hasStyles = processStyles(partition.getBlock(), partition, fontState);

		if (partition.getBlock().getAttributes().getCssStyle() != null) {
			processCssStyles(fontState, defaultState, partition.getBlock().getAttributes().getCssStyle());
		} else {
			if (!hasStyles) {
				return null;
			}
		}
		StyleRange styleRange = styleManager.createStyleRange(fontState, 0, 1);

		TextAttribute textAttribute = createTextAttribute(styleRange);
		return new Token(fontState, textAttribute, partition.getOffset(), partition.getLength());
	}

	private boolean processStyles(Block block, MarkupPartition partition, FontState fontState) {
		boolean hasStyles = false;
		if (block.getParent() != null) {
			hasStyles = processStyles(block.getParent(), partition, fontState);
		}
		String cssStyles = computeCssStyles(block, partition);
		if (cssStyles != null) {
			hasStyles = true;
			processCssStyles(fontState, defaultState, cssStyles);
		}
		return hasStyles;
	}

	private String computeCssStyles(Block block, MarkupPartition partition) {
		String cssStyles = null;
		if (block.getHeadingLevel() > 0) {
			cssStyles = preferences.getCssByBlockModifierType().get(
					Preferences.HEADING_PREFERENCES[block.getHeadingLevel()]);
		} else if (block.getType() != null) {
			String key = null;
			switch (block.getType()) {
			case CODE:
				key = Preferences.BLOCK_BC;
				break;
			case QUOTE:
				key = Preferences.BLOCK_QUOTE;
				break;
			case PREFORMATTED:
				key = Preferences.BLOCK_PRE;
				break;
			}
			cssStyles = preferences.getCssByBlockModifierType().get(key);
		}
		return cssStyles;
	}

	private void processCssStyles(FontState fontState, FontState parentState, String cssStyles) {
		Iterator<CssRule> ruleIterator = cssParser.createRuleIterator(cssStyles);
		while (ruleIterator.hasNext()) {
			styleManager.processCssStyles(fontState, parentState, ruleIterator.next());
		}
	}

	/**
	 * public for testing purposes
	 */
	public static class Token extends org.eclipse.jface.text.rules.Token {

		private final int offset;

		private final int length;

		private final FontState fontState;

		public Token(FontState fontState, TextAttribute attribute, int offset, int length) {
			super(attribute);
			this.fontState = fontState;
			if (offset < 0) {
				throw new IllegalArgumentException();
			}
			if (length < 0) {
				throw new IllegalArgumentException();
			}
			this.offset = offset;
			this.length = length;
		}

		public int getOffset() {
			return offset;
		}

		public int getLength() {
			return length;
		}

		public FontState getFontState() {
			return fontState;
		}

		@Override
		public TextAttribute getData() {
			return (TextAttribute) super.getData();
		}

		@Override
		public String toString() {
			return "Token [offset=" + offset + ", length=" + length + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ 
		}

	}

}
