/**
 * QuickMemo Class
 * Manages interactive sticky notes (memos) with drag-and-drop, resizing,
 * Markdown support, and server-side persistence.
 */
class QuickMemo {

	/**
	 * @constructor
	 * @param {Object} param - Configuration parameters for the instance.
	 */
	constructor(param = {}) {

		/**
		 * @type {Object} defaultParams - Default configuration values
		 */
		const defaultParams = {
			interfaceUrl: '',
			archivesUrl: false,
			userWriteRight: false,
			userDeleteRight: false,
			elementId: 0,
			autoResizeFontSize: true,
			autoResizeFontMin: 1,
			autoResizeFontMax: 1.2,
			elementType: '',
			context: '',
			token: '',
			shareBtnStatus: 0,
			colors: ['#fff8a6', '#ffd6d6', '#d6ffd9', '#d6e6ff', '#f3d6ff', '#ffffff', '#f5f5f5'],
			publicFontAIcon: 'far fa-eye',
			privateFontAIcon: 'far fa-eye-slash',
			archiveFontAIcon: 'far fa-trash-alt',
			sharedFontAIcon: 'far fa-copy',
			notSharedFontAIcon: 'far fa-file',
			modelFontAIcon: 'far fa-share-square',
			menuDropDownId: 'quickmemo-create-dropdown',
			menuDropDownModelListContainerId: 'quickmemo-model-list',
			menuDropDownPresetListContainerId: 'quickmemo-preset-list'
		};

		// Merge default parameters with user-provided parameters
		this.param = {...defaultParams, ...param};

		/** @type {number} countedArchivedMemo - Local counter for archived notes */
		this.countedArchivedMemo = 0;

		// Validation of required parameters
		if (!this.param.interfaceUrl || (!this.param.elementId && this.param.elementType !== '')) {
			console.warn('QuickMemo: missing required parameters');
			return;
		}

		/** @type {HTMLElement} container - Global container (body) */
		this.container = document.body;

		/** @type {number} currentZ - Initial z-index for notes */
		this.currentZ = 200;

		/** @type {Array} memos - Stores objects containing {memoData, domElement} */
		this.memos = [];

		this.init();
	}

	/**
	 * Initializes the component by loading languages, fetching data, and setting up events.
	 * @async
	 */
	async init() {
		// Load translation files via Dolibarr tools
		const loadLangs = async function () {
			await Dolibarr.tools.langs.load('quickmemo');
			await Dolibarr.tools.langs.load('main');
			await Dolibarr.tools.langs.load('other');
		}

		await loadLangs();
		Dolibarr.on('tools:langs:CacheCleared', loadLangs);

		// Load existing memos from server
		this.loadMemos();

		// Set initial z-indexes based on stored position
		this.memos.forEach((m, i) => {
			// Default pos_z if it does not exist
			if (!('pos_z' in m.memo) || m.memo.pos_z < 1) m.memo.pos_z = i + 1;
			// z-index CSS
			m.el.style.zIndex = this.currentZ + m.memo.pos_z;
		});

		// Ensure notes stay within viewport on window resize
		window.addEventListener('resize', () => this.ensureAllVisible());

		// Initialize top-bar menu if user has write permissions
		if (this.param.userWriteRight) {
			this.initMenuMemoDropdown();
		}

		// Setup global shortcut behaviors (Escape key)
		this.escapeKeyBehavior();
	}

	/**
	 * Manages the Escape key behavior to hide/show notes with animations.
	 */
	escapeKeyBehavior() {
		let escTimer = null;
		let escActivated = false;

		/** Hides all notes by setting display to none */
		function applyDisplayNone() {
			document.querySelectorAll('.quickmemo-note').forEach(el => {
				el.style.display = 'none';
			});
		}

		/** Restores notes visibility */
		function removeDisplayNone() {
			document.querySelectorAll('.quickmemo-note').forEach(el => {
				el.style.display = '';
			});
		}

		// Keydown handler: triggers after a delay to hide notes
		document.addEventListener('keydown', function (e) {
			if (e.key === 'Escape' && !escTimer) {
				escTimer = setTimeout(function () {
					document.body.classList.add('hide-quickmemo');
					escActivated = true;

					// Single global listener
					document.addEventListener('transitionend', function handler(ev) {
						if (ev.target.classList.contains('quickmemo-note')) {
							applyDisplayNone();
							document.removeEventListener('transitionend', handler);
						}
					});

				}, 300);
			}
		});

		// Keyup handler: restores notes when Escape is released
		document.addEventListener('keyup', function (e) {
			if (e.key === 'Escape') {
				clearTimeout(escTimer);
				escTimer = null;

				if (escActivated) {
					removeDisplayNone();

					requestAnimationFrame(() => {
						document.body.classList.remove('hide-quickmemo');
					});

					escActivated = false;
				}
			}
		});
	}

	/* ===============================
	   LOAD
	================================= */

	/**
	 * Fetches memos from the server and renders them.
	 * @async
	 */
	async loadMemos() {
		try {

			const res = await fetch(this.param.interfaceUrl + '?action=list', {
				method: 'POST',
				headers: {'Content-Type': 'application/x-www-form-urlencoded'},
				body: new URLSearchParams({
					element_id: this.param.elementId,
					element_type: this.param.elementType,
					context: this.param.context,
					token: this.param.token
				})
			});

			const json = await res.json();

			if (!json.result) {
				throw new Error(json.msg);
			}

			// Render each fetched memo
			json.data.memos.forEach(memo => this.renderMemo(memo));
			// Update the UI with the archive count
			this.setArchivesNb(json.data.nbArchives);

		} catch (err) {
			Dolibarr.tools.setEventMessage(Dolibarr.tools.langs.trans('QuickMemoLoadMemosError') + ' : ' + err, 'errors');
			console.error('QuickMemo load error', err);
		}
	}

	/**
	 * Updates the internal counter and the DOM badge for archived memos.
	 * @param {number|string} nb - Number of archived items.
	 */
	setArchivesNb(nb) {
		this.countedArchivedMemo = parseInt(nb);

		// Update badge
		if (document.getElementById('quick-note-archives-link-mumber')) {
			const nbArchiveBadge = document.getElementById('quick-note-archives-link-mumber');
			nbArchiveBadge.textContent = this.countedArchivedMemo;
		}
	}

	/**
	 * Finds the memo data object associated with a DOM element.
	 * @param {HTMLElement} el
	 * @returns {Object|boolean}
	 */
	getMemoFromElement(el) {
		const m = this.memos.find(x => x.el === el);
		if (!m) return false;
		return m;
	}

	/**
	 * Places the selected note at the end of the stack and re-indexes all notes from 1 to N.
	 * @param {HTMLElement} el
	 */
	bringToTop(el) {
		const m = this.getMemoFromElement(el);
		if (!m) return;

		// 1. Sort all notes by their current z-index
		const sortedMemos = [...this.memos].sort((a, b) =>
			(parseInt(a.memo.pos_z) || 0) - (parseInt(b.memo.pos_z) || 0)
		);

		// 2. Remove the current note from its position and push it to the end (the top)
		const others = sortedMemos.filter(x => x !== m);
		others.push(m);

		// 3. Re-assign clean indexes (1 to N) to everyone
		others.forEach((x, i) => {
			const newZ = i + 1;
			x.memo.pos_z = newZ;
			x.el.style.zIndex = this.currentZ + newZ;
		});

	}


	/* ===============================
	   RENDER
	================================= */

