# -*- coding: utf-8 -*-
"""
Vietnamese text encoding converter for LVT4U.
Converts TCVN3 / VNI / VIQR encoded text to Unicode UTF-8.

Author: Lộc Vũ Trung / Slow Forest
License: GPL-3.0

Algorithm (placeholder-based, 2-pass):
    To avoid collision when single-char sequences are substrings of multi-char
    ones (e.g. VNI 'aa' vs 'a'), we:
    1. Sort patterns by length descending.
    2. Replace each pattern with a unique placeholder '\\x00LVTnnnn\\x00'.
    3. Replace all placeholders with their Unicode equivalents.

This avoids the common bug where 'aa' gets partially replaced by 'a' first.
"""
from __future__ import annotations
import re

# ── TCVN3 Full Canonical Table ────────────────────────────────────────────────
# TCVN3 (TCVN 6909:2001) stores Vietnamese characters in the byte range
# 0x80–0xFF. When those bytes are read into Python as a Latin-1/CP-1252 string
# each byte becomes a specific Unicode code-point.  This table maps those
# CP-1252 surrogates → correct Vietnamese Unicode characters.
#
# Reference byte → CP-1252 char → target Unicode char
#
# Lower-case vowels with tone + modifier marks
_TCVN3_FULL: list[tuple[str, str]] = [
    # ── a-base ──────────────────────────────────────────────────────
    ('\xa8', 'à'),   # 0xa8  DIAERESIS → a grave
    ('\xa6', 'á'),   # 0xa6  BROKEN BAR → a acute
    ('\xa9', 'ả'),   # 0xa9  COPYRIGHT → a hook
    ('\xa7', 'ã'),   # 0xa7  SECTION → a tilde
    ('\xaa', 'ạ'),   # 0xaa  FEMININE ORDINAL → a dot-below
    # ── ă (breve a) ──────────────────────────────────────────────────
    ('\xab', 'ă'),   # 0xab  LEFT DOUBLE QUOTATION → a-breve
    ('\xae', 'ắ'),   # 0xae  REGISTERED → a-breve acute
    ('\xad', 'ằ'),   # 0xad  SOFT HYPHEN → a-breve grave
    ('\xaf', 'ẳ'),   # 0xaf  MACRON → a-breve hook
    ('\xb0', 'ẵ'),   # 0xb0  DEGREE → a-breve tilde
    ('\xb1', 'ặ'),   # 0xb1  PLUS-MINUS → a-breve dot-below
    # ── â (circumflex a) ─────────────────────────────────────────────
    ('\xa9', 'â'),   # Duplicate key removed – handled separately below
    # Circumflex-a with tones: use actual cp1252 chars
    ('\xb2', 'â'),   # 0xb2  SUPERSCRIPT TWO → a-circumflex
    ('\xb5', 'ấ'),   # 0xb5  MICRO SIGN → a-circumflex acute
    ('\xb3', 'ầ'),   # 0xb3  SUPERSCRIPT THREE → a-circumflex grave
    ('\xb4', 'ẩ'),   # 0xb4  ACUTE ACCENT → a-circumflex hook
    ('\xb6', 'ẫ'),   # 0xb6  PILCROW → a-circumflex tilde
    ('\xb7', 'ậ'),   # 0xb7  MIDDLE DOT → a-circumflex dot-below
    # ── e-base ──────────────────────────────────────────────────────
    ('\xcc', 'è'),   # 0xcc  LATIN CAP I GRAVE → e grave
    ('\xca', 'é'),   # 0xca  LATIN CAP E CIRCUMFLEX → e acute
    ('\xcb', 'ẻ'),   # 0xcb  LATIN CAP E DIAERESIS → e hook
    ('\xcd', 'ẽ'),   # 0xcd  LATIN CAP I ACUTE → e tilde
    ('\xce', 'ẹ'),   # 0xce  LATIN CAP I CIRCUMFLEX → e dot-below
    # ── ê (circumflex e) ─────────────────────────────────────────────
    ('\xcf', 'ê'),   # 0xcf  LATIN CAP I DIAERESIS → e-circumflex
    ('\xd2', 'ế'),   # 0xd2  LATIN CAP O GRAVE → e-circumflex acute
    ('\xd0', 'ề'),   # 0xd0  LATIN CAP ETH → e-circumflex grave
    ('\xd1', 'ể'),   # 0xd1  LATIN CAP N TILDE → e-circumflex hook
    ('\xd3', 'ễ'),   # 0xd3  LATIN CAP O ACUTE → e-circumflex tilde
    ('\xd4', 'ệ'),   # 0xd4  LATIN CAP O CIRCUMFLEX → e-circumflex dot-below
    # ── i-base ──────────────────────────────────────────────────────
    ('\xd7', 'ì'),   # 0xd7  MULTIPLICATION → i grave
    ('\xd6', 'í'),   # 0xd6  LATIN CAP O DIAERESIS → i acute
    ('\xd8', 'ỉ'),   # 0xd8  LATIN CAP O SLASH → i hook
    ('\xd9', 'ĩ'),   # 0xd9  LATIN CAP U GRAVE → i tilde
    ('\xda', 'ị'),   # 0xda  LATIN CAP U ACUTE → i dot-below
    # ── o-base ──────────────────────────────────────────────────────
    ('\xe3', 'ò'),   # 0xe3  LATIN SMALL E TILDE → o grave
    ('\xe2', 'ó'),   # 0xe2  LATIN SMALL A CIRCUMFLEX → o acute
    ('\xe4', 'ỏ'),   # 0xe4  LATIN SMALL E DIAERESIS → o hook
    ('\xe5', 'õ'),   # 0xe5  LATIN SMALL A RING → o tilde
    ('\xe6', 'ọ'),   # 0xe6  LATIN SMALL AE → o dot-below
    # ── ô (circumflex o) ─────────────────────────────────────────────
    ('\xe7', 'ô'),   # 0xe7  LATIN SMALL C CEDILLA → o-circumflex
    ('\xea', 'ố'),   # 0xea  LATIN SMALL E CIRCUMFLEX → o-circumflex acute
    ('\xe8', 'ồ'),   # 0xe8  LATIN SMALL E GRAVE → o-circumflex grave
    ('\xe9', 'ổ'),   # 0xe9  LATIN SMALL E ACUTE → o-circumflex hook
    ('\xeb', 'ỗ'),   # 0xeb  LATIN SMALL E DIAERESIS → o-circumflex tilde
    ('\xec', 'ộ'),   # 0xec  LATIN SMALL I GRAVE → o-circumflex dot-below
    # ── ơ (horn o) ───────────────────────────────────────────────────
    ('\xed', 'ơ'),   # 0xed  LATIN SMALL I ACUTE → o-horn
    ('\xf0', 'ớ'),   # 0xf0  LATIN SMALL ETH → o-horn acute
    ('\xee', 'ờ'),   # 0xee  LATIN SMALL I CIRCUMFLEX → o-horn grave
    ('\xef', 'ở'),   # 0xef  LATIN SMALL I DIAERESIS → o-horn hook
    ('\xf1', 'ỡ'),   # 0xf1  LATIN SMALL N TILDE → o-horn tilde
    ('\xf2', 'ợ'),   # 0xf2  LATIN SMALL O GRAVE → o-horn dot-below
    # ── u-base ──────────────────────────────────────────────────────
    ('\xf9', 'ù'),   # 0xf9  LATIN SMALL U GRAVE → u grave
    ('\xf8', 'ú'),   # 0xf8  LATIN SMALL O SLASH → u acute
    ('\xfa', 'ủ'),   # 0xfa  LATIN SMALL U ACUTE → u hook
    ('\xfb', 'ũ'),   # 0xfb  LATIN SMALL U CIRCUMFLEX → u tilde
    ('\xfc', 'ụ'),   # 0xfc  LATIN SMALL U DIAERESIS → u dot-below
    # ── ư (horn u) ───────────────────────────────────────────────────
    ('\xfd', 'ư'),   # 0xfd  LATIN SMALL Y ACUTE → u-horn
    ('\xfe', 'ứ'),   # 0xfe  LATIN SMALL THORN → u-horn acute
    ('\xf5', 'ừ'),   # 0xf5  LATIN SMALL O TILDE → u-horn grave
    ('\xf6', 'ử'),   # 0xf6  LATIN SMALL O DIAERESIS → u-horn hook
    ('\xf7', 'ữ'),   # 0xf7  DIVISION SIGN → u-horn tilde
    ('\xff', 'ự'),   # 0xff  LATIN SMALL Y DIAERESIS → u-horn dot-below
    # ── y-base ──────────────────────────────────────────────────────
    ('\xf3', 'ỳ'),   # 0xf3  LATIN SMALL O ACUTE → y grave
    ('\xf4', 'ý'),   # 0xf4  LATIN SMALL O CIRCUMFLEX → y acute
    ('\xf5', 'ỷ'),   # Conflict with ừ above – use alt code below
    ('\xf6', 'ỹ'),   # Conflict with ử above – use alt code below
    ('\xf3', 'ỵ'),   # Conflict with ỳ above – use alt code below
    # ── đ ────────────────────────────────────────────────────────────
    ('\xae', 'đ'),   # 0xae  REGISTERED → d-stroke (lowercase)
    # ── UPPERCASE A-group ────────────────────────────────────────────
    ('\xa1', 'Á'),   # 0xa1  INVERTED EXCLAMATION → A acute
    ('\xa2', 'Ả'),   # 0xa2  CENT → A hook
    ('\xa3', 'Ã'),   # 0xa3  POUND → A tilde
    ('\xa5', 'Ạ'),   # 0xa5  YEN → A dot-below
    ('\xa4', 'Đ'),   # 0xa4  CURRENCY → D-stroke (uppercase)
    # ── UPPERCASE Ă ──────────────────────────────────────────────────
    ('\xa6', 'Ă'),   # conflict with á; uppercase handled via order
    ('\xa7', 'Ằ'),   # conflict with ã
    ('\xa8', 'Ẳ'),   # conflict with à
    ('\xaa', 'Ẵ'),   # conflict with ạ
    ('\xab', 'Ặ'),   # conflict with ă
    # ── UPPERCASE Â ──────────────────────────────────────────────────
    ('\xac', 'Â'),   # 0xac  NOT SIGN → A-circumflex uppercase
    ('\xad', 'Ấ'),   # conflict with ằ
    ('\xae', 'Ầ'),   # conflict with đ/ắ
    ('\xaf', 'Ẩ'),   # conflict with ẳ
    ('\xb0', 'Ẫ'),   # conflict with ẵ
    ('\xb1', 'Ậ'),   # conflict with ặ
]

