package inf101.v18.gfx.textmode;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import javafx.geometry.Point2D;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.text.Font;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;

/**
 * TextFont – for grid-based text / character graphics
 * <p>
 * TextFonts are used for drawing text and character graphics. Each character is
 * assumed to be uniform in size, fitting within a square with sides of length
 * {@link #getSquareSize()}. The given font file should contain a monospaced
 * font of a type suitable for JavaFX, such as OpenType or TrueType.
 * 
 * <p>
 * Additional horizontal and vertical positioning and scaling can be used to
 * make the font fit with the square-shaped concept.
 * 
 * <p>
 * See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics
 * context for writing with the font, or
 * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly
 * horizontal scaling, e.g. to make half-width letters ("hires" mode).
 *
 */
public class TextFont {
	public static final int ATTR_INVERSE = 0x01;
	public static final int ATTR_ITALIC = 0x02;
	public static final int ATTR_BOLD = 0x04;
	public static final int ATTR_OUTLINE = 0x08;
	public static final int ATTR_UNDERLINE = 0x10;
	public static final int ATTR_OVERLINE = 0x20;
	public static final int ATTR_LINE_THROUGH = 0x40;
	public static final int ATTR_OVERSTRIKE = 0x80;
	public static final int ATTR_CLIP = 0x80;
	public static final int ATTR_NO_FAKE_CHARS = 0x100;
	public static final int ATTR_BLINK = 0x200; // NOT IMPLEMENTED
	public static final int ATTR_FAINT = 0x400; // NOT IMPLEMENTED
	public static final int ATTR_BRIGHT = 0x800;

	private static final String[] searchPath = { "", "../", "../fonts/" };
	private static final Map<String, String> loadedFonts = new HashMap<>();
	private static final double thin = 2.0, thick = 4.0;
	private static final String[] boxDrawingShapes = { // lines
			"--..", "**..", "..--", "..**",
			// dashed lines
			"3--..", "3**..", "3..--", "3..**", "4--..", "4**..", "4..--", "4..**",
			// corners
			".-.-", ".*.-", ".-.*", ".*.*", "-..-", "*..-", "-..*", "*..*", ".--.", ".*-.", ".-*.", ".**.", "-.-.",
			"*.-.", "-.*.", "*.*.",
			// |-
			".---", ".*--", ".-*- ", ".--* ", ".-**", ".**-", ".*-*", ".***", "-.--", "*.--", "-.*-", "-.-*", "-.**",
			"*.*-", "*.-*", "*.**",
			// T
			"--.-", "*-.-", "-*.-", "**.-", "--.*", "*-.*", "-*.*", "**.*", "---.", "*--. ", "-*-.", "**-.", "--*.",
			"*-*.", "-**.", "***.",
			// +
			"----", "*---", "-*--", "**--", "--*-", "---*", "--**", "*-*-", "-**-", "*--*", "-*-*", "***-", "**-*",
			"*-**", "-***", "****",
			// dashes
			"2--..", "2**..", "2..--", "2..**",
			// double lines
			"==..", "..==", ".=.-", ".-.=", ".N.N", "=..-", "-..=", "M..M", ".=-.", ".-=.", ".MM.", "=.-.", "-.=.",
			"N.N.", ".=--", ".s==", ".zMN", "=.--", "s.==", "z.NM", "==.s", "--.=", "MN.z", "==s.", "--=.", "NMz.",
			"==--", "--==", "zzzz",
			// round corners
			"0.-.-", "0-..-", "0-.-.", "0.--.",
			// diagonals
			"////", "\\\\\\\\", "//\\\\\\",
			// short lines
			"-...", "..-.", ".-..", "...-", "*...", "..*.", ".*..", "...*",
			// thin/thick lines
			"-*..", "..-*", "*-..", "..*-" };

	public static void drawMasked(GraphicsContext ctx, Image src, Image mask, double x, double y, boolean invert,
			boolean blend) {
		int w = (int) src.getWidth();
		int h = (int) src.getHeight();

		PixelReader pixelSrc = src.getPixelReader();
		PixelReader pixelMask = mask.getPixelReader();
		PixelWriter pixelWriter = ctx.getPixelWriter();
		Affine transform = ctx.getTransform();
		Point2D point = transform.transform(x, y);
		int dx = (int) point.getX(), dy = (int) point.getY();

		for (int px = 0; px < w; px++) {
			for (int py = 0; py < h; py++) {
				int a = pixelMask.getArgb(px, py) >>> 24;
				int rgb = pixelSrc.getArgb(px, py);
				if (invert)
					a = ~a & 0xff;
				if (blend)
					a = ((rgb >>> 24) * a) >>> 8;
				pixelWriter.setArgb(px + dx, py + dy, (a << 24) | rgb);

			}
		}
	}