	/**
	 * Creates and appends the DOM structure for a memo.
	 * @async
	 * @param {Object} memo - The data object for the note.
	 * @returns {HTMLElement} The created note element.
	 */
	async renderMemo(memo) {

		const note = document.createElement('div');
		this.memos.push({memo, el: note});

		note.className = 'quickmemo-note';
		note.dataset.id = memo.id;


		let posX = parseInt(memo.pos_x);
		let posY = parseInt(memo.pos_y);
		let posW = parseInt(memo.pos_w);
		let posH = parseInt(memo.pos_h);

		const containerWidth = this.container.clientWidth;

		// Prevent note from appearing outside of the viewport
		if (posX + posW > containerWidth) {
			posX = Math.max(32, containerWidth - posW - 32);
		}

		note.style.position = 'absolute';
		note.style.left = posX + 'px';
		note.style.top = posY + 'px';
		note.style.width = posW + 'px';
		note.style.height = posH + 'px';
		// note.style.background = memo.color || '#fff8a6';
		// note.style.setProperty('--memo-color', memo.color || '#fff8a6');
		this.setMemoColor(note, memo.color);
		if (parseInt(memo.id) === 0) { // Uniquement pour les nouvelles notes en cours de création
			const maxZ = this.memos.length > 0 ? Math.max(...this.memos.map(x => parseInt(x.memo.pos_z) || 0)) : 0;
			memo.pos_z = maxZ + 1;
		}
		note.style.zIndex = (this.currentZ || 0) + parseInt(memo.pos_z);


		const noteHeader = document.createElement('div');
		noteHeader.className = 'quickmemo-header';

		/* ================= MEMO FOOTER & ACTIONS ================= */

		const noteFooter = document.createElement('div');
		noteFooter.className = 'quickmemo-footer';

		const meta = document.createElement('div');
		meta.className = 'quickmemo-meta';
		const hasDateChange = memo.date_change && memo.date_change.trim() !== '' && memo.date_creation !== memo.date_change;
		const hasUserChange = memo.user_change_name && memo.user_change_name.trim() !== '' && memo.user_name !== memo.user_change_name;

		let updateBlock = '';

		// Template for "Modified on/by" information
		updateBlock = `
			<span class="quickmemo-info__date_update">
				${hasDateChange ? `
					<span class="quickmemo-info__date_update">
						${await Dolibarr.tools.langs.trans('QuickMemoModified')} : ${memo.date_change}
					</span>
				` : ''}
				${hasUserChange ? `
					<span class="quickmemo-info__user-change-name">
						${await Dolibarr.tools.langs.trans('QuickMemoBy')} ${memo.user_change_name}
					</span>
				` : ''}
			</span>
		`;


		meta.innerHTML = `
			<div class="quickmemo-info">
				<span class="quickmemo-info__create">
					<span class="quickmemo-info__user-create-name">${memo.user_name || ''}</span>
					<span class="quickmemo-info__date_create">${memo.date_creation || ''}</span>
				</span>
				${updateBlock}
			</div>
		`;

		const actions = document.createElement('div');
		actions.className = 'quickmemo-actions';

		/* ===== ARCHIVE ===== */
		const btnArchiveTitleLangs = {
			QuickMemoActionDelete : await Dolibarr.tools.langs.transNoEntities('QuickMemoActionDelete'),
			QuickMemoActionDeleteEmpty : await Dolibarr.tools.langs.transNoEntities('QuickMemoActionDeleteEmpty'),
			QuickMemoActionArchive : await Dolibarr.tools.langs.transNoEntities('QuickMemoActionArchive'),
		}

		const btnArchive = document.createElement('button');
		btnArchive.className = 'btn-low-emphasis --btn-icon quickmemo-btn-archive';
		btnArchive.innerHTML = `<i class="${this.param.archiveFontAIcon}"></i>`;
		btnArchive.title = btnArchiveTitleLangs.QuickMemoActionArchive;
		this.initTooltips(btnArchive);

		/** Updates btn txt based onshift key */
		const updateBtnArchiveTitle = async () => {
			let isHoverArchive = false;

			const updateArchiveTitle = (forceDelete = false) => {
				const textarea = note.querySelector('textarea');
				const textLength = textarea.value.trim().length;

				if (forceDelete) {
					btnArchive.title = btnArchiveTitleLangs.QuickMemoActionDelete;
				} else if (textLength === 0) {
					btnArchive.title = btnArchiveTitleLangs.QuickMemoActionDeleteEmpty;
				} else {
					btnArchive.title = btnArchiveTitleLangs.QuickMemoActionArchive;
				}

				// Refresh tooltip if needed
				btnArchive._quickmemoTooltipSync?.();
			};

			btnArchive.addEventListener('mouseenter', async (e) => {
				isHoverArchive = true;

				updateArchiveTitle(e.shiftKey);
			});

			btnArchive.addEventListener('mouseleave', () => {
				isHoverArchive = false;
			});

			document.addEventListener('keydown', async (e) => {
				if (e.key !== 'Shift' || !isHoverArchive) {
					return;
				}

				updateArchiveTitle(true);
			});

			document.addEventListener('keyup', async (e) => {
				if (e.key !== 'Shift' || !isHoverArchive) {
					return;
				}

				updateArchiveTitle(false);
			});
		};
		updateBtnArchiveTitle();

		btnArchive.addEventListener('click', async (e) => {
			e.stopPropagation();
			this.removeTooltips(btnArchive); // JQUERY tooltip need to be destroy because it active at this moment

			try {
				// If empty, delete permanently, otherwise archive
				const textarea = note.querySelector('textarea');
				const actionDelete =  (e.shiftKey || textarea.value.length === 0) ? 'delete' : 'archive';

				if (actionDelete === 'delete') {
					this.deleteWithAnimation(note);
				} else {
					this.archiveWithAnimation(note);
				}

				const res = await fetch(this.param.interfaceUrl + '?action=' + actionDelete, {
					method: 'POST',
					headers: {'Content-Type': 'application/x-www-form-urlencoded'},
					body: new URLSearchParams({
						id: note.dataset.id,
						token: this.param.token
					})
				});

				const json = await res.json();

				if (!json.result) {
					throw new Error(json.msg);
				}

				this.memos = this.memos.filter(m => m.el !== note);
			} catch (err) {
				Dolibarr.tools.setEventMessage(Dolibarr.tools.langs.trans('QuickMemoArchiveError') + ' : ' + err, 'errors');
				console.error('QuickMemo archive error', err);
			}
		});

		/* ===== TOGGLE SHARING ===== */

		const btnShare = document.createElement('button');
		btnShare.className = 'btn-low-emphasis --btn-icon quickmemo-btn-share';
		btnShare.title = await Dolibarr.tools.langs.transNoEntities('QuickMemoActionShareMode');
		this.initTooltips(btnShare);


		/** Updates icon based on sharing status */
		const updateShareIcon = async () => {
			note.setAttribute('data-shared', parseInt(memo.shared_on_element));
			btnShare.title = await Dolibarr.tools.langs.transNoEntities(memo.shared_on_element == 1 ? 'QuickMemoActionShareModeActive' : 'QuickMemoActionShareModeNotActive');
			btnShare.innerHTML = `<i class="${
				memo.shared_on_element == 1
					? this.param.sharedFontAIcon
					: this.param.notSharedFontAIcon
			}"></i>`;

			this.initTooltips(btnShare);
		};

		updateShareIcon();

		btnShare.addEventListener('click', async (e) => {
			e.stopPropagation();

			memo.shared_on_element = memo.shared_on_element == 1 ? 0 : 1;

			updateShareIcon();

			await fetch(this.param.interfaceUrl + '?action=update-shared-on-element', {
				method: 'POST',
				headers: {'Content-Type': 'application/x-www-form-urlencoded'},
				body: new URLSearchParams({
					id: note.dataset.id,
					shared_on_element: memo.shared_on_element,
					token: this.param.token
				})
			});
		});


		/* ===== SET AS TEMPLATE ===== */
		const btnCreateModel = document.createElement('button');
		btnCreateModel.className = 'btn-low-emphasis --btn-icon';
		btnCreateModel.innerHTML = `<i class="${this.param.modelFontAIcon}"></i>`;
		btnCreateModel.title = await Dolibarr.tools.langs.transNoEntities('QuickMemoCreateAModelFromThisMemo');
		this.initTooltips(btnCreateModel);

		btnCreateModel.addEventListener('click', () => this.openCreateModelDialog(memo, note));

		/* ===== TOGGLE PRIVATE ===== */

		const btnPrivate = document.createElement('button');
		btnPrivate.className = 'btn-low-emphasis --btn-icon quickmemo-btn-private';
		btnPrivate.title = await Dolibarr.tools.langs.transNoEntities('QuickMemoActionPrivateMode');
		this.initTooltips(btnPrivate);

		/** Updates icon based on privacy status */
		const updatePrivateIcon = async () => {
			note.setAttribute('data-private', parseInt(memo.private));

			btnPrivate.title = await Dolibarr.tools.langs.transNoEntities(memo.private == 1 ? 'QuickMemoActionPrivateModeActive' : 'QuickMemoActionPrivateModeNotActive');
			btnPrivate.innerHTML = `<i class="${
				parseInt(memo.private) === 1
					? this.param.privateFontAIcon
					: this.param.publicFontAIcon
			}"></i>`;
			this.initTooltips(btnPrivate);
		};

		updatePrivateIcon();

		btnPrivate.addEventListener('click', async (e) => {
			e.stopPropagation();

			memo.private = parseInt(memo.private) === 1 ? 0 : 1; // Toggle private status

			updatePrivateIcon();

			await fetch(this.param.interfaceUrl + '?action=update-private', {
				method: 'POST',
				headers: {'Content-Type': 'application/x-www-form-urlencoded'},
				body: new URLSearchParams({
					id: note.dataset.id,
					private: memo.private,
					token: this.param.token
				})
			});
		});

		/* ===== COLOR PICKER ===== */

		const colorListId = 'quickmemo-color-list';

		// Create a datalist for predefined colors if not already existing
		if (!document.getElementById(colorListId)) {
			const datalist = document.createElement('datalist');
			datalist.id = colorListId;

			this.param.colors.forEach(c => {
				const option = document.createElement('option');
				option.value = c;
				datalist.appendChild(option);
			});

			document.body.appendChild(datalist);
		}

		const colorInput = document.createElement('input');
		colorInput.type = 'color';
		colorInput.className = 'quickmemo-color-input';
		colorInput.value = memo.color || '#fff8a6';
		colorInput.setAttribute('list', colorListId);
		colorInput.title = await Dolibarr.tools.langs.trans('QuickMemoActionChangeColor');
		this.initTooltips(colorInput);

		colorInput.addEventListener('click', e => {
			e.stopPropagation(); // prevent triggering drag
		});

		colorInput.addEventListener('input', async (e) => {
			e.stopPropagation();

			const newColor = e.target.value;
			// note.style.background = newColor;
			// note.style.setProperty('--memo-color', newColor);
			this.setMemoColor(note, newColor);
			memo.color = newColor;

			await fetch(this.param.interfaceUrl + '?action=update-color', {
				method: 'POST',
				headers: {'Content-Type': 'application/x-www-form-urlencoded'},
				body: new URLSearchParams({
					id: note.dataset.id,
					color: memo.color,
					token: this.param.token
				})
			});
		});

		// Append buttons based on permissions
		if (this.param.userWriteRight) {
			actions.appendChild(btnPrivate);
			if(this.param.shareBtnStatus > 0) {
				actions.appendChild(btnShare);
			}
			actions.appendChild(colorInput);
			actions.appendChild(btnCreateModel);
		}

		if (this.param.userDeleteRight) {
			actions.appendChild(btnArchive);
		}

		noteHeader.appendChild(actions);
		note.appendChild(noteHeader);

		noteFooter.appendChild(meta);

		/* ================= TEXTAREA ================= */

		const textarea = document.createElement('textarea');
		textarea.value = memo.note || '';
		if (!this.param.userWriteRight) {
			textarea.readOnly = true;
		}

		textarea.placeholder = await Dolibarr.tools.langs.transNoEntities('QuickMemoEnterNote');
		textarea.addEventListener('input', () => this.adjustFontSize(note));
		note.appendChild(textarea);


		// MARKDOWN PREVIEW
		const preview = document.createElement('div');
		preview.className = 'quickmemo-markdown-preview';
		note.appendChild(preview);
		if (!textarea.value) {
			// Dynamically retrieve the placeholder
			const ph = textarea.getAttribute('placeholder') || '';
			await this.renderPreview(preview, `<span class="quickmemo-placeholder">${ph}</span>`, false);
		} else {
			await this.renderPreview(preview, textarea.value);
		}


		/** Toggles between edit mode (textarea) and view mode (markdown) */
		const setEditMode = (edit) => {
			note.classList.toggle('--edit-mode', edit);
			// set focus on textarea on edit mode
			if (edit) textarea.focus();
		};

		textarea.addEventListener('focus', () => setEditMode(true));
		textarea.addEventListener('blur', async (e) => {

			// Check if the focus is moving to a toolbar button
			if (e.relatedTarget && e.relatedTarget.closest('.quickmemo-editor-toolbar')) {
				return; // Do nothing, keep --edit-mode active
			}

			// Otherwise, close edit mode
			setTimeout(async () => {
				// ... votre logique de preview ...
				setEditMode(false); // This will automatically hide the toolbar via CSS
			}, 150);

			if (!textarea.value) {
				// Dynamically retrieve the placeholder
				const ph = textarea.getAttribute('placeholder') || '';
				await this.renderPreview(preview, `<span class="quickmemo-placeholder">${ph}</span>`, false);
			} else {
				await this.renderPreview(preview, textarea.value);
			}

		});

		// Interaction with the preview (checkboxes and links)
		preview.addEventListener('click', async (e) => {
			const target = e.target;

			// If a link is clicked or inside a link
			if (target.closest('a')) {
				e.stopPropagation(); // Prevent setEditMode
				return;
			} else if (target.classList.contains('markdown-checkbox')) {
				const lineNumber = target.closest('[data-line]').dataset.line;
				const lines = textarea.value.split('\n');
				const line = lines[lineNumber];
				const todoMatch = line.match(/^\[([ xX])\] (.*)$/);
				if (!todoMatch) return;

				const checked = todoMatch[1].toLowerCase() === 'x';
				lines[lineNumber] = `[${checked ? ' ' : 'x'}] ${todoMatch[2]}`;
				textarea.value = lines.join('\n');

				// Re-render preview
				await this.renderPreview(preview, textarea.value);

				// trigger envent input for bindSave
				const inputEvent = new Event('input', {bubbles: true});
				textarea.dispatchEvent(inputEvent);

				e.stopPropagation(); // Prevent setEditMode
			} else {
				setEditMode(true)
			}
		});


		const editorToolbar = document.createElement('div');
		editorToolbar.className = 'quickmemo-editor-toolbar';


		const btnBold = this.createToolbarButton(textarea,editorToolbar,'bold', 'fa-bold', Dolibarr.tools.langs.transNoEntities('Bold'));
		const btnItalic = this.createToolbarButton(textarea,editorToolbar,'italic', 'fa-italic', Dolibarr.tools.langs.transNoEntities('Italic'));
		const btnUnderline = this.createToolbarButton(textarea,editorToolbar,'underline', 'fa-underline', Dolibarr.tools.langs.transNoEntities('Underline'));
		const btnH1 = this.createToolbarButton(textarea,editorToolbar,'h1', 'fa-heading', Dolibarr.tools.langs.transNoEntities('H1'), '1');
		const btnH2 = this.createToolbarButton(textarea,editorToolbar,'h2', 'fa-heading', Dolibarr.tools.langs.transNoEntities('H2'), '1');
		const btnH3 = this.createToolbarButton(textarea,editorToolbar,'h3', 'fa-heading', Dolibarr.tools.langs.transNoEntities('H3'), '1');
		const btnCheck = this.createToolbarButton(textarea,editorToolbar,'task', 'fa-check-square', Dolibarr.tools.langs.transNoEntities('Checkbox'));

		this.createToolbarSeparator(editorToolbar)

		const btnEditorHelp = document.createElement('button');
		btnEditorHelp.type = 'button';
		btnEditorHelp.className = 'quickmemo-toolbar-btn';
		btnEditorHelp.innerHTML = `<i class="fa fa-question-circle" style="pointer-events: none;"></i>`;
		btnEditorHelp.title = Dolibarr.tools.langs.transNoEntities('Help');
		//<div class="data-tooltip" >${this.getMarkDownHelp()}</div>
		editorToolbar.appendChild(btnEditorHelp);

		btnEditorHelp.addEventListener('click', () => {
			const confirmOptions = {
				title: Dolibarr.tools.langs.trans('QuickMemoEnterNoteMdCompabible'),
				confirmLabel: Dolibarr.tools.langs.trans('CloseDialog'),
				customClass: 'dialog-lg',
			};
			this.openSimpleDialog(this.getMarkDownHelp(), confirmOptions);
		});


		note.appendChild(editorToolbar);


		textarea.addEventListener('keyup', () => this.updateToolbarState(textarea, editorToolbar));
		textarea.addEventListener('click', () => this.updateToolbarState(textarea, editorToolbar));
		textarea.addEventListener('select', () => this.updateToolbarState(textarea, editorToolbar));
		textarea.addEventListener('focus', () => this.updateToolbarState(textarea, editorToolbar));

		// END MARKDOWN


		note.appendChild(noteFooter);

		this.container.appendChild(note);
		this.adjustFontSize(note);

		const state = {isResizing: false};

		// Enable drag, resize and auto-save
		this.makeDraggable(note, state);
		this.makeResizable(note, memo, state);
		this.bindSave(textarea, note);


		return note;
	}

