Processing Ajax...

Title

Message

Confirm

Confirm

Confirm

Confirm

Are you sure you want to delete this item?

Confirm

Are you sure you want to delete this item?

Confirm

Are you sure?
Save up to 33% on our desktop apps during our Black Friday Sale!Save up to 33% on our desktop apps during our Black Friday Sale, including DisplayFusion, ClipboardFusion, FileSeek, LogFusion, TrayStatus, and VoiceBot!Save up to 33% on our desktop apps during our Black Friday Sale!

ClipProcessor v1.0

Description
This macro opens a window that has many options for processing the text on the clipboard including line sorting, quote wrapping, and more.
Language
C#.net
Minimum Version
Created By
Alex399
Contributors
-
Date Created
6d ago
Date Last Modified
6d ago

Macro Code

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Linq;

public static class LocalizationManager
{
	private static Dictionary<string, Dictionary<string, string>> translations = new Dictionary<string, Dictionary<string, string>>();
	private static string currentLanguage = "en";
	private const string LANGUAGE_SETTING = "ClipProcessor_SelectedLanguage";
	public const string WORD_WRAP_SETTING = "ClipProcessor_WordWrapSetting";
	public const string FONT_NAME_SETTING = "ClipProcessor_FontName";
	public const string FONT_SIZE_SETTING = "ClipProcessor_FontSize";
	public const string FONT_STYLE_SETTING = "ClipProcessor_FontStyle";

	public const string WINDOW_WIDTH_SETTING = "ClipProcessor_WindowWidth";
	public const string WINDOW_HEIGHT_SETTING = "ClipProcessor_WindowHeight";

	public const string SINGLE_INSTANCE_SETTING = "ClipProcessor_SingleInstance";
	public const string ALWAYS_ON_TOP_SETTING = "ClipProcessor_AlwaysOnTop";

	public static void InitializeLanguage()
	{
		LoadTranslations();
		string savedLang = BFS.ScriptSettings.ReadValue(LANGUAGE_SETTING);
		if (!string.IsNullOrEmpty(savedLang) && translations.ContainsKey(savedLang))
		{
			currentLanguage = savedLang;
		}
		else
		{
			string systemLang = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
			if (translations.ContainsKey(systemLang))
			{
				currentLanguage = systemLang;
			}
			else
			{
				currentLanguage = "ru";
			}
		}
	}

	private static void LoadTranslations()
	{
		translations.Clear();
		translations["ru"] = new Dictionary<string, string>
		{
			{"windowTitle", "ClipProcessor"},
			{"menuOptions", "Опции"},
			{"submenuLanguage", "Язык"},
			{"menuLangRU", "Русский"},
			{"menuLangEN", "Английский"},
			{"menuWordWrap", "Переносить по словам"},
			{"menuFont", "Шрифт..."},
			{"menuWindowPosition", "Положение окна"},
			{"menuRememberWindowSize", "Запомнить размер окна"},
			{"menuRememberWindowPosition", "Запомнить положение окна"},
			{"menuAlwaysOnTop", "Поверх других окон"},
			{"menuSingleInstance", "Разрешить только одно окно"},
			{"menuHelp", "Справка"},
			
			{"dialogLanguageChanged", "Язык интерфейса изменен. Для полного применения некоторых изменений\nможет потребоваться перезапуск макроса."},
			{"dialogSingleInstanceActive", "Окно ClipProcessor уже открыто. Повторный вызов невозможен при активной опции 'Разрешить только одно окно'."},
			{"menuFraming", "Обрамление"},
			{"menuFramingQuotes", "Кавычки"},
			{"menuFramingGuillemets", "Кавычки-елочки"},
			{"menuFramingParentheses", "Скобки"},
			{"menuFramingSquareBrackets", "Квадратные скобки"},
			{"menuFramingOther", "Другое..."},
			
			{"dialogInputBracketsHint", "Введите символы обрамления. Формат: НАЧАЛО~~КОНЕЦ\nПример: <!--~~--> или НАЧАЛО~~ или ~~КОНЕЦ"},
			{"dialogInputBracketsError", "Неверный формат ввода. Используйте разделитель '~~'."},
			{"menuCase", "Регистр"},
			{"menuCaseUpper", "ВЕРХНИЙ РЕГИСТР"},
			{"menuCaseLower", "нижний регистр"},
			{"menuCaseTitle", "Каждое Слово С Большой Буквы"},
			{"menuCaseSentence", "Как в предложении"},
			{"menuWhitespace", "Пробелы"},
			{"menuWhitespaceTrimBoth", "Удалить начальные и концевые пробелы"},
			{"menuWhitespaceTrimStart", "Удалить начальные пробелы"},
			{"menuWhitespaceTrimEnd", "Удалить концевые пробелы"},
			{"menuWhitespaceReduceMultiple", "Сжать повторяющиеся пробелы"},
			{"menuLines", "Строки"},
			{"menuLinesSortAsc", "Сортировать по возрастанию"},
			{"menuLinesSortDesc", "Сортировать по убыванию"},
			{"menuLinesSortLengthAsc", "Сортировать по длине (возрастание)"},
			{"menuLinesSortLengthDesc", "Сортировать по длине (убывание)"},
			{"menuLinesRemoveDuplicates", "Удалить дубликаты (с учетом регистра)"},
			{"menuLinesRemoveDuplicatesIgnoreCase", "Удалить дубликаты (игнорировать регистр)"},
			{"menuLinesRemoveEmpty", "Удалить пустые строки"},
			{"menuLinesReverse", "Инвертировать порядок строк"},
			{"menuLinesNaturalSortAsc", "Естественная сортировка по возрастанию"},
			{"menuLinesNaturalSortDesc", "Естественная сортировка по убыванию"},
			{"menuLinesNumberLines", "Нумерация строк..."},
			{"menuLinesSplitByChar", "Разделить строки по символу..."},
			{"menuLinesRemoveContaining", "Удалить строки, содержащие текст..."},
			{"menuLinesKeepContaining", "Оставить строки, содержащие текст..."},
			{"menuLinesTrimChars", "Удалить символы в начале и конце..."},
			
			{"dialogInputSplitDelimiterHint", "Введите символ-разделитель (используйте '_space_' для пробела):"},
			{"dialogInputFilterLinesHint", "Введите текст для поиска (для regex используйте префикс r~):\nПример: мой_текст или r~^\\d+$"},
			{"dialogInputTrimCharsHint", "Удалить символы в начале и конце строк.\nФормат: НАЧАЛО~~КОНЕЦ (числа)\nПримеры: 2~~3 (2 с начала, 3 с конца), 5~~ (5 с начала), ~~2 (2 с конца)"},
			{"dialogInputTrimCharsError", "Неверный формат для обрезки символов. Ожидается ЧИСЛО~~ЧИСЛО, ЧИСЛО~~ или ~~ЧИСЛО."},
			{"dialogInputNumberLinesHint", "Введите формат нумерации. Примеры:\n#._space_ → 1. 2. 3.\n0#._space_ → 01. 02. 03.\n00#. → 001. 002. 003.\n[#] → [1] [2] [3]\n(#) → (1) (2) (3)\n\nНачальное значение:\n#._space_|0 → 0. 1. 2.\n#._space_|5 → 5. 6. 7.\n\nИспользуйте '_space_' для пробела."},
			{"dialogInputNumberLinesError", "Формат должен содержать символ # для номера строки."},
			{"menuJoinLines", "Объединить строки"},
			{"menuJoinLinesSpace", "Через пробел"},
			{"menuJoinLinesComma", "Через запятую"},
			{"menuJoinLinesSemicolon", "Через точку с запятой"},
			{"menuJoinLinesOther", "Другой..."},
			{"dialogInputJoinDelimiterHint", "Введите разделитель для объединения строк (используйте '_space_' для пробела):"},
			{"menuSearchReplace", "Поиск и замена"},
			{"menuSearchReplaceReplace", "Заменить..."},
			{"menuSearchReplaceDelete", "Удалить..."},
			{"menuSearchReplaceExtract", "Извлечь..."},
			{"menuSearchReplaceScrubHtml", "Удалить тэги HTML"},
			
			{"dialogInputReplaceHint", "Поиск и замена. Формат: НАЙТИ~~ЗАМЕНИТЬ\nRegex: НАЙТИ~r~ЗАМЕНИТЬ\nИспользуйте '_space_' для пробела (не в regex)."},
			{"dialogInputReplaceSeparatorError", "Ошибка: разделитель должен быть '~~' или '~r~'."},
			{"dialogInputReplaceFormatError", "Ошибка: требуется текст для поиска и замены."},
			{"dialogInputDeleteHint", "Удаление текста. Формат: ТЕКСТ или r~ШАБЛОН\nИспользуйте '_space_' для пробела (не в regex)."},
			{"dialogInputExtractHint", "Извлечение текста. Формат: ТЕКСТ или r~ШАБЛОН\nИспользуйте '_space_' для пробела (не в regex)."},
			{"buttonUndo", "Отменить"},
			{"buttonRedo", "Повторить"},
			{"buttonReset", "Сбросить"},
			{"buttonCancel", "Отмена"},
			{"buttonOK", "ОК"},
			
			{"dialogConfirmReset", "Вы уверены, что хотите сбросить текст к первоначальному состоянию?\nВсе изменения будут потеряны."},
			{"checkboxProcessLinesSeparately", "Обрабатывать каждую строку отдельно"},
			{"checkboxCommandChain", "Цепочка команд"},
			{"buttonExecute", "Выполнить"},
			{"buttonSaveProfile", "Сохранить профиль"},
			{"buttonSaveProfileDirty", "Сохранить профиль*"},
			{"tooltipDeleteProfile", "Удалить выбранный профиль"},
			{"tooltipDirtyIndicator", "Есть несохраненные изменения в профиле"},
			{"tooltipExpandCommandChain", "Развернуть поле команд"},
			{"tooltipCollapseCommandChain", "Свернуть поле команд"},
			
			{"dialogConfirmProfileDirtySaveChanges", "Сохранить изменения в профиле '{0}'?"},
			{"dialogConfirmProfileDirtyConsequence", "Выбранный профиль будет сохранен, все изменения будут потеряны, если вы выберете 'Нет'."},
			{"dialogConfirmProfileDirtySaveChangesOnExit", "Сохранить изменения в профиле '{0}' перед выходом?"},
			{"dialogConfirmProfileDirtyConsequenceOnExit", "Все несохраненные изменения будут потеряны, если вы выберете 'Нет'."},
			{"errorNoProfileSelected", "Нет выбранного профиля для удаления."},
			{"dialogConfirmDeleteProfile", "Вы уверены, что хотите удалить профиль '{0}'?"},
			{"dialogConfirmProfileDirtySwitch", "В профиле '{0}' есть несохраненные изменения. Сохранить перед переключением?"},
			{"statusBarWords", "Слов: {0}"},
			{"statusBarLines", "Строк: {0}"},
			{"statusBarChars", "Знаков: {0}"},
			{"statusBarWordsSelected", "Слов (выдел.): {0}"},
			{"statusBarLinesSelected", "Строк (выдел.): {0}"},
			{"statusBarCharsSelected", "Знаков (выдел.): {0}"},
			{"dialogRegexError", "Ошибка в регулярном выражении: {0}"},
			{"operationInitial", "Начальное состояние"},
			{"operationManualEdit", "Ручное редактирование"},
			{"operationCommandChain", "Цепочка команд"},
			{"operationReset", "Сброс к исходному тексту"},
			{"operationGeneric", "Операция"},
			{"operationFraming", "Обрамление"},
			{"operationCaseupper", "Верхний регистр"},
			{"operationCaselower", "Нижний регистр"},
			{"operationCasetitle", "Регистр заголовка"},
			{"operationCasesentence", "Регистр предложения"},
			{"operationTrimBoth", "Удаление пробелов"},
			{"operationTrimStart", "Удаление начальных пробелов"},
			{"operationTrimEnd", "Удаление конечных пробелов"},
			{"operationReduceMultiple", "Сжатие пробелов"},
			{"operationSortAsc", "Сортировка по возрастанию"},
			{"operationSortDesc", "Сортировка по убыванию"},
			{"operationSortLengthAsc", "Сортировка по длине (возрастание)"},
			{"operationSortLengthDesc", "Сортировка по длине (убывание)"},
			{"operationRemoveDuplicates", "Удаление дубликатов"},
			{"operationRemoveEmpty", "Удаление пустых строк"},
			{"operationReverse", "Инвертирование строк"},
			{"operationNaturalSortAsc", "Естественная сортировка вверх"},
			{"operationNaturalSortDesc", "Естественная сортировка вниз"},
			{"operationNumberLines", "Нумерация строк"},
			{"operationSplitLines", "Разделение строк"},
			{"operationFilterLines", "Фильтрация строк"},
			{"operationTrimChars", "Обрезка символов"},
			{"operationJoinLines", "Объединение строк"},
			{"operationReplace", "Замена текста"},
			{"operationDelete", "Удаление текста"},
			{"operationExtract", "Извлечение текста"},
			{"operationScrubHtml", "Удаление HTML"},
			{"quickCommandNotFound", "Команда {0} не найдена"},
			{"helpTitle", "Справка - ClipProcessor"},
			{"helpTabAbout", "О программе"},
			{"helpTabInterface", "Интерфейс"},
			{"helpTabCommands", "Цепочка команд"},		
			{"helpTabExternal", "Внешний вызов"},
			
			{"helpAbout", @"ClipProcessor — это многофункциональный макрос для ClipboardFusion, который предоставляет графический интерфейс для выполнения различных операций над текстом из буфера обмена, а также поддерживает пакетную обработку через 'цепочки команд' и полностью беззвучный вызов из других скриптов.

=== Основные возможности ===

— Обрамление текста: кавычки, «ёлочки», скобки, квадратные скобки или любые пользовательские символы.
— Изменение регистра: ВЕРХНИЙ, нижний, Каждое Слово С Большой Буквы, Как в предложении.
— Обработка пробелов: удаление начальных/концевых пробелов, сжатие повторяющихся пробелов.
— Работа со строками: сортировка (обычная и естественная), сортировка по длине, удаление дубликатов и пустых строк, инвертирование порядка, нумерация, разделение по символу, фильтрация (удаление/оставление строк по условию), обрезка символов с начала/конца.
— Объединение строк с различными разделителями (пробел, запятая, точка с запятой или любой другой).
— Поиск и замена: простая замена текста, замена по регулярным выражениям, удаление текста, извлечение совпадений, удаление HTML-тэгов.
— История операций: отмена/повтор до 20 действий.
— Профили команд: сохранение часто используемых цепочек команд с быстрым доступом (Alt+0 до Alt+9).
— Статистика: подсчёт слов, строк и символов для всего текста или выделенного фрагмента.
— Вызов команд с клавиатуры: Нажать и удерживать Ctrl и ввести код команды, например, Ctrl+10, Ctrl+25"
			},
			{
				"helpInterface", @"=== Описание интерфейса ===

Основное текстовое поле:
— Здесь отображается и редактируется обрабатываемый текст. Поддерживает отмену (Ctrl+Z) и повтор (Ctrl+Y) действий.

Главное меню:
— В меню Опции находятся настройки окна макроса и пункт вызова этой справки.
— Обрамление, Регистр, Пробелы и др.: Меню для выполнения конкретных операций над текстом. Рядом с каждой операцией в скобках указан её номер для использования в цепочках команд. Также команду можено вызвать с клавиатуры. Для этого нажмите и удерживайте Ctrl и введите код команды, например, Ctrl+15.

Панель опций (над текстом):
— `Обрабатывать каждую строку отдельно`: Если флажок установлен, большинство операций (например, обрамление) будут применяться к каждой строке индивидуально, а не ко всему тексту как единому блоку.
— `Цепочка команд`: Активирует продвинутый режим обработки текста.

Панель цепочки команд (появляется при активации режима):
— Поле ввода команд: Сюда вводится последовательность команд.
— Кнопка `Выполнить` (Ctrl+Shift+Enter): Запускает выполнение цепочки команд.
— Поле со списком профилей: Позволяет сохранять (вводить имя и нажимать `Сохранить профиль`), выбирать из списка, и удалять (`✕`) сохраненные цепочки. Звездочка (`*`) рядом с кнопкой сохранения и слева от списка профилей сигнализирует о наличии несохраненных изменений в текущем профиле.

=== Быстрый доступ к профилям ===

Для ускорения работы предусмотрена система быстрого вызова профилей через клавиатурные сочетания. Добавьте в начало названия профиля цифру от 0 до 9 в круглых скобках — например, '(0) Название профиля' или '(5) Название профиля' — и этот профиль станет доступен через комбинацию Alt + соответствующая цифра.

Примеры использования:
• '(0) Базовая обработка' → вызов по Alt+0
• '(1) Работа со строками' → вызов по Alt+1
• '(2) Поиск и замена' → вызов по Alt+2

Данная функция позволяет создать до 10 быстрых профилей (0–9) для наиболее часто используемых операций, существенно повышая эффективность работы с текстом.

5) Нижняя панель кнопок:
— `Отменить` (Ctrl+Z) / `Повторить` (Ctrl+Y): Стандартные действия отмены/повтора.
— `Сбросить`: Возвращает текст к его исходному состоянию (каким он был при открытии окна).
— `ОК` (Ctrl+Enter): Закрывает окно и возвращает измененный текст в буфер обмена.
— `Отмена` (Esc): Закрывает окно без сохранения изменений.

6) Строка состояния: Отображает статистику по всему тексту или по выделенному фрагменту: количество слов, строк и символов."
			},
			{
				"helpCommands", @"=== Режим 'Цепочка команд' ===

Этот режим позволяет выполнять несколько операций последовательно.

**Синтаксис:**

Команды перечисляются через разделитель `||`. Каждая команда состоит из номера (указан в меню) и, опционально, параметра.

`номер_команды_1||номер_команды_2!параметр||номер_команды_3`

**Параметры:**

Параметр отделяется от номера команды восклицательным знаком `!`. Формат параметра зависит от команды.

**Специальные символы в параметрах:**
    *   `~~`: Используется как разделитель в сложных параметрах (например, начало и конец обрамления).
    *   `~r~`: Используется как разделитель в команде 'Заменить' для указания, что поиск ведется по регулярному выражению.
    *   `r~`: Префикс, указывающий, что параметр является регулярным выражением.
    *   `_space_`: Используется для представления пробела в параметрах, где пробел может быть неоднозначен. Не используется в регулярных выражениях.

**Примеры команд с параметрами:**

5!<!--~~--> — обрамить комментариями HTML (НАЧАЛО~~КОНЕЦ)
5!<~~ — добавить только начальный символ
5!~~> — добавить только конечный символ

24!#._space_ — нумерация: 1. 2. 3.
24!0#._space_ — нумерация с нулями: 01. 02. 03.
24!#._space_|0 — начать с 0: 0. 1. 2.
24!0#._space_|5 — начать с 5: 05. 06. 07.

25!, — разделить строки по запятой
25!_space_ — разделить по пробелу

26!мусор — удалить строки со словом 'мусор'
26!r~[Мм]усор — удалить строки с 'мусор' или 'Мусор' (regex)

27!важное — оставить только строки со словом 'важное'
27!r~\d — оставить строки, содержащие цифры (regex)

28!2~~3 — удалить 2 символа с начала и 3 с конца
28!5~~ — удалить 5 символов с начала
28!~~5 — удалить 5 символов с конца

32!,_space_ — объединить через запятую и пробел

33!найти~~заменить — простая замена
33!(\w+)~r~[$1] — замена по regex

34!слово — удалить все вхождения 'слово'
34!r~<[^>]+> — удалить все HTML-теги (regex)

35!слово — извлечь все вхождения 'слово'
35!r~[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} — извлечь email-адреса (regex)

**Примеры цепочек команд:**

Пример 1: Базовая очистка списка
10||22||14||29 — удалить пробелы (10), удалить пустые строки (22), сортировать (14), объединить через пробел (29)

Пример 2: Извлечение и фильтрация email
35!r~[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}||15||22||20||26!r~(outlook|hotmail) — извлечь email (35), сортировать по убыванию (15), удалить пустые (22) и дубликаты (20), удалить outlook/hotmail (26)

Пример 3:
10||32!,_space_ — удалить пробелы (10), объединить через запятую с пробелом (32)

Пример 4: Замена разделителей
33!,~~;||34!_space_ — заменить запятую на точку с запятой (33), удалить все пробелы (34)

Пример 5: Подготовка нумерованного списка
10||22||14||24!#._space_ — удалить пробелы (10), удалить пустые строки (22), сортировать (14), добавить нумерацию (24)"
			},
			{
				"helpExternal", @"=== Внешний вызов ===

При прямом назначении ClipProcessor на триггер будет отображаться графический интерфейс макроса, что может быть нежелательно для автоматической фоновой обработки. Чтобы выполнять цепочки команд незаметно для пользователя, используйте промежуточный макрос-обертку.

Принцип работы:
1. Создайте новый макрос
2. В этом макросе вызовите ClipProcessor с параметром цепочки команд
3. Назначьте новый макрос на нужный триггер

При таком подходе ClipProcessor будет выполняться в фоновом режиме без показа интерфейса.

Формат входной строки:

COMMAND_CHAIN:цепочка_команд
<перевод_строки>
Обрабатываемый текст...

Пример: ""COMMAND_CHAIN:10||22||14\nТекст для обработки""

Синтаксис вызова из промежуточного макроса:

using System;
public static class ClipboardFusionHelper
{
	public static string ProcessText(string text)
	{
		// Определяем цепочку команд:
		// 10 — удалить начальные/концевые пробелы
		// 22 — удалить пустые строки
		// 14 — сортировать по возрастанию
		string commandChain = ""10||22||14"";

		// Формируем специальный формат для передачи команды
		string inputWithCommand = ""COMMAND_CHAIN:"" + commandChain + ""\n"" + text;

		// Результат обработки
		string processedText;

		// Вызываем макрос ClipProcessor (укажите правильное имя вашего макроса)
		if (BFS.ClipboardFusion.RunMacro(""ClipProcessor_v1.0"", inputWithCommand, out processedText))
		{
			return processedText; // Возвращаем обработанный текст
		}

		return text; // Возвращаем исходный текст, если обработка не удалась
	}
}"
			},
		};
		translations["en"] = new Dictionary<string, string>
		{
			{"windowTitle", "ClipProcessor"},

			// Options menu
			{"menuOptions", "Options"},
			{"submenuLanguage", "Language"},
			{"menuLangRU", "Russian"},
			{"menuLangEN", "English"},
			{"menuWordWrap", "Word Wrap"},
			{"menuFont", "Font..."},
			{"menuWindowPosition", "Window Position"},
			{"menuRememberWindowSize", "Remember window size"},
			{"menuRememberWindowPosition", "Remember window position"},
			{"menuAlwaysOnTop", "Always on top"},
			{"menuSingleInstance", "Allow only one window"},
			{"menuHelp", "Help"},
			
			{"dialogLanguageChanged", "Interface language has been changed.\nSome changes may require a macro restart to take full effect."},
			{"dialogSingleInstanceActive", "ClipProcessor window is already open. Multiple instances are disabled when 'Allow only one window' option is active."},

			// Framing menu
			{"menuFraming", "Framing"},
			{"menuFramingQuotes", "Quotes"},
			{"menuFramingGuillemets", "Guillemets"},
			{"menuFramingParentheses", "Parentheses"},
			{"menuFramingSquareBrackets", "Square Brackets"},
			{"menuFramingOther", "Other..."},
			
			{"dialogInputBracketsHint", "Enter framing characters. Format: START~~END\nExample: <!--~~--> or START~~ or ~~END"},
			{"dialogInputBracketsError", "Invalid input format. Use '~~' separator."},

			// Case menu
			{"menuCase", "Case"},
			{"menuCaseUpper", "UPPER CASE"},
			{"menuCaseLower", "lower case"},
			{"menuCaseTitle", "Title Case"},
			{"menuCaseSentence", "Sentence case"},
			
			// Whitespace menu
			{"menuWhitespace", "Whitespace"},
			{"menuWhitespaceTrimBoth", "Trim leading & trailing"},
			{"menuWhitespaceTrimStart", "Trim leading"},
			{"menuWhitespaceTrimEnd", "Trim trailing"},
			{"menuWhitespaceReduceMultiple", "Reduce multiple spaces"},
			
			// Lines menu
			{"menuLines", "Lines"},
			{"menuLinesSortAsc", "Sort ascending"},
			{"menuLinesSortDesc", "Sort descending"},
			{"menuLinesSortLengthAsc", "Sort by length (ascending)"},
			{"menuLinesSortLengthDesc", "Sort by length (descending)"},
			{"menuLinesRemoveDuplicates", "Remove duplicates (case-sensitive)"},
			{"menuLinesRemoveDuplicatesIgnoreCase", "Remove duplicates (ignore case)"},
			{"menuLinesRemoveEmpty", "Remove empty lines"},
			{"menuLinesReverse", "Reverse line order"},
			{"menuLinesNaturalSortAsc", "Natural sort ascending"},
			{"menuLinesNaturalSortDesc", "Natural sort descending"},
			{"menuLinesNumberLines", "Number lines..."},
			{"menuLinesSplitByChar", "Split lines by character..."},
			{"menuLinesRemoveContaining", "Remove lines containing text..."},
			{"menuLinesKeepContaining", "Keep lines containing text..."},
			{"menuLinesTrimChars", "Remove characters at start/end..."},
			
			{"dialogInputSplitDelimiterHint", "Enter delimiter character (use '_space_' for space):"},
			{"dialogInputFilterLinesHint", "Enter text to find (for regex use r~ prefix):\nExample: my_text or r~^\\d+$"},
			{"dialogInputTrimCharsHint", "Remove characters from start and end of lines.\nFormat: START~~END (numbers)\nExamples: 2~~3 (2 from start, 3 from end), 5~~ (5 from start), ~~2 (2 from end)"},
			{"dialogInputTrimCharsError", "Invalid format for trimming characters. Expected NUMBER~~NUMBER, NUMBER~~, or ~~NUMBER."},
			{"dialogInputNumberLinesHint", "Enter numbering format. Examples:\n#._space_ → 1. 2. 3.\n0#._space_ → 01. 02. 03.\n00#. → 001. 002. 003.\n[#] → [1] [2] [3]\n(#) → (1) (2) (3)\n\nStart value:\n#._space_|0 → 0. 1. 2.\n#._space_|5 → 5. 6. 7.\n\nUse '_space_' for space."},
			{"dialogInputNumberLinesError", "Format must contain # character for line number."},
			
			// Join Lines menu
			{"menuJoinLines", "Join Lines"},
			{"menuJoinLinesSpace", "With space"},
			{"menuJoinLinesComma", "With comma"},
			{"menuJoinLinesSemicolon", "With semicolon"},
			{"menuJoinLinesOther", "Other..."},
			
			{"dialogInputJoinDelimiterHint", "Enter delimiter for joining lines (use '_space_' for space):"},			
			
			// Search & Replace menu
			{"menuSearchReplace", "Search & Replace"},
			{"menuSearchReplaceReplace", "Replace..."},
			{"menuSearchReplaceDelete", "Delete..."},
			{"menuSearchReplaceExtract", "Extract..."},
			{"menuSearchReplaceScrubHtml", "Scrub HTML tags"},
			
			{"dialogInputReplaceHint", "Search and replace. Format: FIND~~REPLACE\nRegex: FIND~r~REPLACE\nUse '_space_' for space (non-regex)."},
			{"dialogInputReplaceSeparatorError", "Error: separator must be '~~' or '~r~'."},
			{"dialogInputReplaceFormatError", "Error: find and replace text required."},
			{"dialogInputDeleteHint", "Delete text. Format: TEXT or r~PATTERN\nUse '_space_' for space (non-regex)."},
			{"dialogInputExtractHint", "Extract text. Format: TEXT or r~PATTERN\nUse '_space_' for space (non-regex)."},

			// Buttons
			{"buttonUndo", "Undo"},
			{"buttonRedo", "Redo"},
			{"buttonReset", "Reset"},
			{"buttonCancel", "Cancel"},
			{"buttonOK", "OK"},
			
			{"dialogConfirmReset", "Are you sure you want to reset the text to its original state? All changes will be lost."},
			
			{"checkboxProcessLinesSeparately", "Process each line separately"},
			{"checkboxCommandChain", "Command chain"},
						
			{"buttonExecute", "Execute"},

			// Profiles
			{"buttonSaveProfile", "Save Profile"},
			{"buttonSaveProfileDirty", "Save Profile*"},
			{"tooltipDeleteProfile", "Delete selected profile"},
			{"tooltipDirtyIndicator", "Profile has unsaved changes"},
			{"tooltipExpandCommandChain", "Expand command field"},
			{"tooltipCollapseCommandChain", "Collapse command field"},
			
			{"dialogConfirmProfileDirtySaveChanges", "Save changes to profile '{0}'?"},
			{"dialogConfirmProfileDirtyConsequence", "All unsaved changes will be lost if you choose 'No'."},
			{"errorNoProfileSelected", "No profile selected to delete."},
			{"dialogConfirmDeleteProfile", "Are you sure you want to delete profile '{0}'?"},
			{"dialogConfirmProfileDirtySwitch", "Profile '{0}' has unsaved changes. Save before switching?"},

			// Status bar
			{"statusBarWords", "Words: {0}"},
			{"statusBarLines", "Lines: {0}"},
			{"statusBarChars", "Chars: {0}"},
			{"statusBarWordsSelected", "Words (sel.): {0}"},
			{"statusBarLinesSelected", "Lines (sel.): {0}"},
			{"statusBarCharsSelected", "Chars (sel.): {0}"},
			
			{"dialogRegexError", "Regex error: {0}"},

			// Operation names (for undo/redo)
			{"operationInitial", "Initial state"},
			{"operationManualEdit", "Manual edit"},
			{"operationCommandChain", "Command chain"},
			{"operationReset", "Reset to original"},
			{"operationGeneric", "Operation"},
			{"operationFraming", "Framing"},
			{"operationCaseupper", "Uppercase"},
			{"operationCaselower", "Lowercase"},
			{"operationCasetitle", "Title case"},
			{"operationCasesentence", "Sentence case"},
			{"operationTrimBoth", "Trim spaces"},
			{"operationTrimStart", "Trim leading"},
			{"operationTrimEnd", "Trim trailing"},
			{"operationReduceMultiple", "Reduce spaces"},
			{"operationSortAsc", "Sort ascending"},
			{"operationSortDesc", "Sort descending"},
			{"operationSortLengthAsc", "Sort by length (ascending)"},
			{"operationSortLengthDesc", "Sort by length (descending)"},
			{"operationRemoveDuplicates", "Remove duplicates"},
			{"operationRemoveEmpty", "Remove empty lines"},
			{"operationReverse", "Reverse lines"},
			{"operationNaturalSortAsc", "Natural sort ascending"},
			{"operationNaturalSortDesc", "Natural sort descending"},
			{"operationNumberLines", "Number lines"},
			{"operationSplitLines", "Split lines"},
			{"operationFilterLines", "Filter lines"},
			{"operationTrimChars", "Trim characters"},
			{"operationJoinLines", "Join lines"},
			{"operationReplace", "Replace text"},
			{"operationDelete", "Delete text"},
			{"operationExtract", "Extract text"},
			{"operationScrubHtml", "Scrub HTML"},
			
			// Quick command access
			{"quickCommandNotFound", "Command {0} not found"},

			//Help window
			{"helpTitle", "Help - ClipProcessor"},
			{"helpTabAbout", "About"},
			{"helpTabInterface", "Interface"},
			{"helpTabCommands", "Command Chain"},		
			{"helpTabExternal", "External Call"},
			{"helpAbout", @"ClipProcessor is a multifunctional macro for ClipboardFusion that provides a graphical interface for performing various operations on text from the clipboard, and supports batch processing via 'command chains' as well as entirely silent invocation from other scripts.

=== Key features ===

— Framing text: quotes, guillemets («»), parentheses, square brackets, or any custom characters.
— Changing case: UPPER, lower, Title Case, Sentence case.
— Handling spaces: removing leading/trailing spaces, reducing multiple spaces.
— Working with lines: sorting (regular and natural), sorting by length, removing duplicates and empty lines, reversing order, numbering, splitting by character, filtering (removing/keeping lines by condition), trimming characters from start/end.
— Joining lines with various delimiters (space, comma, semicolon, or any other).
— Search and replace: simple text replacement, regex replacement, text deletion, match extraction, HTML tag removal.
— Operation history: undo/redo up to 20 actions.
— Command profiles: save frequently used command chains with quick access (Alt+0 to Alt+9).
— Statistics: word, line, and character count for entire text or selected fragment.
— Calling commands from the keyboard: Press and hold Ctrl and enter the command code, for example, Ctrl+10, Ctrl+25."
			},
			{
				"helpInterface", @"=== Interface Description ===

Main text box:
— This is where the processed text is displayed and edited. Supports undo (Ctrl+Z) and redo (Ctrl+Y).

Main menu:
— The Options menu contains the macro window settings and a menu item to open this help.
— Framing, Case, Whitespace, and others: Menu groups for running specific text operations. Each operation has its number shown in parentheses for use in command chains. You can also call the command from the keyboard. To do this, press and hold Ctrl and enter the command code, for example, Ctrl+15.

Options panel (above the text):
— Process each line separately: When checked, most operations (for example, framing) are applied to each line individually rather than to the entire text as a single block.
— Command chain: Activates the advanced text processing mode.

Command chain panel (appears when the mode is enabled):
— Command input field: Enter the command sequence here.
— Execute button (Ctrl+Shift+Enter): Starts executing the command chain.
— Profiles list: Allows saving (enter a name and click Save Profile), selecting from the list, and deleting (✕) saved chains. An asterisk (*) next to the save button and to the left of the profiles list indicates there are unsaved changes in the current profile.

=== Quick Profile Access ===

To streamline your workflow, a quick profile access system via keyboard shortcuts is available. Add a digit from 0 to 9 in parentheses at the beginning of the profile name — for example, '(0) Profile Name' or '(5) Profile Name' — and this profile will become accessible through the Alt + corresponding digit combination.

Usage examples:
• '(0) Basic Processing' → invoked with Alt+0
• '(1) String Operations' → invoked with Alt+1
• '(2) Find and Replace' → invoked with Alt+2

This feature allows you to create up to 10 quick profiles (0–9) for the most frequently used operations, significantly improving text processing efficiency.

Bottom button panel:
— Undo (Ctrl+Z) / Redo (Ctrl+Y): Standard undo/redo actions.
— Reset: Reverts the text to its original state (as it was when the window was opened).
— OK (Ctrl+Enter): Closes the window and puts the modified text on the clipboard.
— Cancel (Esc): Closes the window without saving changes.

Status bar: Displays statistics for the entire text or the selected fragment: word, line, and character counts."
			},
			{
				"helpCommands", @"=== 'Command Chain' Mode ===
				
This mode lets you execute multiple operations in sequence.

**Syntax:**

Commands are specified in sequence using the || separator. Each command consists of a number (as shown in the menu) and, optionally, a parameter.

command_number_1||command_number_2!parameter||command_number_3

**Parameters:**

A parameter is separated from the command number by an exclamation mark !. The parameter format depends on the command.

**Special characters in parameters:**
    *   ~~: Used as a separator in compound parameters (e.g., start and end of framing).
    *   ~r~: Used as a separator in the 'Replace' command to indicate regex search.
    *   r~: Prefix indicating that the parameter is a regular expression.
    *   _space_: Used to represent a space in parameters where a plain space could be ambiguous. Not used in regular expressions.

**Parameter examples:**

5!<!--~~--> — frame with HTML comments (START~~END)
5!<~~ — add only start character
5!~~> — add only end character

24!#._space_ — numbering: 1. 2. 3.
24!0#._space_ — numbering with zeros: 01. 02. 03.
24!#._space_|0 — start from 0: 0. 1. 2.
24!0#._space_|5 — start from 5: 05. 06. 07.

25!, — split lines by comma
25!_space_ — split by space

26!trash — remove lines containing 'trash'
26!r~[Tt]rash — remove lines with 'trash' or 'Trash' (regex)

27!important — keep only lines containing 'important'
27!r~\d — keep lines containing digits (regex)

28!2~~3 — remove 2 chars from start and 3 from end
28!5~~ — remove 5 characters from start
28!~~5 — remove 5 characters from end

32!,_space_ — join with comma and space

33!find~~replace — simple replacement
33!(\w+)~r~[$1] — regex replacement

34!word — delete all occurrences of 'word'
34!r~<[^>]+> — delete all HTML tags (regex)

35!word — extract all occurrences of 'word'
35!r~[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} — extract email addresses (regex)

**Command chain examples:**

Example 1: Basic list cleanup
10||22||14||29 — trim spaces (10), remove empty lines (22), sort (14), join with space (29)

Example 2: Email extraction and filtering
35!r~[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}||15||22||20||26!r~(outlook|hotmail) — extract emails (35), sort descending (15), remove empty (22) and duplicates (20), remove outlook/hotmail (26)

Example 3:
10||32!,_space_ — trim spaces (10), join with comma and space (32)

Example 4: Delimiter replacement
33!,~~;||34!_space_ — replace comma with semicolon (33), remove all spaces (34)

Example 5: Numbered list preparation
10||22||14||24!#._space_ — trim spaces (10), remove empty lines (22), sort (14), add numbering (24)" 
			},
			{
				"helpExternal", @"=== External Call ===

When ClipProcessor is directly assigned to a trigger, the macro's graphical interface will be displayed, which may be undesirable for automatic background processing. To execute command chains silently without user interaction, use an intermediate wrapper macro.

Working principle:
1. Create a new macro
2. In this macro, invoke ClipProcessor with the command chain parameter
3. Assign the new macro to the desired trigger

With this approach, ClipProcessor will execute in the background without displaying the interface.

Input string format:

COMMAND_CHAIN:command_chain
<newline>
Text to process...

Example: ""COMMAND_CHAIN:10||22||14\nText to process""

Syntax for calling from the intermediate macro:

using System;
public static class ClipboardFusionHelper
{
	public static string ProcessText(string text)
	{
		// Define command chain:
		// 10 — trim leading/trailing spaces
		// 22 — remove empty lines
		// 14 — sort ascending
		string commandChain = ""10||22||14"";

		// Construct the special input format for passing the command
		string inputWithCommand = ""COMMAND_CHAIN:"" + commandChain + ""\n"" + text;

		// The processing result
		string processedText;

		// Invoke the ClipProcessor macro (specify the correct name of your macro)
		if (BFS.ClipboardFusion.RunMacro(""ClipProcessor_v1.0"", inputWithCommand, out processedText))
		{
			return processedText; // Return the processed text
		}

		return text; // Return the original text if processing failed
	}
}"
			},
		};
	}

	public static string GetString(string key)
	{
		if (translations.ContainsKey(currentLanguage) && translations[currentLanguage].ContainsKey(key))
		{
			return translations[currentLanguage][key];
		}
		else if (translations.ContainsKey("ru") && translations["ru"].ContainsKey(key))
		{
			return translations["ru"][key];
		}
		return $"[{key}]";
	}

	public static void SetLanguage(string langCode)
	{
		if (translations.ContainsKey(langCode))
		{
			currentLanguage = langCode;
			BFS.ScriptSettings.WriteValue(LANGUAGE_SETTING, langCode);
		}
	}

	public static string GetCurrentLanguage()
	{
		return currentLanguage;
	}

	public static string GetString(string key, params object[] args)
	{
		string formatString = GetString(key);
		try
		{
			return string.Format(formatString, args);
		}
		catch (FormatException)
		{
			return formatString;
		}
	}
}

