# Libraries
import json
import os
import re
import util.dazedwrap as dazedwrap
import threading
import time
import traceback
import tiktoken
from pathlib import Path
from colorama import Fore
from dotenv import load_dotenv
from retry import retry
from tqdm import tqdm
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
import tempfile

# Globals
MODEL = os.getenv("model")
TIMEOUT = int(os.getenv("timeout"))
LANGUAGE = os.getenv("language").capitalize()
from util.paths import PROMPT_PATH, VOCAB_PATH

PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
LOCK = threading.Lock()
WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth"))
NOTEWIDTH = 70
MAXHISTORY = 10
ESTIMATE = ""
TOKENS = [0, 0]
NAMESLIST = []
NAMES = False  # Output a list of all the character names found
BRFLAG = False  # If the game uses <br> instead
FIXTEXTWRAP = True  # Overwrites textwrap
IGNORETLTEXT = False  # Ignores all translated text.
MISMATCH = []  # Lists files that throw a mismatch error (Length of GPT list response is wrong)
FILENAME = None

# tqdm Globals
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
POSITION = 0
LEAVE = False
PBAR = None

# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーａ-ｚＡ-Ｚ０-９\uFF61-\uFF9F]+"

# Get pricing configuration based on the model
PRICING_CONFIG = getPricingConfig(MODEL)
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
BATCHSIZE = PRICING_CONFIG["batchSize"]
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]

# Initialize Translation Config
TRANSLATION_CONFIG = TranslationConfig(
    model=MODEL,
    language=LANGUAGE,
    prompt=PROMPT,
    vocab=VOCAB,
    langRegex=LANGREGEX,
    batchSize=BATCHSIZE,
    maxHistory=MAXHISTORY,
    estimateMode=False  # Will be set dynamically based on ESTIMATE
)
LEAVE = False

def handlePlugin(filename, estimate):
    global ESTIMATE, PBAR, FILENAME
    ESTIMATE = estimate
    FILENAME = filename

    if ESTIMATE:
        start = time.time()
        translatedData = openFiles(filename)

        # Print Result
        end = time.time()
        tqdm.write(getResultString(translatedData, end - start, filename))
        with LOCK:
            TOKENS[0] += translatedData[1][0]
            TOKENS[1] += translatedData[1][1]

        # Print Total
        totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")

        # Print any errors on maps
        if len(MISMATCH) > 0:
            return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
        else:
            return totalString

    else:
        try:
            # Perform translation first; incremental progress writes happen during translation.
            # We no longer keep the destination file open simultaneously to avoid Windows locking issues
            # during atomic os.replace operations inside save_progress_lines.
            start = time.time()
            translatedData = openFiles(filename)

            # Ensure final state is flushed (in case no incremental writes occurred for some reason).
            save_progress_lines(translatedData[0], filename)

            # Print Result
            end = time.time()
            tqdm.write(getResultString(translatedData, end - start, filename))
            with LOCK:
                TOKENS[0] += translatedData[1][0]
                TOKENS[1] += translatedData[1][1]
        except Exception:
            traceback.print_exc()
            return "Fail"

    return getResultString(["", TOKENS, None], end - start, "TOTAL")


def getResultString(translatedData, translationTime, filename):
    # File Print String
    cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
    totalTokenstring = (
        Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
        "[Output: "
        + str(translatedData[1][1])
        + "]" "[Cost: ${:,.4f}".format(cost)
        + "]"
    )
    timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"

    if translatedData[2] == None:
        # Success
        return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET

    else:
        # Fail
        try:
            raise translatedData[2]
        except Exception as e:
            traceback.print_exc()
            errorString = str(e) + Fore.RED
            return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET


def openFiles(filename):
    with open("files/" + filename, "r", encoding="utf_8") as readFile:
        translatedData = parsePlugin(readFile, filename)

        # Delete lines marked for deletion
        finalData = []
        for line in translatedData[0]:
            if line != "\\d\n":
                finalData.append(line)
        translatedData[0] = finalData

    return translatedData


def parsePlugin(readFile, filename):
    global PBAR
    totalTokens = [0, 0]

    # Read File into data
    data = readFile.readlines()

    # Create Progress Bar
    with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
        pbar.desc = filename
        PBAR = pbar

        try:
            result = translatePlugin(data, pbar, filename, [])
            totalTokens[0] += result[0]
            totalTokens[1] += result[1]
        except Exception as e:
            traceback.print_exc()
            return [data, totalTokens, e]
    return [data, totalTokens, None]


