include(Resources.id("mtrsteamloco:scripts/display_helper.js"));
importPackage(java.awt);
importPackage(java.awt.geom);
importPackage(java.awt.image);

/*
 * In-car LED (two-row) simulator
 * - internal dot canvas: 256x32
 * - output canvas: 1280x160 (x5 nearest neighbor)
 * - 3 colors: orange / green / red (use orange+green mainly)
 */

//////////////////////////////
// 0) small utils
//////////////////////////////
function loadLedDirDbOnce(state) {
    if (state.ledDirDb != null) return;

    let path = "mtr:botapinu_lcd/led_dir_major.json";

    try {
        let txt = Resources.readString(Resources.id(path));
        state.ledDirDb = JSON.parse(String(txt));
        if (state.ledDirDb == null || typeof state.ledDirDb !== "object") state.ledDirDb = {};
        print("led_dir_major.json loaded: " + path);
    } catch (e) {
        state.ledDirDb = {};
        print("led_dir_major.json load FAILED: " + e);
    }
}

function joinEnAnd(list) {
    if (!list) return "";
    let arr = [];
    for (let i = 0; i < list.length; i++) {
        let s = String(list[i] || "").trim();
        if (s.length > 0) arr.push(s);
    }
    if (arr.length == 0) return "";
    if (arr.length == 1) return arr[0];
    if (arr.length == 2) return arr[0] + " and " + arr[1];
    return arr.slice(0, arr.length - 1).join(", ") + " and " + arr[arr.length - 1];
}

function staJpFromPlatform(p) {
    if (!p || !p.station) return "";
    return cjk(p.station.name).trim();
}

// ★方面名（例：狭山）を返す。無ければ ""。
function resolveDirectionMajorJp(state, platformList, nextIdx) {
    loadLedDirDbOnce(state);

    if (!platformList || platformList.size() <= 0) return "";
    if (nextIdx == null || nextIdx < 0) nextIdx = 0;

    // nextIdx から終点まで、最初に一致した主要駅＝一番近い方面
    for (let i = nextIdx; i < platformList.size(); i++) {
        let p = platformList.get(i);
        let jp = staJpFromPlatform(p);
        if (!jp) continue;

        // JSONキー完全一致（余計な装飾があるならここで整形してもOK）
        if (state.ledDirDb[jp]) return jp;
    }

    return "";
}

function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }

function cjk(s) { return TextUtil.getCjkParts(String(s || "")); }
function nonCjk(s) { return TextUtil.getNonCjkParts(String(s || "")); }
function extra(s) { return TextUtil.getExtraParts(String(s || "")); }

// ★コード（ナンバリング）を常に大文字にする
function normCode(code) {
    code = String(code || "").trim();
    return code ? code.toUpperCase() : "";
}

function formatStaEngWithCode(x) {
    // Platformっぽい（p.station がある）
    if (x && typeof x === "object" && x.station) {
        let staRaw = x.station.name;

        // 英語名
        let en = nonCjk(staRaw).trim();
        if (en.length == 0) en = cjk(staRaw).trim();

        // コード：stationExtra 優先 → 無ければ extraParts
        let code = "";
        try {
            if (x.stationExtra != null) code = String(x.stationExtra).trim();
        } catch (e) { }
        if (!code) code = extra(staRaw).trim();

        code = normCode(code);
        return code ? (en + "(" + code + ")") : en;
    }

    // 文字列（従来互換）
    let staNameRaw = String(x || "");
    let en = nonCjk(staNameRaw).trim();
    let code = normCode(extra(staNameRaw).trim());
    if (en.length == 0) en = cjk(staNameRaw).trim();
    return code ? (en + "(" + code + ")") : en;
}

// "Kanayama(NH34)" を作る（extraが "NH34" を含む前提）
function formatStaEngWithCodeFromPlatform(p) {
    if (!p || !p.station) return "";

    // 英語名：station.name の nonCjk を使う（あなたの運用通り）
    let staRaw = p.station.name;
    let en = nonCjk(staRaw).trim();
    if (en.length == 0) en = cjk(staRaw).trim();

    // コード：まず stationExtra を優先、無ければ extraParts にフォールバック
    let code = "";
    try {
        if (p.stationExtra != null) code = String(p.stationExtra).trim();
    } catch (e) { }

    if (!code) code = extra(staRaw).trim();
    code = normCode(code);

    if (code) return en + "(" + code + ")";
    return en;
}