public static class WindowInstanceManager
{
	private static MainWindow currentInstance = null;
	private static readonly object lockObject = new object();

	public static bool TryRegisterInstance(MainWindow window)
	{
		lock (lockObject)
		{
			if (currentInstance != null && !currentInstance.IsDisposed)
			{
				return false;
			}
			currentInstance = window;
			return true;
		}
	}

	public static void UnregisterInstance(MainWindow window)
	{
		lock (lockObject)
		{
			if (currentInstance == window)
			{
				currentInstance = null;
			}
		}
	}

	public static void ActivateExistingInstance()
	{
		lock (lockObject)
		{
			if (currentInstance != null && !currentInstance.IsDisposed)
			{
				if (currentInstance.InvokeRequired)
				{
					currentInstance.Invoke(new Action(() => ActivateWindowInternal()));
				}
				else
				{
					ActivateWindowInternal();
				}
			}
		}
	}

	private static void ActivateWindowInternal()
	{
		if (currentInstance.WindowState == FormWindowState.Minimized)
		{
			currentInstance.WindowState = FormWindowState.Normal;
		}

		currentInstance.TopMost = true;
		currentInstance.TopMost = false;
		currentInstance.BringToFront();
		currentInstance.Activate();
		currentInstance.Focus();
	}

	public static bool HasActiveInstance()
	{
		lock (lockObject)
		{
			return currentInstance != null && !currentInstance.IsDisposed;
		}
	}
}

public static class ClipboardFusionHelper
{
	private const string COMMAND_PREFIX = "COMMAND_CHAIN:";

	public static string ProcessText(string inputText)
	{
		LocalizationManager.InitializeLanguage();

		if (inputText.StartsWith(COMMAND_PREFIX))
		{
			int newLineIndex = inputText.IndexOf('\n');
			if (newLineIndex > 0)
			{
				string commandChain = inputText.Substring(COMMAND_PREFIX.Length, newLineIndex - COMMAND_PREFIX.Length).Trim();
				string actualText = inputText.Substring(newLineIndex + 1);

				if (string.IsNullOrEmpty(commandChain))
				{
					return actualText;
				}

				MainWindow window = new MainWindow(actualText);
				return window.ExecuteCommandsSilently(commandChain);
			}
		}

		bool singleInstanceEnabled = BFS.ScriptSettings.ReadValueBool(LocalizationManager.SINGLE_INSTANCE_SETTING);

		if (singleInstanceEnabled && WindowInstanceManager.HasActiveInstance())
		{
			WindowInstanceManager.ActivateExistingInstance();
			return inputText;
		}

		MainWindow guiWindow = new MainWindow(inputText);

		if (singleInstanceEnabled)
		{
			if (!WindowInstanceManager.TryRegisterInstance(guiWindow))
			{
				WindowInstanceManager.ActivateExistingInstance();
				guiWindow.Dispose();
				return inputText;
			}
		}

		if (guiWindow.ShowDialog() == DialogResult.OK)
		{
			return guiWindow.ResultText;
		}
		return inputText;
	}
}

public class MainWindow : Form
{
	private static class CommandTags
	{
		public const string OtherFrame = "other_frame";
		
		public const string Upper = "upper";
		public const string Lower = "lower";
		public const string Title = "title";
		public const string Sentence = "sentence";
		
		public const string TrimBoth = "trim_both";
		public const string TrimStart = "trim_start";
		public const string TrimEnd = "trim_end";
		public const string ReduceMultiple = "reduce_multiple";
		
		public const string LinesSortAsc = "lines_sort_asc";
		public const string LinesSortDesc = "lines_sort_desc";
		public const string LinesSortLengthAsc = "lines_sort_length_asc";
		public const string LinesSortLengthDesc = "lines_sort_length_desc";
		public const string LinesRemoveDuplicates = "lines_remove_duplicates";
		public const string LinesRemoveDuplicatesIgnoreCase = "lines_remove_duplicates_ignore_case";
		public const string LinesRemoveEmpty = "lines_remove_empty";
		public const string LinesReverse = "lines_reverse";
		public const string LinesNaturalSortAsc = "lines_natural_sort_asc";
		public const string LinesNaturalSortDesc = "lines_natural_sort_desc";
		public const string LinesNumberLines = "lines_number_lines";
		public const string LinesSplitChar = "lines_split_char";
		public const string LinesRemoveContaining = "lines_remove_containing";
		public const string LinesKeepContaining = "lines_keep_containing";
		public const string LinesTrimChars = "lines_trim_chars";
		
		public const string JoinLinesOther = "join_lines_other";
		
		public const string SearchReplaceReplace = "search_replace_replace";
		public const string SearchReplaceDelete = "search_replace_delete";
		public const string SearchReplaceExtract = "search_replace_extract";
		public const string SearchReplaceScrubHtml = "search_replace_scrub_html";
	}

	private class HistoryState
	{
		public string Text { get; set; }
		public string OperationName { get; set; }
		
		public HistoryState(string text, string operationName)
		{
			Text = text;
			OperationName = operationName;
		}
	}
	// Main menu
	private MenuStrip menuStrip;
	private ToolStripMenuItem optionsMenuItem, submenuLanguageItem, englishMenuItem, russianMenuItem, wordWrapMenuItem, fontMenuItem, helpMenuItem;
	private ToolStripMenuItem frameMenuItem, quotesMenuItem, guillemetsMenuItem, parenthesesMenuItem, squareBracketsMenuItem, otherFrameMenuItem;
	private ToolStripMenuItem caseMenuItem;
	private ToolStripMenuItem caseUpperMenuItem, caseLowerMenuItem, caseTitleMenuItem, caseSentenceMenuItem;
	private ToolStripMenuItem whitespaceMenuItem;
	private ToolStripMenuItem trimBothMenuItem, trimStartMenuItem, trimEndMenuItem, reduceMultipleMenuItem;
	private ToolStripMenuItem linesMenuItem;
	private ToolStripMenuItem linesSortAscMenuItem, linesSortDescMenuItem, linesSortLengthAscMenuItem, linesSortLengthDescMenuItem;
	private ToolStripMenuItem linesRemoveDuplicatesMenuItem, linesRemoveDuplicatesIgnoreCaseMenuItem, linesRemoveEmptyMenuItem, linesReverseMenuItem;
	private ToolStripMenuItem linesNaturalSortAscMenuItem, linesNaturalSortDescMenuItem, linesNumberLinesMenuItem;
	private ToolStripMenuItem linesSplitByCharMenuItem, linesRemoveContainingMenuItem, linesKeepContainingMenuItem, linesTrimCharsMenuItem;
	private ToolStripMenuItem joinLinesMenuItem;
	private ToolStripMenuItem joinLinesSpaceMenuItem, joinLinesCommaMenuItem, joinLinesSemicolonMenuItem, joinLinesOtherMenuItem;

	private ToolStripMenuItem windowPositionMenuItem;
	private ToolStripMenuItem rememberWindowSizeMenuItem;
	private ToolStripMenuItem rememberWindowPositionMenuItem;
	private ToolStripMenuItem alwaysOnTopMenuItem;
	private ToolStripMenuItem appearNearMouseMenuItem;

	private ToolStripMenuItem singleInstanceMenuItem;

	private ToolStripMenuItem searchReplaceMenuItem;
	private ToolStripMenuItem searchReplaceReplaceMenuItem, searchReplaceDeleteMenuItem, searchReplaceExtractMenuItem, searchReplaceScrubHtmlMenuItem;