	createToolbarSeparator(toolbar, autoAppend = true) {
		const btn = document.createElement('hr');
		btn.className = 'quickmemo-toolbar-separator';

		if(autoAppend) {
			toolbar.appendChild(btn);
		}

		return btn;
	}

	createToolbarButton(textarea,toolbar, type, iconClass, title, autoAppend = true) {
		const btn = document.createElement('button');
		btn.type = 'button';
		btn.className = 'quickmemo-toolbar-btn';
		btn.dataset.type = type;
		let sub = '';
		if(type === 'h1') {
			sub = '<sub style="pointer-events: none;" >1</sub>';
		}else if(type === 'h2'){
			sub = '<sub style="pointer-events: none;">2</sub>';
		}else if(type === 'h3'){
			sub = '<sub style="pointer-events: none;">3</sub>';
		}

		btn.innerHTML = `<i class="fa ${iconClass}" style="pointer-events: none;"></i>${sub}`;
		btn.title = title;

		// On empêche le bouton de voler le focus définitivement,
		// mais on le laisse être le "relatedTarget"
		btn.addEventListener('mousedown', (e) => {
			e.preventDefault(); // Empêche le curseur de quitter le textarea
			this.applyMarkdown(textarea, type, toolbar);
		});

		if(autoAppend) {
			toolbar.appendChild(btn);
		}

		return btn;

	}

	/**
	 * Converts raw text into Markdown HTML and updates the preview area.
	 * @async
	 * @param {HTMLElement} preview - The preview container.
	 * @param {string} txt - The source text.
	 * @param {boolean} convertMd - Whether to apply markdown conversion.
	 */
	async renderPreview(preview, txt, convertMd = true) {
		// Re-render preview
		preview.innerHTML = convertMd ? await this.simpleMarkdown(txt) : txt;

		const badge = document.createElement('span');
		badge.classList.add('quickmemo-markdown-preview__badge');
		const checkboxes = preview.querySelectorAll('input[type="checkbox"]');

		const total = checkboxes.length;

		if (total === 0) {
			return;
		}

		const checked = preview.querySelectorAll('input[type="checkbox"]:checked').length;

		badge.textContent = checked + " / " + total;

		if (checked === total) {
			badge.classList.add("success");
		} else {
			badge.classList.remove("success");
		}

		preview.prepend(badge);
	}

	/**
	 * Visual effect: note flies towards the archive button and disappears.
	 * @param {HTMLElement} note
	 */
	archiveWithAnimation(note) {

		const target = document.getElementById(this.param.menuDropDownId);
		if (!target) {
			note.remove();
			return;
		}

		const noteRect = note.getBoundingClientRect();
		const targetRect = target.getBoundingClientRect();

		const deltaX = targetRect.left - noteRect.left;
		const deltaY = targetRect.top - noteRect.top;

		// Set current size to prevent jumps effect
		note.style.width = noteRect.width + 'px';
		note.style.height = noteRect.height + 'px';

		note.style.position = 'fixed';
		note.style.left = noteRect.left + 'px';
		note.style.top = noteRect.top + 'px';
		note.style.margin = 0;
		note.style.zIndex = 99999;

		note.style.transition = 'transform 400ms ease, opacity 400ms ease';
		note.style.transformOrigin = 'top left';

		requestAnimationFrame(() => {
			note.style.transform = `
            translate(${deltaX}px, ${deltaY}px)
            scale(0.2)
        `;
			note.style.opacity = '0';
		});

		note.addEventListener('transitionend', () => {
			note.remove();
			this.setArchivesNb(this.countedArchivedMemo + 1);
		}, {once: true});
	}


	/**
	 * Visual effect: note flies towards the template list when a model is created.
	 * @param {Object} memo
	 * @param {HTMLElement} note
	 */
	convertToModelWithAnimation(memo, note) {

		const dropDown = document.getElementById(this.param.menuDropDownId);
		if (!dropDown) {
			note.remove();
			return;
		}


		const modelListDiv = document.getElementById(this.param.menuDropDownModelListContainerId);
		if (!modelListDiv) {
			note.remove();
			return;
		}

		if (!memo) {
			note.remove();
			return;
		}


		setTimeout(async () => {
			await this.fetchMenuDropDown();
			dropDown.classList.add('open');

			const target = document.createElement('div');
			target.className = 'quickmemo-model-placeholder';
			modelListDiv.appendChild(target);


			const noteRect = note.getBoundingClientRect();
			const targetRect = target.getBoundingClientRect();

			const deltaX = targetRect.left - noteRect.left;
			const deltaY = targetRect.top - noteRect.top;

			// Set current size to prevent jumps effect
			note.style.width = noteRect.width + 'px';
			note.style.height = noteRect.height + 'px';

			note.style.position = 'fixed';
			note.style.left = noteRect.left + 'px';
			note.style.top = noteRect.top + 'px';
			note.style.margin = 0;
			note.style.zIndex = 99999;

			note.style.transition = 'transform 400ms ease, opacity 400ms ease';
			note.style.transformOrigin = 'top left';

			requestAnimationFrame(() => {
				note.style.transform = `
				translate(${deltaX}px, ${deltaY}px)
				scale(0.2)
       		`;
				note.style.opacity = '0';
			});

			note.addEventListener('transitionend', () => {
				note.remove();
				target.remove();// remove placeholder

				// Check if same btn already exists caused by menu open loading new model before animation ends
				const existingBtn = modelListDiv.querySelector(`.quickmemo-model-btn[data-model-id="${memo.id}"]`);
				if (!existingBtn) {
					const newBtn = this.generateAddMemoBtn(memo);
					this.attachDragEventsForModelReorder(newBtn);
					modelListDiv.appendChild(newBtn);
				}

			}, {once: true});
		}, 10); // hack to avoid close dropdown on click event
	}

	/**
	 * Triggers the deletion animation.
	 * @param {HTMLElement} note
	 */
	deleteWithAnimation(note) {
		note.classList.add('--note-delete-animation');
		note.addEventListener('animationend', () => {
			note.remove();
		}, {once: true});
	}

	/**
	 * Applies background color and calculates brightness to set a dark/light font class.
	 * @param {HTMLElement} note
	 * @param {string} color - Hex color string.
	 */
	setMemoColor(note, color) {
		// Default color if not valide
		if (!color || typeof color !== 'string') color = '#fff8a6';

		note.style.setProperty('--memo-color', color);

		// Check that it is a 6-character hex
		let c = color.startsWith('#') ? color.substring(1) : color;
		if (c.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(c)) {
			// invalide → considère clair par défaut
			note.classList.remove('--memo-is-dark');
			return;
		}

		// Conversion hex -> RGB
		const r = parseInt(c.substr(0, 2), 16);
		const g = parseInt(c.substr(2, 2), 16);
		const b = parseInt(c.substr(4, 2), 16);

		// Calculate brightness
		const brightness = (r * 299 + g * 587 + b * 114) / 1000;

		if (brightness < 128) {
			note.classList.add('--memo-is-dark');
		} else {
			note.classList.remove('--memo-is-dark');
		}
	}

	/**
	 * Dynamically resizes the font based on the content density within the note's dimensions.
	 * @param {HTMLElement} noteEl
	 */
	async adjustFontSize(noteEl) {
		if (!this.param.autoResizeFontSize) {
			return;
		}

		const textarea = noteEl.querySelector('textarea');
		const preview = noteEl.querySelector('.quickmemo-markdown-preview');

		const footerHeight = noteEl.querySelector('.quickmemo-footer')?.offsetHeight || 0;
		const headerHeight = noteEl.querySelector('.quickmemo-header')?.offsetHeight || 0;

		const contentHeight = noteEl.clientHeight - headerHeight - footerHeight;
		const contentWidth = noteEl.clientWidth;

		const textLength = textarea.value.length;

		const btnArchive = noteEl.querySelector('.quickmemo-btn-archive');
		if (btnArchive) {
			btnArchive.title = await Dolibarr.tools.langs.transNoEntities(textLength > 0 ? 'QuickMemoActionArchive' : 'QuickMemoActionDeleteEmpty');
			// Refresh tooltip if needed
			btnArchive._quickmemoTooltipSync?.();
		}

		// Calculate density
		const density = textLength / (contentWidth * contentHeight);

		// Linearly map density to font-size
		const densityMin = 0.0002;
		const densityMax = 0.005;
		const fontMin = this.param.autoResizeFontMin;
		const fontMax = this.param.autoResizeFontMax;

		// Clamp density between min & max
		const clampedDensity = Math.max(densityMin, Math.min(densityMax, density));

		// Linear interpolation
		let fontSize = fontMax - ((clampedDensity - densityMin) / (densityMax - densityMin)) * (fontMax - fontMin);
		// Round to the nearest 0.1 em
		fontSize = Math.round(fontSize * 10) / 10;

		textarea.style.fontSize = fontSize + 'em';
		preview.style.fontSize = fontSize + 'em';
	}


	// /**
	//  * Brings the element to the foreground on mouse click.
	//  * @param {HTMLElement} el
	//  */
	// bindFocusZ(el) {
	// 	el.addEventListener('mousedown', () => {
	// 		el.style.zIndex = ++this.currentZ;
	// 	});
	// }

	/* ===============================
	   DRAG
	================================= */

