# CMake script to apply CJK word wrap patch to MyGUI_TextView.cpp.
# This is called by FetchContent PATCH_COMMAND.
#
# Two patches are applied:
#   1. CJK word wrap (allow break at every CJK glyph) - matches upstream
#      OpenMWCH behaviour.
#   2. [openmw-ja-port] Japanese 行頭禁則: when the next character is one of
#      a small set of "forbidden line-head" punctuation / small kana /
#      sound mark / closing bracket characters, suppress the break point so
#      the punctuation stays glued to the previous glyph.
#
# Robustness notes
# ----------------
# Every replacement below is wrapped in a `_japort_safe_replace()` helper
# that asserts the source pattern exists BEFORE doing the substitution.
# If upstream MyGUI changes whitespace / comment wording so that any of
# these anchors disappears, the build fails loudly with FATAL_ERROR
# instead of silently producing an unpatched MyGUI_TextView.cpp (which
# previously caused the title-menu hang and CJK wrap regressions).

set(TEXTVIEW_FILE "${SOURCE_DIR}/MyGUIEngine/src/MyGUI_TextView.cpp")

if(NOT EXISTS "${TEXTVIEW_FILE}")
    message(FATAL_ERROR "MyGUI_TextView.cpp not found at ${TEXTVIEW_FILE}")
endif()

file(READ "${TEXTVIEW_FILE}" CONTENT)

# [openmw-ja-port] Helper: assert the source pattern exists, then replace.
#
# We need a macro (not a function) because CMake functions create a new
# variable scope, and we want to mutate ${CONTENT} in the caller's scope.
# The optional ARGV3 lets us tag patches as "skip if missing" so partial
# re-runs after the early-exit do not falsely trip the assertion.
macro(_japort_safe_replace LABEL FROM TO)
    string(FIND "${CONTENT}" "${FROM}" _japort_find_pos)
    if(_japort_find_pos EQUAL -1)
        if("${ARGV3}" STREQUAL "OPTIONAL")
            # Anchor legitimately absent in this MyGUI source (e.g. a variant
            # that only some forks/reformattings carry). Skip without failing.
            message(STATUS
                "[openmw-ja-port] MyGUI patch step '${LABEL}' anchor absent - skipping (declared OPTIONAL)")
        else()
            message(FATAL_ERROR
                "[openmw-ja-port] MyGUI patch step '${LABEL}' could not locate its anchor pattern in "
                "${TEXTVIEW_FILE}.\n"
                "This means upstream MyGUI changed the surrounding code and the JP fork patch "
                "needs to be re-derived. Check scripts/apply_mygui_patch.cmake against the "
                "current MyGUI_TextView.cpp.")
        endif()
    else()
        string(REPLACE "${FROM}" "${TO}" CONTENT "${CONTENT}")
    endif()
endmacro()

# Three layers of patches need to be applied:
#   * "lastChar"             -> CJK word wrap
#   * "isLineHeadForbiddenJa" -> Japanese kinsoku (line-head suppression)
#   * "last_rewind_target"   -> Kinsoku infinite-loop guard (mandatory; see
#     scripts/apply_mygui_patch.cmake header for the bug story).
#
# Idempotency: the early-exit below short-circuits when the third (and
# final) marker is already present so re-running PATCH_COMMAND on an
# already-patched tree is a no-op. We deliberately do NOT trust partial
# states - if the loop guard sentinel is missing we re-run every step,
# and the `_japort_safe_replace` assertions guarantee a partial state
# cannot survive silently (any missing anchor is an immediate FATAL).
string(FIND "${CONTENT}" "last_rewind_target" LOOP_GUARD_PATCHED)
if(NOT LOOP_GUARD_PATCHED EQUAL -1)
    message(STATUS "MyGUI_TextView.cpp already fully patched (loop-guard), skipping")
    return()
endif()

message(STATUS "Applying CJK word wrap patch to MyGUI_TextView.cpp")

_japort_safe_replace("cjk-step1-decl-lastchar"
    "result.height += _height;\n\n\t\tfor"
    "result.height += _height;\n\t\tChar lastChar = 0;\n\t\tfor")