	private Dictionary<int, ToolStripMenuItem> commandMap = new Dictionary<int, ToolStripMenuItem>();
	private Dictionary<ToolStripMenuItem, int> _reverseCommandMap; // Reverse lookup cache
	private int commandCounter = 1;

	private Timer _statsUpdateTimer; // Debounce timer for stats
	private Image _cachedDirtyIndicatorIcon; // Star icon cache
	private static readonly TimeSpan REGEX_TIMEOUT = TimeSpan.FromSeconds(2); // ReDoS protection

	// Quick command access fields
	private string _quickCommandBuffer = "";
	private Timer _quickCommandTimer;
	private ToolStripStatusLabel _quickCommandIndicator;
	private const int QUICK_COMMAND_TIMEOUT = 800; // milliseconds

	private TextBox mainTextBox;
	private Panel bottomButtonPanel;
	private Button undoButton, redoButton, resetButton, cancelButton, okButton;
	private StatusStrip statusBar;
	private ToolStripStatusLabel wordsStatusLabel, linesStatusLabel, charsStatusLabel;

	private TableLayoutPanel topCheckBoxPanel;
	private CheckBox processLinesSeparatelyCheckBox;
	private CheckBox commandChainCheckBox;

	private Panel commandChainPanel;
	private TextBox commandChainTextBox;
	private Button executeButton;
	private Button toggleMultilineButton;
	private Panel profilesPanel;
	private ComboBox profilesComboBox;
	private Button saveProfileButton;
	private Button deleteProfileButton;
	private PictureBox dirtyIndicator;

	private string originalText;
	public string ResultText { get; private set; }

	// Undo/Redo system
	private LinkedList<HistoryState> undoList = new LinkedList<HistoryState>();
	private LinkedList<HistoryState> redoList = new LinkedList<HistoryState>();
	private const int HISTORY_LIMIT = 20;
	private bool isUndoRedoOperation = false; // Prevents history pollution
	private bool isProgrammaticTextChange = false; // Skip history on programmatic changes
	private bool isExecutingCommandChain = false; // Skip history for chain steps

	// Setting keys
	private const string SETTING_COMMAND_CHAIN_CHECKED = "ClipProcessor_CommandChainChecked";
	private const string SETTING_LAST_COMMAND_CHAIN_TEXT = "ClipProcessor_LastCommandChainText";
	private const string SETTING_PLS_GLOBAL_CHECKED = "ClipProcessor_PLSGlobalChecked";
	private const string SETTING_PROFILE_NAMES_LIST = "ClipProcessor_ProfileNames";
	private const string PROFILE_SETTING_PREFIX = "ClipProcessor_Profile_";
	private const string PROFILE_PLS_SUFFIX = "_ProcessLinesSeparately";
	private const string SETTING_LAST_SELECTED_PROFILE = "ClipProcessor_LastSelectedProfile";
	private const string SETTING_REMEMBER_WINDOW_SIZE = "ClipProcessor_RememberWindowSize";
	private const string SETTING_REMEMBER_WINDOW_POSITION = "ClipProcessor_RememberWindowPosition";
	private const string SETTING_WINDOW_X = "ClipProcessor_WindowX";
	private const string SETTING_WINDOW_Y = "ClipProcessor_WindowY";
	private const string SETTING_COMMAND_CHAIN_EXPANDED = "ClipProcessor_CommandChainExpanded";
	private const string SETTING_COMMAND_CHAIN_HEIGHT = "ClipProcessor_CommandChainHeight";

	private string currentProfileName = "";
	private bool isProfileDirty = false;
	private ToolTip toolTip;
	private bool isCommandChainExpanded = false;
	private const int COMMAND_CHAIN_COLLAPSED_HEIGHT = 30;
	private const int COMMAND_CHAIN_EXPANDED_HEIGHT = 120;
	private bool _rememberWindowSize = true;
	private bool _rememberWindowPosition = false;
	private bool _alwaysOnTop = false;

	public MainWindow(string initialText)
	{
		this.originalText = initialText;
		this.ResultText = initialText;

		this.Size = new Size(800, 600);
		this.MinimumSize = new Size(600, 450); // Slightly increase minimum height for new UI elements
		this.FormBorderStyle = FormBorderStyle.Sizable;
		this.StartPosition = FormStartPosition.CenterScreen;
		this.KeyPreview = true; // Enable form to receive key events before child controls

		// Stats update timer: 300ms debounce for UI responsiveness
		_statsUpdateTimer = new Timer
		{
			Interval = 300
		};
		_statsUpdateTimer.Tick += (s, e) =>
		{
			_statsUpdateTimer.Stop();
			UpdateStatistics();
		};

		// Quick command timer initialization
		_quickCommandTimer = new Timer
		{
			Interval = QUICK_COMMAND_TIMEOUT
		};
		_quickCommandTimer.Tick += QuickCommandTimer_Tick;

		InitializeTopCheckBoxPanel();
		InitializeCommandChainPanel();
		InitializeProfilesPanel();
		InitializeMainControls(initialText);
		InitializeMenuItems();
		BuildReverseCommandMap();            // Build reverse lookup cache after all menu items created
		InitializeToolTips();
		UpdateUIStrings();                   // Initial UI strings application

		LoadAndApplySettings();
		LoadAndApplyWindowBehaviorSettings();
		UpdateUIStrings();                   // Re-apply UI strings AFTER settings (like font) are loaded

		LoadCommandChainSettings();
		LoadProfiles();

		// Order of adding controls matters for DockStyle.Fill
		this.Controls.Add(mainTextBox);      // TextBox fills remaining space
		this.Controls.Add(commandChainPanel);  // Initially hidden
		this.Controls.Add(profilesPanel);      // Initially hidden
		this.Controls.Add(topCheckBoxPanel); // Panel above mainTextBox
		this.Controls.Add(bottomButtonPanel);
		this.Controls.Add(statusBar);
		this.MainMenuStrip = menuStrip;      // MenuStrip added last to be on top
		this.Controls.Add(menuStrip);

		this.Load += (s, e) =>
		{
			bottomButtonPanel.PerformLayout(); RepositionAnchoredButtons();
			// Apply TopMost after form is fully initialized to avoid Win32 errors
			this.TopMost = _alwaysOnTop;
		};
		UpdateStatistics();
		PushStateForUndo(initialText, LocalizationManager.GetString("operationInitial"));

		commandChainCheckBox.CheckedChanged += CommandChainCheckBox_CheckedChanged;

		// Register handler to unregister instance on window close
		this.FormClosed += (s, e) => WindowInstanceManager.UnregisterInstance(this);
	}

	private void InitializeMainControls(string initialText)
	{
		// Main TextBox setup
		mainTextBox = new TextBox
		{
			Multiline = true, ScrollBars = ScrollBars.Both, Dock = DockStyle.Fill, Text = initialText, MaxLength = 0
		};
		mainTextBox.TextChanged += MainTextBox_TextChanged;
		mainTextBox.MouseUp += MainTextBox_SelectionPossiblyChanged;
		mainTextBox.KeyUp += MainTextBox_SelectionPossiblyChanged;

		// Reset text selection and set cursor to start.
		mainTextBox.SelectionStart = 0;
		mainTextBox.SelectionLength = 0;

		LoadAndApplySettings(); // Load text box specific settings (font, wordwrap) AFTER mainTextBox is created

		// Bottom button panel setup
		bottomButtonPanel = new Panel
		{
			Height = 40, Dock = DockStyle.Bottom
		};

		// Buttons setup
		undoButton = new Button
		{
			Size = new Size(80, 25), Location = new Point(10, 7), Enabled = false
		};
		redoButton = new Button
		{
			Size = new Size(80, 25), Location = new Point(95, 7), Enabled = false
		};
		resetButton = new Button
		{
			Size = new Size(80, 25), Location = new Point(180, 7)
		};
		okButton = new Button
		{
			Name = "okButton", Size = new Size(75, 25), Anchor = AnchorStyles.Top | AnchorStyles.Right
		};
		cancelButton = new Button
		{
			Name = "cancelButton", Size = new Size(75, 25), Anchor = AnchorStyles.Top | AnchorStyles.Right
		};

		okButton.Click += (s, e) =>
		{
			this.ResultText = mainTextBox.Text;
			this.DialogResult = DialogResult.OK;
			if (commandChainCheckBox.Checked) // Only save last profile if profiles were active
			{
				BFS.ScriptSettings.WriteValue(SETTING_LAST_SELECTED_PROFILE, currentProfileName);
			}
			this.Close();
		};
		cancelButton.Click += (s, e) =>
		{
			this.ResultText = this.originalText; this.DialogResult = DialogResult.Cancel; this.Close();
		}; // On Cancel, ResultText reverts or stays original

		undoButton.Click += UndoButton_Click;
		redoButton.Click += RedoButton_Click;
		resetButton.Click += ResetButton_Click;

		bottomButtonPanel.Controls.AddRange(new Control[]
			{
				undoButton, redoButton, resetButton, okButton, cancelButton
			});
		// Bind to panel resize event for correct positioning of right-anchored buttons
		bottomButtonPanel.Resize += (s, e) => RepositionAnchoredButtons();

		// StatusStrip setup
		statusBar = new StatusStrip
		{
			SizingGrip = false
		}; // Sizing grip usually not needed for dialog windows
		
		// Quick command indicator
		_quickCommandIndicator = new ToolStripStatusLabel
		{
			Text = "",
			Visible = false,
			BorderSides = ToolStripStatusLabelBorderSides.All,
			BorderStyle = Border3DStyle.SunkenOuter,
			Spring = false,
			TextAlign = ContentAlignment.MiddleLeft,
			ForeColor = Color.DarkBlue,
			Font = new Font(SystemFonts.StatusFont, FontStyle.Bold)
		};
		
		wordsStatusLabel = new ToolStripStatusLabel();
		linesStatusLabel = new ToolStripStatusLabel();
		charsStatusLabel = new ToolStripStatusLabel();
		statusBar.Items.AddRange(new ToolStripItem[]
			{
				wordsStatusLabel, linesStatusLabel, charsStatusLabel, _quickCommandIndicator
			});
	}

