/*******************************************************************************
 * Copyright (c) 2018, 2020 itemis AG and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Zoey Gerrit Prigge - Initial API and implementation (bug #321775)
 *                        - support for FontName grammar (bug #541056)
 *
 *******************************************************************************/
package org.eclipse.gef.dot.internal.ui.conversion;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang.StringEscapeUtils;
import org.eclipse.gef.dot.internal.language.color.Color;
import org.eclipse.gef.dot.internal.language.fontname.FontName;
import org.eclipse.gef.dot.internal.language.htmllabel.DotHtmlLabelHelper;
import org.eclipse.gef.dot.internal.language.htmllabel.HtmlAttr;
import org.eclipse.gef.dot.internal.language.htmllabel.HtmlContent;
import org.eclipse.gef.dot.internal.language.htmllabel.HtmlLabel;
import org.eclipse.gef.dot.internal.language.htmllabel.HtmlTag;
import org.eclipse.gef.dot.internal.language.parser.antlr.DotHtmlLabelParser;
import org.eclipse.gef.dot.internal.ui.language.DotActivator;
import org.eclipse.xtext.parser.IParseResult;

import com.google.inject.Injector;

import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;

/*
 * This class will only look into the inner HTML label (i.e. not including the shape)
 *
 * The graphviz grammar at http://www.graphviz.org/doc/info/shapes.html#html diverges in naming
 * from the DotHtmlLabel grammar due to a choice for higher availability of syntax help in the UI
 *
 * Hence, this class relies on validation of the model and cites the graphviz grammar to align
 * behaviour with what the graphviz grammar permits.
 *
 * Comments will refer to the graphviz grammar at the URL above.
 *
 * Known Limitations, TODO:
 * - check default padding
 * - consider implementing Port attribute on TD
 */
class DotHTMLLabelJavaFxNode {
	final private DotColorUtil colorUtil;
	final private DotFontUtil fontUtil;

	final private StyleContainer defaultStyle;
	final private FontStyleContainer nodeStyle;

	private HtmlLabel root = null;
	private Pane masterFxElement = new Pane();
	private String colorscheme = null;
	private String label = null;

	/**
	 * Creates a DotHTMLLabelJavaNode creator with default styles
	 *
	 * @param fontUtil
	 *            A DotFontUtil instance.
	 * @param colorUtil
	 *            A DotColorUtil instance.
	 */
	public DotHTMLLabelJavaFxNode(DotColorUtil colorUtil,
			DotFontUtil fontUtil) {
		this.colorUtil = colorUtil;
		this.fontUtil = fontUtil;

		defaultStyle = new FontStyleContainer(null, "Times-Roman", "14", //$NON-NLS-1$ //$NON-NLS-2$
				"black"); //$NON-NLS-1$
		nodeStyle = new FontStyleContainer(defaultStyle, null, null, null);
	}

	public void setDefaults(FontName face, Double size, Color color,
			String colorscheme) {
		nodeStyle.updateTagStyle(face, size, color);
		this.colorscheme = colorscheme;
	}

	public void setLabel(String label) {
		root = parseLabel(label);
		this.label = label;
	}

	public String getLabel() {
		return label;
	}

	/**
	 * Method to retrieve the JavaFX pane
	 *
	 * @return Java Fx Pane
	 */
	public Pane getMasterFxElement() {
		return masterFxElement;
	}

	public void refreshFxElement() {
		if (root != null && fontUtil != null && colorUtil != null) {
			masterFxElement.getChildren().clear();
			masterFxElement.getChildren().add(drawLabel(root));
		}
	}

	private HtmlLabel parseLabel(final String label) {
		Injector labelInjector = DotActivator.getInstance().getInjector(
				DotActivator.ORG_ECLIPSE_GEF_DOT_INTERNAL_LANGUAGE_DOTHTMLLABEL);
		DotHtmlLabelParser parser = labelInjector
				.getInstance(DotHtmlLabelParser.class);
		IParseResult result = parser
				.parse(new StringReader(label != null ? label : new String()));
		return (HtmlLabel) result.getRootASTElement();
	}

	private Pane drawLabel(HtmlLabel label) {
		return drawContents(label.getParts(), nodeStyle, null);
	}