	/**
	 * Makes a note element draggable. Handles selection vs drag logic on textareas.
	 * @param {HTMLElement} el
	 * @param {Object} state - Reference to shared resizing state.
	 */
	makeDraggable(el, state) {

		let offsetX, offsetY, dragging = false;
		let hasMoved = false;


		let startX, startY;
		let dragTimeout = null;
		const startDragDelay = 300; // ms : temps d'attente au click avant de passer en mode drag
		const selectionThreshold = 5; // px : distance en pixels qui désactive le drag pour passer en mode selection
		const textarea = el.querySelector('textarea');

		// Auto-scroll configuration
		const scrollSpeed = 15;
		const scrollThreshold = 50;
		let scrollInterval = null;

		const stopAutoScroll = () => {
			if (scrollInterval) {
				clearInterval(scrollInterval);
				scrollInterval = null;
			}
		};

		const onMouseMove = (ev) => {
			if (!dragging) return;

			// 1. Calculate new positions
			let newX = ev.clientX + window.scrollX - offsetX;
			let newY = ev.clientY + window.scrollY - offsetY;

			// 2. Boundaries logic
			const boundaryW = Math.max(document.documentElement.scrollWidth, window.innerWidth);
			const boundaryH = Math.max(document.documentElement.scrollHeight, window.innerHeight);

			const maxX = boundaryW - el.offsetWidth;
			const maxY = boundaryH - el.offsetHeight;

			newX = Math.max(0, Math.min(newX, maxX));
			newY = Math.max(0, Math.min(newY, maxY));

			el.style.left = newX + 'px';
			el.style.top = newY + 'px';

			hasMoved = true;

			// 3. Auto-scroll logic
			const viewportY = ev.clientY;
			const viewportHeight = window.innerHeight;

			stopAutoScroll();

			// Scroll down if mouse is near bottom edge
			if (viewportY > viewportHeight - scrollThreshold) {
				scrollInterval = setInterval(() => {
					window.scrollBy(0, scrollSpeed);
					// Update position while scrolling even if mouse doesn't move
					let currentY = parseInt(el.style.top) + scrollSpeed;
					const currentMaxY = document.documentElement.scrollHeight - el.offsetHeight;
					el.style.top = Math.min(currentY, currentMaxY) + 'px';
				}, 16);
			}
			// Scroll up if mouse is near top edge
			else if (viewportY < scrollThreshold) {
				scrollInterval = setInterval(() => {
					window.scrollBy(0, -scrollSpeed);
					let currentY = parseInt(el.style.top) - scrollSpeed;
					el.style.top = Math.max(0, currentY) + 'px';
				}, 16);
			}
		};

		const cancelDrag = () => {
			if (dragTimeout) {
				clearTimeout(dragTimeout);
				dragTimeout = null;
			}
			dragging = false;
			el.classList.remove('--dragging');
		};

		const onMouseUp = () => {
			cancelDrag();

			document.removeEventListener('mousemove', onMouseMove);
			document.removeEventListener('mouseup', onMouseUp);

			if (hasMoved) this.saveAllPositions()
			hasMoved = false;
		};

		el.addEventListener('mousedown', e => {

			if (e.button !== 0) return; // Left click only
			if (state.isResizing) return;
			// if (el.style.cursor && el.style.cursor.includes('resize')) return;

			// DO NOT DRAG if mouse hover .quickmemo-actions
			if (e.target.closest('.quickmemo-actions')) return;

			//if (e.target.tagName === 'TEXTAREA') return;
			const isOnTextarea = e.target === textarea;

			this.bringToTop(el);

			hasMoved = false;

			const rect = el.getBoundingClientRect();
			startX = e.clientX + window.scrollX;
			startY = e.clientY + window.scrollY;
			offsetX = startX - (rect.left + window.scrollX);
			offsetY = startY - (rect.top + window.scrollY);

			if (isOnTextarea && !textarea.disabled && !textarea.readOnly) {

				// Delayed drag to allow text selection
				dragTimeout = setTimeout(() => {
					// Check if there is no selection
					const sel = window.getSelection();
					if (sel && sel.toString().length === 0) {
						dragging = true;
						el.classList.add('--dragging');

						// Cancel selection when dragging
						if (sel.rangeCount > 0) sel.removeAllRanges();
						// Remove focus so the cursor stops moving
						if (document.activeElement === textarea) textarea.blur();
					}
				}, startDragDelay);

				// Detect movement for text selection
				const onTempMove = (ev) => {
					const dx = ev.clientX - e.clientX;
					const dy = ev.clientY - e.clientY;
					if (Math.sqrt(dx * dx + dy * dy) > selectionThreshold) {
						cancelDrag(); // Cancel selection when selected
						cleanupTempMove();
					}
				};

				const cleanupTempMove = () => {
					document.removeEventListener('mousemove', onTempMove);
					dragTimeout = null;
				};
				document.addEventListener('mousemove', onTempMove, {once: true});

			} else {
				dragging = true;
				el.classList.add('--dragging');
				e.preventDefault();
			}

			document.addEventListener('mousemove', onMouseMove);
			document.addEventListener('mouseup', onMouseUp);

		});
	}

	/* ===============================
	   RESIZE
	================================= */

	/**
	 * Makes a note resizable from all edges and corners.
	 * @param {HTMLElement} el
	 * @param {Object} memo - Data object to update.
	 * @param {Object} state - Shared resizing state.
	 */
	makeResizable(el, memo, state) {

		const margin = 10;
		let direction = null;
		let startX, startY, startW, startH, startLeft, startTop;

		const onMouseMove = (e) => {
			if (!state.isResizing) return;

			let dx = e.clientX - startX;
			let dy = e.clientY - startY;

			let newW = startW;
			let newH = startH;
			let newLeft = startLeft;
			let newTop = startTop;

			// Handle different directions
			if (direction.includes('e')) {
				newW = startW + dx;
				newW = Math.min(newW, document.documentElement.scrollWidth - startLeft);
			}
			if (direction.includes('s')) {
				newH = startH + dy;
				newH = Math.min(newH, document.documentElement.scrollHeight - startTop);
			}
			if (direction.includes('w')) {
				newW = startW - dx;
				newLeft = startLeft + dx;
				if (newLeft < 0) {
					newW += newLeft; // Reduce width to avoid overflow
					newLeft = 0;
				}
			}
			if (direction.includes('n')) {
				newH = startH - dy;
				newTop = startTop + dy;
				if (newTop < 0) {
					newH += newTop; // Reduce height to avoid overflow
					newTop = 0;
				}
			}

			// Minimum dimensions constraint
			newW = Math.max(250, newW);
			newH = Math.max(100, newH);

			el.style.width = newW + 'px';
			el.style.height = newH + 'px';
			el.style.left = newLeft + 'px';
			el.style.top = newTop + 'px';

			this.adjustFontSize(el);

			memo.pos_w = newW;
			memo.pos_h = newH;
			memo.pos_x = newLeft;
			memo.pos_y = newTop;
		};

		const onMouseUp = () => {
			if (!state.isResizing) return;

			state.isResizing = false;
			document.body.style.userSelect = '';

			document.removeEventListener('mousemove', onMouseMove);
			document.removeEventListener('mouseup', onMouseUp);

			this.savePosition(el);
		};

		// Mouse movement detection for cursor change
		el.addEventListener('mousemove', (e) => {

			if (state.isResizing) return;

			const rect = el.getBoundingClientRect();
			const x = e.clientX;
			const y = e.clientY;

			const onLeft = x >= rect.left - margin && x <= rect.left + margin;
			const onRight = x >= rect.right - margin && x <= rect.right + margin;
			const onTop = y >= rect.top - margin && y <= rect.top + margin;
			const onBottom = y >= rect.bottom - margin && y <= rect.bottom + margin;

			direction = null;

			if (onRight && onBottom) direction = 'se';
			else if (onLeft && onBottom) direction = 'sw';
			else if (onRight && onTop) direction = 'ne';
			else if (onLeft && onTop) direction = 'nw';
			else if (onRight) direction = 'e';
			else if (onLeft) direction = 'w';
			else if (onBottom) direction = 's';
			else if (onTop) direction = 'n';

			el.style.cursor = getCursor(direction);
		});

		el.addEventListener('mousedown', (e) => {
			if (!direction) return;

			// DO NOT RESIZE if mouse hover .quickmemo-actions
			if (e.target.closest('.quickmemo-actions')) return;

			this.bringToTop(el);

			state.isResizing = true;

			const rect = el.getBoundingClientRect();
			startX = e.clientX;
			startY = e.clientY;
			startW = rect.width;
			startH = rect.height;
			startLeft = rect.left + window.scrollX;
			startTop = rect.top + window.scrollY;

			document.body.style.userSelect = 'none';

			document.addEventListener('mousemove', onMouseMove);
			document.addEventListener('mouseup', onMouseUp);

			e.preventDefault();
		});

		/** Helper to return CSS cursor string */
		function getCursor(dir) {
			switch (dir) {
				case 'se':
				case 'nw':
					return 'nwse-resize';
				case 'ne':
				case 'sw':
					return 'nesw-resize';
				case 'e':
				case 'w':
					return 'ew-resize';
				case 'n':
				case 's':
					return 'ns-resize';
				default:
					return 'default';
			}
		}
	}

	/* ===============================
	   SAVE
	================================= */

	/**
	 * Binds auto-save with debounce logic on textarea input.
	 * @param {HTMLTextAreaElement} textarea
	 * @param {HTMLElement} noteEl
	 */
	bindSave(textarea, noteEl) {

		let timeout;

		let m = this.getMemoFromElement(noteEl);

		textarea.addEventListener('input', () => {

			clearTimeout(timeout);

			timeout = setTimeout(() => {
				const id = noteEl.dataset.id;

				if (!id || id == 0) return; // Note not yet created in DB
				m.memo.note = textarea.value;
				this.saveNote(id, m.memo.note, noteEl);

			}, 600);
		});
	}

	/**
	 * Persists note position and dimensions to the server.
	 * @async
	 * @param {HTMLElement} el
	 */
	async savePosition(el) {
		const m = this.memos.find(x => x.el === el);
		if (!m) return;

		if (!el.dataset.id || parseInt(el.dataset.id) === 0) return; // not created yet in database

		try {

			// TODO send all others memo pos_z for update

			const res = await fetch(this.param.interfaceUrl + '?action=update_position', {
				method: 'POST',
				headers: {'Content-Type': 'application/x-www-form-urlencoded'},
				body: new URLSearchParams({
					id: el.dataset.id,
					x: parseInt(el.style.left),
					y: parseInt(el.style.top),
					w: parseInt(el.style.width),
					h: parseInt(el.style.height),
					z: parseInt(m.memo.pos_z) || 1,
					token: this.param.token
				})
			});

			const json = await res.json();

			if (!json.result) {
				throw new Error(json.msg);
			}

			Dolibarr.executeHook('QuickMemo:savePosition:saved', {el, json});

		} catch (err) {
			console.error('QuickMemo save position error', err);
			Dolibarr.tools.setEventMessage(Dolibarr.tools.langs.trans('QuickMemoSavePositionError') + ' : ' + err, 'errors');
		}
	}

	/**
	 * Saves the coordinates and Z-index of ALL memos on the page.
	 */
	saveAllPositions() {
		const memosData = this.memos.map(m => ({
			id: m.el.dataset.id,
			x: parseInt(m.el.style.left) || 0,
			y: parseInt(m.el.style.top) || 0,
			w: parseInt(m.el.style.width) || 0,
			h: parseInt(m.el.style.height) || 0,
			z: parseInt(m.memo.pos_z) || 1
		}));

		fetch(this.param.interfaceUrl + '?action=update_all_positions&token=' + this.param.token, {
			method: 'POST',
			headers: {'Content-Type': 'application/json'},
			body: JSON.stringify({
				memos: memosData
			})
		})
			.then(response => response.json())
			.then(data => {
				if (!data.result) console.error("Error saving all positions:", data.msg);
			})
			.catch(err => console.error("Fetch error:", err));
	}

	/**
	 * Persists note content to the server.
	 * @async
	 * @param {number|string} id
	 * @param {string} note
	 * @param {HTMLElement} noteEl
	 */
	async saveNote(id, note, noteEl) {

		let hookRes = await Dolibarr.executeHookAwait('QuickMemo:saveNote', {id, note});
		if (hookRes > 0) {
			return;
		}

		try {
			const res = await fetch(this.param.interfaceUrl + '?action=update_note', {
				method: 'POST',
				headers: {'Content-Type': 'application/x-www-form-urlencoded'},
				body: new URLSearchParams({
					id: id,
					note: note,
					token: this.param.token
				})
			});

			const json = await res.json();

			if (!json.result) {
				throw new Error(json.msg);
			}

			if (json.data && json.data.memo) {
				await this.updateMemoTxt(json.data.memo, noteEl);
			}

			Dolibarr.executeHook('QuickMemo:saveNote:saved', {id, note, json});

		} catch (err) {
			console.error('QuickMemo save note error', err);
			Dolibarr.tools.setEventMessage(Dolibarr.tools.langs.trans('QuickMemoSaveError') + ' : ' + err, 'errors');
		}
	}