	private void InitializeTopCheckBoxPanel()
	{
		topCheckBoxPanel = new TableLayoutPanel
		{
			Dock = DockStyle.Top,
			AutoSize = true,
			ColumnCount = 2,
			RowCount = 1,
			Padding = new Padding(5)
		};
		// Define column styles to allow checkboxes to take their natural width
		topCheckBoxPanel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
		topCheckBoxPanel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));

		processLinesSeparatelyCheckBox = new CheckBox
		{
			AutoSize = true,
			Margin = new Padding(5, 5, 15, 5) // Right margin for spacing
			// Text is set in UpdateUIStrings
		};
		processLinesSeparatelyCheckBox.CheckedChanged += ProcessLinesSeparatelyCheckBox_CheckedChanged;

		commandChainCheckBox = new CheckBox
		{
			AutoSize = true,
			Margin = new Padding(0, 5, 5, 5) // Left margin for spacing from previous checkbox
			// Text is set in UpdateUIStrings
		};

		topCheckBoxPanel.Controls.Add(processLinesSeparatelyCheckBox, 0, 0);
		topCheckBoxPanel.Controls.Add(commandChainCheckBox, 1, 0);
	}

	private void RepositionAnchoredButtons()
	{
		if (okButton != null && cancelButton != null && bottomButtonPanel.ClientSize.Width > 0)
		{
			okButton.Location = new Point(bottomButtonPanel.ClientSize.Width - okButton.Width - 10, 7);
			cancelButton.Location = new Point(bottomButtonPanel.ClientSize.Width - okButton.Width - cancelButton.Width - 15, 7);
		}
	}

	// Loads and applies persisted Word Wrap and Font settings for the main TextBox.
	private void LoadAndApplySettings()
	{
		if (mainTextBox == null) return; // Should not happen if called correctly, but a safe check

		// WordWrap setting
		string wordWrapSettingKey = LocalizationManager.WORD_WRAP_SETTING;
		string wordWrapValueString = BFS.ScriptSettings.ReadValue(wordWrapSettingKey);
		bool defaultWordWrap = true; // Default WordWrap value

		if (string.IsNullOrEmpty(wordWrapValueString))
		{
			// Setting doesn't exist or is empty; apply the defined default.
			mainTextBox.WordWrap = defaultWordWrap;
			// Write this default value back to settings so it's stored for next time.
			BFS.ScriptSettings.WriteValueBool(wordWrapSettingKey, defaultWordWrap);
		}
		else
		{
			// Setting exists; read its boolean value.
			mainTextBox.WordWrap = BFS.ScriptSettings.ReadValueBool(wordWrapSettingKey);
		}

		// Font settings
		string fontName = BFS.ScriptSettings.ReadValue(LocalizationManager.FONT_NAME_SETTING);
		string fontSizeStr = BFS.ScriptSettings.ReadValue(LocalizationManager.FONT_SIZE_SETTING);
		string fontStyleStr = BFS.ScriptSettings.ReadValue(LocalizationManager.FONT_STYLE_SETTING);

		if (!string.IsNullOrEmpty(fontName) && !string.IsNullOrEmpty(fontSizeStr) && !string.IsNullOrEmpty(fontStyleStr))
		{
			if (float.TryParse(fontSizeStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float fontSize) &&
				Enum.TryParse<FontStyle>(fontStyleStr, out FontStyle fontStyle)) // FontStyle is saved as a string name, e.g., "Bold, Italic"
			{
				try
				{
					mainTextBox.Font = new Font(fontName, fontSize, fontStyle);
				}
				catch (Exception)
				{ /* Failed to create font; default font will be used. Optionally log: BFS.Debug.Write(...) */
				}
			}
		}
		// If settings are not found or parsing fails, TextBox keeps its default font or the one set by the designer.
	}

	// Loads and applies persisted window size settings.
	// Handles default values and ensures mutual exclusivity for position settings.
	private void LoadAndApplyWindowBehaviorSettings()
	{
		// Load behavior flags, applying defaults if settings are missing.
		string rememberSizeStr = BFS.ScriptSettings.ReadValue(SETTING_REMEMBER_WINDOW_SIZE);
		if (string.IsNullOrEmpty(rememberSizeStr))
		{
			_rememberWindowSize = true;
			BFS.ScriptSettings.WriteValueBool(SETTING_REMEMBER_WINDOW_SIZE, _rememberWindowSize);
		}
		else
		{
			_rememberWindowSize = BFS.ScriptSettings.ReadValueBool(SETTING_REMEMBER_WINDOW_SIZE);
		}

		string rememberPosStr = BFS.ScriptSettings.ReadValue(SETTING_REMEMBER_WINDOW_POSITION);
		if (string.IsNullOrEmpty(rememberPosStr))
		{
			_rememberWindowPosition = false;
			BFS.ScriptSettings.WriteValueBool(SETTING_REMEMBER_WINDOW_POSITION, _rememberWindowPosition);
		}
		else
		{
			_rememberWindowPosition = BFS.ScriptSettings.ReadValueBool(SETTING_REMEMBER_WINDOW_POSITION);
		}

		string alwaysOnTopStr = BFS.ScriptSettings.ReadValue(LocalizationManager.ALWAYS_ON_TOP_SETTING);
		if (string.IsNullOrEmpty(alwaysOnTopStr))
		{
			_alwaysOnTop = false;
			BFS.ScriptSettings.WriteValueBool(LocalizationManager.ALWAYS_ON_TOP_SETTING, _alwaysOnTop);
		}
		else
		{
			_alwaysOnTop = BFS.ScriptSettings.ReadValueBool(LocalizationManager.ALWAYS_ON_TOP_SETTING);
		}
		// TopMost will be applied in Form.Load event after form is fully initialized
		
		if (_rememberWindowSize)
		{
			string widthStr = BFS.ScriptSettings.ReadValue(LocalizationManager.WINDOW_WIDTH_SETTING);
			string heightStr = BFS.ScriptSettings.ReadValue(LocalizationManager.WINDOW_HEIGHT_SETTING);
		
			if (int.TryParse(widthStr, out int width) && int.TryParse(heightStr, out int height))
			{
				width = Math.Max(width, this.MinimumSize.Width);
				height = Math.Max(height, this.MinimumSize.Height);
				this.Size = new Size(width, height);
			}
		}
		if (_rememberWindowPosition)
		{
			string xStr = BFS.ScriptSettings.ReadValue(SETTING_WINDOW_X);
			string yStr = BFS.ScriptSettings.ReadValue(SETTING_WINDOW_Y);
		
			if (int.TryParse(xStr, out int x) && int.TryParse(yStr, out int y))
			{
				// Basic validation: ensure window is at least partially visible on some screen.
				// More complex validation might be needed for multi-monitor setups where a screen disappears.
				bool positionIsValid = false;
				foreach (Screen screen in Screen.AllScreens)
				{
					if (screen.WorkingArea.IntersectsWith(new Rectangle(x, y, this.Width, this.Height)))
					{
						positionIsValid = true;
						break;
					}
				}
				if (positionIsValid)
				{
					this.StartPosition = FormStartPosition.Manual;
					this.Location = new Point(x, y);
				}
				else
				{
					this.StartPosition = FormStartPosition.CenterScreen; // Fallback to center screen
				}
			}
			else
			{
				this.StartPosition = FormStartPosition.CenterScreen; // Fallback if position not saved or invalid
			}
		}
		else
		{
			this.StartPosition = FormStartPosition.CenterScreen; // Default if no specific position behavior is set
		}
	}

	// Initializes the structure and elements of the main menu.
	private void InitializeMenuItems()
	{
		menuStrip = new MenuStrip();
		commandCounter = 1; // Reset for potential re-initialization or full UI update (if ever needed).

		InitializeOptionsAndHelpMenu();
		InitializeFramingMenu();
		InitializeCaseMenu();
		InitializeWhitespaceMenu();
		InitializeLinesMenu();
		InitializeJoinLinesMenu();
		InitializeSearchAndReplaceMenu();

		menuStrip.Items.AddRange(new ToolStripItem[]
			{
				optionsMenuItem, frameMenuItem, caseMenuItem, whitespaceMenuItem, linesMenuItem, joinLinesMenuItem, searchReplaceMenuItem
			});
	}

	private void InitializeOptionsAndHelpMenu()
	{
		optionsMenuItem = new ToolStripMenuItem();
		submenuLanguageItem = new ToolStripMenuItem();
		englishMenuItem = new ToolStripMenuItem
		{
			Tag = "en"
		};
		englishMenuItem.Click += LanguageMenuItem_Click;
		russianMenuItem = new ToolStripMenuItem
		{
			Tag = "ru"
		};
		russianMenuItem.Click += LanguageMenuItem_Click;
		submenuLanguageItem.DropDownItems.AddRange(new ToolStripItem[]
			{
				englishMenuItem, russianMenuItem
			});

		wordWrapMenuItem = new ToolStripMenuItem
		{
			CheckOnClick = true
		};
		wordWrapMenuItem.Click += WordWrapMenuItem_Click;

		fontMenuItem = new ToolStripMenuItem();
		fontMenuItem.Click += FontMenuItem_Click;

		// --- Window Position Submenu --- 
		windowPositionMenuItem = new ToolStripMenuItem();
		rememberWindowSizeMenuItem = new ToolStripMenuItem
		{
			CheckOnClick = true
		};
		rememberWindowSizeMenuItem.Click += RememberWindowSizeMenuItem_Click;

		rememberWindowPositionMenuItem = new ToolStripMenuItem
		{
			CheckOnClick = true
		};
		rememberWindowPositionMenuItem.Click += RememberWindowPositionMenuItem_Click;

		alwaysOnTopMenuItem = new ToolStripMenuItem
		{
			CheckOnClick = true
		};
		alwaysOnTopMenuItem.Click += AlwaysOnTopMenuItem_Click;
		
		windowPositionMenuItem.DropDownItems.AddRange(new ToolStripItem[]
		{
			rememberWindowSizeMenuItem,
			rememberWindowPositionMenuItem,
			new ToolStripSeparator(),
			alwaysOnTopMenuItem
		});
		// --- End Window Position Submenu ---

		singleInstanceMenuItem = new ToolStripMenuItem
		{
			CheckOnClick = true
		};
		singleInstanceMenuItem.Click += SingleInstanceMenuItem_Click;

		helpMenuItem = new ToolStripMenuItem();
		helpMenuItem.Click += HelpMenuItem_Click;

		optionsMenuItem.DropDownItems.AddRange(new ToolStripItem[]
			{
				submenuLanguageItem, new ToolStripSeparator(), wordWrapMenuItem, new ToolStripSeparator(), fontMenuItem, new ToolStripSeparator(), windowPositionMenuItem, new ToolStripSeparator(), singleInstanceMenuItem, new ToolStripSeparator(), helpMenuItem
			});
	}

	private void InitializeFramingMenu()
	{
		frameMenuItem = new ToolStripMenuItem();
		quotesMenuItem = CreateNumberedMenuItem("\"|\"", FrameMenuItem_Click);
		guillemetsMenuItem = CreateNumberedMenuItem("«|»", FrameMenuItem_Click);
		parenthesesMenuItem = CreateNumberedMenuItem("(|)", FrameMenuItem_Click);
		squareBracketsMenuItem = CreateNumberedMenuItem("[|]", FrameMenuItem_Click);
		otherFrameMenuItem = CreateNumberedMenuItem(CommandTags.OtherFrame, OtherFrameMenuItem_Click); // Special tag for "Other..." dialog
		frameMenuItem.DropDownItems.AddRange(new ToolStripItem[]
			{
				quotesMenuItem, guillemetsMenuItem, parenthesesMenuItem, squareBracketsMenuItem, new ToolStripSeparator(), otherFrameMenuItem
			});
	}

	private void InitializeCaseMenu()
	{
		caseMenuItem = new ToolStripMenuItem();
		caseUpperMenuItem = CreateNumberedMenuItem(CommandTags.Upper, CaseMenuItem_Click);
		caseLowerMenuItem = CreateNumberedMenuItem(CommandTags.Lower, CaseMenuItem_Click);
		caseTitleMenuItem = CreateNumberedMenuItem(CommandTags.Title, CaseMenuItem_Click);
		caseSentenceMenuItem = CreateNumberedMenuItem(CommandTags.Sentence, CaseMenuItem_Click);
		caseMenuItem.DropDownItems.AddRange(new ToolStripItem[]
			{
				caseUpperMenuItem, caseLowerMenuItem, caseTitleMenuItem, caseSentenceMenuItem
			});
	}

	private void InitializeWhitespaceMenu()
	{
		whitespaceMenuItem = new ToolStripMenuItem();
		trimBothMenuItem = CreateNumberedMenuItem(CommandTags.TrimBoth, WhitespaceMenuItem_Click);
		trimStartMenuItem = CreateNumberedMenuItem(CommandTags.TrimStart, WhitespaceMenuItem_Click);
		trimEndMenuItem = CreateNumberedMenuItem(CommandTags.TrimEnd, WhitespaceMenuItem_Click);
		reduceMultipleMenuItem = CreateNumberedMenuItem(CommandTags.ReduceMultiple, WhitespaceMenuItem_Click);
		whitespaceMenuItem.DropDownItems.AddRange(new ToolStripItem[]
			{
				trimBothMenuItem, trimStartMenuItem, trimEndMenuItem, reduceMultipleMenuItem
			});
	}

	private void InitializeLinesMenu()
	{
		linesMenuItem = new ToolStripMenuItem();
		linesSortAscMenuItem = CreateNumberedMenuItem(CommandTags.LinesSortAsc, LinesMenuItem_Click);
		linesSortDescMenuItem = CreateNumberedMenuItem(CommandTags.LinesSortDesc, LinesMenuItem_Click);
		linesNaturalSortAscMenuItem = CreateNumberedMenuItem(CommandTags.LinesNaturalSortAsc, LinesMenuItem_Click);
		linesNaturalSortDescMenuItem = CreateNumberedMenuItem(CommandTags.LinesNaturalSortDesc, LinesMenuItem_Click);
		linesSortLengthAscMenuItem = CreateNumberedMenuItem(CommandTags.LinesSortLengthAsc, LinesMenuItem_Click);
		linesSortLengthDescMenuItem = CreateNumberedMenuItem(CommandTags.LinesSortLengthDesc, LinesMenuItem_Click);
		linesRemoveDuplicatesMenuItem = CreateNumberedMenuItem(CommandTags.LinesRemoveDuplicates, LinesMenuItem_Click);
		linesRemoveDuplicatesIgnoreCaseMenuItem = CreateNumberedMenuItem(CommandTags.LinesRemoveDuplicatesIgnoreCase, LinesMenuItem_Click);
		linesRemoveEmptyMenuItem = CreateNumberedMenuItem(CommandTags.LinesRemoveEmpty, LinesMenuItem_Click);
		linesReverseMenuItem = CreateNumberedMenuItem(CommandTags.LinesReverse, LinesMenuItem_Click);
		linesNumberLinesMenuItem = CreateNumberedMenuItem(CommandTags.LinesNumberLines, LinesMenuItem_Click);
		linesSplitByCharMenuItem = CreateNumberedMenuItem(CommandTags.LinesSplitChar, LinesMenuItem_Click);
		linesRemoveContainingMenuItem = CreateNumberedMenuItem(CommandTags.LinesRemoveContaining, LinesMenuItem_Click);
		linesKeepContainingMenuItem = CreateNumberedMenuItem(CommandTags.LinesKeepContaining, LinesMenuItem_Click);
		linesTrimCharsMenuItem = CreateNumberedMenuItem(CommandTags.LinesTrimChars, LinesMenuItem_Click);
		linesMenuItem.DropDownItems.AddRange(new ToolStripItem[]
			{
				linesSortAscMenuItem, linesSortDescMenuItem, new ToolStripSeparator(),
				linesNaturalSortAscMenuItem, linesNaturalSortDescMenuItem, new ToolStripSeparator(),
				linesSortLengthAscMenuItem, linesSortLengthDescMenuItem, new ToolStripSeparator(),
			linesRemoveDuplicatesMenuItem, linesRemoveDuplicatesIgnoreCaseMenuItem, linesRemoveEmptyMenuItem, new ToolStripSeparator(),
			linesReverseMenuItem, new ToolStripSeparator(),
			linesNumberLinesMenuItem, new ToolStripSeparator(),
			linesSplitByCharMenuItem, linesRemoveContainingMenuItem, linesKeepContainingMenuItem, linesTrimCharsMenuItem
			});
	}

	private void InitializeJoinLinesMenu()
	{
		joinLinesMenuItem = new ToolStripMenuItem();
		joinLinesSpaceMenuItem = CreateNumberedMenuItem(" ", JoinLinesMenuItem_Click);
		joinLinesCommaMenuItem = CreateNumberedMenuItem(",", JoinLinesMenuItem_Click);
		joinLinesSemicolonMenuItem = CreateNumberedMenuItem(";", JoinLinesMenuItem_Click);
		joinLinesOtherMenuItem = CreateNumberedMenuItem(CommandTags.JoinLinesOther, JoinLinesMenuItem_Click); // For "Other..." dialog
		joinLinesMenuItem.DropDownItems.AddRange(new ToolStripItem[]
			{
				joinLinesSpaceMenuItem, joinLinesCommaMenuItem, joinLinesSemicolonMenuItem, new ToolStripSeparator(), joinLinesOtherMenuItem
			});
	}

	private void InitializeSearchAndReplaceMenu()
	{
		searchReplaceMenuItem = new ToolStripMenuItem();
		searchReplaceReplaceMenuItem = CreateNumberedMenuItem(CommandTags.SearchReplaceReplace, SearchReplaceMenuItem_Click);
		searchReplaceDeleteMenuItem = CreateNumberedMenuItem(CommandTags.SearchReplaceDelete, SearchReplaceMenuItem_Click);
		searchReplaceExtractMenuItem = CreateNumberedMenuItem(CommandTags.SearchReplaceExtract, SearchReplaceMenuItem_Click);
		searchReplaceScrubHtmlMenuItem = CreateNumberedMenuItem(CommandTags.SearchReplaceScrubHtml, SearchReplaceMenuItem_Click);
		searchReplaceMenuItem.DropDownItems.AddRange(new ToolStripItem[]
			{
				searchReplaceReplaceMenuItem, searchReplaceDeleteMenuItem, searchReplaceExtractMenuItem, new ToolStripSeparator(), searchReplaceScrubHtmlMenuItem
			});
	}

	// Helper to create menu items that are part of the command chain system.
	// Assigns a command number and adds them to the commandMap.
	private ToolStripMenuItem CreateNumberedMenuItem(string tag, EventHandler onClickHandler)
	{
		ToolStripMenuItem menuItem = new ToolStripMenuItem
		{
			Tag = tag, Name = tag
		};
		menuItem.Click += onClickHandler;
		commandMap[commandCounter] = menuItem;
		// Text (including command number) is set in UpdateUIStrings.
		commandCounter++;
		return menuItem;
	}

	// Builds reverse command map for fast lookups (called after all menu items are created)
	private void BuildReverseCommandMap()
	{
		_reverseCommandMap = new Dictionary<ToolStripMenuItem, int>();
		foreach (var kvp in commandMap)
		{
			_reverseCommandMap[kvp.Value] = kvp.Key;
		}
	}

	// Helper method to update menu item text with command number
	private void UpdateMenuItemText(ToolStripMenuItem item, string localizationKey)
	{
		if (item != null && _reverseCommandMap != null && _reverseCommandMap.TryGetValue(item, out int cmdNum))
		{
			item.Text = $"({cmdNum}) {LocalizationManager.GetString(localizationKey)}";
		}
	}

	private void LanguageMenuItem_Click(object sender, EventArgs e)
	{
		if (sender is ToolStripMenuItem clickedItem && clickedItem.Tag is string langCode)
		{
			LocalizationManager.SetLanguage(langCode);
			UpdateUIStrings();
			ShowMessageInfoSafe(LocalizationManager.GetString("dialogLanguageChanged"));
		}
	}

	private void WordWrapMenuItem_Click(object sender, EventArgs e)
	{
		if (wordWrapMenuItem != null && mainTextBox != null)
		{
			mainTextBox.WordWrap = wordWrapMenuItem.Checked;
			BFS.ScriptSettings.WriteValueBool(LocalizationManager.WORD_WRAP_SETTING, mainTextBox.WordWrap);
		}
	}

	// Handles Font selection dialog and applies/saves the chosen font.
	private void FontMenuItem_Click(object sender, EventArgs e)
	{
		using (FontDialog fontDialog = new FontDialog())
		{
			fontDialog.Font = mainTextBox.Font;
			if (fontDialog.ShowDialog(this) == DialogResult.OK) // Pass `this` for proper dialog ownership
			{
				mainTextBox.Font = fontDialog.Font;
				BFS.ScriptSettings.WriteValue(LocalizationManager.FONT_NAME_SETTING, fontDialog.Font.Name);
				BFS.ScriptSettings.WriteValue(LocalizationManager.FONT_SIZE_SETTING, fontDialog.Font.Size.ToString(CultureInfo.InvariantCulture));
				BFS.ScriptSettings.WriteValue(LocalizationManager.FONT_STYLE_SETTING, fontDialog.Font.Style.ToString()); // FontStyle saved as string, e.g., "Bold, Italic"
			}
		}
	}

	// Updates the text of all UI elements according to the current language and other states.
	private void UpdateUIStrings()
	{
		this.Text = LocalizationManager.GetString("windowTitle");
		UpdateOptionsAndHelpMenuStrings();
		UpdateFramingMenuStrings();
		UpdateCaseMenuStrings();
		UpdateWhitespaceMenuStrings();
		UpdateLinesMenuStrings();
		UpdateJoinLinesMenuStrings();
		UpdateSearchAndReplaceMenuStrings();
		UpdateButtonsCheckboxesAndTooltipsStrings();
	}

	private void UpdateOptionsAndHelpMenuStrings()
	{
		// Options menu
		optionsMenuItem.Text = LocalizationManager.GetString("menuOptions");
		submenuLanguageItem.Text = LocalizationManager.GetString("submenuLanguage");
		englishMenuItem.Text = LocalizationManager.GetString("menuLangEN");
		russianMenuItem.Text = LocalizationManager.GetString("menuLangRU");
		wordWrapMenuItem.Text = LocalizationManager.GetString("menuWordWrap");
		fontMenuItem.Text = LocalizationManager.GetString("menuFont");
		if (helpMenuItem != null) helpMenuItem.Text = LocalizationManager.GetString("menuHelp");

		// Update checkmarks for selected language and word wrap
		string currentLang = LocalizationManager.GetCurrentLanguage();
		englishMenuItem.Checked = (currentLang == "en");
		russianMenuItem.Checked = (currentLang == "ru");
		if (mainTextBox != null && wordWrapMenuItem != null)
		{
			wordWrapMenuItem.Checked = mainTextBox.WordWrap;
		}

		// Window Position menu texts and checked states
		if (windowPositionMenuItem != null) windowPositionMenuItem.Text = LocalizationManager.GetString("menuWindowPosition");
		if (rememberWindowSizeMenuItem != null)
		{
			rememberWindowSizeMenuItem.Text = LocalizationManager.GetString("menuRememberWindowSize");
			rememberWindowSizeMenuItem.Checked = _rememberWindowSize;
		}
		if (rememberWindowPositionMenuItem != null)
		{
			rememberWindowPositionMenuItem.Text = LocalizationManager.GetString("menuRememberWindowPosition");
			rememberWindowPositionMenuItem.Checked = _rememberWindowPosition;
		}
		if (alwaysOnTopMenuItem != null)
		{
			alwaysOnTopMenuItem.Text = LocalizationManager.GetString("menuAlwaysOnTop");
			alwaysOnTopMenuItem.Checked = _alwaysOnTop;
		}

		// Single instance menu item
		if (singleInstanceMenuItem != null)
		{
			singleInstanceMenuItem.Text = LocalizationManager.GetString("menuSingleInstance");
			string singleInstanceValue = BFS.ScriptSettings.ReadValue(LocalizationManager.SINGLE_INSTANCE_SETTING);
			singleInstanceMenuItem.Checked = !string.IsNullOrEmpty(singleInstanceValue) ? 
				BFS.ScriptSettings.ReadValueBool(LocalizationManager.SINGLE_INSTANCE_SETTING) : false;
		}
	}

	private void UpdateFramingMenuStrings()
	{
		// Framing menu
		frameMenuItem.Text = LocalizationManager.GetString("menuFraming");
		// Update text for numbered menu items using cached reverse lookup
		UpdateMenuItemText(quotesMenuItem, "menuFramingQuotes");
		UpdateMenuItemText(guillemetsMenuItem, "menuFramingGuillemets");
		UpdateMenuItemText(parenthesesMenuItem, "menuFramingParentheses");
		UpdateMenuItemText(squareBracketsMenuItem, "menuFramingSquareBrackets");
		UpdateMenuItemText(otherFrameMenuItem, "menuFramingOther");
	}

	private void UpdateCaseMenuStrings()
	{
		// Case menu
		caseMenuItem.Text = LocalizationManager.GetString("menuCase");
		UpdateMenuItemText(caseUpperMenuItem, "menuCaseUpper");
		UpdateMenuItemText(caseLowerMenuItem, "menuCaseLower");
		UpdateMenuItemText(caseTitleMenuItem, "menuCaseTitle");
		UpdateMenuItemText(caseSentenceMenuItem, "menuCaseSentence");
	}

	private void UpdateWhitespaceMenuStrings()
	{
		// Whitespace menu
		if (whitespaceMenuItem != null) whitespaceMenuItem.Text = LocalizationManager.GetString("menuWhitespace");
		UpdateMenuItemText(trimBothMenuItem, "menuWhitespaceTrimBoth");
		UpdateMenuItemText(trimStartMenuItem, "menuWhitespaceTrimStart");
		UpdateMenuItemText(trimEndMenuItem, "menuWhitespaceTrimEnd");
		UpdateMenuItemText(reduceMultipleMenuItem, "menuWhitespaceReduceMultiple");
	}

	private void UpdateLinesMenuStrings()
	{
		// Lines menu
		if (linesMenuItem != null) linesMenuItem.Text = LocalizationManager.GetString("menuLines");
		UpdateMenuItemText(linesSortAscMenuItem, "menuLinesSortAsc");
		UpdateMenuItemText(linesSortDescMenuItem, "menuLinesSortDesc");
		UpdateMenuItemText(linesNaturalSortAscMenuItem, "menuLinesNaturalSortAsc");
		UpdateMenuItemText(linesNaturalSortDescMenuItem, "menuLinesNaturalSortDesc");
		UpdateMenuItemText(linesSortLengthAscMenuItem, "menuLinesSortLengthAsc");
		UpdateMenuItemText(linesSortLengthDescMenuItem, "menuLinesSortLengthDesc");
		UpdateMenuItemText(linesRemoveDuplicatesMenuItem, "menuLinesRemoveDuplicates");
		UpdateMenuItemText(linesRemoveDuplicatesIgnoreCaseMenuItem, "menuLinesRemoveDuplicatesIgnoreCase");
		UpdateMenuItemText(linesRemoveEmptyMenuItem, "menuLinesRemoveEmpty");
		UpdateMenuItemText(linesReverseMenuItem, "menuLinesReverse");
		UpdateMenuItemText(linesNumberLinesMenuItem, "menuLinesNumberLines");
		UpdateMenuItemText(linesSplitByCharMenuItem, "menuLinesSplitByChar");
		UpdateMenuItemText(linesRemoveContainingMenuItem, "menuLinesRemoveContaining");
		UpdateMenuItemText(linesKeepContainingMenuItem, "menuLinesKeepContaining");
		UpdateMenuItemText(linesTrimCharsMenuItem, "menuLinesTrimChars");
	}

	private void UpdateJoinLinesMenuStrings()
	{
		// Join Lines menu
		if (joinLinesMenuItem != null) joinLinesMenuItem.Text = LocalizationManager.GetString("menuJoinLines");
		UpdateMenuItemText(joinLinesSpaceMenuItem, "menuJoinLinesSpace");
		UpdateMenuItemText(joinLinesCommaMenuItem, "menuJoinLinesComma");
		UpdateMenuItemText(joinLinesSemicolonMenuItem, "menuJoinLinesSemicolon");
		UpdateMenuItemText(joinLinesOtherMenuItem, "menuJoinLinesOther");
	}

	private void UpdateSearchAndReplaceMenuStrings()
	{
		// Search & Replace menu
		if (searchReplaceMenuItem != null) searchReplaceMenuItem.Text = LocalizationManager.GetString("menuSearchReplace");
		UpdateMenuItemText(searchReplaceReplaceMenuItem, "menuSearchReplaceReplace");
		UpdateMenuItemText(searchReplaceDeleteMenuItem, "menuSearchReplaceDelete");
		UpdateMenuItemText(searchReplaceExtractMenuItem, "menuSearchReplaceExtract");
		UpdateMenuItemText(searchReplaceScrubHtmlMenuItem, "menuSearchReplaceScrubHtml");
	}

	private void UpdateButtonsCheckboxesAndTooltipsStrings()
	{
		// Button texts
		undoButton.Text = LocalizationManager.GetString("buttonUndo");
		redoButton.Text = LocalizationManager.GetString("buttonRedo");
		resetButton.Text = LocalizationManager.GetString("buttonReset");
		cancelButton.Text = LocalizationManager.GetString("buttonCancel");
		okButton.Text = LocalizationManager.GetString("buttonOK");

		// Checkbox texts
		if (processLinesSeparatelyCheckBox != null)
			processLinesSeparatelyCheckBox.Text = LocalizationManager.GetString("checkboxProcessLinesSeparately");
		if (commandChainCheckBox != null)
			commandChainCheckBox.Text = LocalizationManager.GetString("checkboxCommandChain");

		// Texts for command chain UI elements
		if (executeButton != null) executeButton.Text = LocalizationManager.GetString("buttonExecute");
		if (saveProfileButton != null)
		{
			UpdateDirtyIndicator(); // Sets Save Profile button text (normal or dirty)
		}

		// Tooltips
		if (toolTip != null)
		{
			if (deleteProfileButton != null) toolTip.SetToolTip(deleteProfileButton, LocalizationManager.GetString("tooltipDeleteProfile"));
			if (dirtyIndicator != null) toolTip.SetToolTip(dirtyIndicator, LocalizationManager.GetString("tooltipDirtyIndicator"));
			// Add/Update tooltips for buttons with hotkeys
			if (cancelButton != null) toolTip.SetToolTip(cancelButton, $"{LocalizationManager.GetString("buttonCancel")} (Esc)");
			if (okButton != null) toolTip.SetToolTip(okButton, $"{LocalizationManager.GetString("buttonOK")} (Ctrl+Enter)");
			if (executeButton != null) toolTip.SetToolTip(executeButton, $"{LocalizationManager.GetString("buttonExecute")} (Ctrl+Shift+Enter)");
		}
		
		// Update toggle multiline button tooltip
		UpdateToggleButtonTooltip();
		
		// Update Undo/Redo tooltips with operation names
		UpdateUndoRedoButtonStates();
	}

	private void MainTextBox_TextChanged(object sender, EventArgs e)
	{
		// Restart timer for delayed statistics update
		_statsUpdateTimer.Stop();
		_statsUpdateTimer.Start();

		// Only add to history if this is a manual edit by the user
		if (!isUndoRedoOperation && !isProgrammaticTextChange)
		{
			PushStateForUndo(mainTextBox.Text, LocalizationManager.GetString("operationManualEdit"));
		}
		UpdateDirtyIndicator();
	}

	// Handles events that might change text selection (MouseUp, KeyUp).
	private void MainTextBox_SelectionPossiblyChanged(object sender, EventArgs e)
	{
		// Restart timer for delayed statistics update
		_statsUpdateTimer.Stop();
		_statsUpdateTimer.Start();
	}

	// Regex for optimized word counting without creating arrays
	private static readonly Regex WordCountRegex = new Regex(@"\S+", RegexOptions.Compiled);

	// Updates text statistics (words, lines, chars) in the status bar.
	// Shows statistics for selected text if any, otherwise for the whole text.
	private void UpdateStatistics()
	{
		string text;
		string wordCountKey, lineCountKey, charCountKey;

		if (mainTextBox != null && mainTextBox.SelectionLength > 0)
		{
			text = mainTextBox.SelectedText;
			wordCountKey = "statusBarWordsSelected";
			lineCountKey = "statusBarLinesSelected";
			charCountKey = "statusBarCharsSelected";
		}
		else if (mainTextBox != null)
		{
			text = mainTextBox.Text;
			wordCountKey = "statusBarWords";
			lineCountKey = "statusBarLines";
			charCountKey = "statusBarChars";
		}
		else
		{
			text = ""; // Should ideally not happen if mainTextBox is always initialized.
			wordCountKey = "statusBarWords";
			lineCountKey = "statusBarLines";
			charCountKey = "statusBarChars";
		}

		// Optimized word count using Regex.Matches - more efficient than Split for counting
		int wordCount = 0;
		if (!string.IsNullOrWhiteSpace(text))
		{
			wordCount = WordCountRegex.Matches(text).Count;
		}

		int lineCount = 0;
		if (!string.IsNullOrEmpty(text))
		{
			lineCount = text.Split(new []
				{
					"\r\n", "\r", "\n"
				}, StringSplitOptions.None).Length;
		}

		int charCount = text.Length;

		wordsStatusLabel.Text = LocalizationManager.GetString(wordCountKey, wordCount);
		linesStatusLabel.Text = LocalizationManager.GetString(lineCountKey, lineCount);
		charsStatusLabel.Text = LocalizationManager.GetString(charCountKey, charCount);
	}

	// Applies framing (brackets, quotes, etc.) to the selected text or the whole text.
	private void ApplyFraming(string opening, string closing)
	{
		mainTextBox.Focus(); // Ensure TextBox has focus for selection operations.
		string originalTextBeforeFraming = mainTextBox.Text;

		// Determine if lines should be processed separately based on CheckBox state.
		bool processSeparately = (processLinesSeparatelyCheckBox != null && processLinesSeparatelyCheckBox.Checked);

		// Mark text change as programmatic
		isProgrammaticTextChange = true;
		try
		{
			TextProcessingService textService = new TextProcessingService(mainTextBox, processSeparately);
			textService.ApplyBrackets(opening, closing);
		}
		finally
		{
			isProgrammaticTextChange = false;
		}

		UpdateStatistics();
		if (mainTextBox.Text != originalTextBeforeFraming && !isUndoRedoOperation) // Push if text actually changed
		{
			PushStateForUndo(mainTextBox.Text, LocalizationManager.GetString("operationFraming"));
		}
	}

	// Click event handler for framing menu items
	private void FrameMenuItem_Click(object sender, EventArgs e)
	{
		if (sender is ToolStripMenuItem clickedItem && clickedItem.Tag is string tag)
		{
			string[] parts = tag.Split('|');
			if (parts.Length == 2)
			{
				string opening = parts[0];
				string closing = parts[1];
				HandleTextOperation(ts => ts.ApplyBrackets(opening, closing), LocalizationManager.GetString("operationFraming"));
			}
		}
	}

	// Handles the "Other..." framing menu item, allowing custom bracket input.
	private void OtherFrameMenuItem_Click(object sender, EventArgs e)
	{
		string opening = null, closing = null;
		bool paramsProvided = false;

		if (sender is ToolStripMenuItem clickedItem)
		{
			string commandIdentifier = clickedItem.Name; // Should be "other_frame" for this handler
			object currentTag = clickedItem.Tag;

			// Parameter provided via command chain (Tag is string[])
			if (currentTag is string[] customBracketsFromChain && customBracketsFromChain.Length == 2)
			{
				opening = customBracketsFromChain[0];
				closing = customBracketsFromChain[1];
				paramsProvided = true;
			}
			// Interactive mode: show dialog only if this is the "other_frame" command 
			// and no valid parameters were already passed via Tag.
			else if (commandIdentifier == "other_frame")
			{
				string userInput = GetUserInputSafe(LocalizationManager.GetString("dialogInputBracketsHint"), "");
				if (string.IsNullOrWhiteSpace(userInput)) return; // User cancelled dialog

				string[] parts = userInput.Split(new string[]
					{
						"~~"
					}, StringSplitOptions.None);
				if (parts.Length == 2)
				{
					opening = parts[0];
					closing = parts[1];
					paramsProvided = true;
				}
				else
				{
					ShowMessageErrorSafe(LocalizationManager.GetString("dialogInputBracketsError"));
					return;
				}
			}
			// Defensive check: if commandIdentifier is not "other_frame", something is wrong with event wiring or Tag.
			else if (commandIdentifier != "other_frame")
			{
				ShowMessageErrorSafe($"Internal error: OtherFrameMenuItem_Click called for unexpected command '{commandIdentifier}'.");
				return;
			}
		}
		else
		{
			return; // Sender was not a ToolStripMenuItem
		}

		if (paramsProvided)
		{
			HandleTextOperation(ts => ts.ApplyBrackets(opening, closing), LocalizationManager.GetString("operationFraming"));
		}
	}

	private void CaseMenuItem_Click(object sender, EventArgs e)
	{
		if (sender is ToolStripMenuItem clickedItem && clickedItem.Tag is string caseType)
		{
			string operationKey = "operationCase" + caseType; // e.g., "operationCaseupper"
			HandleTextOperation(textService =>
				{
					TextProcessingService.CaseOperation operation;
					switch (caseType)
					{
						case "upper": operation = TextProcessingService.CaseOperation.Upper; break;
						case "lower": operation = TextProcessingService.CaseOperation.Lower; break;
						case "title": operation = TextProcessingService.CaseOperation.Title; break;
						case "sentence": operation = TextProcessingService.CaseOperation.Sentence; break;
						default: return; // Should not happen with defined tags
					}
					textService.ProcessCase(operation);
				}, LocalizationManager.GetString(operationKey));
		}
	}

	// Pushes the current text state to the undo stack with operation description.
	private void PushStateForUndo(string state, string operationName = null)
	{
		// Avoid pushing if it's an undo/redo operation or if the state is the same as the last one in undo stack.
		if (isUndoRedoOperation || (undoList.Count > 0 && state == undoList.Last.Value.Text)) return;

		// If no operation name provided, use generic one
		if (string.IsNullOrEmpty(operationName))
		{
			operationName = LocalizationManager.GetString("operationGeneric");
		}

		undoList.AddLast(new HistoryState(state, operationName));
		if (undoList.Count > HISTORY_LIMIT)
		{
			undoList.RemoveFirst(); // Maintain history limit
		}

		redoList.Clear(); // Any new action clears the redo list.

		UpdateUndoRedoButtonStates();
	}

	// Updates Undo/Redo button states and tooltips based on history.
	private void UpdateUndoRedoButtonStates()
	{
		// Update button enabled states
		undoButton.Enabled = undoList.Count > 1; // More than the initial state means undo is possible.
		redoButton.Enabled = redoList.Count > 0;

		// Update tooltips with operation descriptions
		if (toolTip != null)
		{
			if (undoButton.Enabled && undoList.Count > 1)
			{
				// Show the operation that will be undone (current state)
				string operationToUndo = undoList.Last.Value.OperationName;
				string undoText = LocalizationManager.GetString("buttonUndo");
				toolTip.SetToolTip(undoButton, $"{undoText}: {operationToUndo} (Ctrl+Z)");
			}
			else
			{
				string undoText = LocalizationManager.GetString("buttonUndo");
				toolTip.SetToolTip(undoButton, $"{undoText} (Ctrl+Z)");
			}

			if (redoButton.Enabled && redoList.Count > 0)
			{
				// Show the operation that will be redone
				string operationToRedo = redoList.Last.Value.OperationName;
				string redoText = LocalizationManager.GetString("buttonRedo");
				toolTip.SetToolTip(redoButton, $"{redoText}: {operationToRedo} (Ctrl+Y)");
			}
			else
			{
				string redoText = LocalizationManager.GetString("buttonRedo");
				toolTip.SetToolTip(redoButton, $"{redoText} (Ctrl+Y)");
			}
		}
	}

	private void UndoButton_Click(object sender, EventArgs e)
	{
		if (undoList.Count > 1) // Check if there's a state to revert to (beyond initial).
		{
			isUndoRedoOperation = true; // Flag to prevent this action from being added to undo history.

			HistoryState currentState = undoList.Last.Value;
			undoList.RemoveLast();
			redoList.AddLast(currentState); // Move current state to redo stack.

			mainTextBox.Text = undoList.Last.Value.Text;
			mainTextBox.Select(mainTextBox.TextLength, 0); // Move cursor to end of text.

			UpdateUndoRedoButtonStates();

			isUndoRedoOperation = false;
			UpdateStatistics();
		}
	}

	private void RedoButton_Click(object sender, EventArgs e)
	{
		if (redoList.Count > 0)
		{
			isUndoRedoOperation = true; // Flag to prevent this action from being added to undo history.

			HistoryState redoState = redoList.Last.Value;
			redoList.RemoveLast();
			undoList.AddLast(redoState); // Move redo state back to undo stack.
			mainTextBox.Text = redoState.Text;
			mainTextBox.Select(mainTextBox.TextLength, 0); // Move cursor to end of text.

			UpdateUndoRedoButtonStates();

			isUndoRedoOperation = false;
			UpdateStatistics();
		}
	}

	private void ResetButton_Click(object sender, EventArgs e)
	{
		if (GetUserConfirmSafe(LocalizationManager.GetString("dialogConfirmReset")))
		{
			isUndoRedoOperation = true; // Prevent TextChanged event from pushing this reset state.
			mainTextBox.Text = originalText;
			isUndoRedoOperation = false;

			// Clear history and re-initialize with original text.
			undoList.Clear();
			redoList.Clear();
			PushStateForUndo(originalText, LocalizationManager.GetString("operationReset")); // This will also update Undo/Redo button states.
			UpdateStatistics();
		}
	}

	// Handles state changes of the Command Chain checkbox.
	private void CommandChainCheckBox_CheckedChanged(object sender, EventArgs e)
	{
		bool isChecked = commandChainCheckBox.Checked;
		if (commandChainPanel != null) commandChainPanel.Visible = isChecked;
		if (profilesPanel != null) profilesPanel.Visible = isChecked;
		BFS.ScriptSettings.WriteValueBool(SETTING_COMMAND_CHAIN_CHECKED, isChecked);
	}

	// Handles toggle button click to expand/collapse command chain field.
	private void ToggleMultilineButton_Click(object sender, EventArgs e)
	{
		isCommandChainExpanded = !isCommandChainExpanded;
		
		if (isCommandChainExpanded)
		{
			// Expand
			commandChainTextBox.Multiline = true;
			commandChainTextBox.ScrollBars = ScrollBars.Vertical;
			toggleMultilineButton.Text = "▲";  // Up arrow
			
			// Restore saved height or use default
			string savedHeight = BFS.ScriptSettings.ReadValue(SETTING_COMMAND_CHAIN_HEIGHT);
			int height = COMMAND_CHAIN_EXPANDED_HEIGHT;
			if (!string.IsNullOrEmpty(savedHeight) && int.TryParse(savedHeight, out int parsed))
			{
				height = Math.Max(60, Math.Min(300, parsed)); // Limit 60-300px
			}
			commandChainPanel.Height = height;
		}
		else
		{
			// Collapse - save current height first
			BFS.ScriptSettings.WriteValue(SETTING_COMMAND_CHAIN_HEIGHT, commandChainPanel.Height.ToString());
			
			commandChainTextBox.Multiline = false;
			commandChainTextBox.ScrollBars = ScrollBars.None;
			toggleMultilineButton.Text = "▼";  // Down arrow
			commandChainPanel.Height = COMMAND_CHAIN_COLLAPSED_HEIGHT;
		}
		
		// Update tooltip
		UpdateToggleButtonTooltip();
	}

	// Updates the tooltip text for the toggle multiline button based on current state.
	private void UpdateToggleButtonTooltip()
	{
		if (toolTip != null && toggleMultilineButton != null)
		{
			string key = isCommandChainExpanded ? "tooltipCollapseCommandChain" : "tooltipExpandCommandChain";
			toolTip.SetToolTip(toggleMultilineButton, LocalizationManager.GetString(key));
		}
	}

	// Marks profile as dirty when command chain text changes and profiles are active.
	private void CommandChainTextBox_TextChanged(object sender, EventArgs e)
	{
		if (commandChainCheckBox.Checked) // Only mark dirty if profiles UI is active.
		{
			isProfileDirty = true;
			UpdateDirtyIndicator();
		}
	}

	// Executes command chain on Enter key press in the command chain textbox.
	private void CommandChainTextBox_KeyDown(object sender, KeyEventArgs e)
	{
		if (e.KeyCode == Keys.Enter)
		{
			ExecuteButton_Click(this, EventArgs.Empty);
			e.Handled = true; // Mark event as handled to prevent further processing.
			e.SuppressKeyPress = true; // Suppress the annoying "ding" sound on Enter.
		}
	}

	// Executes the command chain entered in the commandChainTextBox.
	private void ExecuteButton_Click(object sender, EventArgs e)
	{
		string commandChainString = commandChainTextBox.Text;
		if (string.IsNullOrWhiteSpace(commandChainString)) return;

		// Set flag to prevent individual commands from adding to history
		isExecutingCommandChain = true;

		string[] commands = commandChainString.Split(new string[]
			{
				"||"
			}, StringSplitOptions.RemoveEmptyEntries);
		List<string> errors = new List<string>();

		// Suspend UI updates during batch processing for performance.
		mainTextBox.SuspendLayout();
		this.SuspendLayout();

		try
		{
			mainTextBox.Focus(); // Ensure main textbox has focus for selection-dependent operations.
			string originalTextForUndo = mainTextBox.Text; // For a single undo step after the entire chain executes.

			foreach (string cmdWithMaybeParam in commands)
			{
				string actualCmdPartString;
				string actualParamPart = null;

				int bangIndex = cmdWithMaybeParam.IndexOf('!');
				if (bangIndex != -1)
				{
					actualCmdPartString = cmdWithMaybeParam.Substring(0, bangIndex).Trim();
					if (bangIndex + 1 < cmdWithMaybeParam.Length)
					{
						actualParamPart = cmdWithMaybeParam.Substring(bangIndex + 1);
					}
					else
					{
						actualParamPart = "";
					}
				}
				else
				{
					actualCmdPartString = cmdWithMaybeParam.Trim();
				}

				if (int.TryParse(actualCmdPartString, out int cmdNumber))
				{
					if (commandMap.TryGetValue(cmdNumber, out ToolStripMenuItem menuItemToExecute))
					{
						object originalTag = menuItemToExecute.Tag;
						bool parameterAppliedToTag = false;
						try
						{
							string baseCommandTag = originalTag as string;

							if (actualParamPart != null)
							{
								if (baseCommandTag == "other_frame")
								{
									string[] bracketParts = actualParamPart.Split(new string[]
										{
											"~~"
										}, StringSplitOptions.None);
									if (bracketParts.Length == 2)
									{
										menuItemToExecute.Tag = new string[]
										{
											bracketParts[0], bracketParts[1]
										};
										parameterAppliedToTag = true;
									}
									else
									{
										errors.Add($"Cmd {cmdNumber} (Other Frame): Invalid param format '{actualParamPart}'. Expected 'start~~end'."); continue ;
									}
								}
								else if (baseCommandTag == "lines_split_char")
								{
									menuItemToExecute.Tag = actualParamPart.Replace("_space_", " ");
									parameterAppliedToTag = true;
								}
								else if (baseCommandTag == "lines_remove_duplicates")
								{
									// Parameter "i" means ignore case
									if (actualParamPart.Trim().ToLower() == "i")
									{
										// Temporarily change the menu item to the ignore case version
										if (commandMap.ContainsValue(linesRemoveDuplicatesIgnoreCaseMenuItem))
										{
											menuItemToExecute = linesRemoveDuplicatesIgnoreCaseMenuItem;
											baseCommandTag = menuItemToExecute.Tag as string;
											originalTag = baseCommandTag;
										}
									}
									// No tag change needed, parameter already processed
								}
								else if (baseCommandTag == "lines_remove_containing" || baseCommandTag == "lines_keep_containing")
								{
									string filterTextChain = actualParamPart;
									bool useRegexChain = false;
									if (filterTextChain.StartsWith("r~"))
									{
										useRegexChain = true;
										filterTextChain = filterTextChain.Substring(2);
									}
									else
									{
										filterTextChain = filterTextChain.Replace("_space_", " ");
									}
									if (string.IsNullOrEmpty(filterTextChain))
									{
										errors.Add($"Cmd {cmdNumber} (Filter Lines): Filter pattern cannot be empty."); continue ;
									}
									menuItemToExecute.Tag = new string[]
									{
										filterTextChain, useRegexChain ? "regex" : "normal"
									};
									parameterAppliedToTag = true;
								}
								else if (baseCommandTag == "lines_number_lines")
								{
									// Format: PATTERN|START_VALUE or just PATTERN
									string[] parts = actualParamPart.Split(new[] { '|' }, 2);
									string formatPart = parts[0];
									
									if (!formatPart.Contains("#"))
									{
										errors.Add($"Cmd {cmdNumber} (Number Lines): Format must contain # character."); continue ;
									}
									
									// Validate start value if provided
									if (parts.Length > 1 && !string.IsNullOrEmpty(parts[1]))
									{
										if (!int.TryParse(parts[1], out _))
										{
											errors.Add($"Cmd {cmdNumber} (Number Lines): Start value must be a valid integer."); continue ;
										}
									}
									
									menuItemToExecute.Tag = actualParamPart;
									parameterAppliedToTag = true;
								}
								else if (baseCommandTag == "lines_trim_chars")
								{
									string[] trimParts = actualParamPart.Split(new string[]
										{
											"~~"
										}, StringSplitOptions.None);
									if (trimParts.Length == 2)
									{
										int startChars = 0, endChars = 0;
										if (!string.IsNullOrEmpty(trimParts[0]) && !int.TryParse(trimParts[0], out startChars))
										{
											errors.Add($"Cmd {cmdNumber} (Trim Chars): Invalid start number '{trimParts[0]}'."); continue ;
										}
										if (!string.IsNullOrEmpty(trimParts[1]) && !int.TryParse(trimParts[1], out endChars))
										{
											errors.Add($"Cmd {cmdNumber} (Trim Chars): Invalid end number '{trimParts[1]}'."); continue ;
										}
										if (startChars < 0 || endChars < 0)
										{
											errors.Add($"Cmd {cmdNumber} (Trim Chars): Numbers must be non-negative."); continue ;
										}
										menuItemToExecute.Tag = new int[]
										{
											startChars, endChars
										};
										parameterAppliedToTag = true;
									}
									else
									{
										errors.Add($"Cmd {cmdNumber} (Trim Chars): Invalid param format '{actualParamPart}'. Expected 'start~~end'."); continue ;
									}
								}
								else if (baseCommandTag == "join_lines_other")
								{
									menuItemToExecute.Tag = actualParamPart.Replace("_space_", " ");
									parameterAppliedToTag = true;
								}
								else if (baseCommandTag == "search_replace_replace")
								{
									string[] replaceParts = actualParamPart.Split(new string[]
										{
											"~~", "~r~"
										}, 2, StringSplitOptions.None);
									if (replaceParts.Length == 2)
									{
										bool isRegex = actualParamPart.Contains("~r~");
										string find = replaceParts[0];
										string replace = replaceParts[1];
										if (!isRegex)
										{
											find = find.Replace("_space_", " ");
											replace = replace.Replace("_space_", " ");
										}
										menuItemToExecute.Tag = new string[]
										{
											find, replace, isRegex ? "regex" : "normal"
										};
										parameterAppliedToTag = true;
									}
									else
									{
										errors.Add($"Cmd {cmdNumber} (Replace): Invalid param format '{actualParamPart}'. Expected 'find~~replace' or 'find~r~replace'."); continue ;
									}
								}
								else if (baseCommandTag == "search_replace_delete" || baseCommandTag == "search_replace_extract")
								{
									bool isRegex = actualParamPart.StartsWith("r~");
									string pattern = isRegex ? actualParamPart.Substring(2) : actualParamPart.Replace("_space_", " ");
									if (string.IsNullOrEmpty(pattern) && baseCommandTag == "search_replace_extract")
									{
										errors.Add($"Cmd {cmdNumber} (Extract): Pattern cannot be empty."); continue ;
									}
									menuItemToExecute.Tag = new string[]
									{
										pattern, isRegex ? "regex" : "normal"
									};
									parameterAppliedToTag = true;
								}
								else
								{
									// Parameter was provided, but this command (identified by its baseCommandTag)
									// either doesn't expect one, or its parameterized case isn't handled here.
									// This could be an error if the command strictly does not take parameters.
									// For now, we assume if it's not one of the explicitly handled cases above,
									// it might be an unhandled parameterized command or a command that ignores extra parameters.
									// errors.Add($"Command {cmdNumber} ({baseCommandTag}): Parameter '{paramPart}' not expected or not handled.");
									// continue; // Or allow to proceed, and the command's handler might ignore the Tag if it's not of expected type.
								}
							}
							else // paramPart == null (no parameter was provided in the command chain string for this command number)
							{
								// Check if this command *requires* a parameter when executed from a command chain.
								if (baseCommandTag == "other_frame" ||
									baseCommandTag == "lines_split_char" ||
									baseCommandTag == "lines_remove_containing" ||
									baseCommandTag == "lines_keep_containing" ||
									baseCommandTag == "lines_number_lines" ||
									baseCommandTag == "lines_trim_chars" ||
									baseCommandTag == "join_lines_other" || // For "Join Lines - Other..." command
									baseCommandTag == "search_replace_replace" ||
									baseCommandTag == "search_replace_delete" ||
									baseCommandTag == "search_replace_extract")
								{
									errors.Add($"Command {cmdNumber} ({baseCommandTag}): Parameter is required in command chain.");
									continue ; // Skip this command as it requires a parameter that was not provided.
								}
								// If no parameter was provided and the command is not listed above (i.e., it doesn't require one for chain execution),
								// its Tag remains the originalTag, and it will be executed as a non-parameterized command.
							}

							menuItemToExecute.PerformClick();

							// Restore the original tag if it was temporarily changed for parameterized execution.
							if (parameterAppliedToTag)
							{
								menuItemToExecute.Tag = originalTag;
							}
						}
						catch (Exception ex)
						{
							errors.Add($"Error executing command {cmdNumber} ({menuItemToExecute.Text.Split(')').LastOrDefault()?.Trim()}): {ex.Message}");
						}
						finally
						{
							// Ensure original tag is restored even if an exception occurred during PerformClick.
							if (parameterAppliedToTag)
							{
								menuItemToExecute.Tag = originalTag;
							}
						}
					}
					else
					{
						errors.Add($"Command number {cmdNumber} not found.");
					}
				}
				else
				{
					errors.Add($"Invalid command format: '{cmdWithMaybeParam}'. Expected number or number!parameter.");
				}
			}

			// After all commands are processed, update statistics and push to undo stack if text changed.
			UpdateStatistics();
			if (mainTextBox.Text != originalTextForUndo && !isUndoRedoOperation)
			{
				PushStateForUndo(mainTextBox.Text, LocalizationManager.GetString("operationCommandChain"));
			}
		}
		finally
		{
			// Reset flag and resume UI updates
			isExecutingCommandChain = false;
			mainTextBox.ResumeLayout();
			this.ResumeLayout();
		}

		if (errors.Any())
		{
			ShowMessageErrorSafe("Errors occurred during command chain execution:\n" + string.Join("\n", errors));
		}
	}

	// Initializes the panel for command chain input.
	private void InitializeCommandChainPanel()
	{
		commandChainPanel = new Panel
		{
			Dock = DockStyle.Top,
			Height = COMMAND_CHAIN_COLLAPSED_HEIGHT,
			Visible = false, // Initially hidden, shown when commandChainCheckBox is checked.
			Padding = new Padding(10, 3, 10, 3)
		};

		executeButton = new Button
		{
			Size = new Size(90, 23),
			Dock = DockStyle.Right
			// Text is set in UpdateUIStrings.
		};
		executeButton.Click += ExecuteButton_Click;

		// Toggle button for expanding/collapsing command field
		toggleMultilineButton = new Button
		{
			Size = new Size(25, 23),
			Text = "▼",  // Down arrow for expanding
			Dock = DockStyle.Right,
			Font = new Font("Segoe UI", 9F, FontStyle.Bold)
		};
		toggleMultilineButton.Click += ToggleMultilineButton_Click;

		commandChainTextBox = new TextBox
		{
			Dock = DockStyle.Fill,
			Multiline = false,
			MaxLength = 0,
			ScrollBars = ScrollBars.None
			// KeyDown event (for Enter) and TextChanged event (for dirty flag) are assigned below.
		};
		commandChainTextBox.KeyDown += CommandChainTextBox_KeyDown; // For Enter key execution.
		commandChainTextBox.TextChanged += CommandChainTextBox_TextChanged; // For profile dirty flag.

		// Order matters for Dock!
		commandChainPanel.Controls.Add(commandChainTextBox); // Add textbox first to fill available space.
		commandChainPanel.Controls.Add(toggleMultilineButton); // Toggle button in the middle
		commandChainPanel.Controls.Add(executeButton); // Execute button on the right
	}

	// Initializes the panel for profile management.
	private void InitializeProfilesPanel()
	{
		profilesPanel = new Panel
		{
			Dock = DockStyle.Top,
			Height = 30,
			Visible = false, // Initially hidden, shown when commandChainCheckBox is checked.
			Padding = new Padding(10, 3, 10, 3)
		};

		saveProfileButton = new Button
		{
			Size = new Size(120, 23), // Wider to accommodate localized text like "Сохранить профиль*"
			Dock = DockStyle.Right
			// Text (normal or dirty state) is set in UpdateUIStrings via UpdateDirtyIndicator.
		};
		saveProfileButton.Click += SaveProfileButton_Click;

		deleteProfileButton = new Button
		{
			Text = "✕", // Universal delete symbol.
			Size = new Size(25, 23),
			Dock = DockStyle.Right,
			Font = new Font("Segoe UI", 9F, FontStyle.Bold) // Make X more prominent.
			// Tooltip is set in UpdateUIStrings.
		};
		deleteProfileButton.Click += DeleteProfileButton_Click;

		dirtyIndicator = new PictureBox
		{
			Size = new Size(16, 16),
			SizeMode = PictureBoxSizeMode.CenterImage,
			Dock = DockStyle.Left, // Positioned to the left of the ComboBox.
			Visible = false,
			Margin = new Padding(0, 0, 5, 0), // Right margin for spacing.
			Image = GetDirtyIndicatorIcon() // Dynamically created star icon.
			// Tooltip is set in UpdateUIStrings.
		};

		profilesComboBox = new ComboBox
		{
			Dock = DockStyle.Fill,
			DropDownStyle = ComboBoxStyle.DropDown, // Allows typing a new profile name or selecting an existing one.
			AutoCompleteMode = AutoCompleteMode.SuggestAppend,
			AutoCompleteSource = AutoCompleteSource.ListItems
		};
		profilesComboBox.SelectedIndexChanged += ProfilesComboBox_SelectedIndexChanged;
		profilesComboBox.TextChanged += ProfilesComboBox_TextChanged;

		profilesPanel.Controls.Add(profilesComboBox); // Add combobox first
		profilesPanel.Controls.Add(dirtyIndicator);
		profilesPanel.Controls.Add(deleteProfileButton);
		profilesPanel.Controls.Add(saveProfileButton);
	}

	private void ProfilesComboBox_SelectedIndexChanged(object sender, EventArgs e)
	{
		if (profilesComboBox.SelectedItem == null) return;

		string selectedProfile = profilesComboBox.SelectedItem.ToString();

		// Only prompt to save if profiles are active and a different profile was dirty
		if (commandChainCheckBox.Checked && isProfileDirty && !string.IsNullOrEmpty(currentProfileName) && currentProfileName != selectedProfile)
		{
			string promptMessage = LocalizationManager.GetString("dialogConfirmProfileDirtySwitch", currentProfileName);
			bool saveChanges = GetUserConfirmSafe(promptMessage);

			if (saveChanges)
			{
				SaveCurrentProfile(currentProfileName); // Assumes currentProfileName is valid for saving
			}
			// else: User chose not to save the dirty profile. Changes are discarded when the new profile loads.
		}

		string profileCommands = BFS.ScriptSettings.ReadValue(PROFILE_SETTING_PREFIX + selectedProfile);

		// Temporarily detach TextChanged to avoid marking profile as dirty or adding to undo history during load.
		commandChainTextBox.TextChanged -= CommandChainTextBox_TextChanged;
		commandChainTextBox.Text = profileCommands;
		commandChainTextBox.TextChanged += CommandChainTextBox_TextChanged;

		// Load "Process Lines Separately" (PLS) setting from the selected profile.
		bool plsProfileValue = BFS.ScriptSettings.ReadValueBool(PROFILE_SETTING_PREFIX + selectedProfile + PROFILE_PLS_SUFFIX);
		if (processLinesSeparatelyCheckBox != null) processLinesSeparatelyCheckBox.CheckedChanged -= ProcessLinesSeparatelyCheckBox_CheckedChanged;
		if (processLinesSeparatelyCheckBox != null) processLinesSeparatelyCheckBox.Checked = plsProfileValue;
		if (processLinesSeparatelyCheckBox != null) processLinesSeparatelyCheckBox.CheckedChanged += ProcessLinesSeparatelyCheckBox_CheckedChanged;

		currentProfileName = selectedProfile;
		isProfileDirty = false; // Loading a profile makes it clean.
		UpdateDirtyIndicator();
		// BFS.ScriptSettings.WriteValue(SETTING_LAST_SELECTED_PROFILE, currentProfileName); // Removed: Now profile selection is saved on OK/Exit, not on change.
	}

	private void LoadProfiles()
	{
		if (profilesComboBox == null) return;
		profilesComboBox.Items.Clear();
		currentProfileName = "";
		isProfileDirty = false;

		string profileNamesString = BFS.ScriptSettings.ReadValue(SETTING_PROFILE_NAMES_LIST);
		if (!string.IsNullOrEmpty(profileNamesString))
		{
			string[] profileNames = profileNamesString.Split('|');
			Array.Sort(profileNames, StringComparer.CurrentCulture);
			foreach (string name in profileNames)
			{
				if (!string.IsNullOrWhiteSpace(name))
				{
					profilesComboBox.Items.Add(name);
				}
			}
		}

		string lastSelectedProfile = BFS.ScriptSettings.ReadValue(SETTING_LAST_SELECTED_PROFILE);
		if (!string.IsNullOrEmpty(lastSelectedProfile) && profilesComboBox.Items.Contains(lastSelectedProfile))
		{
			profilesComboBox.SelectedItem = lastSelectedProfile;
			// This triggers ProfilesComboBox_SelectedIndexChanged, which loads commands, sets currentProfileName, and marks as clean.
		}
		else
		{
			// No last selected profile, or it's invalid/deleted, or profile list is empty.
			// currentProfileName remains empty, and isProfileDirty remains false.
			UpdateDirtyIndicator();
		}
		// If no profile was selected (e.g., after deletion or on first launch with no profiles),
		// currentProfileName is empty, and isProfileDirty is false.
	}

	// Creates a star icon (*, for dirty indicator) if not cached, then returns it.
	private Image GetDirtyIndicatorIcon()
	{
		// Return cached icon if already created.
		if (_cachedDirtyIndicatorIcon != null)
			return _cachedDirtyIndicatorIcon;

		// Create new icon if cache is empty.
		Bitmap bitmap = new Bitmap(16, 16);
		using (Graphics g = Graphics.FromImage(bitmap))
		{
			g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
			g.Clear(Color.Transparent); // Use form's BackColor for transparency if needed, or a specific color for debugging.

			using (Brush brush = new SolidBrush(Color.Goldenrod))
			{
				// Define points for a 5-pointed star.
				PointF[] starPoints = new PointF[10];
				float outerRadius = 7f;
				float innerRadius = 3f;
				float angle = (float)(- Math.PI / 2); // Start at the top point of the star.
				float angleIncrement = (float)(Math.PI / 5); // Each turn is 36 degrees for 10 points (5 outer, 5 inner).

				for (int i = 0; i < 10; i++)
				{
					float radius = (i % 2 == 0) ? outerRadius : innerRadius;
					starPoints[i] = new PointF(
						8f + radius * (float) Math.Cos(angle), // 8f is centerX
						8f + radius * (float) Math.Sin(angle)  // 8f is centerY
					);
					angle += angleIncrement;
				}
				g.FillPolygon(brush, starPoints);
			}
		}

		// Cache the newly created icon.
		_cachedDirtyIndicatorIcon = bitmap;
		return _cachedDirtyIndicatorIcon;
	}

	// Updates visibility of the dirty indicator icon and text of the save profile button.
	private void UpdateDirtyIndicator()
	{
		if (dirtyIndicator != null) dirtyIndicator.Visible = isProfileDirty;
		if (saveProfileButton != null)
		{
			saveProfileButton.Text = LocalizationManager.GetString(isProfileDirty ? "buttonSaveProfileDirty" : "buttonSaveProfile");
		}
	}

	// Loads saved states for command chain UI: checkbox, last text, and global "Process Lines Separately" (PLS).
	private void LoadCommandChainSettings()
	{
		if (commandChainCheckBox != null)
		{
			commandChainCheckBox.Checked = BFS.ScriptSettings.ReadValueBool(SETTING_COMMAND_CHAIN_CHECKED);
			CommandChainCheckBox_CheckedChanged(commandChainCheckBox, EventArgs.Empty); // Update panel visibility.
		}
		if (commandChainTextBox != null)
		{
			commandChainTextBox.Text = BFS.ScriptSettings.ReadValue(SETTING_LAST_COMMAND_CHAIN_TEXT);
		}
		if (processLinesSeparatelyCheckBox != null)
		{
			// Load global state for PLS checkbox. This will be overridden if a profile is subsequently loaded.
			processLinesSeparatelyCheckBox.Checked = BFS.ScriptSettings.ReadValueBool(SETTING_PLS_GLOBAL_CHECKED);
		}
		
		// Load expanded/collapsed state for command chain field
		isCommandChainExpanded = BFS.ScriptSettings.ReadValueBool(SETTING_COMMAND_CHAIN_EXPANDED);
		if (isCommandChainExpanded && toggleMultilineButton != null)
		{
			// Apply expanded state without triggering Click event
			commandChainTextBox.Multiline = true;
			commandChainTextBox.ScrollBars = ScrollBars.Vertical;
			toggleMultilineButton.Text = "▲";
			
			string savedHeight = BFS.ScriptSettings.ReadValue(SETTING_COMMAND_CHAIN_HEIGHT);
			int height = COMMAND_CHAIN_EXPANDED_HEIGHT;
			if (!string.IsNullOrEmpty(savedHeight) && int.TryParse(savedHeight, out int parsed))
			{
				height = Math.Max(60, Math.Min(300, parsed));
			}
			commandChainPanel.Height = height;
		}
		UpdateToggleButtonTooltip();
	}

	protected override void OnFormClosing(FormClosingEventArgs e)
	{
		base.OnFormClosing(e);

		// Offer to save the current profile if it's dirty, profiles are active, and form isn't closing due to OK button.
		if (commandChainCheckBox.Checked && this.DialogResult != DialogResult.OK && isProfileDirty && !string.IsNullOrEmpty(currentProfileName))
		{
			// Use a specific prompt for closing via X, Cancel, Alt+F4, etc.
			bool saveChanges = GetUserConfirmSafe(
				LocalizationManager.GetString("dialogConfirmProfileDirtySaveChangesOnExit", currentProfileName) +
				"\n\n" + LocalizationManager.GetString("dialogConfirmProfileDirtyConsequenceOnExit")
			);

			if (saveChanges)
			{
				SaveCurrentProfile(currentProfileName);
			}
			// If user chooses not to save, changes are lost; the form continues closing (e.Reset is not set).
		}

		// Logic for saving the text in commandChainTextBox as the SETTING_LAST_COMMAND_CHAIN_TEXT:
		// This setting stores the command chain that was last visible/used, for convenience on next launch if not using profiles.
		// 1. If no profile is active OR if a profile is active AND dirty (and user didn't save it above),
		//    save the current (potentially unsaved profile's or ad-hoc) commandChainTextBox.Text.
		// 2. If a profile is active AND clean, save its commands (which are in commandChainTextBox.Text) as the last used chain.
		// 3. If no profile is active and commandChainTextBox is empty, optionally clear the setting or leave it as is.
		if (string.IsNullOrEmpty(currentProfileName) || (isProfileDirty && !string.IsNullOrEmpty(currentProfileName)))
		{
			// Case 1: No profile selected, or selected profile is dirty (and wasn't saved in the block above).
			// The text in commandChainTextBox represents either an ad-hoc chain or unsaved changes to a profile.
			BFS.ScriptSettings.WriteValue(SETTING_LAST_COMMAND_CHAIN_TEXT, commandChainTextBox.Text);
		}
		else if (!string.IsNullOrEmpty(currentProfileName) && !isProfileDirty)
		{
			// Case 2: A profile is selected and it's clean. Save its commands.
			BFS.ScriptSettings.WriteValue(SETTING_LAST_COMMAND_CHAIN_TEXT, commandChainTextBox.Text);
		}
		// Save checkbox state for Command Chain feature itself.
		if (commandChainCheckBox != null)
		{
			BFS.ScriptSettings.WriteValueBool(SETTING_COMMAND_CHAIN_CHECKED, commandChainCheckBox.Checked);
		}
		// Save global state for "Process Lines Separately" checkbox.
		if (processLinesSeparatelyCheckBox != null)
		{
			BFS.ScriptSettings.WriteValueBool(SETTING_PLS_GLOBAL_CHECKED, processLinesSeparatelyCheckBox.Checked);
		}
		
		// Save command chain expanded/collapsed state
		BFS.ScriptSettings.WriteValueBool(SETTING_COMMAND_CHAIN_EXPANDED, isCommandChainExpanded);
		if (isCommandChainExpanded)
		{
			BFS.ScriptSettings.WriteValue(SETTING_COMMAND_CHAIN_HEIGHT, commandChainPanel.Height.ToString());
		}

		// Save window behavior settings (size, position preferences).
		BFS.ScriptSettings.WriteValueBool(SETTING_REMEMBER_WINDOW_SIZE, _rememberWindowSize);
		BFS.ScriptSettings.WriteValueBool(SETTING_REMEMBER_WINDOW_POSITION, _rememberWindowPosition);
		
		// Save window dimensions if 'Remember size' is enabled and window is in normal state.
		if (_rememberWindowSize && this.WindowState == FormWindowState.Normal)
		{
			BFS.ScriptSettings.WriteValue(LocalizationManager.WINDOW_WIDTH_SETTING, this.Width.ToString());
			BFS.ScriptSettings.WriteValue(LocalizationManager.WINDOW_HEIGHT_SETTING, this.Height.ToString());
		}
		
		// Save window location if 'Remember position' is enabled (and 'Appear near mouse' is off) and window is in normal state.
		if (_rememberWindowPosition && this.WindowState == FormWindowState.Normal)
		{
			BFS.ScriptSettings.WriteValue(SETTING_WINDOW_X, this.Location.X.ToString());
			BFS.ScriptSettings.WriteValue(SETTING_WINDOW_Y, this.Location.Y.ToString());
		}
	}

	private void InitializeToolTips()
	{
		toolTip = new ToolTip();
		// Actual tooltip text is set in UpdateUIStrings to support localization.
	}

	// Safe dialog wrappers that temporarily disable TopMost to prevent dialogs appearing behind the window
	private void ShowMessageInfoSafe(string message)
	{
		bool wasTopMost = this.TopMost;
		this.TopMost = false;
		BFS.Dialog.ShowMessageInfo(message);
		this.TopMost = wasTopMost;
	}

	private void ShowMessageErrorSafe(string message)
	{
		bool wasTopMost = this.TopMost;
		this.TopMost = false;
		BFS.Dialog.ShowMessageError(message);
		this.TopMost = wasTopMost;
	}

	private bool GetUserConfirmSafe(string message)
	{
		bool wasTopMost = this.TopMost;
		this.TopMost = false;
		bool result = BFS.Dialog.GetUserConfirm(message);
		this.TopMost = wasTopMost;
		return result;
	}

	private string GetUserInputSafe(string prompt, string defaultValue)
	{
		bool wasTopMost = this.TopMost;
		this.TopMost = false;
		string result = BFS.Dialog.GetUserInput(prompt, defaultValue);
		this.TopMost = wasTopMost;
		return result;
	}

	// Helper method to validate regex pattern with timeout protection
	private bool TryValidateRegex(string pattern, out string errorMessage)
	{
		try
		{
			// Try to create regex with timeout to catch invalid patterns and potential ReDoS
			var testRegex = new Regex(pattern, RegexOptions.None, TimeSpan.FromMilliseconds(100));
			errorMessage = null;
			return true;
		}
		catch (ArgumentException ex)
		{
			errorMessage = ex.Message;
			return false;
		}
		catch (Exception ex)
		{
			errorMessage = ex.Message;
			return false;
		}
	}

	private void SaveProfileButton_Click(object sender, EventArgs e)
	{
		string suggestedProfileName = profilesComboBox.Text;

		if (string.IsNullOrWhiteSpace(suggestedProfileName))
		{
			// If ComboBox is empty, prompt for a name, defaulting to current if exists, or "New Profile".
			suggestedProfileName = GetUserInputSafe("Enter profile name:",
				!string.IsNullOrEmpty(currentProfileName) ? currentProfileName : "New Profile");
			if (string.IsNullOrWhiteSpace(suggestedProfileName)) return; // User cancelled input dialog.
		}

		// At this point, suggestedProfileName is not null or whitespace.
		// Check if we are overwriting an existing profile that is different from the currently active one.
		if (profilesComboBox.Items.Contains(suggestedProfileName) &&
			suggestedProfileName != currentProfileName &&
			!string.IsNullOrEmpty(currentProfileName)) // Ensure currentProfileName was actually set (not saving a new profile over existing while no profile was active).
		{
			if (!GetUserConfirmSafe($"Profile '{suggestedProfileName}' already exists. Overwrite?"))
			{
				return;
			}
		}
		// Handle case: No profile is currently selected (currentProfileName is empty), but user typed the name of an existing profile.
		else if (profilesComboBox.Items.Contains(suggestedProfileName) && string.IsNullOrEmpty(currentProfileName) && profilesComboBox.SelectedItem == null)
		{
			// This implies user typed an existing profile name when no profile was selected from dropdown.
			if (!GetUserConfirmSafe($"Profile '{suggestedProfileName}' already exists. Overwrite with current command chain?"))
			{
				return;
			}
		}

		SaveCurrentProfile(suggestedProfileName);
	}

	private void SaveCurrentProfile(string profileNameToSave)
	{
		if (string.IsNullOrWhiteSpace(profileNameToSave)) return;

		BFS.ScriptSettings.WriteValue(PROFILE_SETTING_PREFIX + profileNameToSave, commandChainTextBox.Text);
		// Save "Process Lines Separately" (PLS) setting for this profile.
		if (processLinesSeparatelyCheckBox != null)
		{
			BFS.ScriptSettings.WriteValueBool(PROFILE_SETTING_PREFIX + profileNameToSave + PROFILE_PLS_SUFFIX, processLinesSeparatelyCheckBox.Checked);
		}

		// Update the persisted list of all profile names.
		List<string> profileNames = new List<string>();
		if (profilesComboBox != null)
		{
			foreach (var item in profilesComboBox.Items)
			{
				profileNames.Add(item.ToString());
			}
		}
		if (!profileNames.Contains(profileNameToSave))
		{
			profileNames.Add(profileNameToSave);
		}

		var distinctNames = profileNames.Distinct().ToList();
		distinctNames.Sort(StringComparer.CurrentCulture);

		BFS.ScriptSettings.WriteValue(SETTING_PROFILE_NAMES_LIST, string.Join("|", distinctNames.ToArray()));

		if (profilesComboBox != null)
		{
			profilesComboBox.Items.Clear();
			profilesComboBox.Items.AddRange(distinctNames.ToArray());
		}

		currentProfileName = profileNameToSave;
		isProfileDirty = false;
		UpdateDirtyIndicator();

		if (profilesComboBox != null) profilesComboBox.SelectedItem = profileNameToSave; // Ensure the saved profile is selected in ComboBox.
	}

	private void DeleteProfileButton_Click(object sender, EventArgs e)
	{
		if (profilesComboBox.SelectedItem == null)
		{
			ShowMessageErrorSafe(LocalizationManager.GetString("errorNoProfileSelected"));
			return;
		}

		string profileToDelete = profilesComboBox.SelectedItem.ToString();

		if (GetUserConfirmSafe(LocalizationManager.GetString("dialogConfirmDeleteProfile", profileToDelete)))
		{
			BFS.ScriptSettings.DeleteValue(PROFILE_SETTING_PREFIX + profileToDelete);

			List<string> profileNames = new List<string>();
			foreach (var item in profilesComboBox.Items)
			{
				if (item.ToString() != profileToDelete)
				{
					profileNames.Add(item.ToString());
				}
			}
			BFS.ScriptSettings.WriteValue(SETTING_PROFILE_NAMES_LIST, string.Join("|", profileNames.ToArray()));

			// Manage state after deleting the profile.
			string previouslySelected = null;
			if (currentProfileName == profileToDelete)
			{
				currentProfileName = "";
				isProfileDirty = !string.IsNullOrWhiteSpace(commandChainTextBox.Text); // If text remains, it's now an unsaved ad-hoc chain.
				// When active profile is deleted, "Process Lines Separately" (PLS) checkbox should revert to global setting.
				if (processLinesSeparatelyCheckBox != null)
				{
					processLinesSeparatelyCheckBox.CheckedChanged -= ProcessLinesSeparatelyCheckBox_CheckedChanged;
					processLinesSeparatelyCheckBox.Checked = BFS.ScriptSettings.ReadValueBool(SETTING_PLS_GLOBAL_CHECKED);
					processLinesSeparatelyCheckBox.CheckedChanged += ProcessLinesSeparatelyCheckBox_CheckedChanged;
				}
			} else
			{
				previouslySelected = currentProfileName; // Another profile was active, will try to reselect it.
			}

			LoadProfiles(); // Reloads ComboBox items and attempts to select the last used or first profile.

			if (!string.IsNullOrEmpty(previouslySelected) && profilesComboBox.Items.Contains(previouslySelected))
			{
				profilesComboBox.SelectedItem = previouslySelected;
			} else if (profilesComboBox.Items.Count > 0)
			{
				profilesComboBox.SelectedIndex = 0; // Select first available profile if previous one (or deleted one) cannot be selected.
			} else
			{
				// No profiles left; ensure command chain text is treated as non-profile specific.
				currentProfileName = "";
				// isProfileDirty will be (re)set by commandChainTextBox.TextChanged if text exists.
			}

			UpdateDirtyIndicator();
		}
	}

	private void ProcessLinesSeparatelyCheckBox_CheckedChanged(object sender, EventArgs e)
	{
		// Only mark profile dirty if profiles are active, a profile is selected, and this is not an undo/redo operation.
		if (commandChainCheckBox.Checked && !string.IsNullOrEmpty(currentProfileName) && !isUndoRedoOperation)
		{
			bool loadedPlsState = BFS.ScriptSettings.ReadValueBool(PROFILE_SETTING_PREFIX + currentProfileName + PROFILE_PLS_SUFFIX);
			if (processLinesSeparatelyCheckBox.Checked != loadedPlsState)
			{
				isProfileDirty = true;
				UpdateDirtyIndicator();
			}
		}
		// If profiles are not active or no profile is selected, this change affects the global PLS setting (saved separately on form closing).
	}

	private void ProfilesComboBox_TextChanged(object sender, EventArgs e)
	{
		// Only mark profile dirty if profiles are active, ComboBox has focus (user is typing),
		// and the typed text is not an existing profile name.
		if (commandChainCheckBox.Checked && profilesComboBox.IsHandleCreated && profilesComboBox.Focused)
		{
			string currentText = profilesComboBox.Text;
			bool isExistingProfile = profilesComboBox.Items.Contains(currentText);

			if (!string.IsNullOrWhiteSpace(currentText) && !isExistingProfile)
			{
				isProfileDirty = true;
				UpdateDirtyIndicator();
			}
		}
	}

	// Executes a given command chain string silently (without user interaction for parameters) and returns the processed text.
	public string ExecuteCommandsSilently(string commandChainString)
	{
		// Assumes MainWindow constructor has already populated mainTextBox with initial text and initialized controls.

		if (string.IsNullOrWhiteSpace(commandChainString))
		{
			return mainTextBox.Text; // Return original text if no commands.
		}

		// Safety check for essential UI elements.
		if (commandChainTextBox == null || commandChainCheckBox == null || executeButton == null || mainTextBox == null)
		{
			return this.originalText; // Fallback to original input text if UI not ready.
		}

		commandChainTextBox.Text = commandChainString;
		commandChainCheckBox.Checked = true; // Ensure command chain mode is active for ExecuteButton logic.

		ExecuteButton_Click(this.executeButton, EventArgs.Empty);

		return mainTextBox.Text;
	}

	protected override void OnKeyDown(KeyEventArgs e)
	{
		// Проверяем Ctrl (без Alt) двумя способами для надежности
		bool ctrlOnlyPressed = (e.Control && !e.Alt) || 
		                       (Control.ModifierKeys == Keys.Control);
		
		// Перехватываем Ctrl+цифры для быстрого доступа к командам
		if (ctrlOnlyPressed)
		{
			int digit = -1;
			
			// Проверяем основной ряд цифр
			if (e.KeyCode >= Keys.D0 && e.KeyCode <= Keys.D9)
			{
				digit = (int)e.KeyCode - (int)Keys.D0;
			}
			// Проверяем NumPad
			else if (e.KeyCode >= Keys.NumPad0 && e.KeyCode <= Keys.NumPad9)
			{
				digit = (int)e.KeyCode - (int)Keys.NumPad0;
			}
			
			if (digit >= 0)
			{
				// Блокируем стандартную обработку
				e.SuppressKeyPress = true;
				e.Handled = true;
				
				// Добавляем цифру в буфер быстрых команд
				_quickCommandBuffer += digit.ToString();
				UpdateQuickCommandIndicator();
				CheckAndExecuteQuickCommand();
				
				base.OnKeyDown(e);
				return;
			}
		}
		
		base.OnKeyDown(e);
	}

	protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
	{
		// Ctrl+Enter for OK
		if (keyData == (Keys.Control | Keys.Enter))
		{
			okButton.PerformClick();
			return true;
		}

		// Escape for Cancel or clear quick command buffer
		if (keyData == Keys.Escape)
		{
			// If quick command is active, clear it first
			if (!string.IsNullOrEmpty(_quickCommandBuffer))
			{
				ClearQuickCommandBuffer();
				return true;
			}
			
			// Otherwise, perform cancel
			cancelButton.PerformClick();
			return true;
		}

		// Ctrl+Z for Undo
		if (keyData == (Keys.Control | Keys.Z))
		{
			if (undoButton.Enabled)
			{
				undoButton.PerformClick();
			}
			return true;
		}

		// Ctrl+Y for Redo
		if (keyData == (Keys.Control | Keys.Y))
		{
			if (redoButton.Enabled)
			{
				redoButton.PerformClick();
			}
			return true;
		}

		// Ctrl+Shift+Enter for Execute Command Chain
		if (keyData == (Keys.Control | Keys.Shift | Keys.Enter))
		{
			if (commandChainCheckBox.Checked && executeButton.Enabled)
			{
				executeButton.PerformClick();
			}
			return true;
		}

		// Alt+0 to Alt+9 for profile selection
		if ((keyData & Keys.Alt) == Keys.Alt &&
		    keyData >= (Keys.Alt | Keys.D0) && keyData <= (Keys.Alt | Keys.D9))
		{
			if (commandChainCheckBox.Checked && profilesComboBox != null)
			{
				Keys digitKey = keyData & ~Keys.Alt;
				char digitChar = (char)digitKey;
				string prefix = "(" + digitChar + ")";
				string matchingProfile = profilesComboBox.Items.Cast<string>().FirstOrDefault(name => name.StartsWith(prefix));
				if (!string.IsNullOrEmpty(matchingProfile))
				{
					profilesComboBox.SelectedItem = matchingProfile;
				}
			}
			return true;
		}

		return base.ProcessCmdKey(ref msg, keyData);
	}

	protected override void Dispose(bool disposing)
	{
		if (disposing)
		{
			// Stop and dispose timer to prevent callbacks after disposal
			if (_statsUpdateTimer != null)
			{
				_statsUpdateTimer.Stop();
				_statsUpdateTimer.Dispose();
			}
			if (_quickCommandTimer != null)
			{
				_quickCommandTimer.Stop();
				_quickCommandTimer.Dispose();
			}
			_cachedDirtyIndicatorIcon?.Dispose();
			toolTip?.Dispose();
			// Other resources to release/dispose (if any).
		}
		base.Dispose(disposing);
	}

	// Updates quick command indicator in status bar
	private void UpdateQuickCommandIndicator()
	{
		if (_quickCommandIndicator != null)
		{
			_quickCommandIndicator.Text = $"⌨ {_quickCommandBuffer}";
			_quickCommandIndicator.Visible = true;
		}
		
		// Restart timer
		_quickCommandTimer.Stop();
		_quickCommandTimer.Start();
	}

	// Checks if command can be executed immediately
	private void CheckAndExecuteQuickCommand()
	{
		if (!int.TryParse(_quickCommandBuffer, out int currentNum))
			return;
		
		// Check if this exact command exists
		if (!commandMap.ContainsKey(currentNum))
			return;
		
		// Check if there are commands that start with current buffer
		// e.g., buffer "1" could be start of 10, 11, ..., 19
		bool hasLongerCommands = commandMap.Keys.Any(k => 
			k.ToString().StartsWith(_quickCommandBuffer) && 
			k.ToString().Length > _quickCommandBuffer.Length
		);
		
		if (!hasLongerCommands)
		{
			// No ambiguity - execute immediately
			_quickCommandTimer.Stop();
			ExecuteQuickCommand(currentNum);
			ClearQuickCommandBuffer();
		}
		// else: wait for more input or timeout
	}

	// Executes command by number
	private void ExecuteQuickCommand(int commandNumber)
	{
		if (commandMap.TryGetValue(commandNumber, out ToolStripMenuItem menuItem))
		{
			// Show feedback
			if (_quickCommandIndicator != null)
			{
				_quickCommandIndicator.Text = $"✓ ({commandNumber}) {GetMenuItemShortText(menuItem)}";
				_quickCommandIndicator.ForeColor = Color.DarkGreen;
			}
			
			// Execute command
			menuItem.PerformClick();
			
			// Clear indicator after short delay
			Timer feedbackTimer = new Timer { Interval = 1500 };
			feedbackTimer.Tick += (s, e) =>
			{
				if (_quickCommandIndicator != null)
				{
					_quickCommandIndicator.Visible = false;
					_quickCommandIndicator.ForeColor = Color.DarkBlue;
				}
				feedbackTimer.Stop();
				feedbackTimer.Dispose();
			};
			feedbackTimer.Start();
		}
	}

	// Gets short text from menu item (without command number)
	private string GetMenuItemShortText(ToolStripMenuItem menuItem)
	{
		if (menuItem == null || string.IsNullOrEmpty(menuItem.Text))
			return "";
		
		// Extract text after ") " - e.g., "(14) Sort ascending" -> "Sort ascending"
		int index = menuItem.Text.IndexOf(") ");
		return index >= 0 ? menuItem.Text.Substring(index + 2) : menuItem.Text;
	}

	// Timer tick - executes command on timeout
	private void QuickCommandTimer_Tick(object sender, EventArgs e)
	{
		_quickCommandTimer.Stop();
		
		if (int.TryParse(_quickCommandBuffer, out int cmdNum))
		{
			if (commandMap.ContainsKey(cmdNum))
			{
				ExecuteQuickCommand(cmdNum);
			}
			else
			{
				// Show error for invalid command
				if (_quickCommandIndicator != null)
				{
					_quickCommandIndicator.Text = $"✗ {LocalizationManager.GetString("quickCommandNotFound", _quickCommandBuffer)}";
					_quickCommandIndicator.ForeColor = Color.DarkRed;
					_quickCommandIndicator.Visible = true;
				}
				
				// Clear after delay
				Timer errorTimer = new Timer { Interval = 2000 };
				errorTimer.Tick += (s, ev) =>
				{
					if (_quickCommandIndicator != null)
					{
						_quickCommandIndicator.Visible = false;
						_quickCommandIndicator.ForeColor = Color.DarkBlue;
					}
					errorTimer.Stop();
					errorTimer.Dispose();
				};
				errorTimer.Start();
			}
		}
		
		ClearQuickCommandBuffer();
	}

	// Clears quick command buffer
	private void ClearQuickCommandBuffer()
	{
		_quickCommandBuffer = "";
		if (_quickCommandIndicator != null && _quickCommandIndicator.Text.StartsWith("⌨"))
		{
			_quickCommandIndicator.Visible = false;
		}
	}

	// Handles click events for Whitespace menu items.
	private void WhitespaceMenuItem_Click(object sender, EventArgs e)
	{
		if (sender is ToolStripMenuItem clickedItem && clickedItem.Tag is string whitespaceOpTag)
		{
			string operationName = null;
			switch (whitespaceOpTag)
			{
				case "trim_both": operationName = LocalizationManager.GetString("operationTrimBoth"); break;
				case "trim_start": operationName = LocalizationManager.GetString("operationTrimStart"); break;
				case "trim_end": operationName = LocalizationManager.GetString("operationTrimEnd"); break;
				case "reduce_multiple": operationName = LocalizationManager.GetString("operationReduceMultiple"); break;
			}
			
			HandleTextOperation(textService =>
				{
					TextProcessingService.WhitespaceOperation operation;
					switch (whitespaceOpTag)
					{
						case "trim_both":
							operation = TextProcessingService.WhitespaceOperation.TrimBoth;
							break;
						case "trim_start":
							operation = TextProcessingService.WhitespaceOperation.TrimStart;
							break;
						case "trim_end":
							operation = TextProcessingService.WhitespaceOperation.TrimEnd;
							break;
						case "reduce_multiple":
							operation = TextProcessingService.WhitespaceOperation.ReduceMultiple;
							break;
						default:
							ShowMessageInfoSafe($"Unknown whitespace action '{whitespaceOpTag}'.");
							return; // Should not happen with correctly configured tags.
					}

					textService.ProcessWhitespace(operation);
				}, operationName);
		}
	}

	// Handles click events for "Lines" menu items and their sub-options.
	private void LinesMenuItem_Click(object sender, EventArgs e)
	{
		if (sender is ToolStripMenuItem clickedItem)
		{
			string commandIdentifier = clickedItem.Name;
			string operationName = null;
			
			// Determine operation name based on command
			switch (commandIdentifier)
			{
				case "lines_sort_asc": operationName = LocalizationManager.GetString("operationSortAsc"); break;
				case "lines_sort_desc": operationName = LocalizationManager.GetString("operationSortDesc"); break;
				case "lines_natural_sort_asc": operationName = LocalizationManager.GetString("operationNaturalSortAsc"); break;
				case "lines_natural_sort_desc": operationName = LocalizationManager.GetString("operationNaturalSortDesc"); break;
				case "lines_sort_length_asc": operationName = LocalizationManager.GetString("operationSortLengthAsc"); break;
				case "lines_sort_length_desc": operationName = LocalizationManager.GetString("operationSortLengthDesc"); break;
				case "lines_remove_duplicates": 
				case "lines_remove_duplicates_ignore_case": operationName = LocalizationManager.GetString("operationRemoveDuplicates"); break;
				case "lines_remove_empty": operationName = LocalizationManager.GetString("operationRemoveEmpty"); break;
				case "lines_reverse": operationName = LocalizationManager.GetString("operationReverse"); break;
				case "lines_number_lines": operationName = LocalizationManager.GetString("operationNumberLines"); break;
				case "lines_split_char": operationName = LocalizationManager.GetString("operationSplitLines"); break;
				case "lines_remove_containing":
				case "lines_keep_containing": operationName = LocalizationManager.GetString("operationFilterLines"); break;
				case "lines_trim_chars": operationName = LocalizationManager.GetString("operationTrimChars"); break;
			}
			
			HandleTextOperation(textService =>
				{
					object currentTag = clickedItem.Tag;       // Tag may contain parameters if passed via command chain, otherwise it's the commandIdentifier string itself.

					if (string.IsNullOrEmpty(commandIdentifier))
					{
						ShowMessageErrorSafe("Internal error: Command identifier not found for Lines menu item.");
						return;
					}

					switch (commandIdentifier)
					{
						case "lines_sort_asc":
							textService.SortLines(true);
							break;
						case "lines_sort_desc":
							textService.SortLines(false);
							break;
						case "lines_natural_sort_asc":
							textService.NaturalSortLines(true);
							break;
						case "lines_natural_sort_desc":
							textService.NaturalSortLines(false);
							break;
						case "lines_sort_length_asc":
							textService.SortLinesByLength(true);
							break;
						case "lines_sort_length_desc":
							textService.SortLinesByLength(false);
							break;
						case "lines_remove_duplicates":
							textService.RemoveDuplicateLines(false);
							break;
						case "lines_remove_duplicates_ignore_case":
							textService.RemoveDuplicateLines(true);
							break;
						case "lines_remove_empty":
							textService.RemoveEmptyLines();
							break;
						case "lines_reverse":
							textService.ReverseLines();
							break;
						case "lines_number_lines":
							string numberFormat = null;
							// Check if currentTag is a parameter (string from command chain) vs. the commandIdentifier itself.
							if (currentTag is string paramFormat && paramFormat != commandIdentifier)
							{
								numberFormat = paramFormat;
							}
							else // Interactive mode: Tag is the command name or no specific parameter handling here; show dialog.
							{
								numberFormat = GetUserInputSafe(LocalizationManager.GetString("dialogInputNumberLinesHint"), "#._space_");
							}

							if (string.IsNullOrEmpty(numberFormat)) return; // User cancelled or empty input
						
						// Replace _space_ placeholder with actual space
						numberFormat = numberFormat.Replace("_space_", " ");
						
						// Parse format and optional start value: PATTERN|START_VALUE

						string[] parts = numberFormat.Split(new[] { '|' }, 2);

						string formatPart = parts[0];

						int startValue = 1;

						

						if (!formatPart.Contains("#"))

							{

								if (!string.IsNullOrEmpty(formatPart))

									ShowMessageErrorSafe(LocalizationManager.GetString("dialogInputNumberLinesError"));

								return;

							}

						

						if (parts.Length > 1 && !string.IsNullOrEmpty(parts[1]))
						{
							if (!int.TryParse(parts[1], out startValue))
							{
								ShowMessageErrorSafe(LocalizationManager.GetString("dialogInputNumberLinesError"));
								return;
							}
						}
						
						textService.NumberLines(formatPart, startValue);
							break;
						case "lines_split_char":
							string delimiter = null;
							// Check if currentTag is a parameter (string from command chain) vs. the commandIdentifier itself.
							if (currentTag is string paramDelimiter && paramDelimiter != commandIdentifier)
							{
								delimiter = paramDelimiter; // Parameter provided by command chain.
							}
							else // Interactive mode: Tag is the command name or no specific parameter handling here; show dialog.
							{
								delimiter = GetUserInputSafe(LocalizationManager.GetString("dialogInputSplitDelimiterHint"), "");
							}

							if (delimiter == null) return; // User cancelled dialog or parameter determination failed.
							delimiter = delimiter.Replace("_space_", " ");
							textService.SplitLinesByDelimiter(delimiter);
							break;
						case "lines_remove_containing":
						case "lines_keep_containing":
							string filterPattern = null;
							bool useRegex = false;
							// Check for parameters from command chain (string array: [pattern, "regex"/"normal"]).
							if (currentTag is string[] tagFilterParams && tagFilterParams.Length == 2)
							{
								filterPattern = tagFilterParams[0];
								useRegex = tagFilterParams[1] == "regex";
							}
							else // Interactive mode: show dialog.
							{
								string userInput = GetUserInputSafe(LocalizationManager.GetString("dialogInputFilterLinesHint"), "");
								if (string.IsNullOrWhiteSpace(userInput)) return; // User cancelled or entered empty.
								if (userInput.StartsWith("r~"))
								{
									useRegex = true;
									filterPattern = userInput.Substring(2);
								}
								else
								{
									filterPattern = userInput.Replace("_space_", " ");
								}
							}
							if (string.IsNullOrEmpty(filterPattern)) return; // No pattern to filter by.
							textService.FilterLines(filterPattern, useRegex, commandIdentifier == "lines_keep_containing");
							break;
						case "lines_trim_chars":
							int charsFromStart = 0, charsFromEnd = 0;
							bool trimParamsProvided = false;
							// Check for parameters from command chain (int array: [startChars, endChars]).
							if (currentTag is int[] tagTrimParams && tagTrimParams.Length == 2)
							{
								charsFromStart = tagTrimParams[0];
								charsFromEnd = tagTrimParams[1];
								trimParamsProvided = true;
							}
							else // Interactive mode: show dialog.
							{
								string trimInput = GetUserInputSafe(LocalizationManager.GetString("dialogInputTrimCharsHint"), "");
								if (string.IsNullOrWhiteSpace(trimInput)) return; // User cancelled or entered empty.
								string[] trimParts = trimInput.Split(new string[]
									{
										"~~"
									}, StringSplitOptions.None);
								if (trimParts.Length == 2)
								{
									if (!string.IsNullOrEmpty(trimParts[0]) && !int.TryParse(trimParts[0], out charsFromStart))
									{
										ShowMessageErrorSafe(LocalizationManager.GetString("dialogInputTrimCharsError")); return;
									}
									if (!string.IsNullOrEmpty(trimParts[1]) && !int.TryParse(trimParts[1], out charsFromEnd))
									{
										ShowMessageErrorSafe(LocalizationManager.GetString("dialogInputTrimCharsError")); return;
									}
									if (charsFromStart < 0 || charsFromEnd < 0)
									{
										ShowMessageErrorSafe(LocalizationManager.GetString("dialogInputTrimCharsError")); return;
									} // Ensure non-negative.
									trimParamsProvided = true;
								}
								else
								{
									ShowMessageErrorSafe(LocalizationManager.GetString("dialogInputTrimCharsError")); return;
								} // Invalid format.
							}

							if (trimParamsProvided) textService.TrimLinesStartEnd(charsFromStart, charsFromEnd);
							else
							{ /* No valid parameters were provided (e.g., dialog cancelled or input was invalid), so do nothing. */
							}
							break;
						default:
							ShowMessageInfoSafe($"Unknown lines action: '{commandIdentifier}'.");
							return; // Should not happen if menu items are correctly configured with valid Name tags.
					}
				}, operationName);
		}
	}

	// Handles click events for "Join Lines" menu items.
	private void JoinLinesMenuItem_Click(object sender, EventArgs e)
	{
		if (sender is ToolStripMenuItem clickedItem)
		{
			// "Process Lines Separately" is typically false for join operations, so it's passed directly.
			HandleTextOperation(textService =>
				{
					string commandIdentifier = clickedItem.Name; // Base command (e.g., " ", ",", "join_lines_other")
					object currentTag = clickedItem.Tag;       // Parameter from command chain for "join_lines_other", or same as Name.
					string delimiterToUse = null;

					if (string.IsNullOrEmpty(commandIdentifier))
					{
						ShowMessageErrorSafe("Internal error: Command identifier not found for Join Lines menu item.");
						return;
					}

					// Standard delimiters are taken directly from the commandIdentifier (which is also the Tag in these cases).
					if (commandIdentifier == " " || commandIdentifier == "," || commandIdentifier == ";")
					{
						delimiterToUse = commandIdentifier;
					}
					else if (commandIdentifier == "join_lines_other")
					{
						// If Tag is a string and different from commandIdentifier, it's a parameter from command chain.
						if (currentTag is string paramDelimiter && paramDelimiter != commandIdentifier)
						{
							delimiterToUse = paramDelimiter.Replace("_space_", " ");
						}
						else // Interactive mode for "join_lines_other": show dialog.
						{
							string userInput = GetUserInputSafe(LocalizationManager.GetString("dialogInputJoinDelimiterHint"), "");
							// Check if user cancelled. Note: Empty string is valid for direct concatenation.
							if (string.IsNullOrWhiteSpace(userInput))
							{
								return; // User cancelled the dialog or provided only whitespace.
							}
							delimiterToUse = userInput.Replace("_space_", " ");
						}
					}
					else
					{
						ShowMessageInfoSafe($"Unknown join lines action: '{commandIdentifier}'.");
						return; // Should not happen with valid menu item configurations.
					}
					textService.JoinLines(delimiterToUse);
				}, LocalizationManager.GetString("operationJoinLines"));
		}
	}

	// --- Event Handlers for Window Position Menu Items ---
	private void RememberWindowSizeMenuItem_Click(object sender, EventArgs e)
	{
		_rememberWindowSize = rememberWindowSizeMenuItem.Checked;
	}

	private void RememberWindowPositionMenuItem_Click(object sender, EventArgs e)
	{
		_rememberWindowPosition = rememberWindowPositionMenuItem.Checked;
	}

	private void AlwaysOnTopMenuItem_Click(object sender, EventArgs e)
	{
		_alwaysOnTop = alwaysOnTopMenuItem.Checked;
		this.TopMost = _alwaysOnTop;
		BFS.ScriptSettings.WriteValueBool(LocalizationManager.ALWAYS_ON_TOP_SETTING, _alwaysOnTop);
	}

	private void SingleInstanceMenuItem_Click(object sender, EventArgs e)
	{
		BFS.ScriptSettings.WriteValueBool(LocalizationManager.SINGLE_INSTANCE_SETTING, singleInstanceMenuItem.Checked);
	}
	// --- End Event Handlers for Window Position Menu Items ---

	// Handles click events for "Search & Replace" menu items and their sub-options.
	private void SearchReplaceMenuItem_Click(object sender, EventArgs e)
	{
		if (sender is ToolStripMenuItem clickedItem)
		{
			string commandIdentifier = clickedItem.Name;
			string operationName = null;
			
			// Determine operation name based on command
			switch (commandIdentifier)
			{
				case "search_replace_replace": operationName = LocalizationManager.GetString("operationReplace"); break;
				case "search_replace_delete": operationName = LocalizationManager.GetString("operationDelete"); break;
				case "search_replace_extract": operationName = LocalizationManager.GetString("operationExtract"); break;
				case "search_replace_scrub_html": operationName = LocalizationManager.GetString("operationScrubHtml"); break;
			}
			
			HandleTextOperation(textService =>
				{
					object currentTag = clickedItem.Tag;       // Tag may contain parameters if passed via command chain.

					if (string.IsNullOrEmpty(commandIdentifier))
					{
						ShowMessageErrorSafe("Internal error: Command identifier not found for Search & Replace menu item.");
						return;
					}

					switch (commandIdentifier)
					{
						case "search_replace_replace":
							string findText = null, replaceWithText = null;
							bool useRegexReplace = false;
							// Parameters from command chain: string array [find, replace, (optional)"regex"]
							if (currentTag is string[] replaceParams && replaceParams.Length >= 2)
							{
								findText = replaceParams[0];
								replaceWithText = replaceParams[1];
								useRegexReplace = replaceParams.Length > 2 && replaceParams[2] == "regex";
							}
							else // Interactive mode: prompt user for input.
							{
								string userInput = GetUserInputSafe(LocalizationManager.GetString("dialogInputReplaceHint"), "");
								if (string.IsNullOrWhiteSpace(userInput)) return; // User cancelled or entered nothing.

								string[] parts;
								string separator = userInput.Contains("~r~") ? "~r~" : (userInput.Contains("~~") ? "~~" : null);

								if (separator == null)
								{
									ShowMessageErrorSafe(LocalizationManager.GetString("dialogInputReplaceSeparatorError"));
									return;
								}
								parts = userInput.Split(new string[]
									{
										separator
									}, StringSplitOptions.None);
								if (parts.Length >= 2)
								{
									findText = parts[0];
									// Join remaining parts for replacement text in case separator was part of replacement itself.
									replaceWithText = string.Join(separator, parts.Skip(1).ToArray());
									useRegexReplace = (separator == "~r~");
									if (!useRegexReplace) // Handle _space_ placeholder for non-regex.
									{
										findText = findText.Replace("_space_", " ");
										replaceWithText = replaceWithText.Replace("_space_", " ");
									}
								}
								else
								{
									ShowMessageErrorSafe(LocalizationManager.GetString("dialogInputReplaceFormatError"));
									return;
								}
							}
							if (findText == null) return; // Should not be reached if logic for param/input handling is correct.
							textService.ReplaceText(findText, replaceWithText ?? "", useRegexReplace);
							break;

						case "search_replace_delete":
							string deletePattern = null;
							bool useRegexDelete = false;
							// Parameters from command chain: string array [pattern, (optional)"regex"]
							if (currentTag is string[] deleteParams && deleteParams.Length >= 1)
							{
								deletePattern = deleteParams[0];
								useRegexDelete = deleteParams.Length > 1 && deleteParams[1] == "regex";
							}
							else // Interactive mode: prompt user.
							{
								string userInput = GetUserInputSafe(LocalizationManager.GetString("dialogInputDeleteHint"), "");
								if (string.IsNullOrWhiteSpace(userInput)) return; // User cancelled or entered nothing.

								if (userInput.StartsWith("r~"))
								{
									useRegexDelete = true;
									deletePattern = userInput.Substring(2);
								}
								else
								{
									deletePattern = userInput.Replace("_space_", " ");
								}
								// An empty deletePattern is allowed (will result in no changes).
							}
							textService.ReplaceText(deletePattern, "", useRegexDelete); // Delete is essentially replacing with an empty string.
							break;

						case "search_replace_extract":
							string extractPattern = null;
							bool useRegexExtract = false;
							// Parameters from command chain: string array [pattern, (optional)"regex"]
							if (currentTag is string[] extractParams && extractParams.Length >= 1)
							{
								extractPattern = extractParams[0];
								useRegexExtract = extractParams.Length > 1 && extractParams[1] == "regex";
							}
							else // Interactive mode: prompt user.
							{
								string userInput = GetUserInputSafe(LocalizationManager.GetString("dialogInputExtractHint"), "");
								if (string.IsNullOrWhiteSpace(userInput)) return; // User cancelled or entered nothing.

								if (userInput.StartsWith("r~"))
								{
									useRegexExtract = true;
									extractPattern = userInput.Substring(2);
								}
								else
								{
									extractPattern = userInput.Replace("_space_", " ");
								}
								if (string.IsNullOrEmpty(extractPattern)) return; // Cannot extract with an empty pattern.
							}
							textService.ExtractMatches(extractPattern, useRegexExtract);
							break;

						case "search_replace_scrub_html":
							textService.ScrubHtml();
							break;

						default:
							ShowMessageInfoSafe($"Unknown search/replace action: '{commandIdentifier}'.");
							return; // Should not happen with correctly configured menu items.
					}
				}, operationName);
		}
	}

	private void HelpMenuItem_Click(object sender, EventArgs e)
	{
		Form helpForm = new Form
		{
			Text = LocalizationManager.GetString("helpTitle"),
			Size = new Size(850, 650),
			FormBorderStyle = FormBorderStyle.Sizable,
			StartPosition = FormStartPosition.CenterParent,
			MinimumSize = new Size(500, 400),
			Icon = this.Icon,
			TopMost = this.TopMost
		};

		TabControl tabControl = new TabControl
		{
			Dock = DockStyle.Fill,
			Padding = new Point(10, 5)
		};

		// Создаем вкладки
		string[] tabKeys = { "helpAbout", "helpInterface", "helpCommands", "helpExternal" };
		string[] tabTitles = { LocalizationManager.GetString("helpTabAbout"), LocalizationManager.GetString("helpTabInterface"), LocalizationManager.GetString("helpTabCommands"), LocalizationManager.GetString("helpTabExternal")};

		for (int i = 0; i < tabKeys.Length; i++)
		{
			string key = tabKeys[i];
			string title = tabTitles[i];
			TabPage tabPage = new TabPage(title);
			TextBox textBox = new TextBox
			{
				Multiline = true,
				ReadOnly = true,
				WordWrap = true,
				ScrollBars = ScrollBars.Vertical,
				Dock = DockStyle.Fill,
				Text = LocalizationManager.GetString(key),
				Font = mainTextBox.Font,
				BorderStyle = BorderStyle.None,
				MaxLength = 0
			};

			tabPage.Controls.Add(textBox);
			tabControl.TabPages.Add(tabPage);
		}

		helpForm.Controls.Add(tabControl);
		helpForm.ShowDialog(this);
	}

	private void HandleTextOperation(Action<TextProcessingService> operation, string operationName = null)
	{
		mainTextBox.Focus();
		string originalText = mainTextBox.Text;

		try
		{
			// Mark text change as programmatic to prevent MainTextBox_TextChanged from adding to history
			isProgrammaticTextChange = true;
			
			bool processSeparately = (processLinesSeparatelyCheckBox != null && processLinesSeparatelyCheckBox.Checked);
			TextProcessingService textService = new TextProcessingService(mainTextBox, processSeparately);
			operation(textService);
		}
		catch (ArgumentException regexEx) // Catch specific Regex errors from pattern compilation.
		{
			isProgrammaticTextChange = false; // Reset flag
			ShowMessageErrorSafe(LocalizationManager.GetString("dialogRegexError", regexEx.Message));
			return; // Stop processing this command
		}
		catch (Exception ex) // Catch any other unexpected errors during processing
		{
			isProgrammaticTextChange = false; // Reset flag
			ShowMessageErrorSafe($"An unexpected error occurred: {ex.Message}");
			// Depending on severity, you might want to log this error.
			return; // Stop processing this command
		}
		finally
		{
			// Always reset the flag
			isProgrammaticTextChange = false;
		}

		UpdateStatistics();
		if (mainTextBox.Text != originalText && !isUndoRedoOperation && !isExecutingCommandChain)
		{
			// Use passed operation name or generic
			string opName = operationName ?? LocalizationManager.GetString("operationGeneric");
			PushStateForUndo(mainTextBox.Text, opName);
		}
	}
}