_japort_safe_replace("cjk-step2-rollback-cjk-condition"
    "if (FontCodeType::Space == character || FontCodeType::Tab == character)\n\t\t\t{\n\t\t\t\troll_back.set"
    "if (FontCodeType::Space == character || FontCodeType::Tab == character || character >= 0x2000 || lastChar >= 0x2000)\n\t\t\t{\n\t\t\t\troll_back.set")

_japort_safe_replace("cjk-step3-update-lastchar"
    "roll_back.set(line_info.symbols.size(), index, count, width);\n\t\t\t}\n\n\t\t\tfloat char_width"
    "roll_back.set(line_info.symbols.size(), index, count, width);\n\t\t\t}\n\t\t\tlastChar = character;\n\n\t\t\tfloat char_width")

_japort_safe_replace("cjk-step4-zero-width-guard-singleline"
    "if (_maxWidth != -1 && (width + char_fullAdvance) > _maxWidth && !roll_back.empty())"
    "if (_maxWidth != -1 && width > 0 && (width + char_fullAdvance) > _maxWidth && !roll_back.empty())")

# Optional step: official MyGUI 3.4.3 only carries the single-line `_maxWidth`
# overflow check (already handled by cjk-step4). Some forked / reformatted
# MyGUI sources additionally have a multi-line variant of the same `if`; patch
# it when present, but do NOT fail if it is absent - 3.4.3 legitimately lacks
# it, so a hard assertion here is wrong. Declared OPTIONAL so the safe-replace
# guard downgrades a missing anchor to a skip instead of FATAL_ERROR.
_japort_safe_replace("cjk-step5-zero-width-guard-multiline"
    "if (_maxWidth != -1\n\t\t\t\t&& (width + char_fullAdvance) > _maxWidth"
    "if (_maxWidth != -1\n\t\t\t\t&& width > 0\n\t\t\t\t&& (width + char_fullAdvance) > _maxWidth"
    OPTIONAL)

_japort_safe_replace("cjk-step6-resume-after-rewind"
    "// отменяем откат\n\t\t\t\troll_back.clear();\n\n\t\t\t\tcontinue;"
    "// отменяем откат\n\t\t\t\troll_back.clear();\n\n\t\t\t\tlastChar = 0;\n\t\t\t\tcharacter = *index;\n\t\t\t\tif (FontCodeType::Space != character && FontCodeType::Tab != character)\n\t\t\t\t\tindex--;\n\t\t\t\tcontinue;")

# [openmw-ja-port] BEGIN: Japanese 行頭禁則 - keep punctuation / small kana /
# sound mark / closing brackets glued to the previous glyph instead of letting
# them start a new line.
#
# Step 1: inject a static helper into the anonymous namespace at the top of
# the file. It encodes the same set of code points as
# apps/openmw/mwgui/bookpagekinsokuja.cpp so books / notes / scrolls (which
# go through MyGUI EditBox + TextView) match the BookTypesetter behaviour.
_japort_safe_replace("kinsoku-step1-inject-helper"
    "\tnamespace\n\t{\n\n\t\ttemplate<typename T>\n\t\tvoid setMin("
    "\tnamespace\n\t{\n\n\t\t// [openmw-ja-port] Japanese line-head forbidden character table.\n\t\t// Keep in sync with apps/openmw/mwgui/bookpagekinsokuja.cpp.\n\t\tinline bool isLineHeadForbiddenJa(Char c)\n\t\t{\n\t\t\tswitch (c)\n\t\t\t{\n\t\t\t\tcase 0x3001: case 0x3002: case 0xFF0C: case 0xFF0E:\n\t\t\t\tcase 0xFF1A: case 0xFF1B: case 0xFF01: case 0xFF1F:\n\t\t\t\tcase 0xFF09: case 0xFF3D: case 0xFF5D:\n\t\t\t\tcase 0x300D: case 0x300F: case 0x3011: case 0x3015:\n\t\t\t\tcase 0x3017: case 0x3019: case 0x301F:\n\t\t\t\tcase 0x30FC: case 0x30FB:\n\t\t\t\tcase 0x3041: case 0x3043: case 0x3045: case 0x3047: case 0x3049:\n\t\t\t\tcase 0x3063: case 0x3083: case 0x3085: case 0x3087: case 0x308E:\n\t\t\t\tcase 0x30A1: case 0x30A3: case 0x30A5: case 0x30A7: case 0x30A9:\n\t\t\t\tcase 0x30C3: case 0x30E3: case 0x30E5: case 0x30E7: case 0x30EE:\n\t\t\t\tcase ',': case '.': case '!': case '?': case ';': case ':':\n\t\t\t\tcase ')': case ']': case '}':\n\t\t\t\t\treturn true;\n\t\t\t\tdefault:\n\t\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\ttemplate<typename T>\n\t\tvoid setMin(")