	/* ===============================
	   VISIBILITY
	================================= */

	/**
	 * Checks all notes and ensures they are visible in the current container.
	 */
	ensureAllVisible() {
		this.memos.forEach(({el}) => this.ensureVisible(el));
	}

	/**
	 * Clamps a note's position to keep it within the horizontal boundaries.
	 * Vertical clamping is disabled to allow notes to be placed anywhere
	 * in long documents, as vertical scrolling is standard.
	 * @param {HTMLElement} el - The memo DOM element to check.
	 */
	ensureVisible(el) {
		// Get current CSS positions and actual element dimensions
		const posX = parseInt(el.style.left) || 0;
		const posW = el.offsetWidth;

		/**
		 * We use the maximum between the container width and the window width
		 * to determine the right boundary.
		 */
		const boundaryW = Math.max(this.container.scrollWidth, window.innerWidth);

		// 1. Horizontal constraint (Right border)
		// We only force the note back if it exceeds the right edge.
		if (posX + posW > boundaryW) {
			// Calculate a new position with a 32px safety margin
			let newLeft = boundaryW - posW - 32;

			// Ensure we don't push the note into negative space on the left
			el.style.left = Math.max(32, newLeft) + 'px';
		}

		// 2. Vertical constraint
		// Disabled: Let the note stay where it is, even if it's below the current body height.
		// The browser will naturally handle the scroll if needed.
	}

	/**
	 * Updates the metadata text (User/Date) in the memo DOM element.
	 * @async
	 * @param {Object} memo
	 * @param {HTMLElement} note
	 */
	async updateMemoTxt(memo, note) {
		// Sélectionne les éléments existants
		const userCreateEl = note.querySelector('.quickmemo-info__user-create-name');
		const dateCreateEl = note.querySelector('.quickmemo-info__date_create');
		const dateUpdateEl = note.querySelector('.quickmemo-info__date_update');
		const userChangeEl = note.querySelector('.quickmemo-info__user-change-name');

		// Mets à jour la création
		if (userCreateEl) userCreateEl.textContent = memo.user_name || '';
		if (dateCreateEl) dateCreateEl.textContent = memo.date_creation || '';

		// Détermine si des changements existent
		const hasDateChange = memo.date_change && memo.date_change.trim() !== '' && memo.date_creation !== memo.date_change;
		const hasUserChange = memo.user_change_name && memo.user_change_name.trim() !== '' && memo.user_name !== memo.user_change_name;

		// Mets à jour ou vide les infos de mise à jour
		if (dateUpdateEl) {
			dateUpdateEl.textContent = hasDateChange ? `${await Dolibarr.tools.langs.transNoEntities('QuickMemoModified')} : ${memo.date_change}` : '';
		}
		if (userChangeEl) {
			userChangeEl.textContent = hasUserChange ? `${await Dolibarr.tools.langs.transNoEntities('QuickMemoBy')} ${memo.user_change_name}` : '';
		}
	}

	/**
	 * Generates a button for the template list based on a model object.
	 * @param {Object} model
	 * @returns {HTMLElement} The created button.
	 */
	generateAddMemoBtn(model) {
		const dropdown = document.getElementById(this.param.menuDropDownId);
		const defaultModel = {
			id: false,
			name: "",
			color: null,
			note: "",
			pos_x: null,
			pos_y: null,
			pos_w: null,
			pos_h: null,
			pos_z: null,
			rank_tpl: 0,
			shared_on_element: "0",
			private: 0,
			date_creation: "",
			fk_user_creat: "",
			user_name: "",
			date_change: "",
			user_change_name: ""
		};

		model = {...defaultModel, ...model};

		const btn = document.createElement('button');
		btn.className = 'quickmemo-model-btn';
		btn.style.background = model.color || '#fff8a6';
		btn.dataset.modelId = model.id;
		btn.title = Dolibarr.tools.langs.transNoEntities('QuickMemoAdd')
			+ (model.name ? ` : ${model.name}` : '');

		// if(model.name) {
		// 	btn.title = `${model.name}`
		// 	this.initTooltips(btn);
		// }

		// ===== création du bouton delete =====
		const deleteBtn = document.createElement('span');
		deleteBtn.className = 'quickmemo-model-delete';
		deleteBtn.title = Dolibarr.tools.langs.transNoEntities('QuickMemoDeleteModel')
		btn.dataset.rank = model.rank_tpl || 0;

		btn.appendChild(deleteBtn);

		// DELETE
		if (this.param.userDeleteRight && model.id) {
			// Hover delay
			let hoverTimer = null;

			btn.addEventListener('mouseenter', (e) => {
				// If shiftKey is pressed, direct show
				if (e.shiftKey) {
					clearTimeout(hoverTimer); // In case of a timer already active
					deleteBtn.classList.add('--show');
				} else {
					// instead run timer
					hoverTimer = setTimeout(() => {
						deleteBtn.classList.add('--show');
					}, 800);
				}
			});

			btn.addEventListener('mouseleave', () => {
				clearTimeout(hoverTimer);
				deleteBtn.classList.remove('--show');
			});

			// Delete model
			deleteBtn.addEventListener('click', async (e) => {
				e.stopPropagation();
				e.preventDefault();

				// Fast delete if shiftKey is pressed, otherwise ask for confirmation
				if (!e.shiftKey) {
					const confirm = await this.openConfirmDialog(
						await Dolibarr.tools.langs.trans('QuickMemoConfirmDeleteModel') + (model.name ? ' <br/> <strong>' + this.escapeHTML(model.name) + '</strong>' : ''),
						{
							dialogClass: '--dialog-danger',
							btnConfirmClass: 'dialog-btn-destructive'
						}
					);

					if (!confirm) return;
				}

				try {
					const res = await fetch(this.param.interfaceUrl + '?action=delete_model', {
						method: 'POST',
						headers: {'Content-Type': 'application/x-www-form-urlencoded'},
						body: new URLSearchParams({
							id: model.id,
							token: this.param.token
						})
					});

					const json = await res.json();
					if (!json.result) throw new Error(json.msg);

					btn.remove();
				} catch (err) {
					Dolibarr.tools.setEventMessage(
						Dolibarr.tools.langs.trans('QuickMemoDeleteModelError') + ' : ' + err,
						'errors'
					);
				}
			});
		}


		// Create memo on template click
		btn.addEventListener('click', e => {
			e.preventDefault();
			this.createMemoFromModel(model);
			dropdown.classList.remove('open');
		});

		return btn;
	}

	/**
	 * Initialize standard tooltips.
	 *
	 * @param {HTMLElement|jQuery} root
	 */
	initTooltips(root) {

		if (!root) return;

		root.classList.add('dol-tooltip', '--dark-mode', '--tooltip-top');

		// Convert title -> data-title au moment de l'init
		const title = root.getAttribute('title');
		if (title) {
			root.setAttribute('data-title', title);
			root.setAttribute('aria-label', title);
			root.removeAttribute('title');
		}

		// Mise à jour à chaque survol (si le title a été modifié dynamiquement)
		const syncTitle = () => {
			const currentTitle = root.getAttribute('title');
			if (currentTitle) {
				root.setAttribute('data-title', currentTitle);
				root.setAttribute('aria-label', currentTitle);
				root.removeAttribute('title');
			}
		};

		root.addEventListener('mouseenter', syncTitle);
		root.addEventListener('focus', syncTitle);

		// Stocker les handlers pour pouvoir les retirer proprement
		root._quickmemoTooltipSync = syncTitle;

		// JQUERY STYLE
		// const tooltipClass = 'quick-memo-tooltip'
		// const $el = jQuery(root);
		// if ($el.data("ui-tooltip")) {
		// 	$el.tooltip("destroy");
		// }
		//
		// $el.tooltip({
		// 	tooltipClass: tooltipClass,
		// 	show: { collision: "flipfit", effect: "toggle", delay: 50, duration: 20 },
		// 	hide: { delay: 250, duration: 20 },
		// 	content: function () {
		// 		return $el.prop("title");
		// 	}
		// });
	}


	/**
	 * remove tooltips.
	 *
	 * @param {HTMLElement|jQuery} root
	 */
	removeTooltips(root) {

		if (!root) return;

		root.classList.remove('dol-tooltip', '--dark-mode', '--tooltip-top');

		// Restaurer title si data-title existe
		const dataTitle = root.getAttribute('data-title');
		if (dataTitle) {
			root.setAttribute('title', dataTitle);
		}

		root.removeAttribute('data-title');

		// Remove listeners
		if (root._quickmemoTooltipSync) {
			root.removeEventListener('mouseenter', root._quickmemoTooltipSync);
			root.removeEventListener('focus', root._quickmemoTooltipSync);
			delete root._quickmemoTooltipSync;
		}

		// JQUERY STYLE
		// const $el = jQuery(root);
		// if ($el.data("ui-tooltip")) {
		// 	$el.tooltip("destroy");
		// }
	}

	/**
	 * Initializes drag-and-drop reordering for the template list.
	 */
	initModelReorder() {
		if (!this.param.userWriteRight) return;

		const container = document.getElementById(this.param.menuDropDownModelListContainerId);
		if (!container) return;

		this._modelReorderContainer = container;

		// Placeholder unique pour tout le container
		if (!this._modelReorderPlaceholder) {
			this._modelReorderPlaceholder = document.createElement('div');
			this._modelReorderPlaceholder.className = 'quickmemo-model-placeholder';
		}

		container.querySelectorAll('.quickmemo-model-btn')
			.forEach(btn => this.attachDragEventsForModelReorder(btn));
	}

	/**
	 * Attaches native drag events to a template button for sorting.
	 * @param {HTMLElement} btn
	 */
	attachDragEventsForModelReorder(btn) {

		if (btn.dataset.reorderInit) return; // évite double bind
		btn.dataset.reorderInit = '1';

		const container = this._modelReorderContainer;
		const placeholder = this._modelReorderPlaceholder; // utiliser le placeholder unique

		btn.setAttribute('draggable', true);

		let dragged = null;

		btn.addEventListener('dragstart', (e) => {
			dragged = btn;
			btn.classList.add('--dragging');
			placeholder.style.height = btn.offsetHeight + 'px';
			container.insertBefore(placeholder, btn.nextSibling);
			setTimeout(() => btn.style.display = 'none', 0);
		});

		btn.addEventListener('dragend', () => {
			btn.classList.remove('--dragging');
			btn.style.display = '';

			if (!placeholder.parentNode) return;

			container.insertBefore(dragged, placeholder);
			placeholder.remove();

			this.saveModelOrder(dragged.dataset.modelId);
		});

		btn.addEventListener('dragover', (e) => {
			e.preventDefault();
			const rect = btn.getBoundingClientRect();
			const offset = e.clientY - rect.top;

			if (offset < rect.height / 2) {
				container.insertBefore(placeholder, btn);
			} else {
				container.insertBefore(placeholder, btn.nextSibling);
			}
		});
	}

	/**
	 * Persists the new sorting order of templates to the server.
	 * @async
	 * @param {number|string|null} movedId - ID of the element that was just moved.
	 */
	async saveModelOrder(movedId = null) {

		const container = document.getElementById(this.param.menuDropDownModelListContainerId);
		const buttons = [...container.querySelectorAll('.quickmemo-model-btn')];

		// recalcul complet des positions à partir du DOM
		const orderData = buttons.map((btn, index) => {

			const newRank = buttons.length - index; // cohérent avec ton ORDER BY rank DESC

			// on met à jour le dataset pour rester cohérent en mémoire
			btn.dataset.rank = newRank;

			return {
				id: btn.dataset.modelId,
				rank: newRank
			};
		});

		const payload = {order: orderData};

		// si on veut envoyer moved, on calcule sa vraie position
		if (movedId !== null) {

			const newIndex = buttons.findIndex(
				btn => btn.dataset.modelId == movedId
			);

			if (newIndex !== -1) {
				payload.moved = {
					id: movedId,
					newPos: newIndex + 1 // position visuelle 1 based
				};
			}
		}

		try {

			const res = await fetch(
				this.param.interfaceUrl + '?action=update_model_rank&token=' + this.param.token,
				{
					method: 'POST',
					headers: {'Content-Type': 'application/json'},
					body: JSON.stringify(payload)
				}
			);

			const json = await res.json();
			if (!json.result) throw new Error(json.msg);

		} catch (err) {

			console.error('QuickMemo save model order error', err);

			Dolibarr.tools.setEventMessage(
				Dolibarr.tools.langs.trans('QuickMemoSaveModelOrderError') + ' : ' + err,
				'errors'
			);
		}
	}