// Service for text processing logic
public class TextProcessingService
{
	private TextBox _textBox; // The TextBox control to operate on.
	private bool _processLinesIndividually; // Whether operations should apply to each line separately.

	// State captured at the time of service instantiation to handle text selection correctly.
	private bool _startedWithSelection;    // Was text selected when the service was created?
	private int _originalSelectionStart;   // Original start of selection.
	private int _originalSelectionLength;  // Original length of selection.

	public TextProcessingService(TextBox textBox, bool processLinesIndividually)
	{
		_textBox = textBox;
		_processLinesIndividually = processLinesIndividually;

		// Determine and store the initial selection state from the TextBox.
		_startedWithSelection = _textBox.SelectionLength > 0;
		_originalSelectionStart = _textBox.SelectionStart;
		_originalSelectionLength = _textBox.SelectionLength;
	}

	// Retrieves the text to be processed: either the originally selected text or the entire content of the TextBox.
	private string GetTextToProcess()
	{
		if (_startedWithSelection)
		{
			// Ensure selection bounds are still valid relative to the current text length,
			// as the text might have changed since the service was instantiated (though unlikely for typical single-operation use).
			int currentLength = _textBox.TextLength;
			int validStart = Math.Min(_originalSelectionStart, currentLength);
			int validLength = Math.Min(_originalSelectionLength, currentLength - validStart);
			if (validStart < 0) validStart = 0;
			if (validLength < 0) validLength = 0;

			// Further ensure start + length does not exceed current text length.
			if (validStart + validLength > currentLength)
			{
				validLength = currentLength - validStart;
			}
			if (validLength < 0) validLength = 0; // Final safety check for length.

			return validLength > 0 ? _textBox.Text.Substring(validStart, validLength) : string.Empty;
		}
		else
		{
			return _textBox.Text; // Process entire text if no initial selection.
		}
	}