// JP駅名は漢字だけ
function formatStaJp(staNameRaw) {
    return cjk(staNameRaw).trim();
}

// stop list を作る（platformListの next..dest を使う）
// JP: "この先、A，B，Cの順にとまります"
// EN: "This is Express, with stops at A,B ... and Z."
function buildStopsStrings(platformList, nextIdx, routeTypeRaw, pFinal) {
    let routeTypeJp = cjk(routeTypeRaw);
    let routeTypeEn = nonCjk(routeTypeRaw);

    let arrJp = [];
    let arrEn = [];

    for (let i = nextIdx; i < platformList.size(); i++) {
        let p = platformList.get(i);
        let staRaw = p.station ? p.station.name : "";

        arrJp.push(formatStaJp(staRaw));
        arrEn.push(formatStaEngWithCodeFromPlatform(p));
    }

    let jp = "この先、" + arrJp.join("，") + "の順にとまります";

    let isLocal = (routeTypeJp == "普通" || routeTypeJp == "各停" || routeTypeJp == "各駅停車");

    let destEn = formatStaEngWithCodeFromPlatform(pFinal);
    let enList = joinEnAnd(arrEn);

    let en;
    if (isLocal) {
        en = "This is for " + destEn + ", with stops at " + enList + ".";
    } else {
        en = "This is " + routeTypeEn + ", with stops at " + enList + ".";
    }

    return { jp: jp, en: en, isLocal: isLocal };
}

// 下段文面（★停車中は「停車中の駅」を停車駅リストに含めない）
function buildBottomStrings(state, platformList, nextIdx, routeTypeRaw, pFinal, atStation) {
    let routeTypeJp = cjk(routeTypeRaw).trim();
    let routeTypeEn = nonCjk(routeTypeRaw).trim();

    // 普通判定
    let isLocal = (routeTypeJp == "普通" || routeTypeJp == "各停" || routeTypeJp == "各駅停車");

    // ★停車中は nextIdx+1 から列挙（現在駅を除外）
    let startIdx = nextIdx + (atStation ? 1 : 0);
    if (startIdx < 0) startIdx = 0;
    if (startIdx > platformList.size() - 1) startIdx = platformList.size() - 1;

    let arrJp = [];
    let arrEn = [];
    for (let i = startIdx; i < platformList.size(); i++) {
        let p = platformList.get(i);
        let staRaw = p.station ? p.station.name : "";
        arrJp.push(formatStaJp(staRaw));
        arrEn.push(formatStaEngWithCodeFromPlatform(p));
    }

    // 終点
    let finalJp = formatStaJp(pFinal.station.name);
    let finalEnWithCode = formatStaEngWithCodeFromPlatform(pFinal);

    // 方面（普通のみ）
    let dirJp = "";
    if (isLocal) {
        dirJp = resolveDirectionMajorJp(state, platformList, nextIdx);
    }
    let dirPart = (dirJp ? (dirJp + "方面　") : "");

    // stops 文面（空対策）
    let stopsJp, stopsEnPart;
    if (arrJp.length == 0) {
        stopsJp = "この先、停車駅はありません";
        stopsEnPart = "with no stops.";
    } else {
        stopsJp = "この先、" + arrJp.join("，") + "の順にとまります";
        let enList = joinEnAnd(arrEn);
        stopsEnPart = "with stops at " + enList + ".";
    }

    // 下段JPヘッダ
    let jpHead = isLocal
        ? (dirPart + "普通　" + finalJp + "行き")
        : ("この電車は" + (routeTypeJp ? routeTypeJp : "") + " " + finalJp + "行き");

    let jp = jpHead + "　" + stopsJp;

    // 下段EN
    let en = isLocal
        ? ("This is for " + finalEnWithCode + ", " + stopsEnPart)
        : ("This is " + routeTypeEn + ", " + stopsEnPart);

    return { jp: jp, en: en, isLocal: isLocal };
}

//////////////////////////////
// 1) dot-canvas init
//////////////////////////////
function ensureLedBuffers(state) {
    if (state.ledBuf256) return;

    state.ledW = 256;
    state.ledH = 32;

    state.ledBuf256 = new BufferedImage(state.ledW, state.ledH, BufferedImage.TYPE_INT_ARGB);
    state.ledG256 = state.ledBuf256.createGraphics();
    state.ledG256.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
    state.ledG256.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);

    // 出力用（5倍）
    state.outW = 1280;
    state.outH = 160;

    // 可能ならLED網画像（任意）
    try {
        state.ledMask = Resources.readBufferedImage(Resources.id("mtr:botapinu_lcd/images/syanai_led.png"));
    } catch (e) {
        state.ledMask = null;
    }

    // フォント
    state.ledFont = Resources.readFont(Resources.id("mtr:fonts/bizudpmincho-regular.ttf"));
}

