import { getBinary, getImage } from "../../loader/loader.js";
import { Color } from "../../math/color.ts";
import { vector2dPool } from "../../math/vector2d.ts";
import Renderable from "../renderable.js";
import { bitmapTextDataPool } from "./bitmaptextdata.ts";
import TextMetrics from "./textmetrics.js";

/**
 * a bitmap font object
 * @category Text
 */
export default class BitmapText extends Renderable {
	/**
	 * @param {number} x - position of the text object
	 * @param {number} y - position of the text object
	 * @param {object} settings - the text configuration
	 * @param {string|Image} settings.font - a font name to identify the corresponing source image
	 * @param {string} [settings.fontData=settings.font] - the bitmap font data corresponding name, or the bitmap font data itself
	 * @param {number} [settings.size] - size a scaling ratio
	 * @param {Color|string} [settings.fillStyle] - a CSS color value used to tint the bitmapText (@see BitmapText.tint)
	 * @param {number} [settings.lineWidth=1] - line width, in pixels, when drawing stroke
	 * @param {string} [settings.textAlign="left"] - horizontal text alignment
	 * @param {string} [settings.textBaseline="top"] - the text baseline
	 * @param {number} [settings.lineHeight=1.0] - line spacing height
	 * @param {Vector2d} [settings.anchorPoint={x:0.0, y:0.0}] - anchor point to draw the text at
	 * @param {number} [settings.wordWrapWidth] - the maximum length in CSS pixel for a single segment of text
	 * @param {(string|string[])} [settings.text] - a string, or an array of strings
	 * @example
	 * // Use loader.preload or loader.load to load assets
	 * loader.preload([
	 *     { name: "arial", type: "binary", src: "data/font/arial.fnt" },
	 *     { name: "arial", type: "image", src: "data/font/arial.png" },
	 * ])
	 * // Then create an instance of your bitmap font:
	 * let myFont = new BitmapText(x, y, {font:"arial", text:"Hello"});
	 * // two possibilities for using "myFont"
	 * // either call the draw function from your Renderable draw function
	 * myFont.draw(renderer, "Hello!", 0, 0);
	 * // or just add it to the world container
	 * app.world.addChild(myFont);
	 */
	constructor(x, y, settings) {
		// call the parent constructor
		super(x, y, settings.width || 0, settings.height || 0);

		/**
		 * Set the default text alignment (or justification),<br>
		 * possible values are "left", "right", and "center".
		 * @public
		 * @type {string}
		 * @default "left"
		 */
		this.textAlign = settings.textAlign || "left";

		/**
		 * Set the text baseline (e.g. the Y-coordinate for the draw operation), <br>
		 * possible values are "top", "hanging", "middle", "alphabetic", "ideographic", "bottom"<br>
		 * @public
		 * @type {string}
		 * @default "top"
		 */
		this.textBaseline = settings.textBaseline || "top";

		/**
		 * Set the line spacing height (when displaying multi-line strings). <br>
		 * Current font height will be multiplied with this value to set the line height.
		 * @public
		 * @type {number}
		 * @default 1.0
		 */
		this.lineHeight = settings.lineHeight || 1.0;

		/**
		 * the maximum length in CSS pixel for a single segment of text.
		 * (use -1 to disable word wrapping)
		 * @public
		 * @type {number}
		 * @default -1
		 */
		this.wordWrapWidth = settings.wordWrapWidth || -1;

		/**
		 * the text to be displayed
		 * @private
		 */
		this._text = [];

		/**
		 * scaled font size
		 * @private
		 */
		this.fontScale = vector2dPool.get(1.0, 1.0);

		/**
		 * font image
		 * @private
		 */
		this.fontImage =
			typeof settings.font === "object"
				? settings.font
				: getImage(settings.font);

		if (typeof settings.fontData !== "string") {
			/**
			 * font data
			 * @private
			 */
			// use settings.font to retrieve the data from the loader
			this.fontData = bitmapTextDataPool.get(getBinary(settings.font));
		} else {
			this.fontData = bitmapTextDataPool.get(
				// if starting/includes "info face" the whole data string was passed as parameter
				settings.fontData.includes("info face")
					? settings.fontData
					: getBinary(settings.fontData),
			);
		}

		// if floating was specified through settings
		if (typeof settings.floating !== "undefined") {
			this.floating = !!settings.floating;
		}

		// apply given fillstyle
		if (typeof settings.fillStyle !== "undefined") {
			this.fillStyle = settings.fillStyle;
		}

		// update anchorPoint if provided
		if (typeof settings.anchorPoint !== "undefined") {
			this.anchorPoint.set(settings.anchorPoint.x, settings.anchorPoint.y);
		} else {
			this.anchorPoint.set(0, 0);
		}

		/**
		 * @ignore
		 */
		this._visibleCharacters = -1;

		// instance to text metrics functions
		this.metrics = new TextMetrics(this);

		// resize if necessary
		if (typeof settings.size === "number" && settings.size !== 1.0) {
			this.resize(settings.size);
		}

		// set the text
		this.setText(settings.text);
	}