	private Pane drawContents(List<HtmlContent> contents,
			StyleContainer parentStyle, Pos bAlign) {
		if (contents.size() <= 0)
			return new Pane(); // an empty HTML label, ought not to occur

		/*
		 * if there is more than one label, this is, strictly speaking a dot
		 * syntax error; however, the grammar does not recognize this. hence, we
		 * need to check if the first tag is a table (or a parent, parent of
		 * parent, ...) and else treat contents as text.
		 *
		 * StyleTags are a special case where it could be either a textitem or a
		 * table The documented graphviz grammar only specifies this for font
		 * tags, but (undocumented) graphviz behaviour allows other style tags
		 * (B, I, ...) to occur before tables, too.
		 *
		 * The table rule is as follows:
		 *
		 * table : [ <FONT> ] <TABLE> rows </TABLE> [ </FONT> ]
		 *
		 * We need to differentiate between the the table and textitem cases.
		 * The tag could have multiple children in the latter case.
		 */
		if (isTableCase(contents)) {
			return drawTableParents(firstTagWhitespaceIgnored(contents),
					parentStyle);
		}

		// by the grammar we assume it's all text, if the in the line of first
		// children no table tag indicates a table
		return drawText(contents, parentStyle, bAlign);
	}

	private boolean isTableCase(List<HtmlContent> contents) {
		HtmlTag tag = firstTagWhitespaceIgnored(contents);
		// Text (exit) case
		if (tag == null) {
			return false;
		}
		if (tag.getName().equalsIgnoreCase("table")) { //$NON-NLS-1$
			return true;
		}
		return isTableCase(tag.getChildren());
	}

	private HtmlTag firstTagWhitespaceIgnored(List<HtmlContent> contents) {
		if (contents.size() <= 0) {
			return null;
		}
		HtmlContent content0 = contents.get(0);
		// all whitespace would be grouped into a single tag by the grammar
		if (isWhitespaceOnlyTag(content0) && contents.size() > 1) {
			content0 = contents.get(1);
		}
		return content0.getTag();
	}

	private boolean isWhitespaceOnlyTag(HtmlContent content) {
		return content.getTag() == null && content.getText() != null
				&& content.getText().matches("\\A\\s*\\z") //$NON-NLS-1$
		;
	}

	private Pane drawTableParents(HtmlTag tag, StyleContainer parentStyle) {
		if (tag.getName().equalsIgnoreCase("table")) { //$NON-NLS-1$
			return drawTable(tag, parentStyle);
		}
		return drawTableParents(firstTagWhitespaceIgnored(tag.getChildren()),
				tagStyleContainer(tag, parentStyle));
	}

	private Pane drawText(List<HtmlContent> contents,
			StyleContainer parentStyle, Pos bAlign) {
		TextFXBuilder builder = new TextFXBuilder(bAlign);
		contents.forEach(
				content -> handleTextContent(builder, content, parentStyle));
		return builder.getFxElement();
	}

	/**
	 *
	 * @param builder
	 *            NOT null
	 * @param tag
	 *            NOT null
	 * @param parentStyle
	 *            May be null
	 */
	private void handleTextTag(TextFXBuilder builder, HtmlTag tag,
			StyleContainer parentStyle) {
		StyleContainer tagStyleContainer;
		switch (tag.getName().toLowerCase()) {
		case "br": //$NON-NLS-1$
			builder.breakLine(getPosForBr(tag));
			return;
		case "table": //$NON-NLS-1$
			// table tags in this place are illegal
			return;
		// we can assume it's a style (including font) Tag
		default:
			tagStyleContainer = tagStyleContainer(tag, parentStyle);
			break;
		}
		for (HtmlContent child : tag.getChildren())
			handleTextContent(builder, child, tagStyleContainer);
	}

	/*
	 * A non-style tag supplied (cases of illegal grammar) is ignored
	 */
	private StyleContainer tagStyleContainer(HtmlTag tag,
			StyleContainer parentStyle) {
		switch (tag.getName().toLowerCase()) {
		case "font": //$NON-NLS-1$
			return fontTagStyleContainer(tag, parentStyle);
		default:
			return simpleTagStyleContainer(tag, parentStyle);
		}
	}