//////////////////////////////
// 2) text draw helpers (256x32 space)
//////////////////////////////
function clearLed(g, bg) {
    g.setComposite(AlphaComposite.Src);
    g.setColor(bg);
    g.fillRect(0, 0, 256, 32);
    g.setComposite(AlphaComposite.SrcOver);
}

// parts: [{text,color}, ...]
// ★leadInPx: 開始時に画面右外から流れ込ませる
function scrollParts(g, parts, font, baseline, startTime, width, speedPxPerMs, leadInPx) {
    let fm = g.getFontMetrics(font);

    // total width
    let totalW = 0;
    for (let i = 0; i < parts.length; i++) totalW += fm.stringWidth(parts[i].text);

    let lead = Number(leadInPx || 0);

    let offset = (Date.now() - startTime) * speedPxPerMs;
    let x = width + lead - offset;

    for (let i = 0; i < parts.length; i++) {
        g.setColor(parts[i].color);
        g.setFont(font);
        g.drawString(parts[i].text, x, baseline);
        x += fm.stringWidth(parts[i].text);
    }

    return { offset: offset, totalW: totalW, lead: lead };
}

function drawCentered(g, text, font, color, baseline, width) {
    text = String(text || "");
    g.setFont(font);
    g.setColor(color);
    let w = g.getFontMetrics(font).stringWidth(text);
    g.drawString(text, Math.floor((width - w) / 2), baseline);
}

function drawLeft(g, text, font, color, baseline, leftX) {
    text = String(text || "");
    g.setFont(font);
    g.setColor(color);
    g.drawString(text, leftX, baseline);
}

function drawPartsLeft(g, parts, font, baseline, leftX) {
    g.setFont(font);
    let fm = g.getFontMetrics(font);
    let x = leftX;

    for (let i = 0; i < parts.length; i++) {
        let t = String(parts[i].text || "");
        if (t.length == 0) continue;
        g.setColor(parts[i].color);
        g.drawString(t, x, baseline);
        x += fm.stringWidth(t);
    }
}

//////////////////////////////
// 3) timing / mode control
//////////////////////////////
function getSpeedKmh(train) {
    try {
        let sp = Number(train.speed());
        if (isNaN(sp)) return 0;
        return sp * 3.6;
    } catch (e) {
        return 0;
    }
}

function hasNextOperation(train) {
    try {
        if (train.getNextRoutePlatforms) {
            let n = train.getNextRoutePlatforms();
            if (n != null && n.size && n.size() > 0) return true;
        }
    } catch (e) { }
    return false;
}

function ensureModeState(state) {
    if (state.ledMode == null) state.ledMode = "DEST"; // DEST / NEXT / SOON
    if (state.ledLastMode == null) state.ledLastMode = state.ledMode;

    if (state.ledBlankUntil == null) state.ledBlankUntil = 0;

    // 下段スクロール
    if (state.ledPhaseStartTime == null) state.ledPhaseStartTime = Date.now();
    if (state.ledGapStartTime == null) state.ledGapStartTime = null;
    if (state.ledBottomLang == null) state.ledBottomLang = "JP";

    // SOON下段のスクロール開始
    if (state.ledSoonScrollStart == null) state.ledSoonScrollStart = Date.now();

    // 出発/到着検知
    if (state.ledLastDoorOpen == null) state.ledLastDoorOpen = false;

    // 終点到着ラッチ（到着後モード）
    if (state.ledTerminalArrived == null) state.ledTerminalArrived = false;
    if (state.ledTerminalKey == null) state.ledTerminalKey = null;

    // ★上段 JP→EN ループ管理
    if (state.ledTopPhaseStartTime == null) state.ledTopPhaseStartTime = Date.now();
    if (state.ledTopKey == null) state.ledTopKey = "";

    // （将来拡張用）下段キー
    if (state.ledBottomKey == null) state.ledBottomKey = "";
}