def save_progress_lines(lines, filename, encoding="utf_8"):
    """Atomically save current line-based translation progress."""
    try:
        if ESTIMATE:
            return
        global LOCK
        os.makedirs("translated", exist_ok=True)
        # Use a single lock to prevent concurrent replace attempts on Windows (which causes PermissionError)
        with LOCK:
            tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
            try:
                with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
                    tmp_file.writelines(lines)

                dest_path = os.path.join("translated", filename)
                # Retry a few times in case another process/thread still has the file open momentarily.
                for attempt in range(5):
                    try:
                        os.replace(tmp_path, dest_path)
                        break
                    except PermissionError:
                        if attempt == 4:
                            raise
                        time.sleep(0.1 * (attempt + 1))
            finally:
                if os.path.exists(tmp_path):
                    try:
                        os.remove(tmp_path)
                    except OSError:
                        pass
    except Exception:
        traceback.print_exc()


def saveCheckLines(lines, filename, tokens=None, encoding="utf_8"):
    """Save progress only when tokens indicate work or when explicitly called after a mutation."""
    try:
        if tokens is not None:
            if not (isinstance(tokens, (list, tuple)) and len(tokens) >= 2 and (tokens[0] or tokens[1])):
                return
        save_progress_lines(lines, filename, encoding=encoding)
    except Exception:
        traceback.print_exc()