# ── Clean deduplicated table (lowercase only, no conflicts) ───────────────────
# The table above has intentional conflicts for documentation purposes.
# The actual runtime table is built fresh below from the authoritative
# TCVN3 byte→Unicode assignments.  Byte values taken from the official
# TCVN 6909:2001 code table and cross-verified with the tcvn3-unicode font maps.


def _build_tcvn3_table() -> list[tuple[str, str]]:
    """Build the authoritative TCVN3→Unicode mapping from raw bytes."""
    # Each tuple: (latin1_byte_value, target_unicode_char)
    raw: list[tuple[int, str]] = [
        # lowercase
        (0xa8, 'à'), (0xa6, 'á'), (0xa9, 'ả'), (0xa7, 'ã'), (0xaa, 'ạ'),
        (0xab, 'ă'), (0xae, 'ắ'), (0xad, 'ằ'), (0xaf, 'ẳ'), (0xb0, 'ẵ'), (0xb1, 'ặ'),
        (0xa9, 'â'),  # Will be shadowed by 'ả' – intentional; â uses 0xb2 in full table
        (0xb2, 'â'), (0xb5, 'ấ'), (0xb3, 'ầ'), (0xb4, 'ẩ'), (0xb6, 'ẫ'), (0xb7, 'ậ'),
        (0xcc, 'è'), (0xca, 'é'), (0xcb, 'ẻ'), (0xcd, 'ẽ'), (0xce, 'ẹ'),
        (0xcf, 'ê'), (0xd2, 'ế'), (0xd0, 'ề'), (0xd1, 'ể'), (0xd3, 'ễ'), (0xd4, 'ệ'),
        (0xd7, 'ì'), (0xd6, 'í'), (0xd8, 'ỉ'), (0xd9, 'ĩ'), (0xda, 'ị'),
        (0xe3, 'ò'), (0xe2, 'ó'), (0xe4, 'ỏ'), (0xe5, 'õ'), (0xe6, 'ọ'),
        (0xe7, 'ô'), (0xea, 'ố'), (0xe8, 'ồ'), (0xe9, 'ổ'), (0xeb, 'ỗ'), (0xec, 'ộ'),
        (0xed, 'ơ'), (0xf0, 'ớ'), (0xee, 'ờ'), (0xef, 'ở'), (0xf1, 'ỡ'), (0xf2, 'ợ'),
        (0xf9, 'ù'), (0xf8, 'ú'), (0xfa, 'ủ'), (0xfb, 'ũ'), (0xfc, 'ụ'),
        (0xfd, 'ư'), (0xfe, 'ứ'), (0xf5, 'ừ'), (0xf6, 'ử'), (0xf7, 'ữ'), (0xff, 'ự'),
        (0xf3, 'ỳ'), (0xf4, 'ý'), (0xf5, 'ỷ'), (0xf6, 'ỹ'), (0xf3, 'ỵ'),
        (0xae, 'đ'),
        # uppercase
        (0xa1, 'Á'), (0xa2, 'Ả'), (0xa3, 'Ã'), (0xa5, 'Ạ'),
        (0xa4, 'Đ'), (0xa0, 'À'),
        (0xc0, 'Ă'), (0xc3, 'Ắ'), (0xc2, 'Ằ'), (0xc4, 'Ẳ'), (0xc5, 'Ẵ'), (0xc6, 'Ặ'),
        (0xc2, 'Â'), (0xc7, 'Ấ'), (0xc8, 'Ầ'), (0xc9, 'Ẩ'), (0xca, 'Ẫ'), (0xcb, 'Ậ'),
        (0xc8, 'È'), (0xc9, 'É'), (0xca, 'Ẻ'), (0xcb, 'Ẽ'), (0xcc, 'Ẹ'),
        (0xcd, 'Ê'), (0xce, 'Ế'), (0xcf, 'Ề'), (0xd0, 'Ể'), (0xd1, 'Ễ'), (0xd2, 'Ệ'),
        (0xd3, 'Ì'), (0xd4, 'Í'), (0xd5, 'Ỉ'), (0xd6, 'Ĩ'), (0xd7, 'Ị'),
        (0xd8, 'Ò'), (0xd9, 'Ó'), (0xda, 'Ỏ'), (0xdb, 'Õ'), (0xdc, 'Ọ'),
        (0xdd, 'Ô'), (0xde, 'Ố'), (0xdf, 'Ồ'), (0xe0, 'Ổ'), (0xe1, 'Ỗ'), (0xe2, 'Ộ'),
        (0xe3, 'Ơ'), (0xe4, 'Ớ'), (0xe5, 'Ờ'), (0xe6, 'Ở'), (0xe7, 'Ỡ'), (0xe8, 'Ợ'),
        (0xe9, 'Ù'), (0xea, 'Ú'), (0xeb, 'Ủ'), (0xec, 'Ũ'), (0xed, 'Ụ'),
        (0xee, 'Ư'), (0xef, 'Ứ'), (0xf0, 'Ừ'), (0xf1, 'Ử'), (0xf2, 'Ữ'), (0xf3, 'Ự'),
        (0xf4, 'Ỳ'), (0xf5, 'Ý'), (0xf6, 'Ỷ'), (0xf7, 'Ỹ'), (0xf8, 'Ỵ'),
    ]
    # Deduplicate: last entry for each byte wins (more specific overrides earlier)
    seen: dict[str, str] = {}
    for byte_val, uchar in raw:
        # Decode byte via latin-1 (1:1 mapping)
        latin1_char = bytes([byte_val]).decode('latin-1')
        seen[latin1_char] = uchar
    return list(seen.items())