function setBlank(state, msBase, msRand) {
    let r = 0;
    if (msRand && msRand > 0) r = Math.floor(Math.random() * msRand);
    state.ledBlankUntil = Date.now() + msBase + r;
}

// ★リセット関数
function resetTopCycle(state) {
    state.ledTopPhaseStartTime = Date.now(); // 上段JP→ENの基準
}
function resetBottomCycle(state) {
    // 下段は必ずJPから＆スクロール先頭から
    state.ledBottomLang = "JP";
    state.ledPhaseStartTime = Date.now();
    state.ledGapStartTime = null;

    // SOON用スクロールも先頭へ
    state.ledSoonScrollStart = Date.now();
}

//////////////////////////////
// 4) build display strings
//////////////////////////////
function buildTopLines(pNext, pFinal, routeTypeJpn, isSoon, isTerminal) {
    let nextJp = formatStaJp(pNext.station.name);
    let nextEnWithCode = formatStaEngWithCodeFromPlatform(pNext);

    let finalJp = formatStaJp(pFinal.station.name);
    let finalEnWithCode = formatStaEngWithCodeFromPlatform(pFinal);

    let suffixJp = isTerminal ? " 終点です" : "";
    let suffixEn = isTerminal ? ", the last stop." : ".";

    if (isSoon) {
        return {
            soonJp: "まもなく " + nextJp + suffixJp,
            soonEn: nextEnWithCode + suffixEn,  // ★SOONの英語は Next を付けない
            nextJp: "次は " + nextJp + suffixJp,
            nextEn: "Next " + nextEnWithCode + suffixEn,
            destJp: "この電車は" + (routeTypeJpn ? routeTypeJpn : "") + " " + finalJp + "行き",
            destEn: "For " + finalEnWithCode + "."
        };
    }

    return {
        nextJp: "次は " + nextJp + suffixJp,
        nextEn: "Next " + nextEnWithCode + suffixEn,
        destJp: "この電車は" + (routeTypeJpn ? routeTypeJpn : "") + " " + finalJp + "行き",
        destEn: "For " + finalEnWithCode + "."
    };
}

function buildSoonLowerEn(pNext) {
    return "We will soon make a brief stop at " + formatStaEngWithCodeFromPlatform(pNext) + ".";
}