def translatePlugin(data, pbar, filename, translatedList):
    if len(translatedList) > 0:
        questList = translatedList[0]
        custom = translatedList[1]
        sceneMenuText = translatedList[2] if len(translatedList) > 2 else []
        sceneMenuCommonHelpText = translatedList[3] if len(translatedList) > 3 else []
        sceneMenuHelpText = translatedList[4] if len(translatedList) > 4 else []
        sceneMenuDrawText = translatedList[5] if len(translatedList) > 5 else []
        sceneMenuStatLabel = translatedList[6] if len(translatedList) > 6 else []
        saveParamName = translatedList[7] if len(translatedList) > 7 else []
        setData = True
    else:
        questList = [[], [], [], [], [], []]
        custom = []
        sceneMenuText = []
        sceneMenuCommonHelpText = []
        sceneMenuHelpText = []
        sceneMenuDrawText = []
        sceneMenuStatLabel = []
        saveParamName = []
        setData = False
    currentGroup = []
    tokens = [0, 0]
    speaker = ""
    voice = False
    global LOCK, ESTIMATE
    i = 0
    in_disp_list = False
    brace_count = 0
    _pending_drawtext = {}   # Deferred drawTextEx brace label replacements (longest-first)
    _pending_statlabel = {}  # Deferred drawTextEx stat label replacements (longest-first)

    # Category
    with open("log/translations.txt", "a+", encoding="utf-8") as tlFile:
                tlFile.write(f"\nCustom:\n")
                tlFile.close()

    while i < len(data):
        voice = False
        speaker = ""

        # Track if we're inside CBR_travel_data block
        if 'data_map_name_list' in data[i]:
            in_disp_list = True
            brace_count = 0
        
        if in_disp_list:
            # Count braces to know when we exit the object
            brace_count += data[i].count('{') - data[i].count('}')
            if brace_count < 0:
                in_disp_list = False

        # Nested array strings (e.g., this.disp_list = { key: { subkey: ["string1", "string2"] } })
        # Matches strings inside arrays within object literals
        if in_disp_list:
            regex_nested = r'"([^"]*[一-龠ぁ-ゔァ-ヴー]+[^"]*)"'
            matchList = re.findall(regex_nested, data[i])
            if len(matchList) > 0:
                for match in matchList:
                    # Save Original String
                    jaString = match
                    originalString = jaString

                    # Replace \n for translation
                    jaStringClean = jaString.replace("\\n", " ")

                    if jaStringClean.strip():
                        # Pass 1
                        if setData == False:
                            custom.append(jaStringClean.strip())

                        # Pass 2
                        else:
                            if custom:
                                # Grab and Pop
                                translatedText = custom[0]
                                custom.pop(0)

                                # Set to None if empty list
                                if len(custom) <= 0:
                                    custom = []

                                # Restore original newlines but keep translated text
                                # Count original newlines
                                newline_count = jaString.count("\\n")
                                if newline_count > 0:
                                    # Textwrap the translation
                                    translatedText = dazedwrap.wrapText(translatedText, 50)
                                    translatedText = translatedText.replace("\n", "\\n")

                                # Escape quotes in translation
                                translatedText = translatedText.replace('"', '\\"')

                                # Set Data
                                data[i] = data[i].replace(f'"{originalString}"', f'"{translatedText}"')
                                saveCheckLines(data, filename)

        # Custom
        # Useful Regex's
        # r'"Text[\\]+":[\\]+"(.+?)[\\]+",'
        # r'"HelpText[\\]+":[\\]+"(.+?)[\\]+",'
        # r"this.drawTextEx\(\\'(.+?)\',"
        # r'txtSubject.+?"(.+)"'
        regex = r'txtSubject.+?"(.+)"'
        matchList = re.findall(regex, data[i])
        if len(matchList) > 0:
            for match in matchList:
                # Save Original String
                jaString = match
                originalString = jaString
                newline = None
                colorCode = None

                # Make sure it contains Japanese
                if not re.search(LANGREGEX, jaString):
                    continue

                # Make sure didn't grab \\
                if re.search(r"^[\\]+$", jaString):
                    continue

                # Replace \n and \c
                if re.search(r"\\+n", jaString):
                    newline = re.search(r"\\+n", jaString).group(0)
                    jaString = re.sub(r"\\+n", r"\\n", jaString)
                if re.search(r"\\+C", jaString):
                    colorCode = re.search(r"\\+C", jaString).group(0)
                    jaString = re.sub(r"\\+C", r"\\C", jaString)

                # Remove any textwrap
                jaString = jaString.replace("\\n", " ")

                if jaString.replace("\u3000", "") and jaString:
                    # Pass 1
                    if setData == False and jaString.strip():
                        custom.append(jaString.strip())

                    # Pass 2
                    else:
                        if custom:
                            # Grab and Pop
                            translatedText = custom[0]
                            custom.pop(0)

                            # Set to None if empty list
                            if len(translatedList) <= 0:
                                translatedList = None

                            # Replace Single Quotes
                            translatedText = re.sub(r"([^\\'])'", r"\1՚", translatedText)
                            translatedText = re.sub(r"([^\\'])\"", r"\1՚", translatedText)

                            # Replace \n and \c
                            if newline:
                                # Textwrap
                                # translatedText = dazedwrap.wrapText(translatedText, WIDTH)
                                translatedText = re.sub(r"\n", re.escape(newline), translatedText)
                            if colorCode:
                                translatedText = re.sub(r"\n", re.escape(colorCode), translatedText)

                            # Set Data
                            with open("log/translations.txt", "a+", encoding="utf-8") as tlFile:
                                tlFile.write(f"{originalString} ({translatedText})\n")
                            data[i] = data[i].replace(originalString, translatedText)
                            saveCheckLines(data, filename)

        # Quest Name
        regex = r'[\\]+"QuestName[\\]+":[\\]+"(.*?)[\\]+"'
        matchList = re.findall(regex, data[i])
        if len(matchList) > 0:
            for match in matchList:
                if match:
                    # Save Original String
                    originalString = match

                    # Remove any textwrap
                    match = match.replace(newline, " ")

                    # Pass 1
                    if setData == False:
                        # Add String
                        if match != "\\\\\\\\":
                            questList[0].append(match.strip())

                    # Pass 2
                    else:
                        if questList[0]:
                            # Grab and Pop
                            translatedText = questList[0][0]
                            questList[0].pop(0)

                            # Set to None if empty list
                            if len(translatedList) <= 0:
                                translatedList = None

                            # Replace Single Quotes
                            translatedText = translatedText.replace('"', "'")
                            translatedText = re.sub(r"([^\\'])'", r"\1\\'", translatedText)

                            # Set Data
                            data[i] = data[i].replace(originalString, translatedText)
                            saveCheckLines(data, filename)

        # Quest Client
        regex = r'QuestClientName[\\]+":[\\]+"(.*?)[\\]+"'
        matchList = re.findall(regex, data[i])
        if len(matchList) > 0:
            for match in matchList:
                if match:
                    # Save Original String
                    originalString = match

                    # Pass 1
                    if setData == False:
                        # Add String
                        if match != "\\\\\\\\":
                            questList[1].append(match.strip())

                    # Pass 2
                    else:
                        if questList[1]:
                            # Grab and Pop
                            translatedText = questList[1][0]
                            questList[1].pop(0)

                            # Set to None if empty list
                            if len(translatedList) <= 0:
                                translatedList = None

                            # Replace Single Quotes
                            translatedText = translatedText.replace('"', "'")
                            translatedText = re.sub(r"([^\\'])'", r"\1\\'", translatedText)

                            # Set Data
                            data[i] = data[i].replace(originalString, translatedText)
                            saveCheckLines(data, filename)

        # Quest Location
        regex = r'QuestLocation[\\]+":[\\]+"(.*?)[\\]+"'
        matchList = re.findall(regex, data[i])
        if len(matchList) > 0:
            for match in matchList:
                if match:
                    # Save Original String
                    originalString = match

                    # Pass 1
                    if setData == False:
                        # Add String
                        if match != "\\\\\\\\":
                            questList[2].append(match.strip())

                    # Pass 2
                    else:
                        if questList[2]:
                            # Grab and Pop
                            translatedText = questList[2][0]
                            questList[2].pop(0)

                            # Set to None if empty list
                            if len(translatedList) <= 0:
                                translatedList = None

                            # Replace Single Quotes
                            translatedText = translatedText.replace('"', "'")
                            translatedText = re.sub(r"([^\\'])'", r"\1\\'", translatedText)

                            # Set Data
                            data[i] = data[i].replace(originalString, translatedText)
                            saveCheckLines(data, filename)

        # Quest Target
        regex = r'PlaceInformation[\\]+":[\\]+"(.*?)[\\]+"'
        matchList = re.findall(regex, data[i])
        if len(matchList) > 0:
            for match in matchList:
                if match:
                    # Save Original String
                    originalString = match

                    # Pass 1
                    if setData == False:
                        # Add String
                        if match != "\\\\\\\\":
                            questList[3].append(match.strip())

                    # Pass 2
                    else:
                        if questList[3]:
                            # Grab and Pop
                            translatedText = questList[3][0]
                            questList[3].pop(0)

                            # Set to None if empty list
                            if len(translatedList) <= 0:
                                translatedList = None

                            # Replace Single Quotes
                            translatedText = translatedText.replace('"', "'")
                            translatedText = re.sub(r"([^\\'])'", r"\1\\'", translatedText)

                            # Set Data
                            data[i] = data[i].replace(originalString, translatedText)
                            saveCheckLines(data, filename)

        # Quest Summary
        regex = r'[\\]+"QuestContent[\\]+":[\\]+"[\\]+"(.*?)[\\]+"[\\]+"'
        matchList = re.findall(regex, data[i])
        if len(matchList) > 0:
            for match in matchList:
                if match:
                    # Save Original String
                    originalString = match

                    # Remove any textwrap
                    match = match.replace(r"\\\\\\\\n", " ")
                    match = match.replace(r"\\\\\\\\\\\\\\\\c", "\\c")

                    # Pass 1
                    if setData == False:
                        # Add String
                        if match != "\\\\\\\\":
                            questList[4].append(match.strip())

                    # Pass 2
                    else:
                        if questList[4]:
                            # Grab and Pop
                            translatedText = questList[4][0]
                            questList[4].pop(0)

                            # Set to None if empty list
                            if len(translatedList) <= 0:
                                translatedList = None

                            # Textwrap
                            translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
                            translatedText = translatedText.replace("\n", r"\\\\\\\\n")
                            match = match.replace("\\c", r"\\\\\\\\\\\\\\\\c")

                            # Replace Single Quotes
                            translatedText = translatedText.replace('"', "'")
                            translatedText = re.sub(r"([^\\'])'", r"\1\\'", translatedText)

                            # Set Data
                            data[i] = data[i].replace(originalString, translatedText)
                            saveCheckLines(data, filename)

        # Quest Goal 1
        regex = r'ObjectiveContent[\\]+":[\\]+"[\\]+"(.*?)[\\]+"'
        matchList = re.findall(regex, data[i])
        if len(matchList) > 0:
            for match in matchList:
                if match:
                    # Save Original String
                    originalString = match

                    # Remove any textwrap
                    match = match.replace(r"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n", " ")

                    # Pass 1
                    if setData == False:
                        # Add String
                        if match != "\\\\\\\\":
                            questList[5].append(match.strip())

                    # Pass 2
                    else:
                        if questList[5]:
                            # Grab and Pop
                            translatedText = questList[5][0]
                            questList[5].pop(0)

                            # Set to None if empty list
                            if len(translatedList) <= 0:
                                translatedList = None

                            # Textwrap
                            translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
                            translatedText = translatedText.replace("\n", r"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n")

                            # Replace Single Quotes
                            translatedText = translatedText.replace('"', "'")
                            translatedText = re.sub(r"([^\\'])'", r"\1\\'", translatedText)

                            # Set Data
                            data[i] = data[i].replace(originalString, translatedText)
                            saveCheckLines(data, filename)

        # SceneCustomMenu - Text (Command Labels)
        regex = r'[\\]+"Text[\\]+":[\\]+"(.+?)[\\]+"'
        matchList = re.findall(regex, data[i])
        if len(matchList) > 0:
            for match in matchList:
                if match:
                    jaString = match
                    originalString = jaString

                    # Make sure it contains Japanese
                    if not re.search(LANGREGEX, jaString):
                        continue

                    # Make sure didn't grab only backslashes
                    if re.search(r"^[\\]+$", jaString):
                        continue

                    if jaString.replace("\u3000", "").strip():
                        # Pass 1
                        if setData == False:
                            sceneMenuText.append(jaString.strip())

                        # Pass 2
                        else:
                            if sceneMenuText:
                                # Grab and Pop
                                translatedText = sceneMenuText[0]
                                sceneMenuText.pop(0)

                                # Replace quotes
                                translatedText = translatedText.replace('"', "'")
                                translatedText = re.sub(r"([^\\'])'" , r"\1\\'", translatedText)

                                # Set Data
                                data[i] = data[i].replace(originalString, translatedText)
                                saveCheckLines(data, filename)

        # SceneCustomMenu - CommonHelpText
        regex = r'[\\]+"CommonHelpText[\\]+":[\\]+"(.+?)[\\]+"'
        matchList = re.findall(regex, data[i])
        if len(matchList) > 0:
            for match in matchList:
                if match:
                    jaString = match
                    originalString = jaString

                    # Make sure it contains Japanese
                    if not re.search(LANGREGEX, jaString):
                        continue

                    # Make sure didn't grab only backslashes
                    if re.search(r"^[\\]+$", jaString):
                        continue

                    if jaString.replace("\u3000", "").strip():
                        # Pass 1
                        if setData == False:
                            sceneMenuCommonHelpText.append(jaString.strip())

                        # Pass 2
                        else:
                            if sceneMenuCommonHelpText:
                                # Grab and Pop
                                translatedText = sceneMenuCommonHelpText[0]
                                sceneMenuCommonHelpText.pop(0)

                                # Replace quotes
                                translatedText = translatedText.replace('"', "'")
                                translatedText = re.sub(r"([^\\'])'" , r"\1\\'", translatedText)

                                # Set Data
                                data[i] = data[i].replace(originalString, translatedText)
                                saveCheckLines(data, filename)

        # SceneCustomMenu - HelpText (Command Help)
        regex = r'[\\]+"HelpText[\\]+":[\\]+"(.+?)[\\]+"'
        matchList = re.findall(regex, data[i])
        if len(matchList) > 0:
            for match in matchList:
                if match:
                    jaString = match
                    originalString = jaString

                    # Make sure it contains Japanese
                    if not re.search(LANGREGEX, jaString):
                        continue

                    # Make sure didn't grab only backslashes
                    if re.search(r"^[\\]+$", jaString):
                        continue

                    if jaString.replace("\u3000", "").strip():
                        # Pass 1
                        if setData == False:
                            sceneMenuHelpText.append(jaString.strip())

                        # Pass 2
                        else:
                            if sceneMenuHelpText:
                                # Grab and Pop
                                translatedText = sceneMenuHelpText[0]
                                sceneMenuHelpText.pop(0)

                                # Replace quotes
                                translatedText = translatedText.replace('"', "'")
                                translatedText = re.sub(r"([^\\'])'" , r"\1\\'", translatedText)

                                # Set Data
                                data[i] = data[i].replace(originalString, translatedText)
                                saveCheckLines(data, filename)

        # NUUN_SaveScreen - ParamName (save screen labels like 現在地, プレイ時間, 所持金)
        regex = r'[\\]+"ParamName[\\]+":[\\]+"(.+?)[\\]+"'
        matchList = re.findall(regex, data[i])
        if len(matchList) > 0:
            for match in matchList:
                if match:
                    jaString = match
                    originalString = jaString

                    # Make sure it contains Japanese
                    if not re.search(LANGREGEX, jaString):
                        continue

                    # Make sure didn't grab only backslashes
                    if re.search(r"^[\\]+$", jaString):
                        continue

                    if jaString.replace("\u3000", "").strip():
                        # Pass 1
                        if setData == False:
                            saveParamName.append(jaString.strip())

                        # Pass 2
                        else:
                            if saveParamName:
                                # Grab and Pop
                                translatedText = saveParamName[0]
                                saveParamName.pop(0)

                                # Replace quotes
                                translatedText = translatedText.replace('"', "'")
                                translatedText = re.sub(r"([^\\'])'" , r"\1\\'", translatedText)

                                # Set Data
                                data[i] = data[i].replace(originalString, translatedText)
                                saveCheckLines(data, filename)

        # DrawTextEx - Brace-delimited labels (e.g., \}【口づけ】\{, \}回数：\{)
        # Only process lines containing drawTextEx calls
        if 'drawTextEx' in data[i]:
            regex = r'[\\]+\}(.+?)[\\]+\{'
            matchList = re.findall(regex, data[i])
            if len(matchList) > 0:
                for match in matchList:
                    if match:
                        jaString = match
                        originalString = jaString

                        # Skip game variable references (handled separately)
                        if '$gameVariables' in jaString:
                            continue

                        # Make sure it contains Japanese
                        if not re.search(LANGREGEX, jaString):
                            continue

                        if jaString.replace("\u3000", "").strip():
                            # Pass 1 (deduplicated)
                            if setData == False:
                                if jaString.strip() not in sceneMenuDrawText:
                                    sceneMenuDrawText.append(jaString.strip())

                            # Pass 2 (collect mapping, skip duplicates)
                            else:
                                if sceneMenuDrawText and originalString not in _pending_drawtext:
                                    # Grab and Pop
                                    translatedText = sceneMenuDrawText[0]
                                    sceneMenuDrawText.pop(0)

                                    # Replace quotes
                                    translatedText = translatedText.replace('"', "'")

                                    # Queue for deferred replacement
                                    _pending_drawtext[originalString] = translatedText

            # Apply deferred replacements longest-first to avoid substring collisions
            # (e.g., replacing 回数： before 使用回数： would corrupt the longer string)
            if setData and _pending_drawtext:
                for orig in sorted(_pending_drawtext, key=len, reverse=True):
                    data[i] = data[i].replace(orig, _pending_drawtext[orig])
                _pending_drawtext.clear()
                saveCheckLines(data, filename)

        # DrawTextEx - Stat labels (e.g., C[16]攻撃力\\...C[0])
        if 'drawTextEx' in data[i]:
            regex = r'C\[\d+\]([^C\\,`\[\]]+?)[\\]+C\[0\]'
            matchList = re.findall(regex, data[i])
            if len(matchList) > 0:
                for match in matchList:
                    if match:
                        jaString = match
                        originalString = jaString

                        # Make sure it contains Japanese
                        if not re.search(LANGREGEX, jaString):
                            continue

                        if jaString.replace("\u3000", "").strip():
                            # Pass 1 (deduplicated)
                            if setData == False:
                                if jaString.strip() not in sceneMenuStatLabel:
                                    sceneMenuStatLabel.append(jaString.strip())

                            # Pass 2 (collect mapping, skip duplicates)
                            else:
                                if sceneMenuStatLabel and originalString not in _pending_statlabel:
                                    # Grab and Pop
                                    translatedText = sceneMenuStatLabel[0]
                                    sceneMenuStatLabel.pop(0)

                                    # Queue for deferred replacement
                                    _pending_statlabel[originalString] = translatedText

            # Apply deferred replacements longest-first to avoid substring collisions
            if setData and _pending_statlabel:
                for orig in sorted(_pending_statlabel, key=len, reverse=True):
                    data[i] = data[i].replace(orig, _pending_statlabel[orig])
                _pending_statlabel.clear()
                saveCheckLines(data, filename)

        # DrawTextEx - Game variable counter word (hardcoded: 回 → time(s))
        if setData and 'drawTextEx' in data[i] and '$gameVariables' in data[i]:
            data[i] = re.sub(
                r'(\$\{\$gameVariables\.value\(\d+\)\}\s*)回',
                r'\1x',
                data[i]
            )
            saveCheckLines(data, filename)

        # Next Line
        i += 1

    # EOF
    translate = False
    questListTL = [[], [], [], [], [], []]
    customTL = []

    # Compute combined total for progress bar across all categories
    combinedTotal = (
        sum(len(quest) for quest in questList)
        + len(custom)
        + len(sceneMenuText)
        + len(sceneMenuCommonHelpText)
        + len(sceneMenuHelpText)
        + len(sceneMenuDrawText)
        + len(sceneMenuStatLabel)
        + len(saveParamName)
    )
    if combinedTotal > 0:
        pbar.total = combinedTotal
        pbar.refresh()

    # Quest
    if len(questList) > 0:
        # Quest Name
        response = translateAI(questList[0], "Quest Name")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        questName = response[0]

        # Quest Client
        response = translateAI(questList[1], "Quest Client")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        questClient = response[0]

        # Quest Location
        response = translateAI(questList[2], "Quest Location")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        questLocation = response[0]

        # Quest Target
        response = translateAI(questList[3], "Quest Location")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        questTarget = response[0]

        # Quest Summary
        response = translateAI(questList[4], "Quest Summary")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        questSummary = response[0]

        # Quest Goal 1
        response = translateAI(questList[5], "Quest Goal")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        questGoal1 = response[0]

        # Check Mismatch
        if (
            len(questName) == len(questList[0])
            or len(questClient) == len(questList[1])
            or len(questLocation) == len(questList[2])
            or len(questTarget) == len(questList[3])
            or len(questSummary) == len(questList[4])
            or len(questGoal1) == len(questList[5])
        ):
            # Set Strings
            questListTL = [questName, questClient, questLocation, questTarget, questSummary, questGoal1]
            translate = True

        # Mismatch
        else:
            with LOCK:
                if filename not in MISMATCH:
                    MISMATCH.append(filename)

    # Custom
    if custom:
        # TL
        response = translateAI(custom, "Relic Name")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        customResponse = response[0]

        # Check Mismatch
        if len(custom) == len(customResponse):
            customTL = customResponse
            translate = True

        # Mismatch
        else:
            with LOCK:
                if filename not in MISMATCH:
                    MISMATCH.append(filename)

    # SceneCustomMenu Text
    sceneMenuTextTL = []
    sceneMenuCommonHelpTextTL = []
    sceneMenuHelpTextTL = []
    sceneMenuDrawTextTL = []
    sceneMenuStatLabelTL = []
    saveParamNameTL = []

    if sceneMenuText:
        # TL
        response = translateAI(sceneMenuText, "Menu Item")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        sceneMenuTextResponse = response[0]

        # Check Mismatch
        if len(sceneMenuText) == len(sceneMenuTextResponse):
            sceneMenuTextTL = sceneMenuTextResponse
            translate = True

        # Mismatch
        else:
            with LOCK:
                if filename not in MISMATCH:
                    MISMATCH.append(filename)

    # SceneCustomMenu CommonHelpText
    if sceneMenuCommonHelpText:
        # TL
        response = translateAI(sceneMenuCommonHelpText, "Menu Help Text")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        sceneMenuCommonHelpTextResponse = response[0]

        # Check Mismatch
        if len(sceneMenuCommonHelpText) == len(sceneMenuCommonHelpTextResponse):
            sceneMenuCommonHelpTextTL = sceneMenuCommonHelpTextResponse
            translate = True

        # Mismatch
        else:
            with LOCK:
                if filename not in MISMATCH:
                    MISMATCH.append(filename)

    # SceneCustomMenu HelpText
    if sceneMenuHelpText:
        # TL
        response = translateAI(sceneMenuHelpText, "Menu Help Text")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        sceneMenuHelpTextResponse = response[0]

        # Check Mismatch
        if len(sceneMenuHelpText) == len(sceneMenuHelpTextResponse):
            sceneMenuHelpTextTL = sceneMenuHelpTextResponse
            translate = True

        # Mismatch
        else:
            with LOCK:
                if filename not in MISMATCH:
                    MISMATCH.append(filename)

    # DrawTextEx - Brace-delimited labels (tracker section headers and field labels)
    if sceneMenuDrawText:
        # TL
        response = translateAI(sceneMenuDrawText, "Status Tracker Label")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        sceneMenuDrawTextResponse = response[0]

        # Check Mismatch
        if len(sceneMenuDrawText) == len(sceneMenuDrawTextResponse):
            sceneMenuDrawTextTL = sceneMenuDrawTextResponse
            translate = True

        # Mismatch
        else:
            with LOCK:
                if filename not in MISMATCH:
                    MISMATCH.append(filename)

    # DrawTextEx - Stat labels (e.g., 攻撃力, 魔力, etc.)
    if sceneMenuStatLabel:
        # TL
        response = translateAI(sceneMenuStatLabel, "Character Stat Name")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        sceneMenuStatLabelResponse = response[0]

        # Check Mismatch
        if len(sceneMenuStatLabel) == len(sceneMenuStatLabelResponse):
            sceneMenuStatLabelTL = sceneMenuStatLabelResponse
            translate = True

        # Mismatch
        else:
            with LOCK:
                if filename not in MISMATCH:
                    MISMATCH.append(filename)

    # NUUN_SaveScreen ParamName
    if saveParamName:
        # TL
        response = translateAI(saveParamName, "Save Screen Label")
        tokens[0] += response[1][0]
        tokens[1] += response[1][1]
        saveParamNameResponse = response[0]

        # Check Mismatch
        if len(saveParamName) == len(saveParamNameResponse):
            saveParamNameTL = saveParamNameResponse
            translate = True

        # Mismatch
        else:
            with LOCK:
                if filename not in MISMATCH:
                    MISMATCH.append(filename)

    # Pass 2
    if translate and not setData:
        translatePlugin(data, pbar, filename, [questListTL, customTL, sceneMenuTextTL, sceneMenuCommonHelpTextTL, sceneMenuHelpTextTL, sceneMenuDrawTextTL, sceneMenuStatLabelTL, saveParamNameTL])
    return tokens