	// Updates the TextBox with new text, attempting to preserve selection or caret position.
	private void UpdateTextBox(string newText)
	{
		// Design note: Comparing newText with GetTextToProcess() here might be complex if GetTextToProcess() re-evaluates selection against a potentially already modified _textBox.Text.
		// The current approach assumes direct replacement of what GetTextToProcess() initially returned.

		if (_startedWithSelection)
		{
			// Re-validate original selection bounds against current text before replacing.
			int currentTextBoxLength = _textBox.TextLength;
			int validStart = Math.Min(_originalSelectionStart, currentTextBoxLength);
			int validLength = Math.Min(_originalSelectionLength, currentTextBoxLength - validStart);
			if (validStart < 0) validStart = 0;
			if (validLength < 0) validLength = 0;
			if (validStart + validLength > currentTextBoxLength)
			{
				validLength = currentTextBoxLength - validStart;
			}
			if (validLength < 0) validLength = 0;

			_textBox.Select(validStart, validLength); // Select the part to be replaced.

			// Avoid redundant assignment if the selected text is already identical to the new text.
			// This helps preserve focus and prevents unnecessary UI updates.
			if (_textBox.SelectedText == newText) return;

			_textBox.SelectedText = newText;
			// Keep the newly processed text selected
			_textBox.Select(validStart, newText.Length);
		}
		else // Entire text was processed.
		{
			// Avoid redundant assignment if the entire text is already identical.
			if (_textBox.Text == newText) return;

			int currentCaretPosition = _textBox.SelectionStart; // Try to preserve caret position.

			_textBox.Text = newText;

			// Restore caret position, ensuring it's within new text bounds.
			try
			{
				_textBox.Select(Math.Min(currentCaretPosition, _textBox.TextLength), 0);
			}
			catch // Fallback if error during selection.
			{
				_textBox.Select(_textBox.TextLength, 0); // Move caret to end.
			}
			_textBox.ScrollToCaret();
		}
	}