	private StyleContainer simpleTagStyleContainer(HtmlTag tag,
			StyleContainer parentStyle) {
		TagStyle tagStyle = null;
		try {
			tagStyle = TagStyle.valueOf(tag.getName().toUpperCase());
		} catch (IllegalArgumentException e) {
			// we have an illegal tag, which is odd but can continue
		}
		return new TagStyleContainer(parentStyle, tagStyle);
	}

	private StyleContainer fontTagStyleContainer(HtmlTag tag,
			StyleContainer parentStyle) {
		final String color = unquotedValueForAttr(
				DotHtmlLabelHelper.getAttributeForTag(tag, "color")); //$NON-NLS-1$
		final String face = unquotedValueForAttr(
				DotHtmlLabelHelper.getAttributeForTag(tag, "face")); //$NON-NLS-1$
		final String size = unquotedValueForAttr(
				DotHtmlLabelHelper.getAttributeForTag(tag, "point-size")); //$NON-NLS-1$
		return new FontStyleContainer(parentStyle, face, size, color);
	}

	/**
	 *
	 * @param builder
	 *            NOT null
	 * @param content
	 *            NOT null
	 * @param parentStyle
	 *            May be null
	 */
	private void handleTextContent(TextFXBuilder builder, HtmlContent content,
			StyleContainer parentStyle) {
		if (content.getTag() != null) {
			handleTextTag(builder, content.getTag(), parentStyle);
		} else {
			String unescapedText = StringEscapeUtils.unescapeHtml(
					content.getText().replaceAll("[\\t\\n\\x0B\\f\\r]", "")); //$NON-NLS-1$ //$NON-NLS-2$
			builder.addFormattedString(new FormattedString(
					parentStyle != null ? parentStyle
							: new TagStyleContainer(null, null),
					unescapedText));
		}
	}

	private Pos getPosForBr(HtmlTag brTag) {
		return getAlignPosForAttributeNameAndTag("align", brTag); //$NON-NLS-1$
	}

	private Pos getPosForTdBalign(HtmlTag tdTag) {
		return getAlignPosForAttributeNameAndTag("balign", tdTag); //$NON-NLS-1$
	}

	private Pos getAlignPosForAttributeNameAndTag(String attributeName,
			HtmlTag tag) {
		HtmlAttr attr = DotHtmlLabelHelper.getAttributeForTag(tag,
				attributeName);
		if (attr == null) {
			return null;
		}
		switch (unquotedValueForAttr(attr).toLowerCase()) {
		case "right": //$NON-NLS-1$
			return Pos.CENTER_RIGHT;
		case "left": //$NON-NLS-1$
			return Pos.CENTER_LEFT;
		case "center": //$NON-NLS-1$
		default:
			return Pos.CENTER;
		}
	}

	private Pane drawTable(HtmlTag tag, StyleContainer parentStyle) {
		// TODO VR, HR support

		GridPane fullPane = new GridPane();
		applyCssAttributesOnTablePane(fullPane, tag);
		List<HtmlTag> trTags = childHtmlTagsOfKind(tag, "TR"); //$NON-NLS-1$
		Map<Integer, BitSet> rowsToFilledCellsMap = initializedRowsToFilledCellsMap(
				trTags.size());
		for (HtmlTag tr : trTags)
			addRowToPane(tag, rowsToFilledCellsMap, tr, trTags.indexOf(tr),
					fullPane, parentStyle);
		return fullPane;
	}

	private void addRowToPane(HtmlTag tag,
			Map<Integer, BitSet> rowsToFilledCellsMap, HtmlTag tr, int rowIndex,
			GridPane fullPane, StyleContainer parentStyle) {
		for (HtmlTag td : childHtmlTagsOfKind(tr, "TD")) { //$NON-NLS-1$
			addCellToPane(tag, rowsToFilledCellsMap, rowIndex, fullPane,
					parentStyle, td);
		}
	}