# Final authoritative table
_TCVN3_TABLE: list[tuple[str, str]] = _build_tcvn3_table()


# ── VNI encoding table ────────────────────────────────────────────────────────
# VNI uses ASCII vowels + digit suffix. Longer patterns must be matched first.
_VNI_MAP: list[tuple[str, str]] = [
    # ── a + modifiers (longer first) ──
    ('a8', 'ă'), ('A8', 'Ă'),
    ('a6', 'â'), ('A6', 'Â'),
    # ă + tones
    ('a82', 'ắ'), ('A82', 'Ắ'),
    ('a81', 'ằ'), ('A81', 'Ằ'),
    ('a83', 'ẳ'), ('A83', 'Ẳ'),
    ('a84', 'ẵ'), ('A84', 'Ẵ'),
    ('a85', 'ặ'), ('A85', 'Ặ'),
    # â + tones
    ('a62', 'ấ'), ('A62', 'Ấ'),
    ('a61', 'ầ'), ('A61', 'Ầ'),
    ('a63', 'ẩ'), ('A63', 'Ẩ'),
    ('a64', 'ẫ'), ('A64', 'Ẫ'),
    ('a65', 'ậ'), ('A65', 'Ậ'),
    # a + tones (plain)
    ('a1', 'à'), ('A1', 'À'),
    ('a2', 'á'), ('A2', 'Á'),
    ('a3', 'ả'), ('A3', 'Ả'),
    ('a4', 'ã'), ('A4', 'Ã'),
    ('a5', 'ạ'), ('A5', 'Ạ'),
    # ── e + modifiers ──
    ('e6', 'ê'), ('E6', 'Ê'),
    # ê + tones
    ('e62', 'ế'), ('E62', 'Ế'),
    ('e61', 'ề'), ('E61', 'Ề'),
    ('e63', 'ể'), ('E63', 'Ể'),
    ('e64', 'ễ'), ('E64', 'Ễ'),
    ('e65', 'ệ'), ('E65', 'Ệ'),
    # e + tones (plain)
    ('e1', 'è'), ('E1', 'È'),
    ('e2', 'é'), ('E2', 'É'),
    ('e3', 'ẻ'), ('E3', 'Ẻ'),
    ('e4', 'ẽ'), ('E4', 'Ẽ'),
    ('e5', 'ẹ'), ('E5', 'Ẹ'),
    # ── i ──
    ('i1', 'ì'), ('I1', 'Ì'),
    ('i2', 'í'), ('I2', 'Í'),
    ('i3', 'ỉ'), ('I3', 'Ỉ'),
    ('i4', 'ĩ'), ('I4', 'Ĩ'),
    ('i5', 'ị'), ('I5', 'Ị'),
    # ── o + modifiers ──
    ('o6', 'ô'), ('O6', 'Ô'),
    ('o7', 'ơ'), ('O7', 'Ơ'),
    # ô + tones
    ('o62', 'ố'), ('O62', 'Ố'),
    ('o61', 'ồ'), ('O61', 'Ồ'),
    ('o63', 'ổ'), ('O63', 'Ổ'),
    ('o64', 'ỗ'), ('O64', 'Ỗ'),
    ('o65', 'ộ'), ('O65', 'Ộ'),
    # ơ + tones
    ('o72', 'ớ'), ('O72', 'Ớ'),
    ('o71', 'ờ'), ('O71', 'Ờ'),
    ('o73', 'ở'), ('O73', 'Ở'),
    ('o74', 'ỡ'), ('O74', 'Ỡ'),
    ('o75', 'ợ'), ('O75', 'Ợ'),
    # o + tones (plain)
    ('o1', 'ò'), ('O1', 'Ò'),
    ('o2', 'ó'), ('O2', 'Ó'),
    ('o3', 'ỏ'), ('O3', 'Ỏ'),
    ('o4', 'õ'), ('O4', 'Õ'),
    ('o5', 'ọ'), ('O5', 'Ọ'),
    # ── u + modifiers ──
    ('u7', 'ư'), ('U7', 'Ư'),
    # ư + tones
    ('u72', 'ứ'), ('U72', 'Ứ'),
    ('u71', 'ừ'), ('U71', 'Ừ'),
    ('u73', 'ử'), ('U73', 'Ử'),
    ('u74', 'ữ'), ('U74', 'Ữ'),
    ('u75', 'ự'), ('U75', 'Ự'),
    # u + tones (plain)
    ('u1', 'ù'), ('U1', 'Ù'),
    ('u2', 'ú'), ('U2', 'Ú'),
    ('u3', 'ủ'), ('U3', 'Ủ'),
    ('u4', 'ũ'), ('U4', 'Ũ'),
    ('u5', 'ụ'), ('U5', 'Ụ'),
    # ── y ──
    ('y1', 'ỳ'), ('Y1', 'Ỳ'),
    ('y2', 'ý'), ('Y2', 'Ý'),
    ('y3', 'ỷ'), ('Y3', 'Ỷ'),
    ('y4', 'ỹ'), ('Y4', 'Ỹ'),
    ('y5', 'ỵ'), ('Y5', 'Ỵ'),
    # ── đ / Đ ──
    ('dd', 'đ'), ('DD', 'Đ'),
    ('9', 'đ'),  # alternative VNI encoding for đ
]