	/**
	 * Builds and injects the top menu dropdown into the Dolibarr interface.
	 * @async
	 */
	async initMenuMemoDropdown() {
		const DOL_VERSION = parseInt(Dolibarr.getContextVar('DOL_VERSION', false));
		const menuContainer = document.querySelector(DOL_VERSION > 20 || !DOL_VERSION ? '#id-top .login_block_tools .inline-block' : '#id-top .inline-block .login_block_elem');
		if (!menuContainer) return;

		const dropdown = document.createElement('div');
		dropdown.className = 'dropdown inline-block mod-quick-memo-menu';
		dropdown.id = 'quickmemo-create-dropdown'

		let textLoading = await Dolibarr.tools.langs.trans('Loading');
		let textNewNote = await Dolibarr.tools.langs.trans('NewNote');
		dropdown.innerHTML = `
		<a class="dropdown-toggle login-dropdown-a nofocusvisible" data-toggle="dropdown" href="#" title="${textNewNote}" >
			<i class="fa fa-sticky-note" aria-hidden="true"></i>
		</a>
		<div class="dropdown-menu dropdown-quickmemo-create">
			<div class="dropdown-header">
				<span class="dropdown-header-title">${await Dolibarr.tools.langs.trans('QuickMemoDropDownTitle')}</span>
				<span class="dropdown-header-actions"></span>
			</div>
			<div class="dropdown-body ">
				<div class="quickmemo-tpl-title">
					${await Dolibarr.tools.langs.trans('QuickMemoColorsPresets')}
				</div>
				<div id="${this.param.menuDropDownPresetListContainerId}" class="quickmemo-tpl-list">
					<div class="loading">${textLoading}...</div>
				</div>
				<div class="quickmemo-tpl-title">
					${await Dolibarr.tools.langs.trans('QuickMemoSavedTemplate')}
				</div>
				<div id="${this.param.menuDropDownModelListContainerId}"  class="quickmemo-tpl-list">
					<div class="loading">${textLoading}...</div>
				</div>
				${this.param.archivesUrl ? `
					<div class="quickmemo-archive-list">
						<a id="quick-note-archives-link" class="top-menu-dropdown-quicknote-link" href="${this.param.archivesUrl}" >${await Dolibarr.tools.langs.trans('QuickMemoShowArchives')}</a>
						<span id="quick-note-archives-link-mumber" class="badge badge-pill badge-light" >${this.countedArchivedMemo}</span>
					</div>
				` : ''}
			</div>

			<div class="dropdown-footer">

			</div>
		</div>
	`;

		menuContainer.prepend(dropdown);

		const dropDownMenuToggleBtn = dropdown.querySelector('.dropdown-toggle');
		const presetListDiv = document.getElementById(this.param.menuDropDownPresetListContainerId);
		presetListDiv.innerHTML = '';

		const modelListDiv = document.getElementById(this.param.menuDropDownModelListContainerId);
		modelListDiv.innerHTML = '';


		dropdown.setAttribute('data-loaded', 0);

		// Toggle ouverture / fermeture
		dropDownMenuToggleBtn.addEventListener('click', e => {
			e.preventDefault();
			e.stopPropagation();

			// LOAD DATA IN DROPDOWN MENU
			this.fetchMenuDropDown();

			// OPEN DROP DOWN
			dropdown.classList.toggle('open');
		});

		document.addEventListener('click', e => {
			if (!dropdown.contains(e.target)) {
				dropdown.classList.remove('open');
			}
		});


		const btnHelp = document.createElement('button');
		btnHelp.type = 'button';
		btnHelp.className = 'quickmemo-dropdown-btn btn-low-emphasis --btn-icon';
		btnHelp.innerHTML = `<i class="fa fa-question-circle" ></i>`;
		btnHelp.title = Dolibarr.tools.langs.transNoEntities('Help');

		const dropDownMenuHeader = dropdown.querySelector('.dropdown-quickmemo-create .dropdown-header .dropdown-header-actions');
		dropDownMenuHeader.appendChild(btnHelp);

		btnHelp.addEventListener('click', () => {
			const confirmOptions = {
				title: Dolibarr.tools.langs.trans('QuickMemoHelp_Title'),
				confirmLabel: Dolibarr.tools.langs.trans('CloseDialog'),
				customClass: 'dialog-lg',
			};
			this.openSimpleDialog(this.getDropDownHelp(), confirmOptions);
		});


		Dolibarr.executeHook('QuickMemo:initMenuMemoDropdown', {dropdown});
	}

	/**
	 * Fetches available presets and user-created models from the server for the dropdown menu.
	 * @async
	 * @param {boolean} force - Force refresh even if already loaded.
	 */
	async fetchMenuDropDown(force = false) {

		const dropDown = document.getElementById(this.param.menuDropDownId);
		const dropDownMenuToggleBtn = dropDown.querySelector('.dropdown-toggle');
		const presetListDiv = document.getElementById(this.param.menuDropDownPresetListContainerId);
		const modelListDiv = document.getElementById(this.param.menuDropDownModelListContainerId);

		// Check attribute flag, used to check if data already loaded ONCE
		if (dropDown.getAttribute('data-loaded') === "1" && !force) {
			return;
		}

		// Fetch models
		try {
			const res = await fetch(this.param.interfaceUrl + '?action=list_models', {
				method: 'POST',
				headers: {'Content-Type': 'application/x-www-form-urlencoded'},
				body: new URLSearchParams({
					element_type: this.param.elementType,
					context: this.param.context,
					token: this.param.token
				})
			});
			const json = await res.json();

			if (!json.result) {
				throw new Error(json.msg);
			}

			if (!json.data) {
				throw new Error('QuickMemo list models get empty data');
			}


			let models = json.data.modelTemplate.length ? json.data.modelTemplate : [];
			let presets = json.data.presetTemplate.length ? json.data.presetTemplate : [];

			// CREATE BTN TEMPLATE LIST
			if (!models.length) {
				modelListDiv.innerHTML = '<div class="no-models">' + await Dolibarr.tools.langs.trans('NoTemplatesFound') + '</div>';
			} else {
				models.forEach(model => {
					const btn = this.generateAddMemoBtn(model);
					modelListDiv.appendChild(btn);
				});
			}

			this.initModelReorder();

			// CREATE BTN FOR COLOR PRESETS LIST
			if (!presets.length) {
				modelListDiv.innerHTML = '<div class="no-models">' + await Dolibarr.tools.langs.trans('NoPresetsFound') + '</div>';
			} else {

				presets.forEach(preset => {
					const btn = this.generateAddMemoBtn(preset);
					presetListDiv.appendChild(btn);
				});

				// Quick add default note on double click
				dropDownMenuToggleBtn.addEventListener('dblclick', e => {
					e.preventDefault();
					this.createMemoFromModel(presets[0]);
					dropdown.classList.remove('open');
				});
			}

			// Set loaded data flag
			dropDown.setAttribute('data-loaded', 1);
		} catch (err) {
			Dolibarr.tools.setEventMessage(Dolibarr.tools.langs.trans('QuickMemoLoadModelError') + ' : ' + err, 'errors');
			console.error('QuickMemo load models error', err);
		}
	}

	/* ===============================
	   CREATION MEMO DEPUIS MODELE
	================================= */
	/**
	 * Creates a new note based on a specific template (model).
	 * @async
	 * @param {Object} model - The template data.
	 */
	async createMemoFromModel(model) {
		// 1. Trouver le z-index maximum actuel parmi les notes affichées
		const allMemos = this.container.querySelectorAll('.quick-memo-note'); // Adaptez le sélecteur si besoin
		let maxZ = 0;
		allMemos.forEach(el => {
			const z = parseInt(el.style.zIndex) || 0;
			if (z > maxZ) maxZ = z;
		});

		// La nouvelle note doit être au-dessus de tout le monde
		const newZ = maxZ + 1;


		// Crée un objet memo minimal à partir du modèle
		const memo = {
			id: 0, // rowid temporaire avant sauvegarde
			deleteBtn: false,
			note: model.note || '',
			pos_w: model.pos_w || 250,   // largeur par défaut
			pos_h: model.pos_h || 250,  // hauteur par défaut
			pos_x: model.pos_x || 0,
			pos_y: model.pos_y || 0,
			pos_z: newZ || 0,
			color: model.color || '#fff8a6',
			shared_on_element: model.shared_on_element !== undefined ? model.shared_on_element : 0,
			private: model.private !== undefined ? parseInt(model.private) : 0
		};

		// Center the new note in the current viewport
		const containerWidth = this.container.clientWidth;
		const containerHeight = this.container.clientHeight;
		const viewportHeight = window.innerHeight;
		const scrollY = window.scrollY || window.pageYOffset;

		memo.pos_x = Math.max(32, (containerWidth - memo.pos_w) / 2);
		memo.pos_y = scrollY + Math.max(32, (viewportHeight - memo.pos_h) / 2);

		// Render le memo
		const noteEl = await this.renderMemo(memo);
		this.bringToTop(noteEl);

		// Focus sur le textarea
		const textarea = noteEl.querySelector('textarea');
		if (textarea) {
			textarea.focus();
			textarea.setSelectionRange(textarea.value.length, textarea.value.length);
		}

		try {
			const res = await fetch(this.param.interfaceUrl + '?action=create', {
				method: 'POST',
				headers: {'Content-Type': 'application/x-www-form-urlencoded'},
				body: new URLSearchParams({
					element_id: this.param.elementId,
					element_type: this.param.elementType,
					context: this.param.context,
					note: memo.note,
					color: memo.color,
					private: memo.private,
					shared_on_element: memo.shared_on_element,
					token: this.param.token,
					x: parseInt(noteEl.style.left),
					y: parseInt(noteEl.style.top),
					w: parseInt(noteEl.style.width),
					h: parseInt(noteEl.style.height),
					z: parseInt(memo.pos_z)
				})
			});

			const json = await res.json();

			if (!json.result) {
				throw new Error(json.msg);
				return;
			}

			if (json.data && json.data.id) {
				memo.id = json.data.id;
				noteEl.dataset.id = json.data.id;
			}

			if (json.data && json.data.memo) {
				await this.updateMemoTxt(json.data.memo, noteEl);
			}

			Dolibarr.executeHook('QuickMemo:createMemoFromModel', {memo, noteEl, json});

		} catch (err) {
			Dolibarr.tools.setEventMessage(Dolibarr.tools.langs.trans('QuickMemoCreateError') + ' : ' + err, 'errors');
			console.error('QuickMemo create error', err);
		}
	}