# Step 2: gate the existing roll_back.set(...) call by the new helper so the
# wrap loop refuses to mark a break point right BEFORE a forbidden character.
# That way the rollback never lands on a position whose next glyph would be
# a forbidden line-head character.
_japort_safe_replace("kinsoku-step2-gate-rollback"
    "if (FontCodeType::Space == character || FontCodeType::Tab == character || character >= 0x2000 || lastChar >= 0x2000)\n\t\t\t{\n\t\t\t\troll_back.set"
    "if ((FontCodeType::Space == character || FontCodeType::Tab == character || character >= 0x2000 || lastChar >= 0x2000) && !isLineHeadForbiddenJa(character))\n\t\t\t{\n\t\t\t\troll_back.set")

# Step 3 (kinsoku infinite-loop guard):
#
# Without this guard the wrap loop deadlocks whenever an entire line is
# composed of forbidden line-head characters following a single allowed
# break candidate (e.g. "A。。。。。" with a narrow maxWidth). The
# original logic would rewind to "A", re-run the same forbidden tail,
# overflow at the same position, and rewind to "A" forever.
#
# The fix tracks the iterator we last rewound to. If two consecutive
# overflow events would rewind to the same target we know kinsoku has
# painted us into a corner; we then force a break at the current
# character (overwriting roll_back so the existing rewind body acts on
# it) and reset the tracker so the next line can re-enable kinsoku.
_japort_safe_replace("loop-guard-step1-decl-tracker"
    "Char lastChar = 0;\n\t\tfor (; index != end; ++index)"
    "Char lastChar = 0;\n\t\t// [openmw-ja-port] kinsoku infinite-loop guard tracker\n\t\tUString::utf32string::const_iterator last_rewind_target = end;\n\t\tfor (; index != end; ++index)")

_japort_safe_replace("loop-guard-step2-reset-on-newline"
    "\t\t\t\troll_back.clear();\n\n\t\t\t\tcontinue;"
    "\t\t\t\troll_back.clear();\n\t\t\t\tlast_rewind_target = end; // [openmw-ja-port] reset kinsoku guard\n\n\t\t\t\tcontinue;")

_japort_safe_replace("loop-guard-step3-force-break"
    "if (_maxWidth != -1 && width > 0 && (width + char_fullAdvance) > _maxWidth && !roll_back.empty())\n\t\t\t{\n\t\t\t\t// откатываем до последнего пробела"
    "if (_maxWidth != -1 && width > 0 && (width + char_fullAdvance) > _maxWidth && !roll_back.empty())\n\t\t\t{\n\t\t\t\t// [openmw-ja-port] kinsoku loop guard: force break if rewinding to same target twice\n\t\t\t\tif (roll_back.getTextIter() == last_rewind_target)\n\t\t\t\t\troll_back.set(line_info.symbols.size(), index, count, width);\n\t\t\t\tlast_rewind_target = roll_back.getTextIter();\n\t\t\t\t// откатываем до последнего пробела")
# [openmw-ja-port] END

# Final post-condition: every patch we expect MUST be in the output. If
# any of these markers is missing we refuse to write the file.
foreach(marker IN ITEMS "lastChar" "isLineHeadForbiddenJa" "last_rewind_target")
    string(FIND "${CONTENT}" "${marker}" _japort_marker_pos)
    if(_japort_marker_pos EQUAL -1)
        message(FATAL_ERROR
            "[openmw-ja-port] MyGUI patch post-condition failed: marker '${marker}' "
            "is missing from the patched MyGUI_TextView.cpp. The patch is corrupt; "
            "delete build-vcpkg/extern/fetched/mygui and re-run CMake.")
    endif()
endforeach()

file(WRITE "${TEXTVIEW_FILE}" "${CONTENT}")
message(STATUS "CJK word wrap + 行頭禁則 patch applied successfully")