	/**
	 * change the font settings
	 * @param {string} textAlign - ("left", "center", "right")
	 * @param {number} [scale]
	 * @returns {BitmapText} this object for chaining
	 */
	set(textAlign, scale) {
		this.textAlign = textAlign;
		// updated scaled Size
		if (scale) {
			this.resize(scale);
		}
		this.isDirty = true;

		return this;
	}

	/**
	 * change the text to be displayed
	 * @param {number|string|string[]} value - a string, or an array of strings
	 * @returns {BitmapText} this object for chaining
	 */
	setText(value = "") {
		if (this._text.toString() !== value.toString()) {
			if (!Array.isArray(value)) {
				this._text = ("" + value).split("\n");
			} else {
				this._text = value;
			}
			this.isDirty = true;
		}

		if (this._text.length > 0 && this.wordWrapWidth > 0) {
			this._text = this.metrics.wordWrap(this._text, this.wordWrapWidth);
		}

		// measure text dimensions (cached for updateBounds)
		this.metrics.measureText(this._text);
		this.updateBounds();

		return this;
	}

	/**
	 * the number of characters to display (use -1 to show all).
	 * Useful for typewriter effects combined with Tween.
	 * @public
	 * @type {number}
	 * @default -1
	 * @see BitmapText#visibleRatio
	 * @example
	 * // show only the first 5 characters
	 * bitmapText.visibleCharacters = 5;
	 * // typewriter effect
	 * bitmapText.visibleCharacters = 0;
	 * new Tween(bitmapText).to({ visibleRatio: 1.0 }, { duration: 2000 }).start();
	 */
	get visibleCharacters() {
		return this._visibleCharacters;
	}

	set visibleCharacters(value) {
		if (this._visibleCharacters !== value) {
			this._visibleCharacters = value;
			this.isDirty = true;
		}
	}

	/**
	 * the ratio of visible characters (0.0 to 1.0).
	 * Setting this automatically updates {@link visibleCharacters}.
	 * @public
	 * @type {number}
	 */
	get visibleRatio() {
		if (this._visibleCharacters === -1) {
			return 1.0;
		}
		const total = this._text.reduce((sum, line) => {
			return sum + line.length;
		}, 0);
		return total > 0 ? this._visibleCharacters / total : 1.0;
	}

	set visibleRatio(value) {
		const clamped = Math.max(0, Math.min(1, isFinite(value) ? value : 1));
		if (clamped >= 1.0) {
			this.visibleCharacters = -1;
		} else {
			const total = this._text.reduce((sum, line) => {
				return sum + line.length;
			}, 0);
			this.visibleCharacters = Math.floor(clamped * total);
		}
	}

	/**
	 * update the bounding box for this Bitmap Text.
	 * @param {boolean} [absolute=true] - update the bounds size and position in (world) absolute coordinates
	 * @returns {Bounds} this Bitmap Text bounding box Rectangle object
	 */
	updateBounds(absolute = true) {
		const bounds = this.getBounds();

		bounds.clear();

		if (typeof this.metrics !== "undefined") {
			const w = this.metrics.width;
			const h = this.metrics.height;

			// compute x offset based on textAlign
			let ax = 0;
			switch (this.textAlign) {
				case "right":
					ax = w;
					break;
				case "center":
					ax = w / 2;
					break;
			}

			// baseline shift based on total text height (matching draw code)
			const gy = this.metrics.glyphYOffset || 0;
			let ay = 0;
			switch (this.textBaseline) {
				case "middle":
					ay = gy + h * 0.5;
					break;
				case "ideographic":
				case "alphabetic":
				case "bottom":
					ay = gy + h;
					break;
			}

			bounds.addFrame(-ax, -ay + gy, w - ax, -ay + gy + h);
		}

		if (absolute === true) {
			const absPos = this.getAbsolutePosition();
			bounds.centerOn(
				absPos.x + bounds.x + bounds.width / 2,
				absPos.y + bounds.y + bounds.height / 2,
			);
		}

		return bounds;
	}