	// Applies framing characters (e.g., brackets, quotes) to the text.
	public void ApplyBrackets(string opening, string closing)
	{
		string textToProcess = GetTextToProcess();
		string resultText;

		if (_processLinesIndividually)
		{
			string[] lines = textToProcess.Split(new string[]
				{
					"\r\n", "\r", "\n"
				}, StringSplitOptions.None);
			for (int i = 0; i < lines.Length; i++)
			{
				// Logic for framing lines when _processLinesIndividually is true:
				// 1. Non-empty lines are always framed.
				// 2. If the original operation started with a selection, AND that selection consisted of a single, empty line,
				//    then this single empty line is framed.
				// 3. Other empty lines (e.g., in a multi-line selection, or when processing whole text) are not framed.
				if (!string.IsNullOrEmpty(lines[i]))
				{
					lines[i] = opening + lines[i] + closing;
				}
				else if (_startedWithSelection && lines.Length == 1 && string.IsNullOrEmpty(lines[i]))
				{ // Handles the specific case of a single, selected, empty line.
					lines[i] = opening + lines[i] + closing;
				}
			}
			resultText = string.Join(Environment.NewLine, lines);
		}
		else // Process the entire textToProcess block as one unit.
		{
			resultText = opening + textToProcess + closing;
		}

		UpdateTextBox(resultText);
	}

