import { Bounds } from "../../physics/bounds.ts";
import Text from "./text.js";
import setContextStyle from "./textstyle.js";

/**
 * a Text Metrics object that contains helper for text manipulation
 */
class TextMetrics extends Bounds {
	/**
	 * @param {Text|BitmapText} ancestor - the parent object that contains this TextMetrics object
	 */
	constructor(ancestor) {
		// parent constructor
		super();

		/**
		 * a reference to the parent object that contains this TextMetrics object
		 * @type {Renderable}
		 * @default undefined
		 */
		this.ancestor = ancestor;

		this.setMinMax(0, 0, 0, 0);
	}

	/**
	 * Returns the height of a segment of inline text in CSS pixels.
	 * @returns {number} the height of a segment of inline text in CSS pixels.
	 */
	lineHeight() {
		if (this.ancestor instanceof Text) {
			return this.ancestor.fontSize * this.ancestor.lineHeight;
		} else {
			// it's a BitmapText
			return (
				this.ancestor.fontData.capHeight *
				this.ancestor.lineHeight *
				this.ancestor.fontScale.y
			);
		}
	}

	/**
	 * Returns the width of the given segment of inline text in CSS pixels.
	 * @param {string} text - the text to be measured
	 * @param {CanvasRenderingContext2D} [context] - reference to an active 2d context for canvas rendering
	 * @returns {number} the width of the given segment of inline text in CSS pixels.
	 */
	lineWidth(text, context) {
		if (this.ancestor instanceof Text) {
			return context.measureText(text).width;
		} else {
			// it's a BitmapText
			const characters = text.split("");
			const charactersLength = characters.length;
			let width = 0;
			let lastGlyph = null;
			for (let i = 0; i < charactersLength; i++) {
				const ch = characters[i].charCodeAt(0);
				const glyph = this.ancestor.fontData.glyphs[ch];
				if (typeof glyph !== "undefined") {
					const kerning =
						lastGlyph && lastGlyph.kerning ? lastGlyph.getKerning(ch) : 0;
					// for the last glyph, use the visual extent if it exceeds xadvance
					const advance =
						i < charactersLength - 1
							? glyph.xadvance
							: Math.max(glyph.xadvance, glyph.xoffset + glyph.width);
					width += (advance + kerning) * this.ancestor.fontScale.x;
					lastGlyph = glyph;
				}
			}
			return width;
		}
	}

	/**
	 * measure the given text size in CSS pixels
	 * @param {string} text - the text to be measured
	 * @param {CanvasRenderingContext2D} [context] - reference to an active 2d context for canvas rendering
	 * @returns {TextMetrics} this
	 */
	measureText(text, context) {
		let strings;

		if (!Array.isArray(text)) {
			strings = ("" + text).split("\n");
		} else {
			strings = text;
		}

		if (typeof context !== "undefined") {
			// save the previous context
			context.save();

			// apply the style font
			setContextStyle(context, this.ancestor);
		}

		// compute the bounding box size
		this.width = this.height = 0;

		const isBitmapText = !(this.ancestor instanceof Text);

		for (let i = 0; i < strings.length; i++) {
			this.width = Math.max(
				this.lineWidth(strings[i].trimEnd(), context),
				this.width,
			);
			this.height += this.lineHeight();
		}

		// glyph vertical extents (BitmapText only) — use font-wide
		// precomputed values from BitmapTextData instead of per-line iteration
		this.glyphYOffset = 0;
		this.glyphMaxBottom = 0;

		if (isBitmapText && strings.length > 0) {
			const fontData = this.ancestor.fontData;
			const scaleY = this.ancestor.fontScale.y;
			const visualLineH =
				(fontData.glyphMaxBottom - fontData.glyphMinTop) * scaleY;
			// replace the last capHeight-based line with the actual visual height
			this.height += visualLineH - this.lineHeight();
			this.glyphYOffset = fontData.glyphMinTop * scaleY;
			this.glyphMaxBottom = fontData.glyphMaxBottom * scaleY;
		}

		this.width = Math.ceil(this.width);
		this.height = Math.ceil(this.height);

		// compute the bounding box position
		this.x = Math.floor(
			this.ancestor.textAlign === "right"
				? this.ancestor.pos.x - this.width
				: this.ancestor.textAlign === "center"
					? this.ancestor.pos.x - this.width / 2
					: this.ancestor.pos.x,
		);
		this.y = Math.floor(
			this.ancestor.textBaseline.search(/^(top|hanging)$/) === 0
				? this.ancestor.pos.y
				: this.ancestor.textBaseline === "middle"
					? this.ancestor.pos.y - this.lineHeight() / 2
					: this.ancestor.pos.y - this.lineHeight(),
		);

		if (typeof context !== "undefined") {
			// restore the context
			context.restore();
		}

		return this;
	}

	/**
	 * wrap the given text based on the given width
	 * @param {string|string[]} text - the text to be wrapped
	 * @param {number} width - maximum width of one segment of text in css pixel
	 * @param {CanvasRenderingContext2D} [context] - reference to an active 2d context for canvas rendering
	 * @returns {string[]} an array of string representing wrapped text
	 */
	wordWrap(text, width, context) {
		let currentLine = "";
		const output = [];

		if (Array.isArray(text)) {
			// join into a single string
			text = text.join(" ");
		}
		// word splitting to be improved as it replaces \n by space if present
		const words = text.replace(/[\r\n]+/g, " ").split(" ");

		if (typeof context !== "undefined") {
			// save the previous context
			context.save();

			// apply the style font
			setContextStyle(context, this.ancestor);
		}

		for (let i = 0; i < words.length; i++) {
			const word = words[i];
			const lineWidth = this.lineWidth(currentLine + word + " ", context);
			if (lineWidth < width) {
				// add the word to the current line
				currentLine += word + " ";
			} else {
				output.push(currentLine + "\n");
				currentLine = word + " ";
			}
		}
		// last line
		output.push(currentLine);

		if (typeof context !== "undefined") {
			// restore the context
			context.restore();
		}

		return output;
	}
}
export default TextMetrics;