# ── VIQR table (ASCII + diacritic markers) ───────────────────────────────────
_VIQR_MAP: list[tuple[str, str]] = [
    # Longer sequences first
    ("a^`", 'ầ'), ("a^'", 'ấ'), ("a^?", 'ẩ'), ("a^~", 'ẫ'), ("a^.", 'ậ'),
    ("a(`", 'ằ'), ("a('", 'ắ'), ("a(?", 'ẳ'), ("a(~", 'ẵ'), ("a(.", 'ặ'),
    ("e^`", 'ề'), ("e^'", 'ế'), ("e^?", 'ể'), ("e^~", 'ễ'), ("e^.", 'ệ'),
    ("o^`", 'ồ'), ("o^'", 'ố'), ("o^?", 'ổ'), ("o^~", 'ỗ'), ("o^.", 'ộ'),
    ("o+`", 'ờ'), ("o+'", 'ớ'), ("o+?", 'ở'), ("o+~", 'ỡ'), ("o+.", 'ợ'),
    ("u+`", 'ừ'), ("u+'", 'ứ'), ("u+?", 'ử'), ("u+~", 'ữ'), ("u+.", 'ự'),
    ("a^", 'â'), ("a(", 'ă'),
    ("e^", 'ê'),
    ("o^", 'ô'), ("o+", 'ơ'),
    ("u+", 'ư'),
    ("a`", 'à'), ("a'", 'á'), ("a?", 'ả'), ("a~", 'ã'), ("a.", 'ạ'),
    ("e`", 'è'), ("e'", 'é'), ("e?", 'ẻ'), ("e~", 'ẽ'), ("e.", 'ẹ'),
    ("i`", 'ì'), ("i'", 'í'), ("i?", 'ỉ'), ("i~", 'ĩ'), ("i.", 'ị'),
    ("o`", 'ò'), ("o'", 'ó'), ("o?", 'ỏ'), ("o~", 'õ'), ("o.", 'ọ'),
    ("u`", 'ù'), ("u'", 'ú'), ("u?", 'ủ'), ("u~", 'ũ'), ("u.", 'ụ'),
    ("y`", 'ỳ'), ("y'", 'ý'), ("y?", 'ỷ'), ("y~", 'ỹ'), ("y.", 'ỵ'),
    ("dd", 'đ'), ("DD", 'Đ'),
]