	public static void fillInverse(GraphicsContext ctx, Image img, double x, double y) {
		int w = (int) img.getWidth();
		int h = (int) img.getHeight();
		PixelReader pixelReader = img.getPixelReader();
		PixelWriter pixelWriter = ctx.getPixelWriter();
		Affine transform = ctx.getTransform();
		Point2D point = transform.transform(x, y);
		int dx = (int) point.getX(), dy = (int) point.getY();
		Color c = ctx.getFill() instanceof Color ? (Color) ctx.getFill() : Color.BLACK;
		int rgb = ((int) (c.getRed() * 255)) << 16 | ((int) (c.getGreen() * 255)) << 8 | ((int) (c.getBlue() * 255));

		for (int px = 0; px < w; px++) {
			for (int py = 0; py < h; py++) {
				int a = (~pixelReader.getArgb(px, py) & 0xff000000);
				// if(a != 0)
				pixelWriter.setArgb(px + dx, py + dy, a | rgb);

			}
		}
	}

	/**
	 * Load a font.
	 * 
	 * Will first try to use the provided string as a file name, and load the font
	 * from a font file (should be in a format supported by JavaFX). If loading from
	 * file fails, we assume we have been given the name of a font which is passed
	 * directly to JavaFX for loading. A fallback font is used if this fails.
	 * 
	 * When looking for files, his method will search for the file in a search path
	 * starting with the folder containing the {@link TextFont} class file (using
	 * Java's standard {@link Class#getResourceAsStream(String)} system). The search
	 * path also includes ".." (parent directory) and "../fonts".
	 * 
	 * The loaded font will be cached, so that additional calls with the same file
	 * name will not cause the file to be loaded again.
	 * 
	 * If the font cannot be loaded, a default font will be substituted. You may
	 * check which font you got using {@link Font#getName()} or
	 * {@link Font#getFamily()}.
	 * 
	 * @param name
	 *            Font name, or relative path to the file
	 * @param size
	 *            Desired point size of the font
	 * @return A JavaFX font
	 */
	public static final Font findFont(String name, double size) {
		return findFont(name, size, TextFont.class);
	}

