import app from './../app.js';
import config from './../config.js';
import zoomView from './../libs/zoomView.js';
import Base_tools_class from './../core/base-tools.js';
import Base_selection_class from './../core/base-selection.js';
import Base_layers_class from './../core/base-layers.js';
import GUI_tools_class from './../core/gui/gui-tools.js';
import Helper_class from './../libs/helpers.js';
import Dialog_class from './../libs/popup.js';
import WebFont from 'webfontloader';
import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';

/**
 * TODO
 * - Add leading, superscript, subscript
 * - Implement text direction (right to left, top to bottom, etc.); currently partial implementation
 * - Allow search & add google fonts
 * - Undo history
 */

// Default text styling
// WARNING - changing this could break backwards compatibility!
// Defaults aren't saved in text layer in order to reduce data size and increase meta comparison performance.
export const metaDefaults = {
	size: 40,
	family: 'Arial',
	kerning: 0,
	leading: 0,
	bold: false,
	italic: false,
	underline: false,
	strikethrough: false,
	fill_color: '#008800',
	stroke_size: 0,
	stroke_color: '#000000'
};

// Global map of font name to font metrics information.
const fontMetricsMap = new Map();
const layerEditors = new WeakMap();
const fontLoadPromiseMap = new Map();
const fontLoadMap = new Map();
fontLoadMap.set('Arial', true);
fontLoadMap.set('Courier', true);
fontLoadMap.set('Impact', true);
fontLoadMap.set('Helvetica', true);
fontLoadMap.set('Monospace', true);
fontLoadMap.set('Tahoma', true);
fontLoadMap.set('Times New Roman', true);
fontLoadMap.set('Verdana', true);

function load_font_family({ family, variants }, successCallback) {
	if (fontLoadMap.get(family) == null) {
		fontLoadMap.set(family, false);
		const loadPromise = new Promise((resolve, reject) => {
			WebFont.load({
				google: {
					families: [family + (variants ? ':' + variants.join(',') : '')]
				},
				fontactive: (family) => {
					fontLoadMap.set(family, true);
					fontLoadPromiseMap.delete(family);
					resolve();
				},
				fontinactive: (family) => {
					alertify.error('Font ' + family + ' could not be loaded.');
					fontLoadPromiseMap.delete(family);
					reject();
				}
			});
		});
		fontLoadPromiseMap.set(family, loadPromise);
	}
	if (successCallback) {
		const loadPromise = fontLoadPromiseMap.get(family);
		if (loadPromise) {
			loadPromise.then(successCallback);
		} else if (fontLoadMap.get(family) == true) {
			requestAnimationFrame(() => {
				successCallback();
			});
		}
	}
}

/**
 * The canvas's native font metrics implementation doesn't really give us enough information...
 */
const kerningTestCanvas = document.createElement('canvas');
kerningTestCanvas.width = 10;
kerningTestCanvas.height = 10;
kerningTestCanvas.style = 'font-kerning: normal; text-rendering: optimizeLegibility;';
const kerningTestCtx = kerningTestCanvas.getContext('2d');
class Font_metrics_class {
	constructor(family, size) {
		this.family = family || (family = "Arial");
		this.size = parseInt(size) || (size = 12);
		this.kerningMap = new Map();

		// Preparing container
		const line = document.createElement('div');
		const body = document.body;
		line.style.position = 'absolute';
		line.style.whiteSpace = 'nowrap';
		line.style.font = size + 'px ' + family;
		body.appendChild(line);

		// Now we can measure width and height of the letter
		const text = '——————————'; // 10 symbols to be more accurate with width
		line.innerHTML = text;
		this.width = line.offsetWidth / text.length;
		this.height = line.offsetHeight;

		// Now creating 1px sized item that will be aligned to baseline
		// to calculate baseline shift
		const baseline = document.createElement('span');
		baseline.style.display = 'inline-block';
		baseline.style.overflow = 'hidden';
		baseline.style.width = '1px';
		baseline.style.height = '1px';
		line.appendChild(baseline);

		// Baseline is important for positioning text on canvas
		this.baseline = baseline.offsetTop + baseline.offsetHeight;

		document.body.removeChild(line);
	}

	/**
	 * Attempts to determine the height of a letter via pixel comparison
	 * @param {string} letter - The letter to check
	 * @param {string} [baseline] - Baseline position override
	 */
	calculate_letter_bounds(letter, baseline) {
		baseline = baseline || 'alphabetic'
		kerningTestCanvas.width = this.width;
		kerningTestCanvas.height = this.height;
		kerningTestCtx.clearRect(0, 0, this.width, this.height);
		kerningTestCtx.font =
		' ' + (this.size) + 'px' +
		' ' + this.family;
		kerningTestCtx.textAlign = 'left';
		kerningTestCtx.textBaseline = baseline;
		kerningTestCtx.fillStyle = '#000000';
		kerningTestCtx.fillText(letter, 0, baseline === 'alphabetic' ? this.baseline : 0);
		const pixels = kerningTestCtx.getImageData(0, 0, this.width, this.height).data;
		const pixelLength = pixels.length;
		let start = 0;
		let end = this.height;
		for (let i = 0; i < pixelLength; i += 4) {
			if (pixels[i + 3] !== 0) {
				start = Math.floor(i / 4 / this.width);
				break;
			}
		}
		for (let i = pixelLength - 4; i >= 0; i -= 4) {
			if (pixels[i + 3] !== 0) {
				end = Math.floor(i / 4 / this.width);
				break;
			}
		}
		kerningTestCanvas.width = 10;
		kerningTestCanvas.height = 10;
		return {
			top: start,
			bottom: end,
			height: end - start
		}
	}

	/**
	 * Calculate the kerning offset between two letters.
	 * @param {string} letters - a two character string of the two letters to determine font kerning from. Returns the kerning offset that should be used to draw the 2nd letter. 
	 * @param {object} flags - font style, such as bold or italic
	 */
	get_kerning_offset(letters, flags = {}) {
		let offset = this.kerningMap.get(letters);
		if (offset == null) {
			kerningTestCtx.font =
			' ' + (flags.italic ? 'italic' : '') +
			' ' + (flags.bold ? 'bold' : '') +
			' ' + (this.size) + 'px' +
			' ' + this.family;
			offset = kerningTestCtx.measureText(letters).width - (kerningTestCtx.measureText(letters[0]).width + kerningTestCtx.measureText(letters[1]).width);
			this.kerningMap.set(letters, offset);
		}
		return offset;
	}
}

/**
 * This class's job is to store and modify the internal JSON format of a text layer.
 */
class Text_document_class {
	constructor() {
		this.lines = [];
		this.on_change = null;

		// If user edits params while no selection, queue meta insertion for next type.
		this.queuedMetaChanges = null;
	}

	/**
	 * Returns the number of lines in the document.
	 */
	get_line_count() {
		return this.lines.length;
	}

	/**
	 * Returns the length of a given line
	 * @param {number} lineNumber - The number of the line to get the length of
	 */
	get_line_character_count(lineNumber) {
		return this.get_line_text(lineNumber).length;
	}
	
	/**
	 * Returns the text string at a given line (ignores formatting).
	 * @param {number} lineNumber - The number of the line to get the text from
	 */
	get_line_text(lineNumber) {
		let lineText = '';
		for (let i = 0; i < this.lines[lineNumber].length; i++) {
			lineText += this.lines[lineNumber][i].text;
		}
		return lineText;
	}
	
	/**
	 * Returns the position of the end of the the word at the line/character provided
	 * @param {number} line - The reference line number (0 indexed) 
	 * @param {number} character - The reference character position (0 indexed)
	 * @param {boolean} noJump - Dont jump to the next word if at the end of current one
	 */
	get_word_end_position(line, character, noJump) {
		let newLine = line;
		let newCharacter = character;
		let fullText = this.get_line_text(newLine);
		if (character === fullText.length && newLine < this.lines.length - 1) {
			if (noJump) {
				return { line, character };
			}
			newLine += 1;
			character = 0;
			fullText = this.get_line_text(newLine);
		}
		const text = fullText.slice(character);
		if (noJump && text[0] === ' ') {
			return { line, character };
		}
		for (let i = 1; i < text.length; i++) {
			if (text[i] === ' ') {
				newCharacter = character + i;
				break;
			}
		}
		if (newCharacter === character) {
			newCharacter = fullText.length + 1;
		}
		return {
			line: newLine,
			character: newCharacter
		}
	}

	/**
	 * Returns the position of the start of the the word at the line/character provided
	 * @param {number} line - The reference line number (0 indexed) 
	 * @param {number} character - The reference character position (0 indexed)
	 * @param {boolean} noJump - Dont jump to the next word if at the end of current one
	 */
	get_word_start_position(line, character, noJump) {
		let newLine = line;
		let newCharacter = character;
		let isWrap = false;
		if (character === 0 && newLine > 0) {
			if (noJump) {
				return { line, character };
			}
			isWrap = true;
			newLine -= 1;
		}
		const fullText = this.get_line_text(newLine);
		if (isWrap) {
			character = fullText.length;
		}
		const text = fullText.slice(0, character);
		if (noJump && text[text.length - 1] === ' ') {
			return { line, character };
		}
		for (let i = -1; i >= -text.length; i--) {
			if (text[i + text.length - 1] === ' ') {
				newCharacter = character + i;
				break;
			}
		}
		if (newCharacter === character) {
			newCharacter = 0;
		}
		return {
			line: newLine,
			character: newCharacter
		}
	}
	