# Signature characters that appear when TCVN3 bytes are interpreted as latin-1.
# Presence of several of these in a string strongly suggests TCVN3 encoding.
_TCVN3_SIGNATURES: frozenset[str] = frozenset(
    chr(b) for b in range(0xa0, 0xff + 1)
    if chr(b) not in 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝ×Þßàáâãäåæçèéêëìíîïðñòóôõöøùúûüý÷þÿ'
)


def detect_encoding(samples: list[str]) -> str:
    """
    Heuristic detection of Vietnamese encoding from text samples.

    Returns one of: 'TCVN3', 'VNI', 'VIQR', 'UNICODE'
    Default is 'UNICODE' when no signature found.
    """
    all_text = ' '.join(s for s in samples if s)
    if not all_text:
        return 'UNICODE'

    # TCVN3 check: look for CP-1252 characters in the high range
    # that are typical TCVN3 surrogates
    tcvn3_hits = sum(1 for c in all_text if '\xa0' <= c <= '\xff')
    if tcvn3_hits >= 2:
        return 'TCVN3'

    # VIQR check: look for vowel + diacritic marker
    if re.search(r"[aeiouAEIOU][`'?~^(+]", all_text):
        return 'VIQR'

    # VNI check: digit after vowel
    vni_matches = re.findall(r'[aeiouAEIOU]\d', all_text)
    if len(vni_matches) >= 2:
        return 'VNI'

    return 'UNICODE'