//////////////////////////////
// 5) main draw (call this)
//////////////////////////////
function carLedMain(g, state, train) {
    ensureLedBuffers(state);
    ensureModeState(state);

    let g2 = state.ledG256;

    let bg = new Color(0.05, 0.05, 0.05);
    let ledOrange = new Color(0.896, 0.547, 0.09);
    let ledGreen = new Color(0.154, 0.721, 0.136);
    let ledRed = new Color(0.90, 0.12, 0.12);

    let fontEn = state.ledFont.deriveFont(Font.PLAIN, 14);
    let fontJp = state.ledFont.deriveFont(Font.PLAIN, 16);

    let topBase = 14;
    let botBase = 30;

    let platformList = train.getThisRoutePlatforms();
    let nextIdx = train.getThisRoutePlatformsNextIndex();
    if (platformList == null || platformList.size() == 0 || nextIdx < 0 || nextIdx >= platformList.size()) {
        clearLed(g2, bg);
        g.drawImage(state.ledBuf256, 0, 0, 1280, 160, null);
        if (state.ledMask) g.drawImage(state.ledMask, 0, 0, 1280, 160, null);
        return;
    }

    let pNext = platformList.get(nextIdx);
    let pFinal = platformList.get(platformList.size() - 1);

    let routeTypeRaw = String(pNext.route ? pNext.route.lightRailRouteNumber : "");
    let routeTypeJpn = cjk(routeTypeRaw);

    // 終点判定（次駅が終点）
    let isTerminal = (nextIdx == platformList.size() - 1);

    // 位置/速度/ドア
    let railProgress = train.getRailProgress(0);
    let nextDist = Number(pNext.distance || 0);
    let remain = nextDist - railProgress;

    let doorOpen = (train.doorValue() > 0);
    let spKmh = getSpeedKmh(train);

    // 停車中判定（下段の「現在駅除外」に使う）
    let atStation = doorOpen && (spKmh < 1.0 || remain < 2);

    // 到着/出発検知（リセット規則用）
    let justArrived = (!state.ledLastDoorOpen && doorOpen); // ドア閉→開
    let justDeparted = (state.ledLastDoorOpen && !doorOpen); // ドア開→閉
    state.ledLastDoorOpen = doorOpen;

    // リセット規則：
    // 到着（ドア開）：上段/下段どっちも
    if (justArrived) {
        resetTopCycle(state);
        resetBottomCycle(state);
    }
    // 出発（ドア閉）：上段のみ
    if (justDeparted) {
        resetTopCycle(state);
    }

    // ★SOON判定：ドア閉〜加速までSOONにならない（走り出してからSOON）
    let isSoon = (!doorOpen) && (remain < 500) && (spKmh >= 1.0);

    // 終点到着判定
    let noNextOp = !hasNextOperation(train);
    let arrivedTerminalNow =
        noNextOp &&
        isTerminal &&
        doorOpen &&
        (spKmh < 1.0 || remain < 2);

    // 終点キー（同じ終点でラッチ維持）
    let terminalKey = String(pFinal.station ? pFinal.station.name : "");

    if (arrivedTerminalNow) {
        state.ledTerminalArrived = true;
        state.ledTerminalKey = terminalKey;

        // ★終点到着時は両段ともJP開始にしておく（復帰時の整合）
        resetTopCycle(state);
        resetBottomCycle(state);
    }

    // 終点ラッチ解除条件
    if (state.ledTerminalArrived) {
        if (!isTerminal || state.ledTerminalKey != terminalKey) {
            state.ledTerminalArrived = false;
            state.ledTerminalKey = null;

            // ★通常へ戻る時もJP開始
            resetTopCycle(state);
            resetBottomCycle(state);
        }
    }

    // まず「終点到着後」表示が最優先
    if (state.ledTerminalArrived) {
        clearLed(g2, bg);

        drawLeft(g2, "ご乗車ありがとうございました", fontJp, ledOrange, topBase, 0);

        // 下段：終点英語をスクロール
        let PAD = "   ";
        let SCROLL_SPEED = 0.060;
        let LEAD_IN = 40;

        let finalEnWithCode = formatStaEngWithCodeFromPlatform(pFinal);
        let txt = finalEnWithCode + ", the last stop";

        let parts = [
            { text: PAD, color: ledGreen },
            { text: txt, color: ledOrange },
            { text: PAD, color: ledGreen }
        ];

        let res = scrollParts(g2, parts, fontEn, botBase, state.ledSoonScrollStart, 256, SCROLL_SPEED, LEAD_IN);
        if (res.offset > res.totalW + 256 + res.lead) state.ledSoonScrollStart = Date.now();

        g.drawImage(state.ledBuf256, 0, 0, 1280, 160, null);
        if (state.ledMask) g.drawImage(state.ledMask, 0, 0, 1280, 160, null);
        return;
    }

    // 走行中モード決定
    let FAR_M = 500;
    let moving = (spKmh >= 0.1);

    let newMode;
    if (isSoon) {
        newMode = "SOON";
    } else if (moving && remain > FAR_M) {
        newMode = "NEXT";
    } else {
        // ★停車中〜低速域は DEST より NEXT を優先したい場合：
        //   ここを "NEXT" にすると「ドア閉～発車直前も次は」寄りになる
        //   今回は「停車中と同じ表示 or 次は」が希望なので NEXT に寄せる
        newMode = "NEXT";
    }

    // モード変化時の全消灯 + リセット規則（NEXT→SOONは両段）
    if (newMode != state.ledMode) {
        let oldMode = state.ledMode;

        state.ledLastMode = state.ledMode;
        state.ledMode = newMode;

        // ★上段は必ずJPから
        resetTopCycle(state);

        // ★次は→まもなく（NEXT→SOON）は下段もJPから
        if (oldMode == "NEXT" && newMode == "SOON") {
            resetBottomCycle(state);
        }

        if (newMode == "SOON") {
            state.ledSoonScrollStart = Date.now();
            setBlank(state, 2000, 0);
        } else {
            setBlank(state, 400, 300);
        }

        // SOON突入は文面が変わるので下段の位相を整える
        if (newMode == "SOON") {
            state.ledPhaseStartTime = Date.now();
            state.ledGapStartTime = null;
        }
    }

    if (Date.now() < state.ledBlankUntil) {
        clearLed(g2, bg);
        g.drawImage(state.ledBuf256, 0, 0, 1280, 160, null);
        if (state.ledMask) g.drawImage(state.ledMask, 0, 0, 1280, 160, null);
        return;
    }

    // 文面
    let stops = buildStopsStrings(platformList, nextIdx, routeTypeRaw, pFinal);
    let bottom = buildBottomStrings(state, platformList, nextIdx, routeTypeRaw, pFinal, atStation);
    let top = buildTopLines(pNext, pFinal, routeTypeJpn, (state.ledMode == "SOON"), isTerminal);

    // ★上段：リセット基準でJP→EN（必ずJP始まり）
    let TOP_MS = 5000;
    let showJpTop = (Math.floor((Date.now() - state.ledTopPhaseStartTime) / TOP_MS) % 2 == 0);

    // クリア
    clearLed(g2, bg);

    // =========================
    // 上段
    // =========================
    let LEFT_X = 0;

    if (state.ledMode == "DEST") {
        let s = showJpTop ? top.destJp : top.destEn;
        let f = showJpTop ? fontJp : fontEn;
        drawLeft(g2, s, f, ledOrange, topBase, LEFT_X);

    } else if (state.ledMode == "NEXT") {

        if (showJpTop) {
            if (isTerminal) {
                let nextJp = formatStaJp(pNext.station.name);
                drawPartsLeft(g2, [
                    { text: "次は " + nextJp + " ", color: ledOrange },
                    { text: "終点", color: ledRed },
                    { text: "です", color: ledOrange }
                ], fontJp, topBase, LEFT_X);
            } else {
                drawLeft(g2, top.nextJp, fontJp, ledOrange, topBase, LEFT_X);
            }
        } else {
            drawLeft(g2, top.nextEn, fontEn, ledGreen, topBase, LEFT_X);
        }

    } else { // SOON

        if (showJpTop) {
            if (isTerminal) {
                let nextJp = formatStaJp(pNext.station.name);
                drawPartsLeft(g2, [
                    { text: "まもなく " + nextJp + " ", color: ledOrange },
                    { text: "終点", color: ledRed },
                    { text: "です", color: ledOrange }
                ], fontJp, topBase, LEFT_X);
            } else {
                drawLeft(g2, top.soonJp, fontJp, ledOrange, topBase, LEFT_X);
            }
        } else {
            drawLeft(g2, top.soonEn, fontEn, ledGreen, topBase, LEFT_X);
        }
    }

    // =========================
    // 下段（スクロール）
    // =========================
    let PAD = "   ";
    let SCROLL_SPEED = 0.060;
    let PHASE_GAP_MS = 350;
    let LEAD_IN = 40; // ★開始を画面外から

    // SOON中：下段は英語固定スクロール
    if (state.ledMode == "SOON") {
        let soonText = buildSoonLowerEn(pNext);

        let partsSoon = [
            { text: PAD, color: ledGreen },
            { text: soonText, color: ledOrange },
            { text: PAD, color: ledGreen }
        ];

        let resSoon = scrollParts(g2, partsSoon, fontEn, botBase, state.ledSoonScrollStart, 256, SCROLL_SPEED, LEAD_IN);

        // 1周したらループ
        if (resSoon.offset > resSoon.totalW + 256 + resSoon.lead) {
            state.ledSoonScrollStart = Date.now();
        }

    } else {
        // 通常：JP → EN（完走後に切替）
        let bottomIsJp = (state.ledBottomLang == "JP");
        let fBot = bottomIsJp ? fontJp : fontEn;
        let msg = bottomIsJp ? bottom.jp : bottom.en;

        let parts = [
            { text: PAD, color: ledGreen },
            { text: msg, color: ledOrange },
            { text: PAD, color: ledGreen }
        ];

        let res = scrollParts(g2, parts, fBot, botBase, state.ledPhaseStartTime, 256, SCROLL_SPEED, LEAD_IN);

        if (res.offset > res.totalW + 256 + res.lead) {
            if (state.ledGapStartTime == null) {
                state.ledGapStartTime = Date.now();
            } else if (Date.now() - state.ledGapStartTime > PHASE_GAP_MS) {
                state.ledBottomLang = bottomIsJp ? "EN" : "JP";
                state.ledPhaseStartTime = Date.now();
                state.ledGapStartTime = null;
            }
        }
    }

    // 出力
    g.drawImage(state.ledBuf256, 0, 0, 1280, 160, null);
    if (state.ledMask) g.drawImage(state.ledMask, 0, 0, 1280, 160, null);
}

//////////////////////////////
// 6) exported entry
//////////////////////////////
function ledmain(g, state, train) {
    carLedMain(g, state, train);
}