	/**
	 * Escapes special HTML characters for safe rendering.
	 * @param {string} str
	 * @returns {string}
	 */
	escapeHTML(str) {
		return str.replace(/&/g, '&amp;')
			.replace(/</g, '&lt;')
			.replace(/>/g, '&gt;')
			.replace(/"/g, '&quot;')
			.replace(/'/g, '&#39;');
	}


	/**
	 * A basic markdown parser that handles titles, checkboxes, links, bold and italic.
	 * @async
	 * @param {string} md - The raw markdown text.
	 * @returns {string} The resulting HTML string.
	 */
	async simpleMarkdown(md) {

		/**
		 * Handles inline styles like Bold, Italic, Underline, and Links.
		 * Can be used inside headers and checkboxes.
		 */
		const parseInline = (text) => {
			if (!text) return '';
			return this.escapeHTML(text)
				.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')    // **Bold**
				.replace(/__(.*?)__/g, '<u>$1</u>')                 // __Underline__
				.replace(/\*(.*?)\*/g, '<em>$1</em>')               // *Italique*
				.replace(/\[(.*?)\]\((.*?)\)/g, (full, txt, url) => { // [Link](url)
					const safeUrl = /^(https?:\/\/|#|\/)/.test(url) ? url : '#';
					return `<a href="${this.escapeHTML(safeUrl)}" target="_blank" rel="noopener">${this.escapeHTML(txt)}</a>`;
				});
		};

		const lines = md.split('\n');

		// Use .map to ensure every input line creates exactly one .markdown-line output
		let res = lines.map((line, idx) => {
			let html = '';
			let isEmpty = false;
			let match;

			const nextLine = (lines[idx + 1] || "").trim();
			const prevLine = (lines[idx - 1] || "").trim();
			const trimmedLine = line.trim();

			// CASE 1: Setext H1 - Current line is text followed by "==="
			if (trimmedLine !== "" && nextLine.match(/^={3,}$/)) {
				html = `<h1>${parseInline(line)}</h1>`;
			}
			// CASE 2: Setext H2 - Current line is text followed by "---"
			else if (trimmedLine !== "" && nextLine.match(/^-{3,}$/)) {
				html = `<h2>${parseInline(line)}</h2>`;
			}
				// CASE 3: The Underline line itself (=== or ---)
			// We check if the line above was a title to decide if we hide this line
			else if (trimmedLine.match(/^={3,}$/) && prevLine !== "") {
				html = ''; // Hide the syntax line
				isEmpty = true;
			} else if (trimmedLine.match(/^-{3,}$/) && prevLine !== "") {
				html = ''; // Hide the syntax line
				isEmpty = true;
			}
			// CASE 4: Standard ATX Titles (#, ##, ###)
			else if (match = line.match(/^(#{1,3})\s+(.*)$/)) {
				const level = match[1].length;
				html = `<h${level}>${parseInline(match[2])}</h${level}>`;
			}
			// CASE 5: Checkboxes [ ] or [x]
			else if (match = line.match(/^\[([ xX])\]\s+(.*)$/)) {
				const checked = match[1].toLowerCase() === 'x' ? 'checked' : '';
				html = `<input class="markdown-checkbox" type="checkbox" ${checked}> ${parseInline(match[2])}`;
			}
			// CASE 6: Regular Text or actually empty line
			else {
				if (trimmedLine === "") {
					html = '&nbsp;';
					isEmpty = true;
				} else {
					html = parseInline(line);
				}
			}

			// Maintain the div structure for scrolling/indexing consistency
			const classes = `markdown-line ${isEmpty ? 'empty-line' : ''}`;
			return `<div class="${classes}" data-line="${idx}">${html}</div>`;
		}).join('');

		// Hook for external extensions
		if (Dolibarr && Dolibarr.executeHookAwait) {
			await Dolibarr.executeHookAwait('QuickMemo:simpleMarkdown', {md, res});
		}

		return res;
	}

	/**
	 * Handles Toggle for Bold and Italic without infinite accumulation.
	 * @param {string} text - The selected text
	 * @param {string} type - 'bold' or 'italic'
	 * @returns {string} Processed text
	 */
	/**
	 * Toggles inline markdown with support for overlapping styles (***bold italic***)
	 */
	toggleInlineMarkdown(text, type) {
		const patterns = {
			bold: { tag: '**', regex: /^\*\*(.*)\*\*$/ },
			italic: { tag: '*', regex: /^(\*\*\*|(?<!\*)\*(?!\*))(.*)(\*\*\*|(?<!\*)\*(?!\*))$/ },
			underline: { tag: '__', regex: /^__(.*)__$/ }
		};

		const { tag, regex } = patterns[type];

		// Special case for Italic to not conflict with Bold
		if (type === 'italic') {
			// If it starts with *** (Bold+Italic) or exactly * (Italic only)
			if ((text.startsWith('***') && text.endsWith('***')) ||
				(text.startsWith('*') && !text.startsWith('**') && text.endsWith('*') && !text.endsWith('**'))) {
				return text.substring(1, text.length - 1);
			}
			return `*${text}*`;
		}

		// Standard case for Bold and Underline
		if (text.startsWith(tag) && text.endsWith(tag)) {
			return text.substring(tag.length, text.length - tag.length);
		}

		return tag + text + tag;
	}

	/**
	 * Inserts text at cursor position while trying to preserve Undo/Redo history.
	 * Falls back to setRangeText if execCommand is not supported.
	 * * @param {HTMLTextAreaElement} textarea
	 * @param {string} text - The text to insert
	 * @param {number} selectionMode - 'select', 'start', 'end', or 'preserve'
	 */
	/**
	 * Future-proof text insertion that respects the Undo stack.
	 * @param {HTMLTextAreaElement} textarea
	 * @param {string} text - The text to insert
	 * @param {string} selectionMode - 'select', 'start', 'end', or 'preserve' (for setRangeText)
	 * @param {boolean} forceReselect - Manual override to keep selection after execCommand
	 */
	safeInsertText(textarea, text, selectionMode = 'select', forceReselect = false) {
		textarea.focus();
		const start = textarea.selectionStart;
		let success = false;

		// 1. Try native command for Undo/Redo support
		if (document.queryCommandSupported && document.queryCommandSupported('insertText')) {
			success = document.execCommand('insertText', false, text);
		}

		// 2. Fallback to modern API (SelectionMode is used here)
		if (!success) {
			// selectionMode 'select' keeps the new text highlighted
			textarea.setRangeText(text, start, textarea.selectionEnd, selectionMode);
			// We must manually trigger input for the preview to update
			textarea.dispatchEvent(new Event('input', { bubbles: true }));
		}

		// 3. Manual override for execCommand
		// Because execCommand usually moves the cursor to the end,
		// we force a re-selection if requested (very useful for Bold/Italic)
		if (forceReselect !== false) {
			if (forceReselect === 'select') {
				textarea.setSelectionRange(start, start + text.length);
				return;
			}
			else if (forceReselect === 'start') {
				textarea.setSelectionRange(start, start);
				return;
			}
			else if (forceReselect === 'end') {
				textarea.setSelectionRange(start + text.length, start + text.length);
				return;
			}
		}

		if (forceReselect === 'select' || (success && selectionMode === 'select')) {
			textarea.setSelectionRange(start, start + text.length);
		}
	}

	/**
	 * Applies Markdown formatting while preserving Undo history.
	 * Supports inline toggling (bold, italic, underline) and block toggling (h1, h2, h3, task).
	 * * @param {HTMLTextAreaElement} textarea - The source textarea
	 * @param {string} type - Formatting type (bold, italic, underline, h1, h2, h3, task)
	 * @param {HTMLElement} toolbar - Toolbar element for state updates
	 */
	applyMarkdown(textarea, type, toolbar) {
		const start = textarea.selectionStart;
		const end = textarea.selectionEnd;
		const val = textarea.value;
		const selectedText = val.substring(start, end);

		const inlineTags = { bold: '**', italic: '*', underline: '__' };

		// --- 1. Inline Formatting Logic ---
		if (inlineTags[type]) {
			const tag = inlineTags[type];
			let replacementText = "";
			let newCursorPos = start;

			if (selectedText.length > 0) {
				// Toggle existing formatting on selected text
				replacementText = this.toggleInlineMarkdown(selectedText, type);
			} else {
				// No selection: create a pair of tags and place cursor inside
				replacementText = tag + tag;
				newCursorPos = start + tag.length;
			}

			this.safeInsertText(textarea, replacementText, 'select');

			// Adjust cursor position if we just inserted empty tags (e.g., **|**)
			if (selectedText.length === 0) {
				textarea.setSelectionRange(newCursorPos, newCursorPos);
			}
		}
		// --- 2. Block/Line-Based Formatting Logic ---
		else {
			// Find line boundaries
			const lineStart = val.lastIndexOf('\n', start - 1) + 1;
			let lineEnd = val.indexOf('\n', end);
			if (lineEnd === -1) lineEnd = val.length;

			const lineText = val.substring(lineStart, lineEnd);
			const prefixes = { h1: '# ', h2: '## ', h3: '### ', task: '[ ] ' };
			const currentPrefix = prefixes[type];

			// Regex to match existing headers or checkboxes to allow "swapping" styles
			const prefixRegex = /^(\s*#{1,3}\s+|\[[ xX]\]\s+)/;

			let newLineText;
			if (lineText.startsWith(currentPrefix)) {
				// Toggle OFF: Remove the prefix
				newLineText = lineText.replace(currentPrefix, '');
			} else {
				// Toggle ON: Strip any existing block prefix and add the new one
				const cleanLine = lineText.replace(prefixRegex, '');
				newLineText = currentPrefix + cleanLine;
			}

			// Select the whole line and replace it via safeInsert to keep Undo history
			textarea.setSelectionRange(lineStart, lineEnd);
			this.safeInsertText(textarea, newLineText, 'end', (lineStart === lineEnd ? 'end' : 'start'));

			// Special UX: If it's a new empty task, put cursor after the checkbox
			if (type === 'task' && lineText.trim() === "") {
				const newCursorPos = lineStart + currentPrefix.length;
				textarea.setSelectionRange(newCursorPos, newCursorPos);
			}
		}

		// Update Toolbar & UI
		if (this.updateToolbarState) {
			this.updateToolbarState(textarea, toolbar);
		}
	}

	/**
	 * Updates toolbar buttons' active state based on the current selection or cursor position.
	 * Optimized for Post-it notes with support for Setext and Underline.
	 */
	updateToolbarState(textarea, toolbar) {
		const start = textarea.selectionStart;
		const end = textarea.selectionEnd;
		const text = textarea.value;
		const selectedText = text.substring(start, end);

		/**
		 * Helper to detect if the current context is formatted.
		 * It checks both the selection and the immediate surrounding characters.
		 */
		const isFormatActive = (type) => {
			// 1. Check if selection is wrapped
			if (selectedText.length > 0) {
				if (type === 'bold') return selectedText.startsWith('**') && selectedText.endsWith('**');
				if (type === 'underline') return selectedText.startsWith('__') && selectedText.endsWith('__');
				if (type === 'italic') {
					return (selectedText.startsWith('***') && selectedText.endsWith('***')) ||
						(selectedText.startsWith('*') && !selectedText.startsWith('**'));
				}
			}

			// 2. Check cursor context (peek 3 chars around)
			const before = text.substring(start - 3, start);
			const after = text.substring(end, end + 3);
			const context = before + "|" + after;

			if (type === 'bold') return context.includes('**|**') || context.includes('***|***');
			if (type === 'italic') return context.includes('*|*') || context.includes('***|***');
			if (type === 'underline') return context.includes('__|__');

			return false;
		};

		// Update UI
		const states = {
			bold: isFormatActive('bold'),
			italic: isFormatActive('italic'),
			underline: isFormatActive('underline')
		};

		Object.keys(states).forEach(type => {
			toolbar.querySelector(`[data-type="${type}"]`)?.classList.toggle('--active', states[type]);
		});

		// --- Line-based logic (H1, H2, Tasks) remains the same ---
		const lineStart = text.substring(0, start).lastIndexOf('\n') + 1;
		const currentLine = text.substring(lineStart, text.indexOf('\n', start) === -1 ? text.length : text.indexOf('\n', start)).trim();

		toolbar.querySelector('[data-type="h1"]')?.classList.toggle('--active', currentLine.startsWith('# '));
		toolbar.querySelector('[data-type="h2"]')?.classList.toggle('--active', currentLine.startsWith('## '));
		toolbar.querySelector('[data-type="h3"]')?.classList.toggle('--active', currentLine.startsWith('### '));
		toolbar.querySelector('[data-type="task"]')?.classList.toggle('--active', /^\[[ xX]\] /.test(currentLine));
	}

	getDropDownHelp() {
		return `
      <div class="quickmemo-help-content">

         <div class="help-section">
            <h4><i class="fa fa-eye-slash"></i> ${Dolibarr.tools.langs.trans('QuickMemoHelp_Visibility')}</h4>
            <p>${Dolibarr.tools.langs.trans('QuickMemoHelp_EscKey')}</p>
         </div>

         <div class="help-section">
            <h4><i class="far fa-copy"></i> ${Dolibarr.tools.langs.trans('QuickMemoHelp_Management')}</h4>
            <ul>
               <li>${Dolibarr.tools.langs.trans('QuickMemoHelp_DeleteHover')}</li>
               <li>${Dolibarr.tools.langs.trans('QuickMemoHelp_ShiftKeyDelete')}</li>
            </ul>
         </div>

         <div class="help-section">
            <h4><i class="fa fa-sort-numeric-down"></i> ${Dolibarr.tools.langs.trans('QuickMemoHelp_Organization')}</h4>
            <p>${Dolibarr.tools.langs.trans('QuickMemoHelp_DragDrop')}</p>
         </div>

         <p class="help-footer"><em>${Dolibarr.tools.langs.trans('QuickMemoHelp_Footer')}</em></p>
      </div>
   `;
	}

	/**
	 * Returns a help string describing the supported markdown syntax.
	 * @returns {string}
	 */
	getMarkDownHelp() {
		return `
      <div class="quickmemo-help-content">

         <div class="help-section universal-logic">
            <h4><i class="fa fa-globe"></i> ${Dolibarr.tools.langs.trans('QuickMemoMd_SectionUniversal')}</h4>
            <p>${Dolibarr.tools.langs.trans('QuickMemoMd_UniversalDesc')}</p>
         	<p class="help-intro"><strong>${Dolibarr.tools.langs.trans('QuickMemoMd_HelpIntro')}</strong></p>
         </div>


         <div class="help-section">
            <h4><i class="fa fa-text-height"></i> ${Dolibarr.tools.langs.trans('QuickMemoMd_SectionStyle')}</h4>
            <code>*${Dolibarr.tools.langs.trans('QuickMemoMd_Italic')}*</code> => <em>${Dolibarr.tools.langs.trans('QuickMemoMd_Italic')}</em><br/>
            <code>**${Dolibarr.tools.langs.trans('QuickMemoMd_Bold')}**</code> => <strong>${Dolibarr.tools.langs.trans('QuickMemoMd_Bold')}</strong>
         </div>

         <div class="help-section">
            <h4><i class="fa fa-heading"></i> ${Dolibarr.tools.langs.trans('QuickMemoMd_SectionTitles')}</h4>
            <code># ${Dolibarr.tools.langs.trans('QuickMemoMd_Title')} 1</code><br/>
            <code>## ${Dolibarr.tools.langs.trans('QuickMemoMd_Title')} 2</code><br/>
            <code>### ${Dolibarr.tools.langs.trans('QuickMemoMd_Title')} 3</code>
         </div>

         <div class="help-section">
            <h4><i class="fa fa-check-square"></i> ${Dolibarr.tools.langs.trans('QuickMemoMd_SectionTasks')}</h4>
            <code>[ ] ${Dolibarr.tools.langs.trans('QuickMemoMd_TaskNotDone')}</code> => <input type="checkbox" disabled> ...<br/>
            <code>[x] ${Dolibarr.tools.langs.trans('QuickMemoMd_TaskDone')}</code> => <input type="checkbox" checked disabled> ...
         </div>

         <div class="help-section">
            <h4><i class="fa fa-link"></i> ${Dolibarr.tools.langs.trans('QuickMemoMd_SectionLinks')}</h4>
            <code>[${Dolibarr.tools.langs.trans('QuickMemoMd_LinkText')}](url)</code> => <a href="#" onclick="return false;">${Dolibarr.tools.langs.trans('QuickMemoMd_LinkText')}</a>
         </div>

         <p class="help-footer"><em>${Dolibarr.tools.langs.trans('QuickMemoMd_HelpFooter')}</em></p>
      </div>
   `;
	}

	/**
	 * Opens a modal dialog to define parameters before creating a new template from a note.
	 * @param {Object} memo
	 * @param {HTMLElement} note
	 */
	openCreateModelDialog(memo, note) {

		const dialog = document.createElement('dialog');
		dialog.className = 'quickmemo-modal noselect';

		dialog.innerHTML = `
		<form>
			<header>
				<h3 class="quickmemo-modal-title">${Dolibarr.tools.langs.trans('QuickMemoAskCreateAModelFromThisMemo')}</h3>
			</header>

			<section>
				<div class="quickmemo-modal-input-group">
					<label class="quickmemo-modal-label">
						${Dolibarr.tools.langs.trans('QuickMemoModelName')}
					</label>
					<input
						class="quickmemo-modal-text-input"
						type="text"
						name="modelName"
						required
						placeholder="${Dolibarr.tools.langs.transNoEntities('QuickMemoModelNamePlaceHolder')}"
					/>
				</div>

				${this.param.elementId > 0 ? `
				<div class="quickmemo-modal-input-group">
					<label class="quickmemo-modal-label">
						<input type="checkbox" name="commonElementType" value="1"  />
						${Dolibarr.tools.langs.trans('QuickMemoAskLabelModelMemoIsCommon')}
					</label>
				</div>
				` : `<input type="hidden" name="commonElementType" value="0" />` }

				<div class="quickmemo-modal-input-group">
					<label class="quickmemo-modal-label">
						<input type="checkbox" name="privateModel" checked />
						${Dolibarr.tools.langs.trans('QuickMemoAskLabelModelMemoIsPrivate')}
					</label>
				</div>
			</section>

			<footer class="quickmemo-modal-actions">
				<button type="button" class="dialog-btn dialog-btn-secondary quickmemo-modal-actions-cancel-btn">
					${Dolibarr.tools.langs.trans('Cancel')}
				</button>
				<button type="button" class="dialog-btn dialog-btn-primary quickmemo-modal-actions-confirm-btn">
					${Dolibarr.tools.langs.trans('Confirm')}
				</button>
			</footer>
        </form>
    	`;

		document.body.appendChild(dialog);

		// Quand le dialog se ferme (Esc, cancel, close()), on le détruit
		dialog.addEventListener('close', () => {
			dialog.remove();
		});

		// Empêche le close automatique
		dialog.addEventListener('cancel', (e) => {
			e.preventDefault(); // optionnel
			dialog.close();
		});

		// Bouton Annuler
		dialog.querySelector('.quickmemo-modal-actions-cancel-btn')
			.addEventListener('click', () => {
				dialog.close();
				dialog.remove();
			});

		// Confirmer
		dialog.querySelector('.quickmemo-modal-actions-confirm-btn')
			.addEventListener('click', async () => {

				const commonInput = dialog.querySelector('[name=commonElementType]');
				const TplCommon = commonInput
					? (commonInput.type === 'checkbox' ? commonInput.checked : commonInput.value === '1')
					: false;
				const TplPrivate = dialog.querySelector('[name=privateModel]').checked;
				const TplModelName = dialog.querySelector('[name=modelName]').value || '';
				const elementType = TplCommon ? '' : this.param.elementType || '';

				let hookRes = await Dolibarr.executeHookAwait('QuickMemo:openCreateModelDialog:confirm', {
					dialog,
					memo,
					note,
					TplCommon,
					TplPrivate,
					TplModelName,
					elementType
				});
				if (hookRes > 0) {
					return;
				}

				try {
					const res = await fetch(this.param.interfaceUrl + '?action=create_model', {
						method: 'POST',
						headers: {'Content-Type': 'application/x-www-form-urlencoded'},
						body: new URLSearchParams({
							id: memo.id,
							element_type: elementType,
							token: this.param.token,
							tpl_private: TplPrivate,
							tpl_name: TplModelName
						})
					});

					const json = await res.json();

					if (!json.result) {
						throw new Error(json.msg);
					}

					Dolibarr.executeHook('QuickMemo:openCreateModelDialog:ModelCreated', {
						dialog,
						memo,
						note,
						TplCommon,
						TplPrivate,
						TplModelName,
						elementType,
						json
					});
				} catch (err) {

					Dolibarr.tools.setEventMessage(
						Dolibarr.tools.langs.trans('QuickMemoCreateError') + ' : ' + err,
						'errors'
					);

					console.error('Erreur fetch création modèle', err);

				} finally {
					dialog.close();
					this.convertToModelWithAnimation(memo, note);
				}
			});


		Dolibarr.executeHook('QuickMemo:openCreateModelDialog', {dialog, memo, note});

		dialog.showModal();
	}

	/**
	 * Specific confirmation dialog wrapper.
	 * @param {string} message - HTML content.
	 * @param {Object} param - Overrides for the confirmation UI.
	 * @returns {Promise<boolean>}
	 */
	openConfirmDialog(message, param = {}) {
		// Map old param names to the new config names if necessary,
		// or just set confirmation defaults.
		const confirmOptions = {
			title: param.dialogTitle || Dolibarr.tools.langs.trans('DialogAskConfirm'),
			customClass: 'noselect' + ( param.dialogClass || ''),
			confirmLabel: param.btnConfirmTxt || Dolibarr.tools.langs.trans('Confirm'),
			confirmClass: param.btnConfirmClass || 'btn-primary',
			btn: {
				confirm: true,
				cancel: true,
				close: true
			},
		};

		return this.openSimpleDialog(message, confirmOptions);
	}



	/**
	 * Generic dialog engine based on HTML5 <dialog>.
	 * @param {string} message - HTML content to display.
	 * @param {Object} options - UI parameters.
	 * @returns {Promise<boolean>} Resolves to true if confirmed, false otherwise.
	 */
	async openSimpleDialog(message, options = {}) {
		const config = {
			title: '',
			customClass: '',
			confirmLabel: Dolibarr.tools.langs.trans('Confirm'),
			confirmClass: 'btn-primary',
			cancelLabel: Dolibarr.tools.langs.trans('Cancel'),
			...options,
			btn: {
				close: true,
				confirm: true,
				cancel: false,
				...(options.btn || {}) // Merge seulement les boutons précisés dans options
			}
		};

		return new Promise((resolve) => {
			const dialog = document.createElement('dialog');
			dialog.className = `quickmemo-modal fade-dialog ${config.customClass}`;

			dialog.innerHTML = `
            <div class="dialog-content">
                <header >
                    <h3 class="quickmemo-modal-title">${config.title}</h3>
                    <div class="header-actions noselect">
                     ${config.btn.close ? `
                    	<button type="button" value="cancel" class="dialog-close-icon">
                    		&times;
                    	</button>` : ''}
                    </div>
                </header>
                <section class="dialog-body">
                    ${message}
                </section>
                <footer class="quickmemo-modal-actions noselect">
                    ${config.btn.cancel ? `
                        <button type="button" value="cancel" class="dialog-btn btn-secondary">
                            ${config.cancelLabel}
                        </button>` : ''}

                     ${config.btn.confirm ? `
                    <button type="button" value="confirm" class="dialog-btn ${config.confirmClass}">
                        ${config.confirmLabel}
                    </button>` : ''}
                </footer>
            </div>
        `;

			document.body.appendChild(dialog);
			dialog.showModal();

			const handleClose = (result) => {
				dialog.close();
				resolve(result);
			};

			// Unified event listener
			dialog.addEventListener('click', (e) => {
				const btn = e.target.closest('button[value]');
				if (btn) return handleClose(btn.value === 'confirm');
				if (e.target === dialog) handleClose(false); // Backdrop click
			});

			dialog.addEventListener('cancel', (e) => {
				e.preventDefault();
				handleClose(false);
			});

			dialog.addEventListener('close', () => dialog.remove());
		});
	}
}