	private void addCellToPane(HtmlTag tag,
			Map<Integer, BitSet> rowsToFilledCellsMap, int rowIndex,
			GridPane fullPane, StyleContainer parentStyle, HtmlTag td) {
		// "label" rule in the original graphviz grammar
		// > cell : <TD> label </TD>
		Pane labelPane = styledTdContent(tag, parentStyle, td);

		int colspan = getIntSpanAttrValue(td, "colspan"); //$NON-NLS-1$
		int rowspan = getIntSpanAttrValue(td, "rowspan"); //$NON-NLS-1$
		int index = rowsToFilledCellsMap.get(rowIndex).nextClearBit(0);

		for (int row = rowIndex; row < rowIndex + rowspan
				&& rowsToFilledCellsMap.containsKey(row); row++)
			rowsToFilledCellsMap.get(row).set(index, index + colspan);

		fullPane.add(labelPane, index, rowIndex, colspan, rowspan);
	}

	private GridPane styledTdContent(HtmlTag tag, StyleContainer parentStyle,
			HtmlTag td) {
		Pos bAlign = getPosForTdBalign(td);
		Pane unstyled = drawContents(td.getChildren(), parentStyle, bAlign);

		// to set Alignment and Growth the labelPane content must be wrapped
		GridPane wrapper = new GridPane();
		wrapper.add(unstyled, 0, 0);

		if (!isTableCase(tag.getChildren())) {
			applyTextAlignAttributesOnTdPane(wrapper, td);
		} else {
			applyTableAlignAttributesOnTdPane(wrapper);
		}
		applyCssAttributesOnTdPane(wrapper, td, tag);

		return wrapper;
	}

	private List<HtmlTag> childHtmlTagsOfKind(HtmlTag tag, String name) {
		return tag.getChildren().stream()
				.filter(child -> child.getTag() != null)
				.map(HtmlContent::getTag)
				.filter(trCandidate -> trCandidate.getName()
						.equalsIgnoreCase(name)) // $NON-NLS-1$
				.collect(Collectors.toList());
	}

	private Map<Integer, BitSet> initializedRowsToFilledCellsMap(int size) {
		Map<Integer, BitSet> tableCellMap = new HashMap<Integer, BitSet>();
		for (int key = 0; key < size; key++)
			tableCellMap.put(key, new BitSet());
		return tableCellMap;
	}

	private void applyCssAttributesOnTablePane(GridPane fullPane, HtmlTag tag) {
		StringBuilder css = new StringBuilder();
		/*
		 * Attributes TODO
		 *
		 * Align (Note: For table ALIGN="CENTER|LEFT|RIGHT"), Cellspacing,
		 * COLUMNS="*" for a vertical line between every column, ROWS="*" for a
		 * horizontal line between every line, VALIGN="MIDDLE|BOTTOM|TOP"
		 *
		 */

		// FIXEDSIZE="FALSE|TRUE" used in HEIGHT and WIDTH attributes
		HtmlAttr fixedSize = DotHtmlLabelHelper.getAttributeForTag(tag,
				"fixedsize"); //$NON-NLS-1$

		appendBgcolorAttribute(css,
				DotHtmlLabelHelper.getAttributeForTag(tag, "bgcolor"), //$NON-NLS-1$
				DotHtmlLabelHelper.getAttributeForTag(tag, "style"), //$NON-NLS-1$
				DotHtmlLabelHelper.getAttributeForTag(tag, "gradientangle")); //$NON-NLS-1$
		appendBorderAttribute(css,
				DotHtmlLabelHelper.getAttributeForTag(tag, "border")); //$NON-NLS-1$
		appendColorAttribute(css,
				DotHtmlLabelHelper.getAttributeForTag(tag, "color")); //$NON-NLS-1$
		appendHeightAttribute(css,
				DotHtmlLabelHelper.getAttributeForTag(tag, "height"), //$NON-NLS-1$
				fixedSize);
		appendSidesAttribute(css,
				DotHtmlLabelHelper.getAttributeForTag(tag, "sides")); //$NON-NLS-1$
		appendStyleTableAttribute(css,
				DotHtmlLabelHelper.getAttributeForTag(tag, "style")); //$NON-NLS-1$
		appendWidthAttribute(css,
				DotHtmlLabelHelper.getAttributeForTag(tag, "width"), fixedSize); //$NON-NLS-1$

		fullPane.setStyle(css.toString());
	}

