#target illustrator
app.preferences.setBooleanPreference('ShowExternalJSXWarning', false);

/*

### 概要

リンク切れの配置画像を検出し、指定したフォルダーから同名ファイルを探して自動的に再リンクします。

詳細は README を参照してください。

### Overview

Detects missing linked images and relinks them automatically from a folder you choose, matching by file name.

See the README for details.

*/

// =========================================
// 基本情報 / Basic info
// =========================================
var SCRIPT_NAME     = "RelinkMissingLinks";           /* スクリプト名 / script name */
var SCRIPT_VERSION  = "v1.4";                         /* バージョン / version */
var SCRIPT_AUTHOR   = "Masahiro Takano (@swwwitch)";  /* 作者 / author */
var SCRIPT_RELEASED = "2025-07-18";                   /* 最初のリリース日 / first release date */
var SCRIPT_UPDATED  = "2025-08-02";                   /* 更新日 / last updated */

// README (Japanese)
// https://github.com/swwwitch/illustrator-scripts/blob/master/readme-ja/RelinkMissingLinks.md
// README (English)
// https://github.com/swwwitch/illustrator-scripts/blob/master/readme-en/RelinkMissingLinks.md

// Released under the MIT license
// http://opensource.org/licenses/mit-license.php

(function () {

    function getCurrentLang() {
      return ($.locale.indexOf("ja") === 0) ? "ja" : "en";
    }
    var lang = getCurrentLang();

    /* ラベル定義 / Label definitions */
    var LABELS = {
        dialogTitle: {
            ja: "リンク切れの再リンク " + SCRIPT_VERSION,
            en: "Relink Missing Links " + SCRIPT_VERSION
        },
        folderLabel: {
            ja: "再リンク用フォルダー:",
            en: "Relink Folder:"
        },
        chooseButton: {
            ja: "指定",
            en: "Choose"
        },
        matchGroup: {
            ja: "拡張子の扱い",
            en: "Extension Handling"
        },
        targetGroup: {
            ja: "対象",
            en: "Target"
        },
        chkMissing: {
            ja: "リンク切れのみ",
            en: "Missing Links Only"
        },
        chkAll: {
            ja: "リンク変更",
            en: "Relink Existing"
        },
        ok: {
            ja: "再リンク",
            en: "Relink"
        },
        cancel: {
            ja: "キャンセル",
            en: "Cancel"
        }
        ,
        options: {
            ja: [
                { label: "完全一致", value: "exact" },
                { label: "ファイル名のみ", value: "nameOnly" },
                { label: "pngを優先", value: "priority", ext: "png" },
                { label: "psdを優先", value: "priority", ext: "psd" },
                { label: "jpgを優先", value: "priority", ext: "jpg" }
            ],
            en: [
                { label: "Exact Match", value: "exact" },
                { label: "Name Only", value: "nameOnly" },
                { label: "Prefer PNG", value: "priority", ext: "png" },
                { label: "Prefer PSD", value: "priority", ext: "psd" },
                { label: "Prefer JPG", value: "priority", ext: "jpg" }
            ]
        }
    };

    /* ダイアログを表示してユーザーに設定を選ばせる / Show dialog and get user settings */
    function showRelinkDialog() {
        var dialog = new Window("dialog", LABELS.dialogTitle[lang]);
        dialog.alignChildren = "fill";

        var folderGroup = dialog.add("panel", undefined, LABELS.folderLabel[lang]);
        folderGroup.orientation = "column";
        folderGroup.alignment = "fill";
        folderGroup.margins = [5, 20, 5, 10];

        var folderPath = folderGroup.add("edittext", undefined, "");
        folderPath.characters = 30;

        var chooseBtn = folderGroup.add("button", undefined, LABELS.chooseButton[lang]);
        chooseBtn.onClick = function() {
            var target = Folder.selectDialog(LABELS.folderLabel[lang]);
            if (target) folderPath.text = target.fsName;
        };

        var middleGroup = dialog.add("group");
        middleGroup.orientation = "row";
        middleGroup.alignment = "center";
        middleGroup.spacing = 20;

        var matchGroup = middleGroup.add("panel", undefined, LABELS.matchGroup[lang]);
        matchGroup.orientation = "column";
        matchGroup.alignment = "top";
        matchGroup.margins = [15, 20, 15, 10];

        // --- 対象パネル ---
        var targetGroup = middleGroup.add("panel", undefined, LABELS.targetGroup[lang]);
        targetGroup.orientation = "column";
        targetGroup.alignment = "top";
        targetGroup.margins = [15, 20, 15, 10];

        var chkMissingOnly = targetGroup.add("checkbox", undefined, LABELS.chkMissing[lang]);
        chkMissingOnly.alignment = "left";
        chkMissingOnly.value = true; // デフォルト

        var chkAll = targetGroup.add("checkbox", undefined, LABELS.chkAll[lang]);
        chkAll.alignment = "left";

        // --- 追加: リンク切れ/有効リンクの有無をチェック ---
        try {
            var doc = app.activeDocument;
            var hasMissing = false;
            var hasAlive = false;
            for (var i = 0; i < doc.placedItems.length; i++) {
                var pi = doc.placedItems[i];
                if (isLinkBroken(pi)) {
                    hasMissing = true;
                } else {
                    hasAlive = true;
                }
            }
            if (!hasMissing) {
                chkMissingOnly.enabled = false;
                chkMissingOnly.value = false;
            }
            if (!hasAlive) {
                chkAll.enabled = false;
                chkAll.value = false;
            } else {
                // リンクが生きているものがあればONにする
                chkAll.value = true;
            }
        } catch (e) {}

        var options = LABELS.options[lang];

        var radioButtons = [];
        for (var i = 0; i < options.length; i++) {
            radioButtons[i] = matchGroup.add("radiobutton", undefined, options[i].label);
            radioButtons[i].alignment = "left";
        }
        radioButtons[0].value = true;

        var btnGroup = dialog.add("group");
        btnGroup.orientation = "row";
        btnGroup.alignment = "center";
        btnGroup.add("button", undefined, LABELS.cancel[lang], {name:"cancel"});
        var okBtn = btnGroup.add("button", undefined, LABELS.ok[lang], {name:"ok"});

        return validateDialogInput(dialog, folderPath, options, radioButtons, chkMissingOnly, chkAll);
    }

    // ダイアログ入力を検証 / Validate dialog input
    function validateDialogInput(dialog, folderPath, options, radioButtons, chkMissingOnly, chkAll) {
        var targetFolder, mode, priorityExt;
        while (true) {
            if (dialog.show() != 1) return null;
            if (folderPath.text === "") {
                alert("再リンク用のフォルダーを指定してください。");
                continue;
            }
            targetFolder = new Folder(folderPath.text);
            if (!targetFolder.exists) {
                alert("有効なフォルダーを指定してください。");
                continue;
            }
            for (var i = 0; i < options.length; i++) {
                if (radioButtons[i].value) {
                    mode = options[i].value;
                    if (options[i].ext) priorityExt = options[i].ext;
                    break;
                }
            }
            break;
        }
        return {
            targetFolder: targetFolder,
            mode: mode,
            priorityExt: priorityExt,
            targetMissingOnly: chkMissingOnly.value,
            targetAll: chkAll.value
        };
    }

    // メイン処理 / Main process
    function main() {
        var dialogResult = showRelinkDialog();
        if (!dialogResult) return;

        var doc = app.activeDocument;

        // XMPメタデータからfilePathノードを抽出しファイル名リスト化
        var xmp;
        try {
            xmp = new XML(doc.XMPString);
        } catch (e) {
            xmp = null;
        }
        var paths = [];
        if (xmp) {
            try {
                var nodes = xmp.xpath('//stRef:filePath');
                for (var i = 0; i < nodes.length(); i++) {
                    var pathStr = nodes[i].toString();
                    var fname = pathStr.replace(/^.*[\/\\]/, "");
                    paths.push(fname);
                }
            } catch (e) {}
        }

        for (var j = 0; j < doc.placedItems.length; j++) {
            var pi = doc.placedItems[j];

            var shouldRelink = false;
            if (dialogResult.targetAll) {
                shouldRelink = true;
            } else if (dialogResult.targetMissingOnly && isLinkBroken(pi)) {
                shouldRelink = true;
            }

            if (shouldRelink) {
                var fname = "";
                try {
                    if (pi.file && pi.file.name) {
                        fname = pi.file.name;
                    } else if (isLinkBroken(pi)) {
                        // XMPから収集したファイル名を利用
                        if (paths[j]) {
                            fname = paths[j];
                        } else if (pi.name) {
                            fname = pi.name;
                        }
                    }
                } catch (e) {
                    if (paths[j]) {
                        fname = paths[j];
                    }
                }
                relinkSingleItem(
                    pi,
                    fname,
                    dialogResult.targetFolder,
                    dialogResult.mode,
                    dialogResult.priorityExt,
                    dialogResult
                );
            }
        }
    }

    main();

    // 個別再リンク処理 / Function to relink a single missing file
    function relinkSingleItem(item, brokenName, targetFolder, mode, priorityExt, dialogResult) {
        if (!(item instanceof PlacedItem)) return;

        var shouldRelink = false;

        // 「リンク切れのみ」の場合 → リンク切れのみ対象
        if (dialogResult.targetMissingOnly && isLinkBroken(item)) {
            shouldRelink = true;
        }

        // 「リンク変更」の場合 → リンクが生きている場合のみ対象
        if (dialogResult.targetAll && !isLinkBroken(item)) {
            shouldRelink = true;
        }

        if (!shouldRelink) return;

        var originalName = "";
        try {
            if (item.file && item.file.name) {
                originalName = item.file.name.toLowerCase();
            } else if (brokenName) {
                originalName = brokenName.toLowerCase();
            }
        } catch (e) {
            if (brokenName) originalName = brokenName.toLowerCase();
        }
        if (!originalName) return;

        var originalBase = stripExt(originalName);

        var filesInFolder = targetFolder.getFiles();
        var candidates = [];
        for (var k = 0; k < filesInFolder.length; k++) {
            var candidate = filesInFolder[k];
            if (!(candidate instanceof File)) continue;

            var candidateName = candidate.name.toLowerCase();
            if (matchCandidate(candidateName, originalName, originalBase, mode, priorityExt)) {
                candidates.push(candidate);
            }
        }

        if (candidates.length === 1) {
            try {
                item.file = candidates[0];
            } catch (e) {
                alert("再リンク失敗：" + candidates[0].name + "\n" + e);
            }
        } else if (candidates.length > 1) {
            var chooseDlg = new Window("dialog", "候補を選択");
            chooseDlg.alignChildren = "fill";
            chooseDlg.add("statictext", undefined, "再リンクするファイルを選んでください:");

            var list = chooseDlg.add("listbox", [0,0,400,150]);
            for (var c = 0; c < candidates.length; c++) {
                list.add("item", candidates[c].name);
            }
            list.selection = 0;

            var btnGroup = chooseDlg.add("group");
            btnGroup.alignment = "right";
            btnGroup.add("button", undefined, "キャンセル", {name:"cancel"});
            var okBtn = btnGroup.add("button", undefined, "OK");

            if (chooseDlg.show() == 1 && list.selection) {
                try {
                    item.file = candidates[list.selection.index];
                } catch (e) {
                    alert("再リンク失敗：" + candidates[list.selection.index].name + "\n" + e);
                }
            }
        }
    }

    // リンク切れ判定 / Check if link is broken
    function isLinkBroken(item) {
        try {
            if (!item || !(item instanceof PlacedItem)) {
                return false; // PlacedItem以外は対象外 / Not target if not PlacedItem
            }
            if (!item.file) {
                return true; // fileプロパティが存在しない=リンク切れ / No file property means broken link
            }
            return !item.file.exists; // fileが存在しない / file does not exist
        } catch (e) {
            return true; // アクセス時例外=リンク切れ / Exception means broken link
        }
    }

    // 候補ファイルと比較 / Match candidate file
    function matchCandidate(candidateName, brokenName, brokenBase, mode, priorityExt) {
        var candidateBase = stripExt(candidateName); // キャッシュ

        if (mode === "exact") {
            // 拡張子を含めたファイル名が完全一致の場合のみ
            return candidateName === brokenName;
        } else if (mode === "nameOnly") {
            return candidateBase === brokenBase;
        } else if (mode === "priority") {
            // ベース名に優先拡張子をつけたファイル名と完全一致
            var expectedName = brokenBase + "." + priorityExt;
            return candidateName === expectedName;
        }
        return false;
    }

    // 拡張子を除去 / Remove extension
    function stripExt(filename) {
        return filename.replace(/\.[^\.]+$/, "");
    }

    // 拡張子を取得 / Get extension
    function getExt(filename) {
        var match = filename.match(/\.([^\.]+)$/);
        return match ? match[1].toLowerCase() : "";
    }

})();