# Save some money and enter the character before translation
def getSpeaker(speaker):
    match speaker:
        case "ファイン":
            return ["Fine", [0, 0]]
        case "":
            return ["", [0, 0]]
        case _:
            # Find Speaker
            for i in range(len(NAMESLIST)):
                if speaker == NAMESLIST[i][0]:
                    return [NAMESLIST[i][1], [0, 0]]

            # Translate and Store Speaker
            response = translateAI(
                f"{speaker}",
                "Reply with the " + LANGUAGE + " translation of the NPC name.",
                False,
            )
            response[0] = response[0].title()
            response[0] = response[0].replace("'S", "'s")
            response[0] = response[0].replace("Speaker: ", "")

            # Retry if name doesn't translate for some reason
            if re.search(r"([a-zA-Z？?])", response[0]) == None:
                response = translateAI(
                    f"{speaker}",
                    "Reply with the " + LANGUAGE + " translation of the NPC name.",
                    False,
                )
                response[0] = response[0].title()
                response[0] = response[0].replace("'S", "'s")

            speakerList = [speaker, response[0]]
            NAMESLIST.append(speakerList)
            return response
    return [speaker, [0, 0]]

def translateAI(text, history, history_ctx=None):
    """
    Legacy wrapper function for the new shared translation utility.
    This maintains compatibility with existing code while using the new shared implementation.
    """
    global PBAR, MISMATCH, FILENAME
    
    # Update config estimate mode based on global ESTIMATE
    TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
    
    # Call the new shared translation function
    return sharedtranslateAI(
        text=text,
        history=history,
        config=TRANSLATION_CONFIG,
        filename=FILENAME,
        pbar=PBAR,
        lock=LOCK,
        mismatchList=MISMATCH
    )