	private void applyCssAttributesOnTdPane(Pane labelPane, HtmlTag tdTag,
			HtmlTag tableTag) {
		StringBuilder css = new StringBuilder();
		/*
		 * TODO Cellpadding
		 *
		 * Attributes that are not relevant for layouting of the HTML label
		 * Href, Port, Title, Tooltip
		 *
		 * Currently unsupported throughout: Gefdot, Id
		 */

		// FIXEDSIZE="FALSE|TRUE" used in HEIGHT and WIDTH attributes
		HtmlAttr fixedSize = DotHtmlLabelHelper.getAttributeForTag(tdTag,
				"fixedsize"); //$NON-NLS-1$

		appendBgcolorAttribute(css,
				DotHtmlLabelHelper.getAttributeForTag(tdTag, "bgcolor"), //$NON-NLS-1$
				DotHtmlLabelHelper.getAttributeForTag(tdTag, "style"), //$NON-NLS-1$
				DotHtmlLabelHelper.getAttributeForTag(tdTag, "gradientangle")); //$NON-NLS-1$
		appendBorderAttribute(css, borderAttributeForTd(tdTag, tableTag));
		appendColorAttribute(css,
				DotHtmlLabelHelper.getAttributeForTags("color", //$NON-NLS-1$
						tdTag, tableTag));
		appendHeightAttribute(css,
				DotHtmlLabelHelper.getAttributeForTag(tdTag, "height"), //$NON-NLS-1$
				fixedSize);
		appendSidesAttribute(css,
				DotHtmlLabelHelper.getAttributeForTags("sides", tdTag, //$NON-NLS-1$
						tableTag));
		appendWidthAttribute(css,
				DotHtmlLabelHelper.getAttributeForTag(tdTag, "width"), //$NON-NLS-1$
				fixedSize);

		labelPane.setStyle(css.toString());
	}

	private void appendStyleTableAttribute(StringBuilder css, HtmlAttr style) {
		if (style != null) {
			String styleValue = unquotedValueForAttr(style).toLowerCase();
			if (styleValue.contains("rounded")) { //$NON-NLS-1$
				css.append("-fx-border-radius: 5%;"); //$NON-NLS-1$
				css.append("-fx-background-radius: 5%;"); //$NON-NLS-1$
			}
		}
	}

	private void appendSidesAttribute(StringBuilder css, HtmlAttr sides) {
		if (sides != null) {
			String sidesShown = unquotedValueForAttr(sides).toLowerCase();
			if (!sidesShown.contains("l")) //$NON-NLS-1$
				css.append("-fx-border-left: hidden;"); //$NON-NLS-1$
			if (!sidesShown.contains("t")) //$NON-NLS-1$
				css.append("-fx-border-top: hidden;"); //$NON-NLS-1$
			if (!sidesShown.contains("r")) //$NON-NLS-1$
				css.append("-fx-border-right: hidden;"); //$NON-NLS-1$
			if (!sidesShown.contains("b")) //$NON-NLS-1$
				css.append("-fx-border-bottom: hidden;"); //$NON-NLS-1$
		}
	}

	private void appendHeightAttribute(StringBuilder css, HtmlAttr height,
			HtmlAttr fixedSize) {
		appendDimensionAttribute(css, "height", height, fixedSize); //$NON-NLS-1$
	}

	private void appendWidthAttribute(StringBuilder css, HtmlAttr width,
			HtmlAttr fixedSize) {
		appendDimensionAttribute(css, "width", width, fixedSize); //$NON-NLS-1$
	}

	private void appendDimensionAttribute(StringBuilder css, String kind,
			HtmlAttr dimension, HtmlAttr fixedSize) {
		if (dimension != null) {
			css.append("-fx-min-"); //$NON-NLS-1$
			css.append(kind); // $NON-NLS-1$
			css.append(":"); //$NON-NLS-1$
			css.append(unquotedValueForAttr(dimension));
			css.append(";"); //$NON-NLS-1$
			if (fixedSize != null && unquotedValueForAttr(fixedSize)
					.toLowerCase().equals("true")) { //$NON-NLS-1$
				css.append("-fx-max-"); //$NON-NLS-1$
				css.append(kind); // $NON-NLS-1$
				css.append(":"); //$NON-NLS-1$
				css.append(unquotedValueForAttr(dimension));
				css.append(";"); //$NON-NLS-1$
			}
		}
	}