	/**
	 * defines the color used to tint the bitmap text
	 * @public
	 * @type {Color}
	 * @see Renderable#tint
	 */
	get fillStyle() {
		return this.tint;
	}
	set fillStyle(value) {
		if (value instanceof Color) {
			this.tint.copy(value);
		} else {
			// string (#RGB, #ARGB, #RRGGBB, #AARRGGBB)
			this.tint.parseCSS(value);
		}
	}

	/**
	 * change the font display size
	 * @param {number} scale - ratio
	 * @returns {BitmapText} this object for chaining
	 */
	resize(scale) {
		this.fontScale.set(scale, scale);

		// remeasure with new scale (cached for updateBounds)
		this.metrics.measureText(this._text);
		this.updateBounds();

		this.isDirty = true;

		return this;
	}

	/**
	 * measure the given text size in pixels
	 * @param {string} [text]
	 * @returns {TextMetrics} a TextMetrics object with two properties: `width` and `height`, defining the output dimensions
	 */
	measureText(text = this._text) {
		return this.metrics.measureText(text);
	}

	/**
	 * draw the bitmap font
	 * @param {CanvasRenderer|WebGLRenderer} renderer - Reference to the destination renderer instance
	 */
	draw(renderer) {
		let x = this.pos.x;
		let y = this.pos.y;

		const lX = x;
		const stringHeight = this.metrics.lineHeight();
		const gy = this.metrics.glyphYOffset || 0;
		const h = this.metrics.height;

		// apply baseline shift once for the entire text block
		switch (this.textBaseline) {
			case "middle":
				y -= gy + h * 0.5;
				break;

			case "ideographic":
			case "alphabetic":
			case "bottom":
				y -= gy + h;
				break;

			default:
				break;
		}

		// running character counter for visibleCharacters
		let charCount = 0;
		const maxChars = this.visibleCharacters;

		for (let i = 0; i < this._text.length; i++) {
			x = lX;
			const string = this._text[i].trimEnd();
			// adjust x pos based on alignment value
			const stringWidth = this.metrics.lineWidth(string);
			switch (this.textAlign) {
				case "right":
					x -= stringWidth;
					break;

				case "center":
					x -= stringWidth * 0.5;
					break;

				default:
					break;
			}

			// draw the string
			let lastGlyph = null;
			for (let c = 0, len = string.length; c < len; c++) {
				// stop drawing when visibleCharacters limit is reached
				if (maxChars !== -1 && charCount >= maxChars) {
					return;
				}

				// calculate the char index
				const ch = string.charCodeAt(c);
				const glyph = this.fontData.glyphs[ch];

				if (typeof glyph !== "undefined") {
					const glyphWidth = glyph.width;
					const glyphHeight = glyph.height;
					const kerning =
						lastGlyph && lastGlyph.kerning ? lastGlyph.getKerning(ch) : 0;
					const scaleX = this.fontScale.x;
					const scaleY = this.fontScale.y;

					// draw it
					if (glyphWidth !== 0 && glyphHeight !== 0) {
						// some browser throw an exception when drawing a 0 width or height image
						renderer.drawImage(
							this.fontImage,
							glyph.x,
							glyph.y,
							glyphWidth,
							glyphHeight,
							x + glyph.xoffset * scaleX,
							y + glyph.yoffset * scaleY,
							glyphWidth * scaleX,
							glyphHeight * scaleY,
						);
					}

					// increment position
					x += (glyph.xadvance + kerning) * scaleX;
					lastGlyph = glyph;
				} else {
					console.warn(
						"BitmapText: no defined Glyph in for " + String.fromCharCode(ch),
					);
				}

				charCount++;
			}
			// increment line
			y += stringHeight;
		}
	}

	/**
	 * Destroy function
	 * @ignore
	 */
	destroy() {
		vector2dPool.release(this.fontScale);
		this.fontScale = undefined;
		bitmapTextDataPool.release(this.fontData);
		this.fontData = undefined;
		this._text.length = 0;
		this.metrics = undefined;
		super.destroy();
	}
}