	/**
	 * Load a font.
	 * 
	 * Will first try to use the provided string as a file name, and load the font
	 * from a font file (should be in a format supported by JavaFX). If loading from
	 * file fails, we assume we have been given the name of a font which is passed
	 * directly to JavaFX for loading. A fallback font is used if this fails.
	 * 
	 * When looking for files, this method will search for the file in a search path
	 * starting with the folder containing the provided Java class (using Java's
	 * standard {@link Class#getResourceAsStream(String)} system). The search path
	 * also includes ".." (parent directory) and "../fonts".
	 * 
	 * The loaded font will be cached, so that additional calls with the same file
	 * name will not cause the file to be loaded again.
	 * 
	 * If the font cannot be loaded, a default font will be substituted. You may
	 * check which font you got using {@link Font#getName()} or
	 * {@link Font#getFamily()}.
	 * 
	 * @param name
	 *            Name of a font, or relative path to the font file
	 * @param size
	 *            Desired point size of the font
	 * @param clazz
	 *            A class for finding files relative to
	 * @return A JavaFX font
	 */
	public static final Font findFont(String name, double size, Class<?> clazz) {
		if (name == null || size < 0)
			throw new IllegalArgumentException();

		if (loadedFonts.containsKey(name))
			return Font.font(loadedFonts.get(name), size);

		for (String path : searchPath) {
			try (InputStream stream = clazz.getResourceAsStream(path + name)) {
				Font font = Font.loadFont(stream, size);
				if (font != null) {
					loadedFonts.put(name, font.getName());
					// System.err.println("Found: " + font.getName());
					return font;
				}
			} catch (FileNotFoundException e) {
				// we'll just try the next alternative in the search path
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		Font font = Font.font(name, size);
		if (font == null)
			font = Font.font(size);
		if (font == null)
			throw new RuntimeException("Even the default font seems to be unavailable – this shouldn't happen! :(");
		if (font.getName().equals(Font.getDefault().getName())) {
			System.err.println("TextFont: Default font '" + font.getName() + "' substituted for '" + name + "'");
		}
		// System.err.println("Found: " + name + "=" + font.getName());

		loadedFonts.put(name, name);
		return font;
	}

	private Canvas tmpCanvas;
	private final WritableImage img;

	/**
	 * The JavaFX font
	 */
	private Font font;
	/** Font size */
	private final double size;

	/**
	 * Horizontal positioning of letters.
	 * 
	 * Each letter should be approximately centered within its available
	 * square-shaped space.
	 */
	private final double xTranslate;

	/**
	 * Vertical positioning of letters.
	 * 
	 * Each letter should be positioned on the baseline so that ascenders and
	 * descenders fall within its available square-shaped space.
	 */
	private final double yTranslate;

	/**
	 * Horizontal scaling factor (1.0 means no scaling)
	 * 
	 * Most fonts are relatively tall and narrow, and need horizontal scaling to fit
	 * a square shape.
	 */
	private final double xScale;

	/**
	 * Vertical scaling factor (1.0 means no scaling)
	 */
	private final double yScale;

	/**
	 * Width and height of the square-shaped space each letter should fit within
	 */
	private double squareSize;

	private String fileName;

	SnapshotParameters snapshotParameters = new SnapshotParameters();

	{
		snapshotParameters.setFill(Color.TRANSPARENT);
	}

	/**
	 * Create a new TextFont.
	 * 
	 * <p>
	 * TextFonts are used for drawing text and character graphics. Each character is
	 * assumed to be uniform in size, fitting within a square with sides of length
	 * {@link #getSquareSize()}. The given font file should contain a monospaced
	 * font of a type suitable for JavaFX, such as OpenType or TrueType.
	 * 
	 * <p>
	 * Additional horizontal and vertical positioning and scaling can be used to
	 * make the font fit with the square-shaped concept.
	 * 
	 * <p>
	 * See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics
	 * context for writing with the font, or
	 * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly
	 * horizontal scaling, e.g. to make half-width letters ("hires" mode).
	 * 
	 * 
	 * @param font
	 *            Name of the font file. Will search for the file in the same folder
	 *            as the TextFont class, as well as ".." and "../fonts".
	 * @param squareSize
	 *            The width and height of a square defining the bounds of letters
	 * @param xTranslate
	 *            Horizontal positioning of letters
	 * @param yTranslate
	 *            Vertical positioning of letters
	 * @param xScale
	 *            Horizontal scaling factor
	 * @param yScale
	 *            Vertical scaling factor
	 */
	public TextFont(Font font, double squareSize, double xTranslate, double yTranslate, double xScale, double yScale) {
		super();
		this.fileName = font.getName();
		this.font = font;
		this.size = font.getSize();
		this.squareSize = squareSize;
		this.xTranslate = xTranslate;
		this.yTranslate = yTranslate;
		this.xScale = xScale;
		this.yScale = yScale;
		this.img = new WritableImage((int) squareSize, (int) squareSize);
	}

	/**
	 * Create a new TextFont.
	 * 
	 * <p>
	 * TextFonts are used for drawing text and character graphics. Each character is
	 * assumed to be uniform in size, fitting within a square with sides of length
	 * {@link #getSquareSize()}. The given font file should contain a monospaced
	 * font of a type suitable for JavaFX, such as OpenType or TrueType.
	 * 
	 * <p>
	 * Additional horizontal and vertical positioning and scaling can be used to
	 * make the font fit with the square-shaped concept.
	 * 
	 * <p>
	 * See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics
	 * context for writing with the font, or
	 * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly
	 * horizontal scaling, e.g. to make half-width letters ("hires" mode).
	 * 
	 *
	 * @param size
	 *            Point size of the font.
	 * @param squareSize
	 *            The width and height of a square defining the bounds of letters
	 * @param xTranslate
	 *            Horizontal positioning of letters
	 * @param yTranslate
	 *            Vertical positioning of letters
	 * @param xScale
	 *            Horizontal scaling factor
	 * @param yScale
	 *            Vertical scaling factor
	 */
	public TextFont(String fileName, double size, double squareSize, double xTranslate, double yTranslate,
			double xScale, double yScale) {
		super();
		this.fileName = fileName;
		this.font = findFont(fileName, size);
		this.size = size;
		this.squareSize = squareSize;
		this.xTranslate = xTranslate;
		this.yTranslate = yTranslate;
		this.xScale = xScale;
		this.yScale = yScale;
		this.img = new WritableImage((int) squareSize, (int) squareSize);
	}

	/**
	 * Create a new TextFont.
	 * 
	 * <p>
	 * TextFonts are used for drawing text and character graphics. Each character is
	 * assumed to be uniform in size, fitting within a square with sides of length
	 * {@link #getSquareSize()}. The given font file should contain a monospaced
	 * font of a type suitable for JavaFX, such as OpenType or TrueType.
	 * 
	 * <p>
	 * Additional horizontal and vertical positioning and scaling can be used to
	 * make the font fit with the square-shaped concept.
	 * 
	 * <p>
	 * See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics
	 * context for writing with the font, or
	 * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly
	 * horizontal scaling, e.g. to make half-width letters ("hires" mode).
	 * 
	 *
	 * @param size
	 *            Point size of the font.
	 * @param squareSize
	 *            The width and height of a square defining the bounds of letters
	 * @param xTranslate
	 *            Horizontal positioning of letters
	 * @param yTranslate
	 *            Vertical positioning of letters
	 * @param xScale
	 *            Horizontal scaling factor
	 * @param yScale
	 *            Vertical scaling factor
	 * @param deferLoading
	 *            True if the font file shouldn't be loaded before the font is
	 *            actually used
	 */
	public TextFont(String fileName, double size, double squareSize, double xTranslate, double yTranslate,
			double xScale, double yScale, boolean deferLoading) {
		super();
		this.fileName = fileName;
		this.font = deferLoading ? null : findFont(fileName, size);
		this.size = size;
		this.squareSize = squareSize;
		this.xTranslate = xTranslate;
		this.yTranslate = yTranslate;
		this.xScale = xScale;
		this.yScale = yScale;
		this.img = new WritableImage((int) squareSize, (int) squareSize);
	}

	/**
	 * Create a copy of this font, with the given adjustments to the translation and
	 * scaling.
	 * 
	 * @param deltaXTranslate
	 * @param deltaYTranslate
	 * @param deltaXScale
	 * @param deltaYScale
	 * @return
	 */
	public TextFont adjust(double size, double deltaXTranslate, double deltaYTranslate, double deltaXScale,
			double deltaYScale) {
		if (size == 0.0) {
			return new TextFont(fileName, this.size, squareSize, xTranslate + deltaXTranslate,
					yTranslate + deltaYTranslate, xScale + deltaXScale, yScale + deltaYScale);
		} else {
			return new TextFont(fileName, this.size + size, squareSize, xTranslate + deltaXTranslate,
					yTranslate + deltaYTranslate, xScale + deltaXScale, yScale + deltaYScale);
		}

	}

	/**
	 * Draw the given text at position (0,0).
	 * 
	 * The <code>ctx</code> should normally be translated to the appropriate text
	 * position before calling this method.
	 * 
	 * Text will be clipped so each character fits its expected square-shaped area,
	 * and the area will be cleared to transparency before drwaing.
	 * 
	 * The graphics context's current path will be overwritten.
	 * 
	 * @param ctx
	 *            a grapics context
	 * @param text
	 *            string to be printed
	 */
	public void drawText(GraphicsContext ctx, String text) {
		textAt(ctx, 0.0, 0.0, text, 1.0, true, true, ctx.getFill(), null, 0, null);
	}

	/**
	 * Draw the given text at position (0,0), with horizontal scaling.
	 * 
	 * The <code>ctx</code> should normally be translated to the appropriate text
	 * position before calling this method.
	 * 
	 * Text will be clipped so each character fits its expected square-shaped area,
	 * and the area will be cleared to transparency before drwaing.
	 * 
	 * The graphics context's current path will be overwritten.
	 * 
	 * @param ctx
	 *            a grapics context
	 * @param text
	 *            string to be printed
	 * @param xScaleFactor
	 *            a horizontal scaling factor
	 */
	public void drawText(GraphicsContext ctx, String text, double xScaleFactor) {
		textAt(ctx, 0.0, 0.0, text, xScaleFactor, true, false, ctx.getFill(), null, 0, null);
	}

	/**
	 * Draw the given text at position (x,y).
	 * 
	 * Text will be clipped so each character fits its expected square-shaped area,
	 * and the area will be cleared to transparency before drwaing.
	 * 
	 * The graphics context's current path will be overwritten.
	 * 
	 * @param ctx
	 *            a grapics context
	 * @param x
	 *            X-position of the lower left corner of the text
	 * @param y
	 *            Y-position of the lower left corner of the text
	 * @param text
	 *            string to be printed
	 */
	public void drawTextAt(GraphicsContext ctx, double x, double y, String text) {
		textAt(ctx, x, y, text, 1.0, true, false, ctx.getFill(), null, 0, null);
	}

	/**
	 * Draw the given text at position (x, y), with horizontal scaling.
	 * 
	 * The area will be cleared to transparency before drawing.
	 * 
	 * The graphics context's current path will be overwritten.
	 * 
	 * @param ctx
	 *            a graphics context
	 * @param x
	 *            X-position of the lower left corner of the text
	 * @param y
	 *            Y-position of the lower left corner of the text
	 * @param text
	 *            string to be printed
	 * @param xScaleFactor
	 *            a horizontal scaling factor
	 */
	public void drawTextAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, int mode,
			Paint bg) {
		textAt(ctx, x, y, text, xScaleFactor, true, false, ctx.getFill(), null, mode, bg);
	}

	/**
	 * Draw the given text at position (x, y), with horizontal scaling.
	 * 
	 * The area will not be cleared to transparency before drawing.
	 * 
	 * The graphics context's current path will be overwritten.
	 * 
	 * @param ctx
	 *            a graphics context
	 * @param x
	 *            X-position of the lower left corner of the text
	 * @param y
	 *            Y-position of the lower left corner of the text
	 * @param text
	 *            string to be printed
	 * @param xScaleFactor
	 *            a horizontal scaling factor
	 */
	public void drawTextNoClearAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, int mode,
			Paint bg) {
		textAt(ctx, x, y, text, xScaleFactor, false, false, ctx.getFill(), null, mode, bg);
	}