	private void appendColorAttribute(StringBuilder css, HtmlAttr bordercolor) {
		css.append("-fx-border-color:"); //$NON-NLS-1$
		css.append(
				bordercolor != null
						? colorUtil.computeZestColor(colorscheme,
								colorUtil.computeHtmlColor(
										unquotedValueForAttr(bordercolor)))
						: "black"); //$NON-NLS-1$
		css.append(";"); //$NON-NLS-1$
	}

	private void appendBorderAttribute(StringBuilder css, HtmlAttr border) {
		if (border != null) {
			css.append("-fx-border-width:"); //$NON-NLS-1$
			css.append(unquotedValueForAttr(border));
			css.append("pt;"); //$NON-NLS-1$
		}
	}

	private HtmlAttr borderAttributeForTd(HtmlTag tdTag, HtmlTag tableTag) {
		HtmlAttr border = DotHtmlLabelHelper.getAttributeForTag(tdTag,
				"border"); //$NON-NLS-1$
		if (border == null)
			border = DotHtmlLabelHelper.getAttributeForTag(tableTag,
					"cellborder"); //$NON-NLS-1$
		if (border == null)
			border = DotHtmlLabelHelper.getAttributeForTag(tableTag, "border"); //$NON-NLS-1$
		return border;
	}

	private void appendBgcolorAttribute(StringBuilder css, HtmlAttr bgcolor,
			HtmlAttr style, HtmlAttr gradientAngle) {
		if (bgcolor != null) {
			css.append("-fx-background-color:"); //$NON-NLS-1$
			List<String> colors = Arrays
					.stream(unquotedValueForAttr(bgcolor).split(":")) //$NON-NLS-1$
					.map(e -> colorUtil.computeZestColor(colorscheme,
							colorUtil.computeHtmlColor(e)))
					.collect(Collectors.toList());
			if (colors.size() > 1) {
				if (style != null && unquotedValueForAttr(style).toLowerCase()
						.contains("radial")) { //$NON-NLS-1$
					css.append("radial-gradient("); //$NON-NLS-1$
					// TODO gradientangle
					css.append("center "); //$NON-NLS-1$
					css.append("50% 50%"); //$NON-NLS-1$
					css.append(", "); //$NON-NLS-1$
					css.append("radius 50%, "); //$NON-NLS-1$
				} else {
					// TODO gradientangle
					css.append("linear-gradient("); //$NON-NLS-1$
					css.append("from 0% 0% to 100% 0%, "); //$NON-NLS-1$
				}
				css.append(colors.get(0));
				css.append(", "); //$NON-NLS-1$
				css.append(colors.get(1));
				css.append(")"); //$NON-NLS-1$
			} else {
				css.append(colors.get(0));
			}
			css.append(";"); //$NON-NLS-1$
		}
	}

	private void applyTextAlignAttributesOnTdPane(GridPane wrapper,
			HtmlTag td) {
		String hAlign = unquotedValueForAttr(
				DotHtmlLabelHelper.getAttributeForTag(td, "align")); //$NON-NLS-1$
		String vAlign = unquotedValueForAttr(
				DotHtmlLabelHelper.getAttributeForTag(td, "valign")); //$NON-NLS-1$

		if ("text".equalsIgnoreCase(hAlign)) { //$NON-NLS-1$
			GridPane.setHgrow(wrapper.getChildren().get(0), Priority.ALWAYS);
		}

		wrapper.setAlignment(posForTd(hAlign, vAlign));
	}

	private void applyTableAlignAttributesOnTdPane(GridPane wrapper) {
		/*
		 * Graphviz documentation specifies for the ALIGN attribute on cells: If
		 * the cell does not contain text, then the contained image or table is
		 * centered.
		 *
		 * Further, by observation, unless the fixedsize attribute is set, in
		 * graphviz the inner table grows in both horizontal and vertical
		 * direction.
		 *
		 * TODO: revise these settings when the align attribute on table tags is
		 * implemented, as this may change some behaviour.
		 */
		GridPane.setHgrow(wrapper.getChildren().get(0), Priority.ALWAYS);
		GridPane.setVgrow(wrapper.getChildren().get(0), Priority.ALWAYS);
		wrapper.setAlignment(Pos.CENTER);
	}