	public enum CaseOperation
	{
		Upper,
		Lower,
		Title,
		Sentence
	}

	// Regex for splitting sentences. Originally from ClipProcessor_RU.cs.
	// Note: This is culture-agnostic. For more nuanced sentence splitting based on specific language rules,
	// CultureInfo-specific regular expressions or libraries might be considered in the future.
	private static readonly Regex SentenceSplitRegex = new Regex(@"(?<=[.!?])\s+", RegexOptions.Compiled, TimeSpan.FromSeconds(1));

	public void ProcessCase(CaseOperation operation)
	{
		string textToProcess = GetTextToProcess(); // Correctly gets either selected text or the whole text.
		string resultText;

		TextInfo textInfo = CultureInfo.CurrentCulture.TextInfo;

		Func<string, string> specificCaseProcessor = s =>
		{
			switch (operation)
			{
				case CaseOperation.Upper:
					return s.ToUpper();
				case CaseOperation.Lower:
					return s.ToLower();
				case CaseOperation.Title:
					// ToTitleCase makes the first letter of each word in the string uppercase.
					// It's generally recommended to pass a lowercased string to ToTitleCase for predictable behavior.
					return textInfo.ToTitleCase(s.ToLower());
				case CaseOperation.Sentence:
					if (string.IsNullOrWhiteSpace(s)) return s;
					// Sentence casing logic: Tries to capitalize the first letter of each sentence within the string 's'.
					var sentences = SentenceSplitRegex.Split(s.ToLower());
					var resultBuilder = new System.Text.StringBuilder();
					MatchCollection separators = SentenceSplitRegex.Matches(s);
					int sepIndex = 0;
					for (int i = 0; i < sentences.Length; i++)
					{
						string currentSentence = sentences[i].TrimStart();
						if (!string.IsNullOrWhiteSpace(currentSentence))
						{
							resultBuilder.Append(char.ToUpper(currentSentence[0]));
							if (currentSentence.Length > 1)
							{
								resultBuilder.Append(currentSentence.Substring(1));
							}
						}
						if (sepIndex < separators.Count) // Re-append original separators.
						{
							resultBuilder.Append(separators[sepIndex++].Value);
						}
					}
					return resultBuilder.ToString();
				default:
					return s; // Should not happen with a valid CaseOperation.
			}
		};

		// Apply the chosen case processing logic to the text block.
		resultText = specificCaseProcessor(textToProcess);

		UpdateTextBox(resultText);
	}

	public enum WhitespaceOperation
	{
		TrimBoth,
		TrimStart,
		TrimEnd,
		ReduceMultiple
	}

	private static readonly Regex ReduceMultipleSpacesRegex = new Regex(@"\s+", RegexOptions.Compiled, TimeSpan.FromSeconds(1));

	public void ProcessWhitespace(WhitespaceOperation operation)
	{
		string textToProcess = GetTextToProcess();
		string resultText;

		Func<string, string> specificWhitespaceProcessor = s =>
		{
			switch (operation)
			{
				case WhitespaceOperation.TrimBoth:       return s.Trim();
				case WhitespaceOperation.TrimStart:      return s.TrimStart();
				case WhitespaceOperation.TrimEnd:        return s.TrimEnd();
				case WhitespaceOperation.ReduceMultiple: return ReduceMultipleSpacesRegex.Replace(s, " ");
				default: return s; // Should not happen with a valid WhitespaceOperation.
			}
		};

		if (_processLinesIndividually)
		{
			string[] lines = textToProcess.Split(new string[]
				{
					"\r\n", "\r", "\n"
				}, StringSplitOptions.None);
			for (int i = 0; i < lines.Length; i++)
			{
				lines[i] = specificWhitespaceProcessor(lines[i]);
			}
			resultText = string.Join(Environment.NewLine, lines);
		}
		else
		{
			resultText = specificWhitespaceProcessor(textToProcess);
		}

		UpdateTextBox(resultText);
	}

	// --- Line Processing Operations --- (Applied to the whole text or selection as a block)

	// Helper to split text into lines based on standard newline sequences.
	private string[] GetLinesFromText(string text)
	{
		return text.Split(new string[]
			{
				"\r\n", "\r", "\n"
			}, StringSplitOptions.None);
	}

	// Helper to join an array of lines into a single string using Environment.NewLine.
	private string TextFromLines(string[] lines)
	{
		return string.Join(Environment.NewLine, lines);
	}

	public void SortLines(bool ascending )
	{
		string textToProcess = GetTextToProcess();
		string[] lines = GetLinesFromText(textToProcess);

		// Using StringComparer.CurrentCulture for standard lexicographical sort.
		Array.Sort(lines, StringComparer.CurrentCulture);

		if (!ascending )
		{
			Array.Reverse(lines);
		}
		UpdateTextBox(TextFromLines(lines));
	}

	// Natural sort: sorts strings with embedded numbers naturally (e.g., file1.txt, file2.txt, file10.txt)
	public void NaturalSortLines(bool ascending)
	{
		string textToProcess = GetTextToProcess();
		string[] lines = GetLinesFromText(textToProcess);

		// Using custom NaturalStringComparer for natural sort order.
		Array.Sort(lines, new NaturalStringComparer());

		if (!ascending)
		{
			Array.Reverse(lines);
		}
		UpdateTextBox(TextFromLines(lines));
	}

	// Sort lines by their length
	public void SortLinesByLength(bool ascending)
	{
		string textToProcess = GetTextToProcess();
		string[] lines = GetLinesFromText(textToProcess);

		// Sort by line length
		lines = ascending
			? lines.OrderBy(line => line.Length).ToArray()
			: lines.OrderByDescending(line => line.Length).ToArray();

		UpdateTextBox(TextFromLines(lines));
	}

	public void RemoveDuplicateLines(bool ignoreCase = false)
	{
		string textToProcess = GetTextToProcess();
		string[] lines = GetLinesFromText(textToProcess);
		
		// Choose the appropriate string comparer based on case sensitivity
		StringComparer comparer = ignoreCase 
			? StringComparer.CurrentCultureIgnoreCase 
			: StringComparer.CurrentCulture;
		
		// Using Enumerable.Distinct with the chosen comparer preserves the order of the first appearance of each unique line.
		// This is generally preferred over HashSet-based approaches if order is critical and for clarity.
		string[] uniqueLines = lines.Distinct(comparer).ToArray();
		UpdateTextBox(TextFromLines(uniqueLines));
	}

	public void RemoveEmptyLines()
	{
		string textToProcess = GetTextToProcess();
		string[] lines = GetLinesFromText(textToProcess);
		string[] nonEmptyLines = lines.Where(l => !string.IsNullOrWhiteSpace(l)).ToArray();
		UpdateTextBox(TextFromLines(nonEmptyLines));
	}

	public void ReverseLines()
	{
		string textToProcess = GetTextToProcess();
		string[] lines = GetLinesFromText(textToProcess);
		Array.Reverse(lines);
		UpdateTextBox(TextFromLines(lines));
	}

	// Numbers lines with a custom format string where # is replaced with line number
	// Format examples: "#. " -> "1. ", "2. " | "0#. " -> "01. ", "02. " | "[#]" -> "[1]", "[2]"
	public void NumberLines(string format, int startValue = 1)
	{
		string textToProcess = GetTextToProcess();
		string[] lines = GetLinesFromText(textToProcess);
		
		// Parse format to determine padding width
		int paddingWidth = 0;
		int hashIndex = format.IndexOf('#');
		
		if (hashIndex > 0)
		{
			// Count leading zeros before #
			int zeroCount = 0;
			for (int i = hashIndex - 1; i >= 0 && format[i] == '0'; i--)
			{
				zeroCount++;
			}
			if (zeroCount > 0)
			{
				paddingWidth = zeroCount + 1; // +1 for the # itself representing 1 digit minimum
			}
		}
		
		for (int i = 0; i < lines.Length; i++)
		{
			string number = (i + startValue).ToString();
			if (paddingWidth > 0)
				number = number.PadLeft(paddingWidth, '0');
			
			// Build the pattern to replace (e.g., "#", "0#", "00#", etc.)
			string patternToReplace;
			if (paddingWidth > 0)
			{
				// Include leading zeros in the pattern: "0" * (paddingWidth-1) + "#"
				patternToReplace = new string('0', paddingWidth - 1) + "#";
			}
			else
			{
				patternToReplace = "#";
			}
			
			lines[i] = format.Replace(patternToReplace, number) + lines[i];
		}
		
		UpdateTextBox(TextFromLines(lines));
	}

	public void SplitLinesByDelimiter(string delimiter)
	{
		string textToProcess = GetTextToProcess();
		string[] lines = GetLinesFromText(textToProcess);
		List<string> resultLines = new List<string>();

		// Behavior for empty delimiter:
		// If delimiter is empty, no further split of individual line content occurs beyond the original line breaks.
		// This matches original ClipProcessor_RU behavior. Consider if empty should split by char or have another meaning in future.
		if (string.IsNullOrEmpty(delimiter))
		{
			resultLines.AddRange(lines);
		}
		else
		{
			string[] delimiterArray =
			{
				delimiter
			};
			foreach (string line in lines)
			{
				resultLines.AddRange(line.Split(delimiterArray, StringSplitOptions.None));
			}
		}
		UpdateTextBox(TextFromLines(resultLines.ToArray()));
	}

	public void FilterLines(string filterPattern, bool useRegex, bool keepMatching)
	{
		string textToProcess = GetTextToProcess();
		string[] lines = GetLinesFromText(textToProcess);
		Regex regex = useRegex ? new Regex(filterPattern, RegexOptions.None, TimeSpan.FromSeconds(2)) : null; // Compile regex only if needed. Can throw ArgumentException.

		var filteredResult = lines.Where(line =>
			{
				bool isMatch = useRegex ? regex.IsMatch(line) : line.Contains(filterPattern);
				return keepMatching ? isMatch : !isMatch;
			});
		UpdateTextBox(TextFromLines(filteredResult.ToArray()));
	}

	public void TrimLinesStartEnd(int charsFromStart, int charsFromEnd)
	{
		string textToProcess = GetTextToProcess();
		string[] lines = GetLinesFromText(textToProcess);
		List<string> resultLines = new List<string>(lines.Length);

		foreach (string line in lines)
		{
			string currentLine = line;
			int len = currentLine.Length;

			int startTrim = Math.Max(0, Math.Min(charsFromStart, len));
			currentLine = currentLine.Substring(startTrim);
			len = currentLine.Length; // Update length after start trim

			int endTrim = Math.Max(0, Math.Min(charsFromEnd, len));
			currentLine = currentLine.Substring(0, len - endTrim);

			resultLines.Add(currentLine);
		}
		UpdateTextBox(TextFromLines(resultLines.ToArray()));
	}

	public void JoinLines(string delimiter)
	{
		string textToProcess = GetTextToProcess();
		string[] lines = GetLinesFromText(textToProcess);

		// Filter out empty or whitespace-only lines before joining (matches ClipProcessor_RU.cs behavior).
		var nonEmptyLines = lines.Where(l => !string.IsNullOrWhiteSpace(l)).ToArray();

		string resultText = string.Join(delimiter, nonEmptyLines);
		UpdateTextBox(resultText);
	}

	// Removes HTML tags from the processed text.
	public void ScrubHtml()
	{
		string textToProcess = GetTextToProcess();
		string resultText;
		Func<string, string> processor = s => BFS.Text.ScrubHtml(s, true);
		resultText = processor(textToProcess);
		UpdateTextBox(resultText);
	}

	// Extracts all occurrences of a pattern (text or regex) from the processed text.
	// Each match is placed on a new line in the result.
	public void ExtractMatches(string pattern, bool useRegex)
	{
		string textToProcess = GetTextToProcess();
		List<string> result = new List<string>();

		if (useRegex)
		{
			Regex regex = new Regex(pattern, RegexOptions.None, TimeSpan.FromSeconds(2)); // Can throw ArgumentException if pattern is invalid.
			MatchCollection matches = regex.Matches(textToProcess);
			foreach (Match m in matches)
			{
				if (!string.IsNullOrEmpty(m.Value))
					result.Add(m.Value);
			}
		}
		else // Plain text search.
		{
			int currentIndex = 0;
			while (currentIndex < textToProcess.Length)
			{
				int idx = textToProcess.IndexOf(pattern, currentIndex, StringComparison.CurrentCulture);
				if (idx != -1)
				{
					result.Add(pattern); // Add the found pattern itself.
					currentIndex = idx + pattern.Length;
				}
				else
				{
					break; // No more occurrences.
				}
			}
		}
		UpdateTextBox(string.Join(Environment.NewLine, result));
	}

	// Converts escape sequences in a string to their actual character representations.
	private string UnescapeString(string input)
	{
		return input
			.Replace("\\n", "\n")
			.Replace("\\r", "\r")
			.Replace("\\t", "\t")
			.Replace("\\\\", "\\");
	}

	// Replaces occurrences of a pattern (text or regex) with specified replacement text.
	public void ReplaceText(string findText, string replaceWithText, bool useRegex)
	{
		string textToProcess = GetTextToProcess();
		string resultText;

		if (useRegex)
		{
			string processedReplacement = UnescapeString(replaceWithText);
			resultText = Regex.Replace(textToProcess, findText, processedReplacement, RegexOptions.Multiline);
		}
		else
		{
			resultText = textToProcess.Replace(findText, replaceWithText);
		}

		UpdateTextBox(resultText);
	}

	// Helper method to apply a given processor function to each line of the input text.
	private string ProcessLineByLine(string text, Func<string, string> lineProcessor)
	{
		string[] lines = text.Split(new string[]
			{
				"\r\n", "\r", "\n"
			}, StringSplitOptions.None);
		for (int i = 0; i < lines.Length; i++)
		{
			lines[i] = lineProcessor(lines[i]);
		}
		return string.Join(Environment.NewLine, lines);
	}
}

// Natural string comparer for sorting strings with embedded numbers
// Example: "file1.txt", "file2.txt", "file10.txt" instead of "file1.txt", "file10.txt", "file2.txt"
public class NaturalStringComparer : IComparer<string>
{
	public int Compare(string x, string y)
	{
		if (x == null && y == null) return 0;
		if (x == null) return -1;
		if (y == null) return 1;
		
		int ix = 0, iy = 0;
		
		while (ix < x.Length && iy < y.Length)
		{
			// Check if current characters are digits
			if (char.IsDigit(x[ix]) && char.IsDigit(y[iy]))
			{
				// Extract numeric parts
				string numX = ExtractNumber(x, ref ix);
				string numY = ExtractNumber(y, ref iy);
				
				// Compare as numbers
				if (long.TryParse(numX, out long nX) && long.TryParse(numY, out long nY))
				{
					int numCompare = nX.CompareTo(nY);
					if (numCompare != 0)
						return numCompare;
				}
				else
				{
					// If numbers are too large for long, compare as strings
					int strCompare = string.Compare(numX, numY, StringComparison.Ordinal);
					if (strCompare != 0)
						return strCompare;
				}
			}
			else
			{
				// Compare non-numeric characters
				int charCompare = string.Compare(x[ix].ToString(), y[iy].ToString(), StringComparison.CurrentCulture);
				if (charCompare != 0)
					return charCompare;
				
				ix++;
				iy++;
			}
		}
		
		// If one string is a prefix of the other, shorter one comes first
		return x.Length.CompareTo(y.Length);
	}
	
	private string ExtractNumber(string str, ref int index)
	{
		int start = index;
		while (index < str.Length && char.IsDigit(str[index]))
		{
			index++;
		}
		return str.Substring(start, index - start);
	}
}