	/**
	 * Determine if the metadata (formatting) of two text spans is the same, usually used to determine if the spans can be merged together.
	 */
	is_same_span_meta(meta1, meta2) {
		const meta1Keys = Object.keys(meta1).sort();
		const meta2Keys = Object.keys(meta2).sort();
		if (meta1Keys.length !== meta2Keys.length) {
			return false;
		}
		for (let i = 0; i < meta1Keys.length; i++) {
			if (meta1Keys[i] !== meta2Keys[i]) {
				return false;
			}
			const meta1Value = meta1[meta1Keys[i]];
			const meta2Value = meta2[meta2Keys[i]];
			if (JSON.stringify(meta1Value) !== JSON.stringify(meta2Value)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Inserts a span with empty text in the document at the specified line and character position
	 * @param {number} line - The line number to insert at (0 indexed) 
	 * @param {number} character - The character position to insert at (0 indexed)
	 * @param {object} meta - Metadata to associate with span
	 */
	insert_empty_span(line, character, meta) {
		let insertedSpan = null;
		const lineDef = this.lines[line];
		let newLine = [];
		let spanStartCharacter = 0;
		let wasInserted = false;
		for (let span of lineDef) {
			if (!wasInserted && character >= spanStartCharacter && character <= spanStartCharacter + span.text.length) {
				let textBefore = span.text.slice(0, character - spanStartCharacter);
				let textAfter = span.text.slice(character - spanStartCharacter);
				if (textBefore.length > 0) {
					newLine.push({
						text: textBefore,
						meta: JSON.parse(JSON.stringify(span.meta))
					});
				}
				const newMeta = JSON.parse(JSON.stringify(span.meta));
				for (let metaKey in meta) {
					newMeta[metaKey] = meta[metaKey];
				}
				insertedSpan = {
					text: '',
					meta: newMeta
				};
				newLine.push(insertedSpan);
				if (textAfter.length > 0) {
					newLine.push({
						text: textAfter,
						meta: JSON.parse(JSON.stringify(span.meta))
					});
				}
				wasInserted = true;
			} else {
				newLine.push(span);
			}
			spanStartCharacter += span.text.length;
		}
		this.lines[line] = newLine;
		return insertedSpan;
	}
	
	/**
	 * Inserts a text string in the document at the specified line and character position
	 * @param {string} text - The text string to insert
	 * @param {number} line - The line number to insert at (0 indexed) 
	 * @param {number} character - The character position to insert at (0 indexed)
	 */
	insert_text(text, line, character) {

		let insertedSpan;
		if (this.queuedMetaChanges) {
			insertedSpan = this.insert_empty_span(line, character, this.queuedMetaChanges);
			this.queuedMetaChanges = null;
		}

		const insertLine = this.lines[line];
		const textHasNewline = text.includes('\n');
		let characterCount = 0;
		let modifyingSpan = null;
		let previousSpans = [];
		let nextSpans = [];
		let newLine = line;
		let newCharacter = character;

		// Insert text into span at specified line/character
		for (let i = 0; i < insertLine.length; i++) {
			const span = insertLine[i];
			const spanLength = span.text.length;
			if (!modifyingSpan && (character > characterCount || character === 0) && character <= characterCount + spanLength) {
				if (insertLine[i + 1] && insertLine[i + 1].text === '') {
					modifyingSpan = insertLine[i + 1];
				} else {
					modifyingSpan = span;
				}
				const textIdx = character - characterCount;
				modifyingSpan.text = modifyingSpan.text.slice(0, textIdx) + text + modifyingSpan.text.slice(textIdx);
				if (!textHasNewline) {
					newCharacter = characterCount + textIdx + text.length;
					break;
				}
			} else if (textHasNewline) {
				if (modifyingSpan) {
					nextSpans.push(span);
				} else {
					previousSpans.push(span);
				}
			}
			characterCount += spanLength;
		}

		// Create new lines if newline character was used
		if (textHasNewline && modifyingSpan) {
			const modifiedSpans = [];
			const textLines = modifyingSpan.text.split('\n');
			for (let i = 0; i < textLines.length; i++) {
				modifiedSpans.push({
					meta: JSON.parse(JSON.stringify(modifyingSpan.meta)),
					text: textLines[i]
				});
			}
			this.lines[line] = [...previousSpans, modifiedSpans.shift()];
			for (let i = 0; i < modifiedSpans.length; i++) {
				if (i === modifiedSpans.length - 1) {
					if (!modifiedSpans[i].text && nextSpans.length > 0) {
						this.lines.splice(line + i + 1, 0, nextSpans);
					} else {
						this.lines.splice(line + i + 1, 0, [modifiedSpans[i], ...nextSpans]);
					}
					newLine = line + i + 1;
					newCharacter = text.length - 1 - text.lastIndexOf('\n');
				} else {
					this.lines.splice(line + i + 1, 0, [modifiedSpans[i]]);
				}
			}
		}

		// Notify change
		if (this.on_change) {
			this.on_change(this.lines);
		}

		// Return end position
		return {
			line: newLine,
			character: newCharacter
		};
	}
	
	/**
	 * Deletes text withing the specified range
	 * @param {number} startLine - The starting line of the text range
	 * @param {number} startCharacter - The character position at the starting line of the text range
	 * @param {number} endLine - The ending line of the text range
	 * @param {number} endCharacter - The character position at the ending line of the text range
	 */
	delete_range(startLine, startCharacter, endLine, endCharacter) {
		// Check bounds
		startLine >= 0 || (startLine = 0);
		startCharacter >= 0 || (startCharacter = 0);
		endLine < this.lines.length || (endLine = this.lines.length - 1);
		const endLineCharacterCount = this.get_line_character_count(endLine);
		endCharacter <= endLineCharacterCount || (
			endCharacter = endLineCharacterCount
		);

		// Early return if there's nothing to delete
		if (startLine === endLine && startCharacter === endCharacter) {
			return {
				line: startLine,
				character: startCharacter
			};
		}

		// Get spans in start line before range
		const beforeSpans = [];
		const afterSpans = [];
		let characterCount = 0;
		let startSpan = null;
		let startSpanDeleteIndex = 0;
		for (let i = 0; i < this.lines[startLine].length; i++) {
			const span = this.lines[startLine][i];
			const spanLength = span.text.length;
			if (!startSpan && (startCharacter > characterCount || startCharacter === 0) && startCharacter <= characterCount + spanLength) {
				startSpan = span;
				startSpanDeleteIndex = Math.max(0, startCharacter - characterCount);
				break;
			}
			if (!startSpan) {
				beforeSpans.push(span);
			}
			characterCount += spanLength;
		}

		// Get spans in end line after range
		characterCount = 0;
		let endSpan = null;    
		let endSpanDeleteIndex = 0;
		for (let i = 0; i < this.lines[endLine].length; i++) {
			const span = this.lines[endLine][i];
			const spanLength = span.text.length;
			if (!endSpan && (endCharacter > characterCount || endCharacter === 0) && endCharacter <= characterCount + spanLength) {
				endSpan = span;
				endSpanDeleteIndex = Math.max(0, endCharacter - characterCount);
			}
			else if (endSpan) {
				afterSpans.push(span);
			}
			characterCount += spanLength;
		}

		// Merge start and end lines
		this.lines[startLine] = [...beforeSpans];
		if (startSpan === endSpan || this.is_same_span_meta(startSpan.meta, endSpan.meta)) {
			const combinedSpans = {
				meta: startSpan.meta,
				text: startSpan.text.slice(0, startSpanDeleteIndex) + endSpan.text.slice(endSpanDeleteIndex)
			};
			if (combinedSpans.text || (beforeSpans.length === 0 && afterSpans.length === 0)) {
				this.lines[startLine].push(combinedSpans);
			}
		} else {
			const middleSpans = [];
			let isAddedStartSpan = false;
			let isAddedEndSpan = false;
			if (startSpan) {
				startSpan.text = startSpan.text.slice(0, startSpanDeleteIndex);
				if (startSpan.text) {
					middleSpans.push(startSpan);
					isAddedStartSpan = true;
				}
			}
			if (endSpan) {
				endSpan.text = endSpan.text.slice(endSpanDeleteIndex)
				if (endSpan.text || middleSpans.length === 0) {
					middleSpans.push(endSpan);
					isAddedEndSpan = true;
				}
			}
			if (isAddedStartSpan && !isAddedEndSpan) {
				const afterSpan = afterSpans[0];
				if (afterSpan && this.is_same_span_meta(startSpan.meta, afterSpan.meta)) {
					afterSpans.shift();
					startSpan.text += afterSpan.text;
				}
			}
			else if (isAddedEndSpan && !isAddedStartSpan) {
				const beforeSpan = beforeSpans[beforeSpans.length - 1];
				if (beforeSpan && this.is_same_span_meta(beforeSpan.meta, endSpan.meta)) {
					beforeSpans.pop();
					beforeSpan.text += endSpan.text;
				}
			}
			else if (middleSpans.length === 0) {
				const beforeSpan = beforeSpans[beforeSpans.length - 1];
				const afterSpan = afterSpans[0];
				if (beforeSpan && afterSpan && this.is_same_span_meta(beforeSpan.meta, afterSpan.meta)) {
					afterSpans.shift();
					beforeSpan.text += afterSpan.text;
				}
			}
			this.lines[startLine] = this.lines[startLine].concat(middleSpans);
		}
		this.lines[startLine] = this.lines[startLine].concat(afterSpans);

		// Delete lines in-between range
		this.lines.splice(startLine + 1, endLine - startLine);

		// Notify change
		if (this.on_change) {
			this.on_change(this.lines);
		}

		// Return new position
		return {
			line: startLine,
			character: startCharacter
		};
	}
	
	/**
	 * Deletes a single character in front or behind the specified character position, handling deleting new lines, etc.
	 * @param {boolean} forward - True if deleting the next character, otherwise deletes the previous character
	 * @param {number} startLine - The line number to delete from
	 * @param {number} startCharacter - The character position to delete from
	 */
	delete_character(forward, startLine, startCharacter) {
		let endLine = startLine;
		let endCharacter = startCharacter;
		
		// Delete forwards
		if (forward) {
			// If there are characters after cursor on this line we remove one
			if (startCharacter < this.get_line_character_count(startLine)) {
				++endCharacter;
			}
			// if there are Lines after this one we append it
			else if (startLine < this.lines.length - 1) {
				++endLine;
				endCharacter = 0;
			}
		}
		// Delete backwards
		else {
			// If there are characters before the cursor on this line we remove one
			if (startCharacter > 0) {
				--startCharacter;
			}
			// if there are rows before we append current to previous one
			else if (startLine > 0) {
				--startLine;
				startCharacter = this.get_line_character_count(startLine);
			}
		}

		return this.delete_range(startLine, startCharacter, endLine, endCharacter);
	}
	
	/**
	 * Retrieves a metadata summary object for the specified range of text. 
	 * @param {number} startLine - The starting line of the text range
	 * @param {number} startCharacter - The character position at the starting line of the text range
	 * @param {number} endLine - The ending line of the text range
	 * @param {number} endCharacter - The character position at the ending line of the text range
	 */
	get_meta_range(startLine, startCharacter, endLine, endCharacter) {
		// Check bounds
		startLine >= 0 || (startLine = 0);
		startCharacter >= 0 || (startCharacter = 0);
		endLine < this.lines.length || (endLine = this.lines.length - 1);
		const endLineCharacterCount = this.get_line_character_count(endLine);
		endCharacter <= endLineCharacterCount || (
			endCharacter = endLineCharacterCount
		);
		const isEmpty = startLine === endLine && startCharacter === endCharacter;

		// Loop through all spans in range and collect meta values
		const metaCollection = {};
		for (const metaKey in metaDefaults) {
			metaCollection[metaKey] = [];
		}
		let isInsideRange = false;
		for (let lineIndex = startLine; lineIndex <= endLine; lineIndex++) {
			const line = this.lines[lineIndex];
			let spanStartCharacter = 0;
			let startSpan = null;
			let endSpan = null;
			for (let spanIndex = 0; spanIndex < line.length; spanIndex++) {
				const span = line[spanIndex];
				if (lineIndex === startLine) {
					if (
						(!isEmpty && startCharacter >= spanStartCharacter && startCharacter < spanStartCharacter + span.text.length) ||
						(isEmpty && startCharacter > spanStartCharacter && startCharacter <= spanStartCharacter + span.text.length) ||
						(startCharacter === 0 && spanStartCharacter === 0)
					) {
						isInsideRange = true;
						startSpan = span;
					}
				}
				if (lineIndex === endLine && isInsideRange) {
					if (
						(!isEmpty && endCharacter <= spanStartCharacter + span.text.length) ||
						(isEmpty && endCharacter < spanStartCharacter + span.text.length)
					) {
						endSpan = span;
						isInsideRange = false;
					}
				}
				if (isInsideRange || startSpan === span || (!isEmpty && endSpan === span)) {
					for (const metaKey in metaCollection) {
						let metaValue = span.meta[metaKey];
						if (metaValue == null) {
							metaValue = metaDefaults[metaKey];
						}
						if (!metaCollection[metaKey].includes(metaValue)) {
							metaCollection[metaKey].push(metaValue);
						}
					}
				}
				spanStartCharacter += span.text.length;
			}
		}

		// Fill in default values for undefined meta keys
		for (const metaKey in metaDefaults) {
			if (metaCollection[metaKey].length === 0) {
				metaCollection[metaKey] = [metaDefaults[metaKey]];
			}
		}
		return metaCollection;
	}

	/**
	 * Sets styling metadata for the specified range of text. 
	 * @param {number} startLine - The starting line of the text range
	 * @param {number} startCharacter - The character position at the starting line of the text range
	 * @param {number} endLine - The ending line of the text range
	 * @param {number} endCharacter - The character position at the ending line of the text range
	 * @param {object} meta - The meta to set
	 */
	set_meta_range(startLine, startCharacter, endLine, endCharacter, meta) {
		// Check bounds
		startLine >= 0 || (startLine = 0);
		startCharacter >= 0 || (startCharacter = 0);
		endLine < this.lines.length || (endLine = this.lines.length - 1);
		const endLineCharacterCount = this.get_line_character_count(endLine);
		endCharacter <= endLineCharacterCount || (
			endCharacter = endLineCharacterCount
		);

		// Set meta of spans in selection
		let isInsideRange = false;
		for (let lineIndex = startLine; lineIndex <= endLine; lineIndex++) {
			const line = this.lines[lineIndex];
			let newLine = [];
			let spanStartCharacter = 0;
			for (let span of line) {
				const spanText = span.text;
				const spanLength = spanText.length;
				if (lineIndex === startLine) {
					if (startCharacter <= spanStartCharacter) {
						isInsideRange = true;
					}
				}
				if (lineIndex === endLine) {
					if (endCharacter < spanStartCharacter + spanLength) {
						isInsideRange = false;
					}
				}
				// Selection start splits the span it's inside of
				let choppedStartCharacters = 0;
				if (startCharacter > spanStartCharacter && startCharacter < spanStartCharacter + spanLength && lineIndex === startLine) {
					choppedStartCharacters = startCharacter - spanStartCharacter;
					newLine.push({
						text: span.text.slice(0, startCharacter - spanStartCharacter),
						meta: JSON.parse(JSON.stringify(span.meta))
					});
					span.text = span.text.slice(startCharacter - spanStartCharacter);
					isInsideRange = true;
				}
				newLine.push(span);
				// Selection end splits the span it's inside of
				if (endCharacter > spanStartCharacter && endCharacter < spanStartCharacter + spanLength && lineIndex === endLine) {
					newLine.push({
						text: span.text.slice(endCharacter - spanStartCharacter - choppedStartCharacters),
						meta: JSON.parse(JSON.stringify(span.meta))
					});
					span.text = span.text.slice(0, endCharacter - spanStartCharacter - choppedStartCharacters);
					isInsideRange = true;
				}
				// Add meta to span
				if (isInsideRange) {
					for (const metaKey in meta) {
						span.meta[metaKey] = meta[metaKey];
					}
				}
				spanStartCharacter += spanLength;
			}
			this.lines[lineIndex] = newLine;
		}

		this.normalize(startLine, endLine);

		// Notify change
		if (this.on_change) {
			this.on_change(this.lines);
		}
	}

	/**
	 * Merges sibling spans that have the same metadata, and removes empty spans. 
	 * @param {number} startLine - The starting line of the text range
	 * @param {number} endLine - The ending line of the text range
	 */
	normalize(startLine, endLine) {
		for (let lineIndex = startLine; lineIndex <= endLine; lineIndex++) {
			const line = this.lines[lineIndex];
			let spanIndex = 0;
			for (spanIndex = 0; spanIndex < line.length; spanIndex++) {
				const span1 = line[spanIndex];
				const span2 = line[spanIndex + 1];
				if (span1 && span2 && this.is_same_span_meta(span1.meta, span2.meta)) {
					line[spanIndex] = {
						text: span1.text + span2.text,
						meta: span1.meta
					};
					line.splice(spanIndex + 1, 1);
					spanIndex--;
					continue;
				}
				if (span1.text === '' && line.length > 1) {
					line.splice(spanIndex, 1);
					spanIndex--;
					continue;
				}
			}
		}
	}

}


/**
 * This class represents a single selection range in a text editor's document.
 */
class Text_selection_class {
	constructor(/* Text_editor_class */ editor) {
		this.editor = editor;
		this.isVisible = false;
		this.isCursorVisible = false;
		this.isActiveSideEnd = true;
		this.isBlinkVisible = true;
		this.blinkInterval = 500;

		this.start = {
			line: 0,
			character: 0
		};
		
		this.end = {
			line: 0,
			character: 0
		};

		this.set_position(0, 0);
	}
	
	/**
	 * Returns if the current text selection contains no characters
	 * @returns {boolean}
	 */
	is_empty() {
		return this.compare_position(this.start.line, this.start.character, this.end.line, this.end.character) === 0;
	}
	
	/**
	 * Determines the relative position of two line/character sets.
	 * @param {number} line1
	 * @param {number} character1 
	 * @param {number} line2 
	 * @param {number} character2
	 * @returns {number} -1 if line1/character1 is less than line2/character2, 1 if greater, and 0 if equal
	 */
	compare_position(line1, character1, line2, character2) {
		if (line1 < line2) {
			return -1;
		} else if (line1 > line2) {
			return 1;
		} else {
			if (character1 < character2) {
				return -1;
			} else if (character1 > character2) {
				return 1;
			} else {
				return 0;
			}
		}
	}
	
	/**
	 * Sets the head position of the selection to the specified line/character, optionally extends to selection to that position.
	 * @param {number} line - The line number to set the selection to 
	 * @param {number} character - The character index to set the selection to
	 * @param {boolean} [keepSelection] - If true, extends the current selection to the specified position. If false or undefined, sets an empty selection at that position. 
	 */
	set_position(line, character, keepSelection) {
		if (line == null) {
			line = this.end.line;
		}
		if (character == null) {
			character = this.end.character;
		}

		// Check lower bounds
		line >= 0 || (line = 0);
		character >= 0 || (character = 0);

		// Check upper bounds
		const lineCount = this.editor.document.get_line_count();
		line < lineCount || (line = lineCount - 1);
		const lineCharacterCount = this.editor.document.get_line_character_count(line);
		character <= lineCharacterCount || (character = lineCharacterCount);

		// Add to selection
		if (keepSelection) {
			const positionCompare = this.compare_position(
				line,
				character,
				this.start.line,
				this.start.character
			);

			// Determine whether we should make the start side of the range active, selection moving left or up.
			if (positionCompare === -1 && (this.is_empty() || line < this.start.line)) {
				this.isActiveSideEnd = false;
			}

			// Assign new value to the side that is active
			if (this.isActiveSideEnd) {
				this.end.line = line;
				this.end.character = character;
			} else {
				this.start.line = line;
				this.start.character = character;
			}

			// Making sure that end is greater than start and swap if necessary
			if (this.compare_position(this.start.line, this.start.character, this.end.line, this.end.character) > 0) {
				this.isActiveSideEnd = !this.isActiveSideEnd;
				const temp = {
					line: this.start.line,
					character: this.start.character
				}
				this.start.line = this.end.line;
				this.start.character = this.end.character;
				this.end.line = temp.line;
				this.end.character = temp.character;
			}
		}
		// Empty cursor move
		else {
			this.isActiveSideEnd = true;
			this.start.line = this.end.line = line;
			this.start.character = this.end.character = character;
		}

		// Reset cursor blink
		this.isBlinkVisible = true;
		if (this.isVisible) {
			this.start_blinking();
		}
	}
	
	/**
	 * Retrieves the position of the head of the selection (could be the start or end of the selection based on previous operations)
	 * @returns {object} - { line, character }
	 */
	get_position() {
		if (this.isActiveSideEnd) {
			return {
				character: this.end.character,
				line: this.end.line
			};
		} else {
			return {
				character: this.start.character,
				line: this.start.line
			};
		}
	}

	/**
	 * Gets the plain text value in the current selection range.
	 * @returns {string}
	 */
	get_text() {
		const positionCompare = this.compare_position(this.start.line, this.start.character, this.end.line, this.end.character);
		const firstLine = positionCompare === 1 ? this.end.line : this.start.line;
		const lastLine = positionCompare === 1 ? this.start.line : this.end.line;
		const firstCharacter = positionCompare === 1 ? this.end.character : this.start.character;
		const lastCharacter = positionCompare === 1 ? this.start.character : this.end.character;
		let textLines = [];
		for (let i = firstLine; i <= lastLine; i++) {
			if (i === firstLine && i === lastLine) {
				textLines.push(this.editor.document.get_line_text(i).slice(firstCharacter, lastCharacter));
			} else if (i === firstLine) {
				textLines.push(this.editor.document.get_line_text(i).slice(firstCharacter));
			} else if (i === lastLine) {
				textLines.push(this.editor.document.get_line_text(i).slice(0, lastCharacter));
			} else {
				textLines.push(this.editor.document.get_line_text(i));
			}
		}
		return textLines.join('\n');
	}
	
	/**
	 * Sets the visibility of the selection in the editor.
	 * @param {boolean} isVisible 
	 */
	set_visible(isVisible) {
		if (this.isVisible != isVisible) {
			this.isVisible = isVisible;
		}
	}

	/**
	 * Sets the visibility of the selection cursor in the editor.
	 * @param {boolean} isVisible 
	 */
	set_cursor_visible(isVisible) {
		if (this.isCursorVisible != isVisible) {
			this.isCursorVisible = isVisible;
			if (this.isCursorVisible) {
				this.isBlinkVisible = true;
				this.start_blinking();
			} else {
				this.stop_blinking();
			}
		}
	}
	
	/**
	 * Starts the selection cursor blinking.
	 */
	start_blinking() {
		clearInterval(this.blinkIntervalHandle);
		this.blinkIntervalHandle = setInterval(this.blink.bind(this), this.blinkInterval);
	}
	
	/**
	 * Stops the selection cursor blinking.
	 */
	stop_blinking() {
		clearInterval(this.blinkIntervalHandle);
	}
	
	/**
	 * Toggles the visibility of the selection cursor.
	 */
	blink() {
		this.isBlinkVisible = !this.isBlinkVisible;
		const firstLine = Math.min(this.start.line, this.end.line);
		const lastLine = Math.max(this.start.line, this.end.line);
		/*
		this.editor.render({
			lineStart: firstLine,
			lineEnd: lastLine
		});
		*/
		// this.Base_layers.render();
	}
	
	/**
	 * Moves the cursor to a previous line.
	 * @param {number} length - The number of lines to move 
	 * @param {boolean} keepSelection - Whether to move to an empty selection or extend the current selection
	 */
	move_line_previous(length, keepSelection) {
		length = length == null ? 1 : length;
		const position = this.get_position();
		this.set_position(position.line - length, null, keepSelection);
	}
	
	/**
	 * Moves the cursor to a next line.
	 * @param {number} length - The number of lines to move 
	 * @param {boolean} keepSelection - Whether to move to an empty selection or extend the current selection
	 */
	move_line_next(length, keepSelection) {
		length = length == null ? 1 : length;
		const position = this.get_position();
		this.set_position(position.line + length, null, keepSelection);
	}
		
	/**
	 * Moves to the start of the current line.
	 * @param {boolean} keepSelection - Whether to move to an empty selection or extend the current selection 
	 */
	move_line_start(keepSelection) {
		const position = this.get_position();
		this.set_position(position.line, 0, keepSelection);
	}

	/**
	 * Moves to the end of the current line.
	 * @param {boolean} keepSelection - Whether to move to an empty selection or extend the current selection 
	 */
	move_line_end(keepSelection) {
		const position = this.get_position();
		this.set_position(position.line, this.editor.document.get_line_character_count(position.line), keepSelection);
	}
	
	/**
	 * Moves the cursor to a character behind in the document, handles line wrapping.
	 * @param {number} length - The number of characters to move 
	 * @param {boolean} keepSelection - Whether to move to an empty selection or extend the current selection 
	 */
	move_character_previous(length, keepSelection) {
		length = length == null ? 1 : length;
		const position = this.get_position();
		if (position.character - length < 0) {
			if (position.line > 0) {
				this.set_position(position.line - 1, this.editor.document.get_line_character_count(position.line - 1), keepSelection);
			}
		} else {
			this.set_position(position.line, position.character - length, keepSelection);
		}
	}
	
	/**
	 * Moves the cursor to a character ahead in the document, handles line wrapping.
	 * @param {number} length - The number of characters to move 
	 * @param {boolean} keepSelection - Whether to move to an empty selection or extend the current selection 
	 */
	move_character_next(length, keepSelection) {
		length = length == null ? 1 : length;
		const position = this.get_position();
		const characterCount = this.editor.document.get_line_character_count(position.line);
		if (position.character + length > characterCount) {
			if (position.line + 1 < this.editor.document.lines.length) {
				this.set_position(position.line + 1, 0, keepSelection);
			}
		} else {
			this.set_position(position.line, position.character + length, keepSelection);
		}
	}

	/**
	 * Moves the cursor to the beginning of the current word or previous word, handles line wrapping.
	 * @param {boolean} keepSelection - Whether to move to an empty selection or extend the current selection 
	 */
	move_word_previous(keepSelection) {
		const position = this.get_position();
		const newPosition = this.editor.document.get_word_start_position(position.line, position.character);
		this.set_position(newPosition.line, newPosition.character, keepSelection);
	}

	/**
	 * Moves the cursor to the end of the current word or next word, handles line wrapping.
	 * @param {boolean} keepSelection - Whether to move to an empty selection or extend the current selection 
	 */
	move_word_next(keepSelection) {
		const position = this.get_position();
		const newPosition = this.editor.document.get_word_end_position(position.line, position.character);
		this.set_position(newPosition.line, newPosition.character, keepSelection);
	}
}


/**
 * This class handles rendering a text layer and editing it based on keyboard/mouse/touch controls
 */
class Text_editor_class {
	constructor(options) {
		options = options || {};

		this.editingCtx = document.getElementById('canvas_minipaint').getContext("2d");
		this.hasValueChanged = false;

		// Text boundary and offsets are precomputed before drawn
		this.lineRenderInfo = null;
		this.lastCalculatedZoom = 0;
		this.lastCalculatedLayerWidth = 0;
		this.lastCalculatedLayerHeight = 0;
		this.textBoundaryWidth = 0;
		this.textBoundaryHeight = 0;

		// Styling options during render
		this.selectionBackgroundColor = options.selectionBackgroundColor || '#1C79C4';
		this.selectionTextColor = options.selectionTextColor || '#FFFFFF';

		// Offset from top/left of layer for cursor visibility
		this.drawOffsetTop = options.paddingVertical != null ? options.paddingVertical : 6;
		this.drawOffsetLeft = options.paddingHorizontal != null ? options.paddingHorizontal : 10;

		// Tracking internal state for keyboard/mouse/touch control
		this.shiftPressed = false;
		this.ctrlPressed = false;
		this.isMouseSelectionActive = false;
		this.mouseSelectionStartX = 0;
		this.mouseSelectionStartY = 0;
		this.mouseSelectionStartLine = null;
		this.mouseSelectionStartCharacter = null;
		this.mouseSelectionMoveX = null;
		this.mouseSelectionMoveY = null;
		this.mouseSelectionEdgeScrollInterval = null;
		this.focused = false;
		
		// Text document for this editor
		this.document = new Text_document_class();
		this.document.lines = [[{ text: '', meta: {} }]];
		this.wrappedLines = [[]];

		// Text selection for this editor
		this.selection = new Text_selection_class(this);

		// The layer associated with this editor (so data can be updated)
		this.layer = null;
		this.document.on_change = () => {
			this.layer.data = this.document.lines;
		};
	}

	/**
	 * Sets the lines of the document (from layer data)
	 * @param {array} lines 
	 */
	set_lines(lines) {
		this.document.lines = lines || [[{ text: '', meta: {} }]];
	}

	/**
	 * Returns the text string at a given line wrap (ignores formatting).
	 * @param {object} wrap - The wrap definition 
	 */
	get_wrap_text(wrap) {
		let wrapText = '';
		for (let i = 0; i < wrap.spans.length; i++) {
			wrapText += wrap.spans[i].text;
		}
		return wrapText;
	}

	/**
	 * Calculates font metrics for the given span and returns it. Caches by default.
	 * @param {object} span - The span to calculate metrics for
	 * @param {boolean} noCache - Skip caching if the metrics is expected to change in the future (e.g. font family not loaded yet.) 
	 */
	get_span_font_metrics(span, noCache) {
		const fontSize = (span.meta.size || metaDefaults.size);
		const fontName = (span.meta.family || metaDefaults.family);
		let fontMetrics = fontMetricsMap.get(fontName + '_' + fontSize);
		if (!fontMetrics) {
			fontMetrics = new Font_metrics_class(fontName, fontSize);
			if (!noCache) {
				fontMetricsMap.set(fontName + '_' + fontSize, fontMetrics);
			}
		}
		return fontMetrics;
	}

	/**
	 * Returns the complete text of the document.
	 */
	get_complete_text() {
		let completeText = '';
		for (let line of this.document.lines) {
			for (let span of line) {
					completeText += span.text;
			}
			if (this.document.lines.indexOf(line) !== this.document.lines.length - 1) {
					completeText += '\n';
			}
		}
		return completeText;
	}

	replace_entire_IME_text(beforeTempText, newText) {
		const cursorPosition = this.selection.get_position();
		let allText = beforeTempText;
		let lines = allText.split('\n');
		let currentLineText = lines[cursorPosition.line];
		let beforeText = currentLineText.substring(0, cursorPosition.character);
		let afterText = currentLineText.substring(cursorPosition.character);
		let updatedLineText = beforeText + newText + afterText;
		lines[cursorPosition.line] = updatedLineText;

		const newLines = lines.map(lineText => {
				return [{ text: lineText, meta: {} }];
		});
		this.set_lines(newLines);
		this.hasValueChanged = true;
	}

	set_IME_position(newText) {
		const cursorPosition = this.selection.get_position();
		let newTextLines = newText.split('\n');
		let newCursorLine = cursorPosition.line + newTextLines.length - 1;
		let newCursorCharacter = newText.length;
		this.selection.set_position(newCursorLine, newCursorCharacter + cursorPosition.character);
		this.hasValueChanged = true;
	}



	insert_text_at_current_position(text) {
		if (!this.selection.is_empty()) {
			this.delete_character_at_current_position();
		}
		const position = this.selection.get_position();
		const newPosition = this.document.insert_text(text, position.line, position.character);
		this.selection.set_position(newPosition.line, newPosition.character);
		this.hasValueChanged = true;
	}
	
	delete_character_at_current_position(forward) {
		let newPosition;
		if (this.selection.is_empty()) {
			const position = this.selection.get_position();
			newPosition = this.document.delete_character(forward, position.line, position.character);
		} else {
			newPosition = this.document.delete_range(
				this.selection.start.line,
				this.selection.start.character,
				this.selection.end.line,
				this.selection.end.character
			);
		}
		this.selection.set_position(newPosition.line, newPosition.character);
		this.hasValueChanged = true;
	}

	delete_selection() {
		let newPosition = this.document.delete_range(
			this.selection.start.line,
			this.selection.start.character,
			this.selection.end.line,
			this.selection.end.character
		);
		this.selection.set_position(newPosition.line, newPosition.character);
		this.hasValueChanged = true;
	}

	trigger_cursor_start(layer, layerX, layerY) {
		this.isMouseSelectionActive = true;
		this.mouseSelectionStartX = layerX;
		this.mouseSelectionStartY = layerY;
		const cursorStart = this.get_cursor_position_from_absolute_position(layer, layerX, layerY);
		this.mouseSelectionStartLine = cursorStart.line;
		this.mouseSelectionStartCharacter = cursorStart.character;
		this.selection.set_position(cursorStart.line, cursorStart.character, false);
	}
	
	trigger_cursor_move(layer, layerX, layerY) {
		const isInsideCanvas = true; // layerX > 0 && layerY > 0 && layerX < this.lastCalculatedLayerWidth && layerY < this.lastCalculatedLayerHeight;
		if (this.isMouseSelectionActive && isInsideCanvas) {
			this.mouseSelectionMoveX = layerX;
			this.mouseSelectionMoveY = layerY;
			const cursorEnd = this.get_cursor_position_from_absolute_position(layer, layerX, layerY);
			this.selection.set_position(this.mouseSelectionStartLine, this.mouseSelectionStartCharacter, false);
			this.selection.set_position(cursorEnd.line, cursorEnd.character, true);
		}
	}
	
	trigger_cursor_end() {
		this.isMouseSelectionActive = false;
		this.mouseSelectionMoveX = null;
		this.mouseSelectionMoveY = null;
	}
	
	get_cursor_position_from_absolute_position(layer, x, y) {
		let line = -1;
		let character = -1;

		if (this.lineRenderInfo) {
			const textDirection = layer.params.text_direction;
			const wrapDirection = layer.params.wrap_direction;
			const isHorizontalTextDirection = ['ltr', 'rtl'].includes(textDirection);
			const isNegativeTextDirection = ['rtl', 'btt'].includes(textDirection);

			let characterPosition = isHorizontalTextDirection ? x : y;
			let wrapPosition = isHorizontalTextDirection ? y : x;
			
			const wrapSizes = this.lineRenderInfo.wrapSizes;
			let wrapRelativeIndex = -1;
		
			let globalWrapIndex = 0;
			for (let [lineIndex, lineInfo] of this.lineRenderInfo.lines.entries()) {
				wrapRelativeIndex = 0;
				for (let wrap of lineInfo.wraps) {
					if (wrapPosition < wrapSizes[globalWrapIndex].offset + wrapSizes[globalWrapIndex].size) {
						line = lineIndex;
						break;
					}
					globalWrapIndex++;
					wrapRelativeIndex++;
				}
				if (line > -1) {
					break;
				}
			}
			if (line === -1) {
				line = this.lineRenderInfo.lines.length - 1;
				wrapRelativeIndex = -1;
			}
			const wraps = this.lineRenderInfo.lines[line].wraps;
			if (wrapRelativeIndex === -1) {
				wrapRelativeIndex = wraps.length - 1;
			}
			let previousWrapCharacterCount = 0;
			for (let w = 0; w < wrapRelativeIndex; w++) {
				previousWrapCharacterCount += this.get_wrap_text(wraps[w]).length;
			}
			const characterCount = this.get_wrap_text(wraps[wrapRelativeIndex]).length;
			const characterOffsets = wraps[wrapRelativeIndex].characterOffsets;
			for (let characterNumber = 0; characterNumber < characterCount; characterNumber++) {
				const leftPosition = characterOffsets[characterNumber];
				const rightPosition = characterOffsets[characterNumber + 1];
				if (characterPosition <= leftPosition + ((rightPosition - leftPosition) * 0.5)) {
					character = previousWrapCharacterCount + characterNumber;
					break;
				}
				if (characterNumber === characterCount - 1 && character === -1) {
					character = previousWrapCharacterCount + characterCount;
				}
			}
			if (character === -1) {
				character = this.document.get_line_character_count(line);
			}
		}
		return { line, character };
	}

	calculate_text_placement(ctx, layer) {
		const boundary = layer.params.boundary;
		const textDirection = layer.params.text_direction;
		const wrapDirection = layer.params.wrap_direction;
		const halign = layer.params.halign;
		const valign = layer.params.valign;
		const isHorizontalTextDirection = ['ltr', 'rtl'].includes(textDirection);
		const isNegativeTextDirection = ['rtl', 'btt'].includes(textDirection);

		let totalTextDirectionSize = 0;
		let totalWrapDirectionSize = 0;
		let textDirectionMaxSize = isHorizontalTextDirection ? layer.width : layer.height;

		// Determine new lines based on text wrapping, if applicable
		let lineRenderInfo = {
			wrapSizes: [],
			lines: []
		};
		for (let line of this.document.lines) {
			let wrapAccumulativeSize = 0;
			let wrapCharacterOffsets = [0];
			let lineWraps = [];
			let currentWrapSpans = [...line];
			let s = 0;
			let fontMetrics = null;
			let character = null;
			let nextCharacter = null;
			let fontKerning = 0;
			for (s = 0; s < currentWrapSpans.length; s++) {
				const span = currentWrapSpans[s];
				const kerning = span.meta.kerning || metaDefaults.kerning;
				const family = span.meta.family || metaDefaults.family;
				const size = span.meta.size || metaDefaults.size;
				fontMetrics = this.get_span_font_metrics(span, !fontLoadMap.get(family));
				if (isHorizontalTextDirection) {
					ctx.font =
						' ' + (span.meta.italic ? 'italic' : '') +
						' ' + (span.meta.bold ? 'bold' : '') +
						' ' + size + 'px' +
						' ' + family;
				}
				for (let c = 0; c < span.text.length; c++) {
					character = span.text[c];
					if (layer.params.kerning === 'metrics') {
						nextCharacter = span.text[c + 1];
						if (!nextCharacter && c === span.text.length - 1 && currentWrapSpans[s + 1]) {
							const nextSpan = currentWrapSpans[s + 1];
							if (family === (nextSpan.meta.family || metaDefaults.family) && size === (nextSpan.meta.size || metaDefaults.size)) {
								nextCharacter = nextSpan.text[0];
							}
						}
						fontKerning = isHorizontalTextDirection && nextCharacter ? fontMetrics.get_kerning_offset(character + nextCharacter) : 0;
					}
					const characterSize = isHorizontalTextDirection ? ctx.measureText(character).width : fontMetrics.height;
					wrapAccumulativeSize += characterSize + fontKerning + kerning;
					if (boundary !== 'dynamic' && wrapAccumulativeSize > textDirectionMaxSize && ![' ', '-'].includes(character)) {
						// Find last span with space
						let dividerPosition = -1;
						let bs = s;
						for (; bs >= 0; bs--) {
							const backwardsSpan = currentWrapSpans[bs];
							const backwardsSpanText = (bs === s) ? backwardsSpan.text.substring(0, c) : backwardsSpan.text;
							dividerPosition = backwardsSpanText.lastIndexOf(' ');
							const dashPosition = backwardsSpanText.lastIndexOf('-');
							if (dashPosition > dividerPosition) {
								dividerPosition = dashPosition;
							}
							if (dividerPosition > -1) {
								break;
							}
						}
						let beforeSpans = [];
						let afterSpans = [];
						// Found a previous span on the current line wrap that contains a space, split the line
						if (dividerPosition > -1) {
							beforeSpans = currentWrapSpans.slice(0, bs);
							afterSpans = currentWrapSpans.slice(bs + 1);
							const beforeText = currentWrapSpans[bs].text.substring(0, dividerPosition + 1);
							const afterText = currentWrapSpans[bs].text.substring(dividerPosition + 1);
							if (beforeText.length > 0) {
								beforeSpans.push({
									text: beforeText,
									meta: currentWrapSpans[bs].meta
								});
							}
							if (afterText.length > 0) {
								afterSpans.unshift({
									text: afterText,
									meta: currentWrapSpans[bs].meta
								});
							}
						}
						// For word split only, break out.
						else if (layer.params.wrap === 'word') {
							wrapCharacterOffsets.push(wrapAccumulativeSize);
							break;
						}
						// Otherwise, split the word
						else {
							if (s === 0 && c === 0) {
								c++;
								wrapCharacterOffsets.push(wrapAccumulativeSize);
							}
							beforeSpans = currentWrapSpans.slice(0, s);
							afterSpans = currentWrapSpans.slice(s + 1);
							const beforeText = currentWrapSpans[s].text.substring(0, c);
							const afterText = currentWrapSpans[s].text.substring(c);
							if (beforeText.length > 0) {
								beforeSpans.push({
									text: beforeText,
									meta: currentWrapSpans[s].meta
								});
							}
							if (afterText.length > 0) {
								afterSpans.unshift({
									text: afterText,
									meta: currentWrapSpans[s].meta
								});
							}
						}
						let largestOffset = wrapCharacterOffsets[wrapCharacterOffsets.length-1];
						if (largestOffset > totalTextDirectionSize) {
							totalTextDirectionSize = largestOffset;
						}
						const newWrap = {
							characterOffsets: wrapCharacterOffsets,
							spans: beforeSpans
						};
						newWrap.characterOffsets = newWrap.characterOffsets.slice(0, this.get_wrap_text(newWrap).length + 1);
						lineWraps.push(newWrap);
						currentWrapSpans = afterSpans;
						wrapAccumulativeSize = 0;
						wrapCharacterOffsets = [0];
						s = -1;
						break;
					} else {
						wrapCharacterOffsets.push(wrapAccumulativeSize);
					}
				}
				if (s === -1) {
					continue;
				}
			}
			if (currentWrapSpans.length > 0) {
				let largestOffset = wrapCharacterOffsets[wrapCharacterOffsets.length-1];
				if (largestOffset > totalTextDirectionSize) {
					totalTextDirectionSize = largestOffset;
				}
				lineWraps.push({
					characterOffsets: wrapCharacterOffsets,
					spans: currentWrapSpans
				});
			}
			lineRenderInfo.lines.push({
				firstWrapIndex: 0,
				wraps: lineWraps
			});
		}

		// Adjust offsets for alignment along the text direction
		if ((isHorizontalTextDirection && halign !== 'left') || (!isHorizontalTextDirection && valign !== 'top')) {
			const maxTextDirectionSize = boundary === 'dynamic' ? totalTextDirectionSize : (isHorizontalTextDirection ? layer.width : layer.height);
			for (let line of lineRenderInfo.lines) {
				for (let wrap of line.wraps) {
					const isCentered = (isHorizontalTextDirection && halign == 'center') || (!isHorizontalTextDirection && valign === 'middle');
					const lastSpan = wrap.spans[wrap.spans.length - 1];
					const wrapSize = wrap.characterOffsets[wrap.characterOffsets.length - 1 - (lastSpan.text[lastSpan.text.length - 1] === ' ' ? 1 : 0)];
					const startOffset = (isCentered ? maxTextDirectionSize / 2 : maxTextDirectionSize) - (isCentered ? wrapSize / 2 : wrapSize);
					if (startOffset > 0) {
						for (let oi = 0; oi < wrap.characterOffsets.length; oi++) {
							wrap.characterOffsets[oi] += startOffset;
						}
					}
				}
			}
		}

		// Determine the size of each line (e.g. line height if horizontal typing direction)
		let wrapSizeAccumulator = 0;
		let wrapCounter = 0;
		for (let line of lineRenderInfo.lines) {
			line.firstWrapIndex = wrapCounter;
			for (let wrap of line.wraps) {
				let ascenderSize = 0;
				let descenderSize = 0;
				for (let span of wrap.spans) {
					let fontMetrics;
					const family = span.meta.family || metaDefaults.family;
					const leading = span.meta.leading != null ? span.meta.leading : metaDefaults.leading;
					if (isHorizontalTextDirection) {
						fontMetrics = this.get_span_font_metrics(span, !fontLoadMap.get(family));
					} else {
						ctx.font =
							' ' + (span.meta.italic ? 'italic' : '') +
							' ' + (span.meta.bold ? 'bold' : '') +
							' ' + (span.meta.size || metaDefaults.size) + 'px' +
							' ' + family;
					}
					let spanAscenderSize = isHorizontalTextDirection ? fontMetrics.baseline : ctx.measureText(character).width;
					let spanDescenderSize = isHorizontalTextDirection ? Math.abs(fontMetrics.baseline - fontMetrics.height) : ctx.measureText(character).width;
					if (leading) {
						spanAscenderSize += leading;
						if (spanAscenderSize < 0) {
							spanDescenderSize += spanAscenderSize;
							spanAscenderSize = 0;
							if (spanDescenderSize < 0) {
								spanDescenderSize = 0;
							}
						}
					}
					if (spanAscenderSize > ascenderSize) {
						ascenderSize = spanAscenderSize;
					}
					if (spanDescenderSize > descenderSize) {
						descenderSize = spanDescenderSize;
					}
				}
				let lineSize = ascenderSize + descenderSize;
				lineRenderInfo.wrapSizes.push({ size: lineSize, offset: wrapSizeAccumulator, baseline: ascenderSize });
				wrapSizeAccumulator += lineSize;
				wrapCounter++;
			}
		}
		totalWrapDirectionSize = wrapSizeAccumulator;

		this.lastCalculatedLayerWidth = layer.width;
		this.lastCalculatedLayerHeight = layer.height;
		this.textBoundaryWidth = Math.max(1, Math.round(isHorizontalTextDirection ? totalTextDirectionSize : totalWrapDirectionSize));
		this.textBoundaryHeight = Math.max(1, Math.round(isHorizontalTextDirection ? totalWrapDirectionSize : totalTextDirectionSize));
		this.lineRenderInfo = lineRenderInfo;
	}

	render(ctx, layer) {
		if (config.need_render_changed_params || this.hasValueChanged || layer.width != this.lastCalculatedLayerWidth || layer.height != this.lastCalculatedLayerHeight || !this.textBoundaryWidth || !this.textBoundaryHeight) {
			this.calculate_text_placement(ctx, layer);
		}

		if (!this.lineRenderInfo) return;

		try {

			let options = options || {};
			let isSelectionEmpty = this.selection.is_empty();

			ctx.textAlign = 'left';
			ctx.textBaseline = 'alphabetic';

			const boundary = layer.params.boundary;
			let drawOffsetTop = layer.y + 1;
			let drawOffsetLeft = layer.x + 1;
			const textDirection = layer.params.text_direction;
			const wrapDirection = layer.params.wrap_direction;
			const isHorizontalTextDirection = ['ltr', 'rtl'].includes(textDirection);
			const isNegativeTextDirection = ['rtl', 'btt'].includes(textDirection);

			const wrapSizes = this.lineRenderInfo.wrapSizes;
			let lineIndex = 0;
			let wrapIndex = 0;
			const cursorLine = this.selection.isActiveSideEnd ? this.selection.end.line : this.selection.start.line;
			const cursorCharacter = this.selection.isActiveSideEnd ? this.selection.end.character : this.selection.start.character;
			if(layer.rotate){
				const alpha = (layer.rotate * Math.PI) / 180;
				ctx.save();
				// Move the canvas to the center before rotating
				ctx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
				ctx.rotate(alpha);
				// Move it back after it
				ctx.translate(-layer.x - layer.width / 2, -layer.y - layer.height / 2);

			}
			for (let line of this.lineRenderInfo.lines) {
				let lineLetterCount = 0;
				for (let [localWrapIndex, wrap] of line.wraps.entries()) {
					let cursorStartX = null;
					let cursorStartY = null;
					let cursorSize = null;
					let characterIndex = 0;
					const characterOffsets = wrap.characterOffsets;
					for (let [spanIndex, span] of wrap.spans.entries()) {
						const kerning = span.meta.kerning != null ? span.meta.kerning : metaDefaults.kerning;
						const bold = span.meta.bold != null ? span.meta.bold : metaDefaults.bold;
						const italic = span.meta.italic != null ? span.meta.italic : metaDefaults.italic;
						const underline = span.meta.underline != null ? span.meta.underline : metaDefaults.underline;
						const strikethrough = span.meta.strikethrough != null ? span.meta.strikethrough : metaDefaults.strikethrough;
						const family = span.meta.family || metaDefaults.family;

						if (fontLoadMap.get(family) !== true) {
							const variants = config.user_fonts[family] ? config.user_fonts[family].variants : undefined;
							load_font_family({ family, variants }, () => {
								this.hasValueChanged = true;
								this.Base_layers.render();
							});
						}

						let fontMetrics;
						if (underline || strikethrough) {
							fontMetrics = this.get_span_font_metrics(span, !fontLoadMap.get(family));
						}

						// Set styles for drawing
						ctx.font =
							' ' + (italic ? 'italic' : '') +
							' ' + (bold ? 'bold' : '') +
							' ' + Math.round(span.meta.size || metaDefaults.size) + 'px' +
							' ' + family;
						const fill_color = span.meta.fill_color || metaDefaults.fill_color;
						let fillStyle;
						if (fill_color.startsWith('#')) {
							fillStyle = fill_color;
						}
						const stroke_size = ((span.meta.stroke_size != null) ? span.meta.stroke_size : metaDefaults.stroke_size);
						let strokeStyle;
						if (stroke_size) {
							const stroke_color = span.meta.stroke_color || metaDefaults.stroke_color;
							if (stroke_color.startsWith('#')) {
								strokeStyle = stroke_color;
							}
							ctx.lineWidth = stroke_size;
						} else {
							ctx.lineWidth = 0;
						}

						
						
						// Loop through each letter in each span and draw it
						for (let c = 0; c < span.text.length; c++) {
							const letter = span.text.charAt(c);
							const lineStart = Math.round(drawOffsetTop + wrapSizes[wrapIndex].offset);
							const letterWidth = characterOffsets[characterIndex + 1] - characterOffsets[characterIndex];
							const letterHeight = Math.round(wrapSizes[wrapIndex].size);
							const textDirectionOffset = drawOffsetLeft + characterOffsets[characterIndex];
							const wrapDirectionOffset = Math.round(drawOffsetTop + wrapSizes[wrapIndex].offset + wrapSizes[wrapIndex].baseline);
							const letterDrawX = isHorizontalTextDirection ? textDirectionOffset + kerning : wrapDirectionOffset;
							const letterDrawY = isHorizontalTextDirection ? wrapDirectionOffset : textDirectionOffset + kerning;
							let isLetterSelected = false;
							if (this.selection.isVisible) {
								if (!isSelectionEmpty) {
									isLetterSelected = (
										(
											this.selection.start.line === lineIndex &&
											this.selection.start.character <= lineLetterCount &&
											(this.selection.end.line > lineIndex || this.selection.end.character > lineLetterCount)
										) ||
										(
											this.selection.end.line === lineIndex &&
											this.selection.end.character > lineLetterCount &&
											(this.selection.start.line < lineIndex || this.selection.start.character <= lineLetterCount)
										) ||
										(
											this.selection.start.line < lineIndex &&
											this.selection.end.line > lineIndex
										)
									);
								}
								if (cursorLine === lineIndex) {
									if (cursorCharacter === lineLetterCount) {
										cursorStartX = (isHorizontalTextDirection ? textDirectionOffset : lineStart) - 0.5;
										cursorStartY = (isHorizontalTextDirection ? lineStart : textDirectionOffset) - 0.5;
										cursorSize = isHorizontalTextDirection ? letterHeight : letterWidth;
									}
									else if (cursorCharacter === lineLetterCount + 1 && localWrapIndex === line.wraps.length - 1 && spanIndex === wrap.spans.length - 1 && c === span.text.length - 1) {
										cursorStartX = (isHorizontalTextDirection ? textDirectionOffset + letterWidth : lineStart) - 0.5;
										cursorStartY = (isHorizontalTextDirection ? lineStart : textDirectionOffset + letterHeight) - 0.5;
										cursorSize = isHorizontalTextDirection ? letterHeight : letterWidth;
									}
								}
							}
							if (isLetterSelected && this.editingCtx === ctx) {
								const letterStartX = isHorizontalTextDirection ? textDirectionOffset : lineStart;
								const letterStartY = isHorizontalTextDirection ? lineStart : textDirectionOffset;
								const letterSizeX = isHorizontalTextDirection ? letterWidth : letterHeight;
								const letterSizeY = isHorizontalTextDirection ? letterHeight : letterWidth;
								ctx.fillStyle = this.selectionBackgroundColor + '22';
								ctx.fillRect(letterStartX, letterStartY, letterSizeX, letterSizeY);
								ctx.strokeStyle = this.selectionBackgroundColor;
								ctx.lineWidth = 0.75;
								ctx.strokeRect(letterStartX, letterStartY, letterSizeX, letterSizeY);
								ctx.lineWidth = stroke_size;
							}
							ctx.fillStyle = fillStyle;
							ctx.strokeStyle = strokeStyle;
							ctx.fillText(letter, letterDrawX, letterDrawY);
							if (stroke_size) {
								ctx.lineWidth = stroke_size;
								ctx.strokeText(letter, letterDrawX, letterDrawY);
							}
							if (strikethrough) {
								ctx.fillStyle = fillStyle;
								ctx.lineWidth = Math.max(1, fontMetrics.height / 20);
								ctx.fillRect(letterDrawX - 0.25 - kerning, letterDrawY - (fontMetrics.height * .28), letterWidth + 0.5, ctx.lineWidth);
							}
							if (underline) {
								ctx.fillStyle = fillStyle;
								ctx.lineWidth = Math.max(1, fontMetrics.height / 20);
								ctx.fillRect(letterDrawX - 0.25 - kerning, letterDrawY + (ctx.lineWidth), letterWidth + 0.5, ctx.lineWidth);
							}
							characterIndex++;
							lineLetterCount++;
						}

						

						if (span.text.length === 0) {
							if (cursorLine === lineIndex && cursorCharacter === lineLetterCount) {
								const lineStart = Math.round(drawOffsetTop + wrapSizes[wrapIndex].offset);
								const textDirectionOffset = drawOffsetLeft + characterOffsets[0] + (lineIndex === 0 ? (boundary === 'dynamic' ? 5 : 2) : 0);
								const letterWidth = 3;
								const letterHeight = Math.round(wrapSizes[wrapIndex].size);
								cursorStartX = (isHorizontalTextDirection ? textDirectionOffset : lineStart) - 0.5;
								cursorStartY = (isHorizontalTextDirection ? lineStart : textDirectionOffset) - 0.5;
								cursorSize = isHorizontalTextDirection ? letterHeight : letterWidth;
							}
						}
					}

					// Draw cursor
					if (this.selection.isCursorVisible /*&& this.selection.isBlinkVisible*/ && cursorStartX && this.editingCtx == ctx) {
						ctx.lineCap = 'butt';
						ctx.strokeStyle = '#55555577';
						ctx.lineWidth = 3;
						ctx.beginPath();
						ctx.moveTo(cursorStartX, cursorStartY + 1);
						ctx.lineTo(cursorStartX, cursorStartY + cursorSize - 1);
						if (cursorSize > 14) {
							ctx.moveTo(cursorStartX - 3, cursorStartY + 2);
							ctx.lineTo(cursorStartX + 3, cursorStartY + 2);
							ctx.moveTo(cursorStartX - 3, cursorStartY + cursorSize - 2);
							ctx.lineTo(cursorStartX + 3, cursorStartY + cursorSize - 2);
						}
						ctx.stroke();
						ctx.strokeStyle = '#ffffffff';
						ctx.lineWidth = 1;
						ctx.beginPath();
						ctx.moveTo(cursorStartX, cursorStartY + 2);
						ctx.lineTo(cursorStartX, cursorStartY + cursorSize - 2);
						if (cursorSize > 14) {
							ctx.moveTo(cursorStartX - 2, cursorStartY + 2);
							ctx.lineTo(cursorStartX + 2, cursorStartY + 2);
							ctx.moveTo(cursorStartX - 2, cursorStartY + cursorSize - 2);
							ctx.lineTo(cursorStartX + 2, cursorStartY + cursorSize - 2);
						}
						ctx.stroke();
					}
					wrapIndex++;
				}
				lineIndex++;
			}
			if(layer.rotate){
				ctx.restore();
			}
		} catch (error) {
			console.warn(error);
		}

		this.hasValueChanged = false;
	}
}

class Google_fonts_search_class {
	constructor() {
		this.POP = new Dialog_class();
		this.GUI_tools = new GUI_tools_class();
		this.popup = null;
		this.fontsPerPage = 8;
		this.dialogContentNode = null;
		this.fontListNode = null;
		this.fontList = [];
		this.fontListFiltered = [];
		this.selectedFonts = {};
		this.searchTimeoutHandle = null;
	}

	render_font_list(page) {
		page = page || 1;
		const pageCount = Math.ceil(this.fontListFiltered.length / 8);
		const startIndex = (page - 1) * this.fontsPerPage;
		let html = '<div class="selection_card_list">';
		for (let i = startIndex; i < startIndex + this.fontsPerPage; i++) {
			const font = this.fontListFiltered[i];
			if (!font) break;
			const isSelected = !!this.selectedFonts[font.family];
			load_font_family({ family: font.family, variants: font.variants });
			html += `
				<div class="selection_card">
					<input type="checkbox" id="google_font_selection_${font.family}" value="${font.family}" ${isSelected ? 'checked="checked"' : ''}>
					<label for="google_font_selection_${font.family}"">
						<div class="font_preview" style="font-family: '${font.family}'">
							The quick brown fox jumps over the lazy dog.
						</div>
						<div class="text_muted">
							${font.family}
						</div>
					</label>
				</div>
			`;
		}
		html += `
				</div>
				<div class="pagination">
					${page > 1 ? '<button title="Previous Page" data-page="' + (page - 1) + '">&laquo;</button>' : ''}
					${page - 2 > 0 ? '<button title="Page ' + (page - 2) + '" data-page="' + (page - 2) + '">' + (page - 2) + '</button>' : ''}
					${page - 1 > 0 ? '<button title="Page ' + (page - 1) + '" data-page="' + (page - 1) + '">' + (page - 1) + '</button>' : ''}
					<button title="Page ${page}" aria-pressed="true" data-page="${page}">${page}</button>
					${page + 1 <= pageCount ? '<button title="Page ' + (page + 1) + '" data-page="' + (page + 1) + '">' + (page + 1) + '</button>' : ''}
					${page + 2 <= pageCount ? '<button title="Page ' + (page + 2) + '" data-page="' + (page + 2) + '">' + (page + 2) + '</button>' : ''}
					${page < pageCount ? '<button title="Next Page" data-page="' + (page + 1) + '">&raquo;</button>' : ''}
				</div>
			</div>
		`;
		this.fontListNode.innerHTML = html;

		// Attempt to remove vertical scroll by decreasing page size.
		if (this.fontsPerPage > 3 && this.dialogContentNode.scrollHeight > this.dialogContentNode.clientHeight) {
			this.fontsPerPage--;
			this.render_font_list(page);
			return;
		}

		// Handle checkbox
		this.fontListNode.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {
			checkbox.addEventListener('change', (e) => {
				if (checkbox.checked) {
					this.selectedFonts[checkbox.value] = this.fontListFiltered
						.slice(startIndex, startIndex + this.fontsPerPage)
						.filter((font) => { return font.family === checkbox.value; })[0];
				} else {
					delete this.selectedFonts[checkbox.value];
				}
			});
		});

		// Handle pagination
		this.fontListNode.querySelector('.pagination').addEventListener('click', (e) => {
			const page = parseInt(e.target.getAttribute('data-page'), 10);
			this.render_font_list(page);
		});
	}

	show() {
		this.POP.show({
			title: 'Search for Font',
			params: [
				{ name: "query", title: "Search:", value: '', prevent_submission: true }
			],
			on_load: (params, popup) => {
				this.popup = popup;
				var node = document.createElement("div");
				this.dialogContentNode = popup.el.querySelector('.dialog_content');
				this.dialogContentNode.appendChild(node);
				this.fontListNode = node;				

				const queryInput = popup.el.querySelector('#pop_data_query');
				queryInput.addEventListener('input', (e) => {
					const query = (e.target.value || '').toLowerCase();
					if (!query) {
						this.fontListFiltered = this.fontList;
						this.render_font_list();
					} else {
					clearTimeout(this.searchTimeoutHandle);
						this.searchTimeoutHandle = setTimeout(() => {
							this.fontListFiltered = [];
							for (let i = 0; i < this.fontList.length; i++) {
								const fontFamily = this.fontList[i].family.toLowerCase();
								if (fontFamily.includes(query)) {
									this.fontListFiltered.push(this.fontList[i]);
								}
							}
							this.render_font_list();
						}, 350);
					}
				});

				const apiKey = config.google_webfonts_key;
				$.getJSON(`https://www.googleapis.com/webfonts/v1/webfonts?key=${apiKey}&sort=popularity`, (data) => {
					this.fontList = data.items;
					this.fontListFiltered = data.items;
					this.render_font_list();
				}).fail(function () {
					alertify.error('Error loading the list of fonts from Google.');
				});
			},
			on_finish: () => {
				this.popup = null;
				this.POP = null;
				if (Object.keys(this.selectedFonts).length > 0) {
					let firstFont = null;
					for (let font in this.selectedFonts) {
						if (!firstFont) {
							firstFont = font;
						}
						config.user_fonts[font] = this.selectedFonts[font];
					}
					app.GUI.GUI_tools.action_data().attributes.font.value = firstFont;
					app.GUI.GUI_tools.show_action_attributes();
					try {
						const changeEvent = new Event('change');
						document.querySelector('#action_attributes select#font').dispatchEvent(changeEvent);
					} catch (error) {
						console.warn('Application markup may have changed, ', error);
					}
				}
			}
		});
	}
}


class Text_class extends Base_tools_class {

	constructor(ctx) {
		super();
		this.Base_layers = new Base_layers_class();
		this.GUI_tools = new GUI_tools_class();
		this.Helper = new Helper_class();
		this.ctx = ctx;
		this.name = 'text';
		this.layer = {};
		this.creating = false;
		this.selecting = false;
		this.resizing = false;
		this.focused = false;
		this.focusedValue = null;
		this.mousedownX = 0;
		this.mousedownY = 0;
		this.mousedownBounds = {};
		this.is_fonts_loaded = false;
		if (ctx) {
			this.selection = {
				x: null,
				y: null,
				width: null,
				height: null,
			};
			var sel_config = {
				enable_background: false,
				enable_borders: true,
				enable_controls: true,
				enable_rotation: true,
				enable_move: false,
				data_function: () => {
					return this.selection;
				},
			};
			this.Base_selection = new Base_selection_class(ctx, sel_config, this.name);

			// Need a textarea in order to listen for keyboard inputs in an accessible, multi-platform independent way
			this.textarea = document.createElement('textarea');
			this.textarea.id = 'text_tool_keyboard_input';
			this.textarea.setAttribute('autocorrect', 'off');
			this.textarea.setAttribute('autocapitalize', 'off');
			this.textarea.setAttribute('autocomplete', 'off');
			this.textarea.setAttribute('spellcheck', 'false');
			this.textarea.style = `position: absolute; top: 0; left: 0; padding: 0; width: 1px; height: 1px; background: transparent; border: none; outline: none; color: transparent; opacity: 0.01; pointer-events: none;`;
			document.body.appendChild(this.textarea);

			this.textarea.addEventListener('focus', () => {
				this.focused = true;
				let editor = this.get_editor(this.layer);
				if (editor) {
					this.focusedValue = JSON.stringify(editor.document.lines);
				}
			}, true);

			this.textarea.addEventListener('blur', () => {
				this.focused = false;
				let editor = this.get_editor(this.layer);
				if (editor) {
					let value = JSON.stringify(editor.document.lines);
					if (this.focusedValue !== value) {
						this.layer.data = JSON.parse(this.focusedValue);
						app.State.do_action(
							new app.Actions.Update_layer_action(this.layer.id, { data: JSON.parse(value) })
						);
					}
				}
				this.focusedValue = null;
				this.Base_layers.render();
			}, true);

			let isComposing = false;
			let beforeImeText = "";
			this.textarea.addEventListener('compositionstart', () => {
				beforeImeText = "";
					isComposing = true;
					if (config.layer) {
						const editor = this.get_editor(config.layer);
						beforeImeText = editor.get_complete_text();
					}
			});

			this.textarea.addEventListener('compositionend', (e) => {
				const editor = this.get_editor(config.layer);
				editor.set_IME_position(e.target.value);
				beforeImeText = "";
				isComposing = false;
				e.target.value = '';
			});

			this.textarea.addEventListener('input', (e) => {
				if(isComposing){
					const editor = this.get_editor(config.layer);
					editor.replace_entire_IME_text(beforeImeText, e.target.value);
					this.Base_layers.render();
					this.extend_fixed_bounds(config.layer, editor);
				}
				else if (config.layer) {
					const editor = this.get_editor(config.layer);
					editor.insert_text_at_current_position(e.target.value);
					e.target.value = '';
					this.Base_layers.render();
					this.extend_fixed_bounds(config.layer, editor);
				}
			}, true);

			this.textarea.addEventListener('keydown', (e) => {
				if (config.layer) {
					let handled = true;
					const editor = this.get_editor(config.layer);
					switch (e.key) {
						case 'Backspace':
							editor.delete_character_at_current_position(false);
							break;
						case 'Delete':
							editor.delete_character_at_current_position(true);
							break;
						case 'Home':
							editor.selection.move_line_start(e.shiftKey);
							break;
						case 'End':
							editor.selection.move_line_end(e.shiftKey);
							break;
						case 'Left': case 'ArrowLeft':
							if (!e.shiftKey && !editor.selection.is_empty()) {
								editor.selection.isActiveSideEnd = false;
								editor.selection.move_character_previous(0, false);
							} else if (e.ctrlKey) {
								editor.selection.move_word_previous(e.shiftKey);
							} else {
								editor.selection.move_character_previous(1, e.shiftKey);
							}
							break;
						case 'Right': case 'ArrowRight':
							if (!e.shiftKey && !editor.selection.is_empty()) {
								editor.selection.isActiveSideEnd = true;
								editor.selection.move_character_next(0, false);
							} else if (e.ctrlKey) {
								editor.selection.move_word_next(e.shiftKey);
							} else {
								editor.selection.move_character_next(1, e.shiftKey);
							}
							break;
						case 'Up': case 'ArrowUp':
							editor.selection.move_line_previous(1, e.shiftKey);
							break;
						case 'Down': case 'ArrowDown':
							editor.selection.move_line_next(1, e.shiftKey);
							break;
						case 'a':
							if (e.ctrlKey) {
								editor.selection.set_position(0, 0);
								const lastLine = editor.document.lines.length - 1;
								editor.selection.set_position(lastLine, editor.document.get_line_character_count(lastLine), true);
								break;
							}
						case 'b':
							if (e.ctrlKey) {
								e.preventDefault();
								document.querySelector('#action_attributes #bold').click();
								break;
							}
						case 'c':
							if (e.ctrlKey) {
								e.preventDefault();
								this.textarea.value = editor.selection.get_text();
								this.textarea.select();
								this.textarea.setSelectionRange(0, 99999);
								document.execCommand('copy');
								this.textarea.value = '';
								break;
							}
						case 'i':
							if (e.ctrlKey) {
								e.preventDefault();
								document.querySelector('#action_attributes #italic').click();
								break;
							}
						case 'u':
							if (e.ctrlKey) {
								e.preventDefault();
								document.querySelector('#action_attributes #underline').click();
								break;
							}
						case 'x':
							if (e.ctrlKey) {
								e.preventDefault();
								this.textarea.value = editor.selection.get_text();
								this.textarea.select();
								this.textarea.setSelectionRange(0, 99999);
								document.execCommand('copy');
								this.textarea.value = '';
								editor.delete_selection();
								break;
							}
						default:
							handled = false;
					}
					if (handled) {
						this.update_tool_attributes(config.layer, editor);
						this.Base_layers.render();
					}
					this.extend_fixed_bounds(config.layer, editor);
					return !handled;
				}
			}, true);
		}
	}

	dragStart(event) {
		if (config.TOOL.name != this.name)
			return;
		this.mousedown(event);
	}

	dragMove(event) {
		if (config.TOOL.name != this.name)
			return;
		this.mousemove(event);
	}

	dragEnd(event) {
		if (config.TOOL.name != this.name)
			return;
		this.mouseup(event);
	}

	load() {
		// Mouse events
		document.addEventListener('mousedown', (event) => {
			this.dragStart(event);
		});
		document.addEventListener('mousemove', (event) => {
			this.dragMove(event);
		});
		document.addEventListener('mouseup', (event) => {
			this.dragEnd(event);
		});
		document.addEventListener('dblclick', (event) => {
			this.doubleClick(event);
		});

		// Touch events
		document.addEventListener('touchstart', (event) => {
			this.dragStart(event);
		});
		document.addEventListener('touchmove', (event) => {
			this.dragMove(event);
		});
		document.addEventListener('touchend', (event) => {
			this.dragEnd(event);
		});
	}

	mousedown(e) {
		var mouse = this.get_mouse_info(e);
		if (mouse.click_valid == false)
			return;

		this.creating = false;
		this.selecting = false;
		this.resizing = false;

		this.mousedownX = mouse.x;
		this.mousedownY = mouse.y;
		this.mousedownBounds = {
			x: config.layer.x,
			y: config.layer.y,
			width: config.layer.width,
			height: config.layer.height,
			boundary: config.layer.params.boundary
		};

		if (this.Base_selection.mouse_lock !== null) {
			this.resizing = true;
			return;
		}

		const existingLayer = this.get_text_layer_at_mouse(e);
		if (existingLayer) {
			this.selecting = true;
			this.layer = existingLayer;
			const editor = this.get_editor(this.layer);
			editor.trigger_cursor_start(this.layer, -1 + mouse.x - this.layer.x, mouse.y - this.layer.y);
			app.State.do_action(
				new app.Actions.Bundle_action('select_text_layer', 'Select Text Layer', [
					new app.Actions.Select_layer_action(existingLayer.id),
					new app.Actions.Set_selection_action(this.layer.x, this.layer.y, this.layer.width, this.layer.height)
				])
			);
		}
		else {
			// Create a new text layer
			this.creating = true;
			const layer = {
				type: this.name,
				params: {
					boundary: 'dynamic',
					kerning: 'metrics',
					text_direction: 'ltr',
					wrap_direction: 'ttb',
					halign: 'left',
					valign: 'top',
					wrap: 'letter'
				},
				render_function: [this.name, 'render'],
				x: mouse.x,
				y: mouse.y,
				rotate: 0,
				is_vector: true,
			};
			app.State.do_action(
				new app.Actions.Bundle_action('new_text_layer', 'New Text Layer', [
					new app.Actions.Insert_layer_action(layer),
					new app.Actions.Set_selection_action(mouse.x, mouse.y, 0, 0)
				])
			);
			this.layer = config.layer;
		}
	}

	mousemove(e) {
		var mouse = this.get_mouse_info(e);
		if (mouse.is_drag == false)
			return;
		if (mouse.click_valid == false) {
			return;
		}

		if (this.resizing) {
			config.layer.x = this.selection.x;
			config.layer.y = this.selection.y;
			config.layer.width = this.selection.width;
			config.layer.height = this.selection.height;
			if (config.layer.params.boundary === 'dynamic') {
				config.layer.params.boundary = 'box';
			}
		}
		else if (this.creating) {
			const width = Math.abs(mouse.x - this.mousedownX);
			const height = Math.abs(mouse.y - this.mousedownY);

			//more data
			if (config.layer.params.boundary === 'dynamic') {
				config.layer.params.boundary = 'box';
			}
			config.layer.x = Math.min(mouse.x, this.mousedownX);
			config.layer.y = Math.min(mouse.y, this.mousedownY);
			config.layer.width = width;
			config.layer.height = height;
		} else {
			this.get_editor(this.layer).trigger_cursor_move(this.layer, -1 + mouse.x - this.layer.x, mouse.y - this.layer.y);
		}
		this.Base_layers.render();
	}

	mouseup(e) {
		var mouse = this.get_mouse_info(e);
		if (mouse.click_valid == false) {
			return;
		}
		const editor = this.get_editor(this.layer);

		if (this.resizing) {
			config.layer.x = this.mousedownBounds.x;
			config.layer.y = this.mousedownBounds.y;
			config.layer.width = this.mousedownBounds.width;
			config.layer.height = this.mousedownBounds.height;
			const new_params = JSON.parse(JSON.stringify(config.layer.params));
			new_params.boundary = config.layer.params.boundary;
			config.layer.params.boundary = this.mousedownBounds.boundary;
			app.State.do_action(
				new app.Actions.Bundle_action('resize_text_layer', 'Resize Text Layer', [
					new app.Actions.Update_layer_action(config.layer.id, {
						x: this.selection.x,
						y: this.selection.y,
						width: this.selection.width,
						height: this.selection.height,
						params: new_params
					}),
					new app.Actions.Set_selection_action(this.selection.x, this.selection.y, this.selection.width, this.selection.height)
				])
			);
		}
		else if (this.creating) {
			let width = Math.abs(mouse.x - this.mousedownX);
			let height = Math.abs(mouse.y - this.mousedownY);

			if (width == 0 && height == 0) {
				// Same coordinates - let render figure out dynamic width
				width = 1;
				height = 1;
			}
			app.State.do_action(
				new app.Actions.Bundle_action('resize_text_layer', 'Resize Text Layer', [
					new app.Actions.Update_layer_action(config.layer.id, {
						x: Math.min(mouse.x, this.mousedownX),
						y: Math.min(mouse.y, this.mousedownY),
						width,
						height
					})
				]),
				{ merge_with_history: 'new_text_layer' }
			);
			this.textarea.focus();
		}
		else if (this.selecting) {
			editor.trigger_cursor_end();
			this.textarea.focus();
			
			if (editor.selection.is_empty() && editor.document.queuedMetaChanges) {
				let meta = {};
				const existingMeta = editor.document.get_meta_range(editor.selection.start.line, editor.selection.start.character, editor.selection.end.line, editor.selection.end.character);
				for (let metaKey in existingMeta) {
					meta[metaKey] = editor.document.queuedMetaChanges[metaKey] != null ? editor.document.queuedMetaChanges[metaKey] : existingMeta[metaKey][0];
				}
			} else {
				editor.document.queuedMetaChanges = null;
				this.update_tool_attributes(this.layer, editor);
			}
		}

		// Resize layer based on text boundaries.
		this.extend_fixed_bounds(this.layer, editor);
		this.Base_layers.render();

		// Center layer on mouse if not click & drag
		if (this.creating && config.layer.params.boundary === 'dynamic') {
			requestAnimationFrame(() => {
				app.State.do_action(
					new app.Actions.Update_layer_action(config.layer.id, {
						x: config.layer.x - config.layer.width / 2,
						y: config.layer.y - config.layer.height / 2
					}),
					{ merge_with_history: 'new_text_layer' }
				);
			});
		}

		this.resizing = false;
		this.selecting = false;
		this.creating = false;
	}


	doubleClick(event) {
		if (document.activeElement === this.textarea) {
			const editor = this.get_editor(this.layer);
			if (editor.selection.is_empty()) {
				const position = editor.selection.get_position();
				const wordStart = editor.document.get_word_start_position(position.line, position.character, true);
				const wordEnd = editor.document.get_word_end_position(position.line, position.character, true);
				editor.selection.set_position(wordStart.line, wordStart.character);
				editor.selection.set_position(wordEnd.line, wordEnd.character, true);
				this.update_tool_attributes(this.layer, editor);
			}
		}
	}

	on_params_update(param) {
		const editor = this.get_editor(config.layer);
		const value = param.value;
		const meta = {};
		let returnValue = undefined;
		switch (param.key) {
			case 'font':
				if (value.includes('...')) {
					returnValue = {
						new_values: {
							font: ''
						}
					};
					new Google_fonts_search_class().show();
				}
				else if (value) meta.family = value;
				break;
			case 'size':
				if (value) meta.size = value;
				break;
			case 'bold':
				meta.bold = value;
				break;
			case 'italic':
				meta.italic = value;
				break;
			case 'underline':
				meta.underline = value;
				break;
			case 'strikethrough':
				meta.strikethrough = value;
				break;
			case 'fill':
				if (value) meta.fill_color = value;
				break;
			case 'stroke':
				if (value) meta.stroke_color = value;
				break;
			case 'stroke_size':
				if (!isNaN(value)) meta.stroke_size = value;
				break;
			case 'kerning':
				if (!isNaN(value)) meta.kerning = value;
				break;
			case 'leading':
				if (!isNaN(value)) meta.leading = value;
				break;
		}
		if (editor.selection.is_empty()) {
			if (!editor.document.queuedMetaChanges) {
				editor.document.queuedMetaChanges = {};
			}
			for (let metaKey in meta) {
				editor.document.queuedMetaChanges[metaKey] = meta[metaKey];
			}
		} else {
			editor.document.queuedMetaChanges = null;
			let oldData = JSON.parse(JSON.stringify(editor.document.lines));
			editor.document.set_meta_range(editor.selection.start.line, editor.selection.start.character, editor.selection.end.line, editor.selection.end.character, meta);
			editor.hasValueChanged = true;
			this.layer.data = oldData;
			app.State.do_action(
				new app.Actions.Update_layer_action(this.layer.id, { data: JSON.parse(JSON.stringify(editor.document.lines)) })
			);
			this.Base_layers.render();
		}
		return returnValue;
	}

	update_tool_attributes(layer, editor) {
		if (layer && layer.params) {
			const meta = editor.document.get_meta_range(editor.selection.start.line, editor.selection.start.character, editor.selection.end.line, editor.selection.end.character);
			const toolAttributes = this.GUI_tools.action_data().attributes;
			toolAttributes.font.value = meta.family.length === 1 ? meta.family[0] : '';
			toolAttributes.size = meta.size.length === 1 ? meta.size[0] : parseFloat(null);
			toolAttributes.bold.value = meta.bold.includes(false) ? false : true;
			toolAttributes.italic.value = meta.italic.includes(false) ? false : true;
			toolAttributes.underline.value = meta.underline.includes(false) ? false : true;
			toolAttributes.strikethrough.value = meta.strikethrough.includes(false) ? false : true;
			toolAttributes.fill = meta.fill_color.length === 1 ? meta.fill_color[0] : '#000000';
			toolAttributes.stroke = meta.stroke_color.length === 1 ? meta.stroke_color[0] : '#000000';
			toolAttributes.stroke_size.value = meta.stroke_size.length === 1 ? meta.stroke_size[0] : parseFloat(null);
			toolAttributes.kerning.value = meta.kerning.length === 1 ? meta.kerning[0] : parseFloat(null);
			toolAttributes.leading.value = meta.leading.length === 1 ? meta.leading[0] : parseFloat(null);
			this.GUI_tools.show_action_attributes();
		}
	}

	resize_to_dynamic_bounds(layer, editor) {
		if (layer && layer.params && layer.params.boundary === 'dynamic') {
			let new_width = Math.max(9, editor.textBoundaryWidth + 1);
			let new_height = Math.max(9, editor.textBoundaryHeight + 1);
			config.layer.width = new_width;
			config.layer.height = new_height;
		}
	}

	extend_fixed_bounds(layer, editor) {
		if (layer && layer.params && layer.params.boundary !== 'dynamic') {
			const isHorizontalTextDirection = ['ltr', 'rtl'].includes(layer.params.textDirection);
			let new_width = layer.width;
			let new_height = layer.height;
			if (isHorizontalTextDirection) {
				new_width = Math.max(editor.textBoundaryWidth + 1, new_width);
			} else {
				new_height = Math.max(editor.textBoundaryHeight + 1, new_height);
			}
			config.layer.width = new_width;
			config.layer.height = new_height;
		}
	}

	render(ctx, layer) {
		if (layer.width == 0 && layer.height == 0)
			return;
		var params = layer.params;

		const isActiveLayerAndTextTool = layer === config.layer && config.TOOL.name === 'text';
		const editor = this.get_editor(layer);
		editor.selection.set_visible(isActiveLayerAndTextTool);
		editor.selection.set_cursor_visible(isActiveLayerAndTextTool && (this.selecting || this.focused));
		editor.render(ctx, layer);
		if (layer === config.layer) {
			this.resize_to_dynamic_bounds(layer, editor);
		}
		if (!this.resizing && isActiveLayerAndTextTool) {
			this.selection.x = layer.x;
			this.selection.y = layer.y;
			this.selection.width = layer.width;
			this.selection.height = layer.height;
			this.selection.rotate = layer.rotate;
		} else if (config.layer.type !== 'text') {
			this.selection.x = -100000;
			this.selection.y = -100000;
			this.selection.width = 0;
			this.selection.height = 0;
		}
	}

	get_editor(layer) {
		let editor = layerEditors.get(layer);
		if (!editor) {
			editor = new Text_editor_class();

			// Convert legacy to new format
			if (layer.params && layer.params.text) {
				const params = layer.params;
				let lines = [];
				const textLines = layer.params.text.split('\n');
				const family = params.family && params.family.value? params.family.value : params.family;
				for (const textLine of textLines) {
					lines.push([
						{
							text: textLine,
							meta: {
								family,
								size: params.size,
								bold: params.bold,
								italic: params.italic,
								fill_color: params.stroke ? '#ffffff00' : layer.color,
								stroke_color: params.stroke ? layer.color : '#ffffff00',
								stroke_size: params.stroke ? params.stroke_size : 0,
								leading: 0
							}
						}
					]);
				}
				params.boundary = 'box';
				params.kerning = 'metrics';
				params.halign = params.align ? (params.align.value ? params.align.value : params.align).toLowerCase() : 'left';
				params.valign = 'top';
				params.text_direction = 'ltr';
				params.wrap_direction = 'ttb';
				params.wrap = 'word';
				delete params.text;
				delete params.family;
				delete params.size;
				delete params.bold;
				delete params.italic;
				delete params.stroke;
				delete params.stroke_size;
				delete params.align;
				layer.data = lines;
				layer.x -= 1;

				// Change leading offset so line height matches legacy line height calculation... need to load the font first to do this.
				// This is an approximate calculation, but seems to be pretty close.
				load_font_family({ family }, () => {
					const line = layer.data[0];
					if (!line) return;
					const span = line[0];
					if (!span) return;
					const fontMetrics = editor.get_span_font_metrics(span, !fontLoadMap.get(span.meta.family || metaDefaults.family));
					const topBounds = fontMetrics.calculate_letter_bounds('M', 'top');
					span.meta.leading = (span.meta.size || metaDefaults.size) - fontMetrics.height;
					layer.y += Math.abs(span.meta.leading) - (fontMetrics.baseline - topBounds.bottom);
					editor.hasValueChanged = true;
					editor.Base_layers.render();
				});
			}

			// Create initial layer data if new layer
			if (!layer.data) {
				const params = this.getParams();
				layer.data = [[{
					text: '',
					meta: {
						family: params.font.value !== metaDefaults.family && params.font.value ? params.font.value : undefined,
						size: params.size !== metaDefaults.size && !isNaN(params.size) ? params.size : undefined,
						bold: params.bold.value !== metaDefaults.bold ? params.bold.value : undefined,
						italic: params.italic.value !== metaDefaults.italic ? params.italic.value : undefined,
						underline: params.underline.value !== metaDefaults.underline ? params.underline.value : undefined,
						strikethrough: params.strikethrough.value !== metaDefaults.strikethrough ? params.strikethrough.value : undefined,
						fill_color: params.fill !== metaDefaults.fill_color ? params.fill : undefined,
						stroke_color: params.stroke !== metaDefaults.stroke_color ? params.stroke : undefined,
						stroke_size: params.stroke_size !== metaDefaults.stroke_size && !isNaN(params.stroke_size) ? params.stroke_size : undefined,
						kerning: params.kerning !== metaDefaults.kerning && !isNaN(params.kerning) ? params.kerning : undefined,
						leading: params.leading !== metaDefaults.leading && !isNaN(params.leading) ? params.leading : undefined
					}
				}]];
			}

			editor.set_lines(layer.data);
			editor.Base_layers = this.Base_layers;
			editor.layer = layer;
			layerEditors.set(layer, editor);
		}
		if (layer._needs_update_data) {
			delete layer._needs_update_data;
			editor.hasValueChanged = true;
			editor.set_lines(JSON.parse(JSON.stringify(layer.data)));
		}
		return editor;
	}

	get_text_layer_at_mouse(e) {
		const layers_sorted = this.Base_layers.get_sorted_layers();
		if (config.layer.type === 'text') {
			layers_sorted.unshift(config.layer);
		}
		const mouse = this.get_mouse_info(e);
		const clickableMargin = 5;
		for (let layer of layers_sorted) {
			if (layer.type === 'text') {
				// TODO - account for rotation
				if (mouse.x >= layer.x - clickableMargin && mouse.x <= layer.x + layer.width + clickableMargin && mouse.y >= layer.y - clickableMargin && mouse.y <= layer.y + layer.height + clickableMargin) {
					return layer;
				}
			}
		}
		return null;
	}

}

export default Text_class;