	private Pos posForTd(String hAlign, String vAlign) {
		switch (hAlign != null ? hAlign.toLowerCase() : "") { //$NON-NLS-1$
		case "left": //$NON-NLS-1$
			switch (vAlign != null ? vAlign.toLowerCase() : "") { //$NON-NLS-1$
			case "top": //$NON-NLS-1$
				return Pos.TOP_LEFT;
			case "bottom": //$NON-NLS-1$
				return Pos.BOTTOM_LEFT;
			case "middle": //$NON-NLS-1$
			default:
				return Pos.CENTER_LEFT;
			}
		case "right": //$NON-NLS-1$
			switch (vAlign != null ? vAlign.toLowerCase() : "") { //$NON-NLS-1$
			case "top": //$NON-NLS-1$
				return Pos.TOP_RIGHT;
			case "bottom": //$NON-NLS-1$
				return Pos.BOTTOM_RIGHT;
			case "middle": //$NON-NLS-1$
			default:
				return Pos.CENTER_RIGHT;
			}
		case "center": //$NON-NLS-1$
		case "text": //$NON-NLS-1$
		default:
			switch (vAlign != null ? vAlign.toLowerCase() : "") { //$NON-NLS-1$
			case "top": //$NON-NLS-1$
				return Pos.TOP_CENTER;
			case "bottom": //$NON-NLS-1$
				return Pos.BOTTOM_CENTER;
			case "middle": //$NON-NLS-1$
			default:
				return Pos.CENTER;
			}
		}
	}

	private String unquotedValueForAttr(HtmlAttr attr) {
		if (attr == null)
			return null;
		String value = attr.getValue();
		if (value.length() > 2) {
			return value.substring(1, value.length() - 1);
		}
		return ""; //$NON-NLS-1$
	}

	private int getIntSpanAttrValue(HtmlTag tag, String name) {
		HtmlAttr attribute = DotHtmlLabelHelper.getAttributeForTag(tag, name);
		if (attribute == null)
			// the span of a cell is 1 if the attribute is not set
			return 1;
		else
			return Integer.valueOf(unquotedValueForAttr(attribute));
	}

	private enum TagStyle {
		I, B, U, O, SUB, SUP, S;

		public String cssStringForTag() {
			switch (this) {
			case I:
				return "-fx-font-style: italic;"; //$NON-NLS-1$
			case B:
				return "-fx-font-weight: bold;"; //$NON-NLS-1$
			case U:
				return "-fx-underline: true;"; //$NON-NLS-1$
			case O:
				// TODO Not supported by JavaFX, find workaround using border.
				// Consider text color.
				return ""; //$NON-NLS-1$
			case SUB:
				return "-fx-font-size: .83em; -fx-vertical-align: sub"; //$NON-NLS-1$
			case SUP:
				return "-fx-font-size: .83em; -fx-vertical-align: super"; //$NON-NLS-1$
			case S:
				return "-fx-strikethrough: true;"; //$NON-NLS-1$
			default:
				return ""; //$NON-NLS-1$
			}
		}
	}

	private abstract class StyleContainer {
		protected abstract FontName face();

		protected abstract Double size();

		protected abstract Color color();

		protected abstract Set<TagStyle> tagStyles();

		private String fontCss() {
			StringBuilder css = new StringBuilder();
			FontName face = face();
			if (face != null) {
				css.append("-fx-font-family:\""); //$NON-NLS-1$
				css.append(fontUtil.cssLocalFontFamily(face));
				css.append("\";"); //$NON-NLS-1$
				// B, I tags supersede any style/weight info in FontName
				if (!tagStyles().contains(TagStyle.B)) {
					css.append("-fx-font-weight: "); //$NON-NLS-1$
					css.append(fontUtil.cssWeight(face));
					css.append(";"); //$NON-NLS-1$
				}
				if (!tagStyles().contains(TagStyle.I)) {
					css.append("-fx-font-style: "); //$NON-NLS-1$
					css.append(fontUtil.cssStyle(face));
					css.append(";"); //$NON-NLS-1$
				}
			}

			Double size = size();
			if (size != null) {
				css.append("-fx-font-size:"); //$NON-NLS-1$
				css.append(size);
				css.append(";"); //$NON-NLS-1$
			}

			Color color = color();
			if (color != null) {
				css.append("-fx-fill:"); //$NON-NLS-1$
				css.append(colorUtil.computeZestColor(colorscheme, color));
				css.append(";"); //$NON-NLS-1$
			}

			return css.toString();
		}