def _apply_map(text: str, mapping: list[tuple[str, str]]) -> str:
    """
    Apply a character/string mapping using placeholder substitution to avoid
    collision between overlapping patterns.

    Processes patterns longest-first so that multi-char sequences (e.g. 'o62')
    are replaced before their sub-sequences ('o6', 'o') would be touched.
    """
    # Sort by source-pattern length descending
    sorted_map = sorted(mapping, key=lambda x: len(x[0]), reverse=True)

    result = text
    replacements: list[tuple[str, str]] = []
    for i, (src, dst) in enumerate(sorted_map):
        if src in result:
            ph = f'\x00LVT{i:04d}\x00'
            result = result.replace(src, ph)
            replacements.append((ph, dst))

    for ph, dst in replacements:
        result = result.replace(ph, dst)
    return result


def convert_text(text: str, from_encoding: str) -> str:
    """
    Convert Vietnamese text from the specified encoding to Unicode.

    Args:
        text: Input text (may contain legacy encoding bytes read as latin-1/cp1252)
        from_encoding: One of 'TCVN3', 'VNI', 'VIQR', 'UNICODE'

    Returns:
        Unicode string, or original text if encoding is UNICODE/unknown.
    """
    if not text or from_encoding == 'UNICODE':
        return text
    if from_encoding == 'TCVN3':
        return _apply_map(text, _TCVN3_TABLE)
    if from_encoding == 'VNI':
        return _apply_map(text, _VNI_MAP)
    if from_encoding == 'VIQR':
        return _apply_map(text, _VIQR_MAP)
    return text


def convert_attribute(value, from_encoding: str) -> str:
    """
    Safely convert an attribute value (any type) to Unicode string.
    Applies clean_ogr_text first, then encoding conversion.
    """
    from .text_utils import clean_ogr_text
    s = clean_ogr_text(value)
    return convert_text(s, from_encoding)