	private void fakeBlockElement(GraphicsContext ctx, String text, double xScaleFactor) {
		text.codePoints().forEach((int c) -> {
			// System.out.printf("%s %x%n", text, c);
			if (c >= 0x2596 && c <= 0x259f) { // quadrants
				int bits = BlocksAndBoxes.unicodeBlocksString.indexOf(c);
				if ((bits & 1) > 0) { // lower right
					ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize / 2, (squareSize / 2) * xScaleFactor,
							squareSize / 2);
				}
				if ((bits & 2) > 0) { // lower left
					ctx.fillRect(0, -squareSize / 2, (squareSize / 2) * xScaleFactor, squareSize / 2);
				}
				if ((bits & 4) > 0) { // upper right
					ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize, (squareSize / 2) * xScaleFactor,
							squareSize / 2);
				}
				if ((bits & 8) > 0) { // upper left
					ctx.fillRect(0, -squareSize, (squareSize / 2) * xScaleFactor, squareSize / 2);
				}
			} else if (c == 0x2580) { // upper half
				ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize / 2);
			} else if (c > 0x2580 && c <= 0x2588) { // x/8 block
				int height = c - 0x2580;
				ctx.fillRect(0, -(height * squareSize) / 8, (squareSize) * xScaleFactor, (height * squareSize) / 8);
			} else if (c >= 0x2589 && c <= 0x258f) { // x/8 block
				int width = 8 - (c - 0x2588);
				ctx.fillRect(0, -squareSize, ((width * squareSize) / 8) * xScaleFactor, squareSize);
			} else if (c == 0x2590) { // right half
				ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize, (squareSize / 2) * xScaleFactor, squareSize);
			} else if (c == 0x2591) { // light shade
				ctx.save();
				ctx.setGlobalAlpha(0.25);
				ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize);
				ctx.restore();
			} else if (c == 0x2592) { // medium shade
				ctx.save();
				ctx.setGlobalAlpha(0.5);
				ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize);
				ctx.restore();
			} else if (c == 0x2593) { // dark shade
				ctx.save();
				ctx.setGlobalAlpha(0.75);
				ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize);
				ctx.restore();
			} else if (c == 0x2594) { // upper eighth
				ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize / 8);
			} else if (c == 0x2595) { // right eighth
				ctx.fillRect(((7 * squareSize) / 8) * xScaleFactor, -squareSize, (squareSize / 8) * xScaleFactor,
						squareSize);
			} else if (c == 0x2571) {
				ctx.save();
				ctx.setLineWidth(2.0);
				ctx.strokeLine(0, 0, squareSize * xScaleFactor, -squareSize);
				ctx.restore();
			} else if (c == 0x2572) {
				ctx.save();
				ctx.setLineWidth(2.0);
				ctx.strokeLine(0, -squareSize, squareSize * xScaleFactor, 0);
				ctx.restore();
			} else if (c == 0x2573) {
				ctx.save();
				ctx.setLineWidth(2.0);
				ctx.strokeLine(0, 0, squareSize * xScaleFactor, -squareSize);
				ctx.strokeLine(0, -squareSize, squareSize * xScaleFactor, 0);
				ctx.restore();
			} else if (c >= 0x2500 && c <= 0x257f) {
				ctx.save();
				ctx.setLineCap(StrokeLineCap.BUTT);
				String spec = boxDrawingShapes[c - 0x2500];
				int i = 0;
				double extraThickness = 0.0;
				if (Character.isDigit(spec.charAt(i))) {
					extraThickness = setContextFromChar(ctx, spec.charAt(i++));
				}
				char s;
				double hThickness = Math.max(setContextFromChar(ctx, spec.charAt(i)),
						setContextFromChar(ctx, spec.charAt(i + 1))) + extraThickness;
				double vThickness = Math.max(setContextFromChar(ctx, spec.charAt(i + 2)),
						setContextFromChar(ctx, spec.charAt(i + 3))) + extraThickness;
				if (c >= 0x2550 || spec.charAt(i) != spec.charAt(i + 1)) {
					s = spec.charAt(i++);
					setContextFromChar(ctx, s);
					strokeHalfLine(ctx, xScaleFactor, -1, 0, s, vThickness);
					s = spec.charAt(i++);
					setContextFromChar(ctx, s);
					strokeHalfLine(ctx, xScaleFactor, 1, 0, s, vThickness);
				} else {
					s = spec.charAt(i++);
					s = spec.charAt(i++);
					setContextFromChar(ctx, s);
					strokeFullLine(ctx, xScaleFactor, 1, 0, s);
				}
				if (c >= 0x2550 || spec.charAt(i) != spec.charAt(i + 1)) {
					s = spec.charAt(i++);
					setContextFromChar(ctx, s);
					strokeHalfLine(ctx, xScaleFactor, 0, -1, s, hThickness);
					s = spec.charAt(i++);
					setContextFromChar(ctx, s);
					strokeHalfLine(ctx, xScaleFactor, 0, 1, s, hThickness);
				} else {
					s = spec.charAt(i++);
					s = spec.charAt(i++);
					setContextFromChar(ctx, s);
					strokeFullLine(ctx, xScaleFactor, 0, 1, s);

				}
				ctx.restore();
			}
		});

	}

	/**
	 * @return the font
	 */
	public Font getFont() {
		if (font == null)
			font = findFont(fileName, size);
		return font;
	}

	/**
	 * @return the size
	 */
	public double getSize() {
		return size;
	}

	/**
	 * Width and height of the square-shaped space each letter should fit within
	 * 
	 * @return the squareSize
	 */
	public double getSquareSize() {
		return squareSize;
	}

	private Canvas getTmpCanvas() {
		if (tmpCanvas == null) {
			tmpCanvas = new Canvas(squareSize, squareSize);
		} else {
			tmpCanvas.getGraphicsContext2D().clearRect(0, 0, squareSize, squareSize);
		}
		return tmpCanvas;
	}

	/**
	 * Horizontal scaling factor (1.0 means no scaling)
	 * 
	 * Most fonts are relatively tall and narrow, and need horizontal scaling to fit
	 * a square shape.
	 * 
	 * @return the xScale
	 */
	public double getxScale() {
		return xScale;
	}

	/**
	 * Horizontal positioning of letters.
	 * 
	 * Each letter should be approximately centered within its available
	 * square-shaped space.
	 * 
	 * @return the xTranslate
	 */
	public double getxTranslate() {
		return xTranslate;
	}

	/**
	 * Vertical scaling factor (1.0 means no scaling)
	 * 
	 * @return the yScale
	 */
	public double getyScale() {
		return yScale;
	}

	/**
	 * /** Vertical positioning of letters.
	 * 
	 * Each letter should be positioned on the baseline so that ascenders and
	 * descenders fall within its available square-shaped space.
	 * 
	 * @return the yTranslate
	 */
	public double getyTranslate() {
		return yTranslate;
	}

	private double setContextFromChar(GraphicsContext ctx, char c) {
		switch (c) {
		case '0':
			return -2 * thin;
		case '2':
			ctx.setLineDashes(14.75, 2.5);
			break;
		case '3':
			ctx.setLineDashes(9, 2.5);
			break;
		case '4':
			ctx.setLineDashes(6.125, 2.5);
			break;
		case '.':
			return 0.0;
		case '-':
		case 's':
			ctx.setLineWidth(thin);
			return thin;
		case '*':
			ctx.setLineWidth(thick);
			return thick;
		case '=':
		case 'N':
		case 'M':
		case 'z':
			ctx.setLineWidth(thin);
			return thin;
		}
		return 0.0;
	}

	/**
	 * Set up a graphics context for drawing with this font.
	 * 
	 * Caller should call {@link GraphicsContext#save()} first, and then
	 * {@link GraphicsContext#restore()} afterwards, to clean up adjustments to the
	 * transformation matrix (i.e., translation, scaling).
	 * 
	 * The GraphicsContext should be translated to the coordinates where the text
	 * should appear <em>before</em> calling this method, since this method will
	 * modify the coordinate system.
	 * 
	 * @param ctx
	 *            A GraphicsContext
	 */
	public void setGraphicsContext(GraphicsContext ctx) {
		ctx.setFont(getFont());
		ctx.translate(xTranslate, yTranslate);
		ctx.scale(xScale, yScale);
	}

	/**
	 * Set up a graphics context for drawing with this font.
	 * 
	 * Caller should call {@link GraphicsContext#save()} first, and then
	 * {@link GraphicsContext#restore()} afterwards, to clean up adjustments to the
	 * transformation matrix (i.e., translation, scaling).
	 * 
	 * The GraphicsContext should be translated to the coordinates where the text
	 * should appear <em>before</em> calling this method, since this method will
	 * modify the coordinate system.
	 * 
	 * @param ctx
	 *            A GraphicsContext
	 * @param xScaleFactor
	 *            Additional horizontal scaling, normally 0.5 (for half-width
	 *            characters)
	 */
	public void setGraphicsContext(GraphicsContext ctx, double xScaleFactor) {
		ctx.setFont(getFont());
		ctx.translate(xTranslate * xScaleFactor, yTranslate);
		ctx.scale(xScale * xScaleFactor, yScale);
	}

	private void strokeFullLine(GraphicsContext ctx, double xScaleFactor, double xDir, double yDir, char c) {
		double x = squareSize * xScaleFactor / 2, y = -squareSize / 2;
		if (c != '.') {
			// System.out.printf("(%4.1f %4.1f %4.1f %4.1f) w=%1.4f%n", x * yDir, y * xDir,
			// x * yDir + xDir * squareSize,
			// y * xDir + yDir * squareSize, ctx.getLineWidth());
			if (c == '=') {
				ctx.setLineWidth(2.0);
				ctx.strokeLine((x - 2) * yDir, (y - 2) * xDir, (x - 2) * yDir + xDir * squareSize * xScaleFactor,
						(y - 2) * xDir + yDir * -squareSize);
				ctx.strokeLine((x + 2) * yDir, (y + 2) * xDir, (x + 2) * yDir + xDir * squareSize * xScaleFactor,
						(y + 2) * xDir + yDir * -squareSize);
			} else {
				ctx.strokeLine(x * yDir, y * xDir, x * yDir + xDir * squareSize * xScaleFactor,
						y * xDir + yDir * -squareSize);
			}
		}
	}

	private void strokeHalfLine(GraphicsContext ctx, double xScaleFactor, double xDir, double yDir, char c,
			double otherWidth) {
		if (c != '.') {
			double factor = 1.0, dblFactor = 0.0;
			if (c == 's' || c == 'z')
				factor = -1;
			if (c == 'N')
				dblFactor = -2;
			if (c == 'M')
				dblFactor = 2;
			double x = squareSize * xScaleFactor / 2, y = -squareSize / 2;
			x -= xDir * factor * (otherWidth / 2);
			y -= yDir * factor * (otherWidth / 2);

			if (c == '=' || c == 'z' || c == 'N' || c == 'M') {
				ctx.setLineWidth(2.0);
				double x0 = x - 2 * yDir;
				double y0 = y - 2 * xDir;
				x0 += dblFactor * xDir * (otherWidth / 2);
				y0 += dblFactor * yDir * (otherWidth / 2);
				ctx.strokeLine(x0, y0, x0 + xDir * squareSize * xScaleFactor, y0 + yDir * squareSize);
				double x1 = x + 2 * yDir;
				double y1 = y + 2 * xDir;
				x1 -= dblFactor * xDir * (otherWidth / 2);
				y1 -= dblFactor * yDir * (otherWidth / 2);
				ctx.strokeLine(x1, y1, x1 + xDir * squareSize * xScaleFactor, y1 + yDir * squareSize);
			} else {
				ctx.strokeLine(x, y, x + xDir * squareSize * xScaleFactor, y + yDir * squareSize);
			}
		}
	}

	/**
	 * Draw text at the given position.
	 * 
	 * For most cases, the simpler {@link #drawText(GraphicsContext, String)} or
	 * {@link #drawText(GraphicsContext, String, double)} will be easier to use.
	 * 
	 * If <code>clip</code> is true, the graphics context's current path will be
	 * overwritten.
	 * 
	 * @param ctx
	 *            A GraphicsContext
	 * @param x
	 *            X-position of the lower left corner of the text
	 * @param y
	 *            Y-position of the lower left corner of the text
	 * @param text
	 *            The text to be printed
	 * @param xScaleFactor
	 *            Horizontal scaling factor, normally 1.0 (full width) or 0.5 (half
	 *            width)
	 * @param clear
	 *            True if the area should be cleared (to transparency) before
	 *            drawing; normally true.
	 * @param clip
	 *            True if the text drawing should be clipped to fit the expected
	 *            printing area; normally true.
	 * @param fill
	 *            True if the letter shapes should be filled; normally true.
	 * @param stroke
	 *            True if the letter shapes should be stroked (outlined); normally
	 *            false.
	 */
	public void textAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, boolean clear,
			boolean clip, Paint fill, Paint stroke, int mode, Paint bg) {
		if ((mode & ATTR_BRIGHT) != 0) {
			fill = fill instanceof Color ? ((Color) fill).deriveColor(0.0, 1.0, 3.0, 1.0) : fill;
			stroke = stroke instanceof Color ? ((Color) stroke).deriveColor(0.0, 1.0, 3.0, 1.0) : stroke;
		}
		if ((mode & ATTR_FAINT) != 0) {
			fill = fill instanceof Color ? ((Color) fill).deriveColor(0.0, 1.0, 1.0, .5) : fill;
			stroke = stroke instanceof Color ? ((Color) stroke).deriveColor(0.0, 1.0, 1.0, .5) : stroke;
		}
		GraphicsContext target = ctx;
		int width = text.codePointCount(0, text.length());
		ctx.save(); // save 1
		ctx.setFill(fill);
		ctx.setStroke(stroke);
		ctx.translate(x, y);
		if (clear && (mode & ATTR_INVERSE) == 0 && (mode & ATTR_OVERSTRIKE) == 0) {
			ctx.clearRect(0 + 0.5, -squareSize + 0.5, squareSize * width * xScaleFactor - 1, squareSize - 1);
		}
		if (bg != null && bg != Color.TRANSPARENT) {
			ctx.save(); // save 2
			ctx.setFill(bg);
			ctx.fillRect(0, -squareSize, squareSize * width * xScaleFactor, squareSize);
			ctx.restore(); // restore 2
		}
		boolean drawIndirect = clip || (mode & (ATTR_INVERSE | ATTR_CLIP)) != 0;
		Canvas tmpCanvas = getTmpCanvas();
		if (drawIndirect) {
			target = tmpCanvas.getGraphicsContext2D();
			target.save(); // save 2 if drawIndirect
			target.translate(0, squareSize);
			target.setFill((mode & ATTR_INVERSE) != 0 ? Color.BLACK : fill);
			target.setStroke((mode & ATTR_INVERSE) != 0 ? Color.BLACK : stroke);
		}
		if (text.length() > 0 && text.charAt(0) >= 0x2500 && text.charAt(0) <= 0x259f
				&& (mode & ATTR_NO_FAKE_CHARS) == 0) {
			target.save(); // save 3
			target.setStroke(target.getFill());
			fakeBlockElement(target, text, xScaleFactor);
			target.restore(); // restore 3
		} else {
			target.save(); // save 3
			if ((mode & ATTR_ITALIC) != 0) {
				target.translate(-0.2, 0);
				target.transform(new Affine(Transform.shear(-0.2, 0)));
			}
			setGraphicsContext(target, xScaleFactor);
			if (fill != null)
				target.fillText(text, 0.0, 0.0);
			if ((mode & ATTR_BOLD) != 0) {
				// System.err.println("stroke2");
				target.save(); // save 4
				target.setLineWidth(thin);
				target.setStroke(target.getFill());
				target.strokeText(text, 0.0, 0.0);
				target.restore(); // restore 4
			}
			if (stroke != null || (mode & ATTR_OUTLINE) != 0) {
				// System.err.println("stroke2");
				target.setLineWidth(((mode & ATTR_BOLD) != 0) ? thin : thin / 2);
				target.strokeText(text, 0.0, 0.0);
			}
			target.restore(); // restore 3
		}

		if ((mode & ATTR_UNDERLINE) != 0) {
			target.fillRect(0, yTranslate - 2, width * squareSize * xScaleFactor, thin);
		}
		if ((mode & ATTR_OVERLINE) != 0) {
			target.fillRect(0, -squareSize + 2, width * squareSize * xScaleFactor, thin);
		}
		if ((mode & ATTR_LINE_THROUGH) != 0) {
			target.fillRect(0, -squareSize / 2 + 2, width * squareSize * xScaleFactor, thin);
		}

		if (drawIndirect) {
			target.restore(); // restore 2 if drawIndirect
			tmpCanvas.snapshot(snapshotParameters, img);
			if ((mode & ATTR_INVERSE) != 0) {
				fillInverse(ctx, img, 0, -squareSize);
			} else
				ctx.drawImage(img, 0, -squareSize);
		}

		ctx.restore(); // restore 1
	}
}