		public String getCSS() {
			StringBuilder stringBuilder = new StringBuilder();
			tagStyles().forEach(
					style -> stringBuilder.append(style.cssStringForTag()));
			stringBuilder.append(fontCss());
			return stringBuilder.toString();
		}
	}

	private class TagStyleContainer extends StyleContainer {
		final private StyleContainer parent;
		private TagStyle style;

		public TagStyleContainer(StyleContainer parent, TagStyle style) {
			this.parent = parent;
			this.style = style;
		}

		protected FontName face() {
			return parent != null ? parent.face() : null;
		}

		protected Double size() {
			return parent != null ? parent.size() : null;
		}

		protected Color color() {
			return parent != null ? parent.color() : null;
		}

		protected Set<TagStyle> tagStyles() {
			Set<TagStyle> styles = parent == null ? new HashSet<TagStyle>()
					: parent.tagStyles();
			if (style != null)
				styles.add(style);
			return styles;
		}

	}

	private class FontStyleContainer extends StyleContainer {
		final private StyleContainer parent;
		private FontName face;
		private Double size;
		private Color color;

		public FontStyleContainer(StyleContainer parent, String face,
				String size, String color) {
			this.parent = parent;
			updateTagStyle(
					face != null ? fontUtil.parseHtmlFontFace(face) : null,
					size != null ? Double.valueOf(size) : null,
					color != null ? colorUtil.computeHtmlColor(color) : null);
		}

		public void updateTagStyle(FontName face, Double size, Color color) {
			this.face = face;
			this.size = size;
			this.color = color;
		}

		protected FontName face() {
			if (face != null)
				return face;
			if (parent != null)
				return parent.face();
			return null;
		}

		protected Double size() {
			if (size != null)
				return size;
			if (parent != null)
				return parent.size();
			return null;
		}

		protected Color color() {
			if (color != null)
				return color;
			if (parent != null)
				return parent.color();
			return null;
		}

		protected Set<TagStyle> tagStyles() {
			return parent != null ? parent.tagStyles()
					: new HashSet<TagStyle>();
		}
	}

	private class FormattedString {
		private final StyleContainer style;
		private final String text;

		public FormattedString(StyleContainer style, String text) {
			this.style = style;
			this.text = text;
		}

		public Node getFxElement() {
			Text text = new Text(this.text != null ? this.text : ""); //$NON-NLS-1$
			text.setStyle(style.getCSS());
			return text;
		}
	}

	private class FormattedLine {
		private final List<FormattedString> textitems = new ArrayList<>();
		private Pos alignment = null;
		private Pos defaultAlignment;

		public FormattedLine(Pos bAlign) {
			defaultAlignment = bAlign != null ? bAlign : Pos.CENTER;
		}

		public void addFormattedString(FormattedString textItem) {
			textitems.add(textItem);
		}

		public void setAlignment(Pos pos) {
			if (pos != null) {
				alignment = pos;
			}
		}

		public Pane getFxElement() {
			final HBox hbox = new HBox();
			hbox.setAlignment(alignment != null ? alignment : defaultAlignment);
			textitems.forEach(textitem -> hbox.getChildren()
					.add(textitem.getFxElement()));
			return hbox;
		}
	}

	private class TextFXBuilder {
		private final List<FormattedLine> lines = new ArrayList<>();
		private FormattedLine current;
		private Pos bAlign;

		public TextFXBuilder(Pos bAlign) {
			this.bAlign = bAlign;
			newLine();
		}

		public void breakLine(Pos align) {
			current.setAlignment(align);
			newLine();
		}

		private void newLine() {
			FormattedLine line = new FormattedLine(bAlign);
			lines.add(line);
			current = line;
		}

		public void addFormattedString(FormattedString textItem) {
			current.addFormattedString(textItem);
		}

		public Pane getFxElement() {
			VBox vbox = new VBox();
			lines.forEach(line -> vbox.getChildren().add(line.getFxElement()));
			return vbox;
		}
	}
}