import { API_BASE, state } from './state.js';
import { escapeHtml, buildCardTransform, showToast } from './utils.js';
import { openExplorer } from './api.js';

let _openResourceModal = null;

export function injectOpenResourceModal(fn) {
    _openResourceModal = fn;
}

function seedFromString(str) {
    let h = 2166136261 >>> 0;
    for (let i = 0; i < str.length; i++) {
        h ^= str.charCodeAt(i);
        h = Math.imul(h, 16777619);
    }
    return h >>> 0;
}

function mulberry32(seed) {
    let a = seed >>> 0;
    return function () {
        a |= 0;
        a = (a + 0x6D2B79F5) | 0;
        let t = Math.imul(a ^ (a >>> 15), 1 | a);
        t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
        return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
}

export function updateBinderSelectors() {
    const btn = document.getElementById('btnOpenBinderDashboard');

    if (btn) {
        const name = (state.currentBinder && state.currentBinder.title)
            ? state.currentBinder.title
            : '---';
        btn.textContent = `現在: ${name} ▼`;
    }
}

function sortBinderList(binderes, sortKey) {
    const compare = (a, b) => {
        switch (sortKey) {
            case 'created_desc':
                return new Date(b.created_at || 0) - new Date(a.created_at || 0);
            case 'created_asc':
                return new Date(a.created_at || 0) - new Date(b.created_at || 0);
            case 'name_asc':
                return (a.title || a.id).localeCompare(b.title || b.id, 'ja');
            case 'name_desc':
                return (b.title || b.id).localeCompare(a.title || a.id, 'ja');
            case 'updated_desc':
            default:
                return new Date(b.updated_at || 0) - new Date(a.updated_at || 0);
        }
    };

    const pinned = binderes.filter(b => b.pinned).sort(compare);
    const others = binderes.filter(b => !b.pinned).sort(compare);
    return [...pinned, ...others];
}

export function renderBinderDashboard(query = '') {
    const grid = document.getElementById('binderDashboardGrid');
    if (!grid) return;

    const q = query.toLowerCase().trim();
    grid.innerHTML = '';

    const sortedList = sortBinderList(state.binderList, state.binderSort);

    sortedList.forEach(binder => {
        const isCurrent = state.currentBinder && state.currentBinder.id === binder.id;
        const isPinned = !!binder.pinned;
        const title = binder.title || binder.id;
        const desc = binder.description || '';
        const cardCount = binder.placements ? binder.placements.filter(p => p.type !== 'zone').length : 0;

        if (q && !title.toLowerCase().includes(q) && !desc.toLowerCase().includes(q)) {
            return;
        }

        const card = document.createElement('div');
        card.className = 'dashboard-binder-card' + (isCurrent ? ' is-current' : '') + (isPinned ? ' is-pinned' : '');
        card.dataset.binderId = binder.id;

        const currentTag = isCurrent ? `<span class="sp-tag">NOW</span>` : '';

        card.innerHTML = `
            <div class="card-led-indicator"></div>
            <div class="sp-vault-face">
                <div class="sp-rivet top-left"></div>
                <div class="sp-rivet bottom-left"></div>
                <div class="sp-rivet bottom-right"></div>
                <button type="button" class="dashboard-pin-btn" data-binder-id="${binder.id}" title="${isPinned ? 'ピン留めを解除' : 'ピン留めする'}"></button>
                <div class="sp-vault-plate">
                    <span class="sp-vault-plate-text">${escapeHtml(title)}</span>
                </div>
                <div class="dashboard-vault-body">
                    <div class="dashboard-vault-desc-viewport">
                        <div class="dashboard-vault-desc">${escapeHtml(desc || '説明なし')}</div>
                    </div>
                    <div class="dashboard-vault-meta">カード: ${cardCount}枚</div>
                </div>
            </div>
        `;

        grid.appendChild(card);
    });

    if (grid.children.length === 0) {
        const empty = document.createElement('div');
        empty.style.cssText = 'color:#aaa;text-align:center;padding:40px 0;grid-column:1/-1;';
        empty.textContent = q ? '該当するバインダーがありません' : 'バインダーがありません';
        grid.appendChild(empty);
        return;
    }

    requestAnimationFrame(() => {
        grid.querySelectorAll('.dashboard-vault-desc-viewport').forEach(viewport => {
            const content = viewport.querySelector('.dashboard-vault-desc');
            if (!content) return;
            content.classList.remove('is-scrolling');
            content.style.removeProperty('--ticker-start');
            content.style.removeProperty('--ticker-end');
            content.style.removeProperty('animation-duration');

            if (content.scrollHeight <= viewport.clientHeight) return;

            const tickerStart = viewport.clientHeight;
            const tickerEnd = -(content.scrollHeight + 4);
            content.style.setProperty('--ticker-start', `${tickerStart}px`);
            content.style.setProperty('--ticker-end', `${tickerEnd}px`);

            const totalDist = tickerStart - tickerEnd;
            const duration = Math.max(5, totalDist / 20 / 0.75);
            content.style.animationDuration = `${duration.toFixed(1)}s`;

            content.classList.add('is-scrolling');
        });
    });
}

export function renderBinder() {
    const binderContent = document.getElementById('binder-content');
    if (!binderContent || !state.currentBinder) return;

    const titleEl = document.getElementById('binder-display-title');
    const descEl = document.getElementById('binder-display-desc');
    if (titleEl) titleEl.textContent = state.currentBinder.title || 'Untitled Binder';
    if (descEl) descEl.textContent = state.currentBinder.description || '説明はありません';

    if (window.BinderManager && window.BinderManager.setViewport) {
        const savedScale = typeof state.currentBinder.scale === 'number' ? state.currentBinder.scale : 1;
        const savedTranslateX = typeof state.currentBinder.translateX === 'number' ? state.currentBinder.translateX : 0;
        const savedTranslateY = typeof state.currentBinder.translateY === 'number' ? state.currentBinder.translateY : 0;
        window.BinderManager.setViewport(savedScale, savedTranslateX, savedTranslateY);
    }

    binderContent.innerHTML = '';
    
    const resourceMap = new Map();
    state.currentCards.forEach(r => resourceMap.set(r.id, r));

    const placedResourceIds = new Set();
    const placements = state.currentBinder.placements || [];
    
    placements.filter(p => p.type === 'zone').forEach(p => {
        if (window.BinderManager && window.BinderManager.addZone) {
            window.BinderManager.addZone(p.id, p.x, p.y, p.width, p.height, p.status, p.status);
        }
    });

    placements.forEach((p, index) => {
        if (p.type === 'card' && resourceMap.has(p.id)) {
            const res = resourceMap.get(p.id);
            renderCard(res, p.x, p.y, index, p.width, p.height, p.pinned, p.rotation);
            placedResourceIds.add(p.id);
        } else if (p.type === 'binder') {
            const binderInfo = state.binderList.find(c => c.id === p.id);
            if (binderInfo) {
                renderBinderLinkCard(binderInfo, p.x, p.y, index, p.width, p.height, p.pinned, p.rotation);
            }
        }
    });

    checkHazardWarning();
}

export async function checkHazardWarning() {
    try {
        const response = await fetch(`${API_BASE}/cards?tags=Task`);
        if (!response.ok) return;
        const tasks = await response.json();
        
        let doingCount = 0;
        tasks.forEach(r => {
            if (r.status === 'Doing') {
                doingCount++;
            }
        });

        const hazardTape = document.getElementById('hazard-tape');
        const doingZone = document.querySelector('.zone-doing');
        
        if (doingCount > 5) {
            if (hazardTape) hazardTape.style.display = 'block';
            if (doingZone) doingZone.classList.add('zone-overload-pulse');
        } else {
            if (hazardTape) hazardTape.style.display = 'none';
            if (doingZone) doingZone.classList.remove('zone-overload-pulse');
        }
    } catch (e) {
        console.error("Failed to check hazard warning", e);
    }
}

export function renderCard(res, x, y, index, width, height, pinned, rotation) {
    const tagsHtml = (res.tags || []).map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join('');
    
    let isForeignTask = false;
    if (res.status_binder_id && res.status_binder_id !== state.currentBinder.id) {
        isForeignTask = true;
    }

    let customClass = '';
    let previewHtml = '';
    let isTextFile = false;

    const pathOrTitle = res.file_path || res.title || '';
    const isFolder = pathOrTitle.endsWith('\\') || pathOrTitle.endsWith('/') || !pathOrTitle.includes('.');
    const isTask = res.tags && res.tags.some(t => t.toLowerCase() === 'task');
    const isMemo = res.tags && res.tags.some(t => t.toLowerCase() === 'memo' || t === 'メモ');
    const isUrl = pathOrTitle.startsWith('http://') || pathOrTitle.startsWith('https://');
    
    const ENABLE_MEMO_STYLE = true;

    if (isUrl) {
        customClass = 'card-type-url';
        
        let domain = '';
        try {
            domain = new URL(pathOrTitle).hostname;
        } catch(e) {
            domain = pathOrTitle;
        }

        const spTagsHtml = (res.tags || []).map(tag => `<span class="sp-tag">${escapeHtml(tag)}</span>`).join('');

        const nixieFlickerDelay = -(Math.random() * 4).toFixed(2);

        previewHtml = `
            <div class="sp-card-inner">
                <div class="sp-card-face sp-card-front">
                    <div class="card-led-indicator"></div>
                    <div class="sp-rivet top-left"></div>
                    <div class="sp-rivet top-right"></div>
                    <div class="sp-rivet bottom-left"></div>
                    <div class="sp-rivet bottom-right"></div>

                    <div class="sp-domain-display">
                        <a href="${escapeHtml(pathOrTitle)}" target="_blank" rel="noopener" class="sp-ext-link" title="別タブで開く" onmousedown="event.stopPropagation()">
                            <svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
                                <line x1="7" y1="17" x2="17" y2="7"></line>
                                <polyline points="7 7 17 7 17 17"></polyline>
                            </svg>
                        </a>
                        <span class="sp-nixie-text sp-domain-text" style="animation-delay: ${nixieFlickerDelay}s">${escapeHtml(domain)}</span>
                        <span class="sp-nixie-text sp-name-text" style="animation-delay: ${nixieFlickerDelay}s">${escapeHtml(res.title || domain)}</span>
                    </div>

                    <div class="sp-card-footer">
                        <div class="sp-tags-viewport">
                            <div class="sp-tags-content">${spTagsHtml}</div>
                        </div>
                        <button class="sp-gimmick-btn" onclick="event.stopPropagation(); this.closest('.binder-card').classList.toggle('is-flipped');" title="裏面（QRコード）を表示" onmousedown="event.stopPropagation()">
                            <svg viewBox="0 0 24 24" width="10" height="10" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
                                <path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67" />
                            </svg>
                        </button>
                    </div>
                </div>

                <div class="sp-card-face sp-card-back">
                    <div class="sp-rivet top-left"></div>
                    <div class="sp-rivet top-right"></div>
                    <div class="sp-rivet bottom-left"></div>
                    <div class="sp-rivet bottom-right"></div>

                    <div class="sp-qr-zone">
                        <div class="sp-qr-frame qrcode-container" data-url="${escapeHtml(pathOrTitle)}"></div>
                    </div>

                    <div class="sp-card-footer" style="justify-content: flex-end;">
                        <button class="sp-gimmick-btn" onclick="event.stopPropagation(); this.closest('.binder-card').classList.toggle('is-flipped');" title="表面に戻る" onmousedown="event.stopPropagation()">
                            <svg viewBox="0 0 24 24" width="10" height="10" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
                                <path d="M2.5 22v-6h6M2.66 8.43a10 10 0 1 1 .57 8.38l-5.67 5.67" />
                            </svg>
                        </button>
                    </div>
                </div>
            </div>
        `;
    } else if (isTask) {
        customClass = 'card-type-task';
        const taskStatus = (res.status && res.status !== 'none') ? res.status.toUpperCase() : '';
        const taskStatusClass = (res.status && res.status !== 'none') ? `status-${res.status.toLowerCase()}` : 'status-none';
        const taskStampStyle = isForeignTask ? ' style="filter:grayscale(100%);opacity:0.7;"' : '';
        const lockIconHtml = isForeignTask ? '<span title="他バインダーで管理"><svg class="sp-icon" viewBox="0 0 24 24" style="width:12px;height:12px;margin-right:2px;"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span> ' : '';
        const spTagsHtml = (res.tags || []).map(tag => `<span class="sp-tag">${escapeHtml(tag)}</span>`).join('');
        previewHtml = `
            <div class="sp-task-inner">
                <div class="sp-task-face">
                    <div class="card-led-indicator"></div>
                    <div class="sp-task-bolt top-left"></div>
                    <div class="sp-task-bolt top-right"></div>
                    <div class="sp-task-bolt bottom-left"></div>
                    <div class="sp-task-bolt bottom-right"></div>
                    <div class="sp-task-display">
                        <p class="sp-task-title" title="${escapeHtml(res.title)}">${escapeHtml(res.title)}</p>
                        <div class="sp-task-stamp ${taskStatusClass}"${taskStampStyle}>${lockIconHtml}${escapeHtml(taskStatus)}</div>
                    </div>
                    <div class="sp-card-footer">
                        <div class="sp-tags-viewport">
                            <div class="sp-tags-content">${spTagsHtml}</div>
                        </div>
                        <button class="sp-gimmick-btn" title="タスクを編集" onmousedown="event.stopPropagation()">
                            <svg viewBox="0 0 24 24" width="10" height="10" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
                                <circle cx="12" cy="12" r="3"/>
                                <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
                            </svg>
                        </button>
                    </div>
                </div>
            </div>
        `;
    } else if (isMemo && ENABLE_MEMO_STYLE) {
        customClass = 'card-type-memo';

        const rng = mulberry32(seedFromString(res.id || 'memo'));
        const svgSeed = Math.floor(rng() * 100000);

        const fid = 'memo-' + String(res.id || '').replace(/[^a-zA-Z0-9_-]/g, '');

        const stain1X = (40 + rng() * 50).toFixed(1);
        const stain1Y = (10 + rng() * 30).toFixed(1);
        const stain2X = (5 + rng() * 30).toFixed(1);
        const stain2Y = (60 + rng() * 30).toFixed(1);
        const baseAngle = (140 + rng() * 40).toFixed(1);
        const paperBgImage =
            `radial-gradient(circle at ${stain1X}% ${stain1Y}%, rgba(100,60,20,0.4) 0%, transparent 40%),`
            + `radial-gradient(circle at ${stain2X}% ${stain2Y}%, rgba(120,80,40,0.35) 0%, transparent 50%),`
            + `linear-gradient(${baseAngle}deg, #eaddc3 0%, #cebe9f 100%)`;

        const burnDuration = (3.4 + rng() * 2.2).toFixed(2); // 3.4〜5.6秒のゆっくりした呼吸
        const burnDelay = -(rng() * burnDuration).toFixed(2);

        const igniteAngle = rng() * 360; // 出火点の角度（0〜360度）
        const coreHalf = 20 + rng() * 20; // コア（完全表示）部分の半角：20〜40度
        const fadeWidth = 25 + rng() * 20; // 両端のフェード幅：25〜45度
        const localCoreStart = 180 - coreHalf;
        const localCoreEnd = 180 + coreHalf;
        const localFadeStart = Math.max(0, localCoreStart - fadeWidth);
        const localFadeEnd = Math.min(360, localCoreEnd + fadeWidth);
        const gradientFrom = (((igniteAngle - 180) % 360) + 360) % 360;
        const arcMask = `conic-gradient(from ${gradientFrom.toFixed(1)}deg at 50% 50%,`
            + ` transparent 0deg, transparent ${localFadeStart.toFixed(1)}deg,`
            + ` #000 ${localCoreStart.toFixed(1)}deg, #000 ${localCoreEnd.toFixed(1)}deg,`
            + ` transparent ${localFadeEnd.toFixed(1)}deg, transparent 360deg)`;
        const arcMaskStyle = `-webkit-mask-image: ${arcMask}; mask-image: ${arcMask};`;

        const paperTagsHtml = (res.tags || []).map(tag => `<span class="paper-tag">${escapeHtml(tag)}</span>`).join('');

        let bodyHtml = escapeHtml(res.content || '');
        bodyHtml = bodyHtml.replace(
            /(https?:\/\/[^\s]+)/g,
            '<a href="$1" target="_blank" rel="noopener" class="paper-link" onmousedown="event.stopPropagation()">$1</a>'
        );
        const bodyContent = (res.content && res.content.trim() !== '')
            ? bodyHtml
            : '<span class="paper-empty">（本文なし）</span>';

        previewHtml = `
            <div class="sp-memo-inner">
                <div class="sp-memo-face paper-memo-face">

                    <!-- カード固有のSVGフィルタ（破れ・シワ・インク染み込み） -->
                    <svg class="paper-svg-defs" width="0" height="0" aria-hidden="true" focusable="false">
                        <defs>
                            <filter id="torn-${fid}">
                                <feTurbulence type="fractalNoise" baseFrequency="0.06" numOctaves="5" result="fine-noise" seed="${svgSeed}" />
                                <feDisplacementMap in="SourceGraphic" in2="fine-noise" scale="25" xChannelSelector="R" yChannelSelector="G" />
                            </filter>
                            <filter id="wrinkle-${fid}" x="0%" y="0%" width="100%" height="100%">
                                <feTurbulence type="fractalNoise" baseFrequency="0.025" numOctaves="6" result="noise" seed="${svgSeed + 50}" />
                                <feDiffuseLighting in="noise" lighting-color="#fff" surfaceScale="12" result="light">
                                    <feDistantLight azimuth="135" elevation="45" />
                                </feDiffuseLighting>
                                <feBlend mode="multiply" in="SourceGraphic" in2="light" result="blend" />
                                <feComposite operator="in" in="blend" in2="SourceGraphic" />
                            </filter>
                            <filter id="ink-bleed-${fid}">
                                <feGaussianBlur in="SourceGraphic" stdDeviation="0.15" result="blurred" />
                                <feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="3" result="noise" seed="${svgSeed + 200}" />
                                <feDisplacementMap in="blurred" in2="noise" scale="0.8" xChannelSelector="R" yChannelSelector="G" />
                            </filter>
                        </defs>
                    </svg>

                    <!-- 紙の背景＋シワ（破れフィルタは背景層のみに適用する） -->
                    <div class="paper-bg" style="filter: url(#torn-${fid}); background-image: ${paperBgImage};">
                        <div class="paper-wrinkle" style="filter: url(#wrinkle-${fid});"></div>
                    </div>

                    <!-- インク層（乗算で紙に染み込む）：本文は内部スクロール、フッターは下端固定 -->
                    <div class="paper-ink-layer" style="filter: url(#ink-bleed-${fid});">
                        <div class="paper-text-content">
                            <p><strong>${escapeHtml(res.title || '無題のメモ')}</strong></p>
                            <div class="paper-body">${bodyContent}</div>
                        </div>
                        <div class="paper-footer">
                            <div class="paper-tags">${paperTagsHtml}</div>
                            <button class="paper-btn sp-gimmick-btn" title="メモを編集" onmousedown="event.stopPropagation()">
                                <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
                                    <path d="M12 20h9"/>
                                    <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
                                </svg>
                            </button>
                        </div>
                    </div>

                    <!-- 縁がジワッと焦げる演出（ホバー時のみ表示）。出火点を中心とした範囲だけが焦げ、
                         白熱・オレンジはゆっくり明滅を続け、炭化はフェードインしたら止まって固定される。
                         破れフィルタで縁の形に合わせて歪ませる（紙の破れ縁とギザギザがぴったり重なる） -->
                    <div class="ember-burn-layer">
                        <div class="ember-edge-glow" style="filter: url(#torn-${fid}) blur(1.5px); animation-duration:${burnDuration}s; animation-delay:${burnDelay}s; ${arcMaskStyle}">
                            <div class="ember-ring ember-ring-orange"></div>
                            <div class="ember-ring ember-ring-white"></div>
                        </div>
                        <div class="ember-edge-char" style="filter: url(#torn-${fid}) blur(1.5px); ${arcMaskStyle}">
                            <div class="ember-ring ember-ring-char"></div>
                        </div>
                    </div>

                    <!-- 金属ビス（乗算・破れの影響を受けない物理パーツ） -->
                    <div class="metal-hardware">
                        <div class="sp-rivet top-left"></div>
                        <div class="sp-rivet top-right"></div>
                        <div class="sp-rivet bottom-left"></div>
                        <div class="sp-rivet bottom-right"></div>
                    </div>

                </div>
            </div>
        `;
    } else if (isFolder && pathOrTitle) {
        customClass = 'card-type-folder';
        const spTagsHtml = (res.tags || []).map(tag => `<span class="sp-tag">${escapeHtml(tag)}</span>`).join('');
        const folderDisplayName = escapeHtml(res.title || pathOrTitle);
        const folderFullPath = escapeHtml(res.file_path || pathOrTitle);
        previewHtml = `
            <div class="sp-folder-inner">
                <div class="sp-folder-face">
                    <div class="card-led-indicator"></div>
                    <div class="sp-rivet top-left"></div>
                    <div class="sp-rivet top-right"></div>
                    <div class="sp-rivet bottom-left"></div>
                    <div class="sp-rivet bottom-right"></div>
                    <div class="sp-folder-nameplate">
                        <div class="sp-folder-plate">
                            <p class="sp-folder-path" title="${folderFullPath}">${folderDisplayName}</p>
                        </div>
                    </div>
                    <div class="sp-card-footer">
                        <div class="sp-tags-viewport">
                            <div class="sp-tags-content">${spTagsHtml}</div>
                        </div>
                        <button class="sp-gimmick-btn" data-action="open-folder" title="エクスプローラで開く" onmousedown="event.stopPropagation()">
                            <svg viewBox="0 0 24 24" width="10" height="10" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
                                <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
                                <polyline points="16 17 21 12 16 7"/>
                                <line x1="21" y1="12" x2="9" y2="12"/>
                            </svg>
                        </button>
                    </div>
                </div>
            </div>
        `;
    } else {
        const extMatch = pathOrTitle.match(/\.([^.]+)$/);
        const ext = extMatch ? extMatch[1].toLowerCase() : '';
        const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'];
        const textExts = ['txt', 'md', 'csv', 'json', 'ini', 'log', 'js', 'php', 'css', 'html', 'xml'];

        if (imageExts.includes(ext)) {
            customClass = 'card-type-image';
            const spTagsHtml = (res.tags || []).map(tag => `<span class="sp-tag">${escapeHtml(tag)}</span>`).join('');

            let previewUrl = `/api/file/preview?path=${encodeURIComponent(res.file_path || '')}`;
            if (width && height) {
                const targetW = Math.ceil(width * 1.4);
                const targetH = Math.ceil(height * 1.4);
                previewUrl += `&w=${targetW}&h=${targetH}`;
            }
            const imgBgStyle = res.file_path
                ? `background-image: url('${previewUrl}'); animation-delay: ${-(Math.random() * 30).toFixed(2)}s; animation-duration: ${(40 + Math.random() * 30).toFixed(2)}s; animation-direction: ${Math.random() > 0.5 ? 'alternate' : 'alternate-reverse'};`
                : '';
            previewHtml = `
                <div class="card-led-indicator"></div>
                <div class="sp-image-frame">
                    <div class="sp-image-bg" data-file-path="${escapeHtml(res.file_path || '')}" style="${imgBgStyle}"></div>
                    <div class="sp-image-vignette"></div>
                    <div class="sp-rivet top-left"></div>
                    <div class="sp-rivet top-right"></div>
                    <div class="sp-rivet bottom-left"></div>
                    <div class="sp-rivet bottom-right"></div>
                    <div class="sp-image-footer">
                        <div class="sp-tags-viewport">
                            <div class="sp-tags-content">${spTagsHtml}</div>
                        </div>
                        <button class="sp-gimmick-btn" data-action="fullscreen" title="フルスクリーンで表示" onmousedown="event.stopPropagation()">
                            <svg viewBox="0 0 24 24" width="10" height="10" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
                                <circle cx="11" cy="11" r="8"/>
                                <line x1="21" y1="21" x2="16.65" y2="16.65"/>
                            </svg>
                        </button>
                    </div>
                </div>
            `;
        } else if (textExts.includes(ext)) {
            customClass = 'card-type-text';
            isTextFile = true;
            const spTagsHtml = (res.tags || []).map(tag => `<span class="sp-tag">${escapeHtml(tag)}</span>`).join('');
            previewHtml = `
                <div class="sp-text-inner">
                    <div class="sp-text-face">
                        <div class="card-led-indicator"></div>
                        <div class="sp-rivet top-left"></div>
                        <div class="sp-rivet top-right"></div>
                        <div class="sp-rivet bottom-left"></div>
                        <div class="sp-rivet bottom-right"></div>
                        <div class="sp-text-window">
                            <div class="sp-text-tape-content" id="preview-${res.id}">Loading...</div>
                        </div>
                        <div class="sp-card-footer">
                            <div class="sp-tags-viewport">
                                <div class="sp-tags-content">${spTagsHtml}</div>
                            </div>
                            <button class="sp-gimmick-btn" data-action="open-editor" title="外部エディタで開く" onmousedown="event.stopPropagation()">
                                <svg viewBox="0 0 24 24" width="10" height="10" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
                                    <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
                                    <polyline points="15 3 21 3 21 9"/>
                                    <line x1="10" y1="14" x2="21" y2="3"/>
                                </svg>
                            </button>
                        </div>
                    </div>
                </div>
            `;
        } else {
            customClass = 'card-type-other';
            const spTagsHtml = (res.tags || []).map(tag => `<span class="sp-tag">${escapeHtml(tag)}</span>`).join('');
            const extDisplay = ext ? `.${ext.toUpperCase()}` : '';
            previewHtml = `
                <div class="sp-crate-inner">
                    <div class="sp-crate-face">
                        <div class="card-led-indicator"></div>
                        <div class="sp-crate-band sp-crate-band-upper"></div>
                        <div class="sp-crate-band sp-crate-band-lower"></div>
                        <div class="sp-rivet top-left"></div>
                        <div class="sp-rivet top-right"></div>
                        <div class="sp-rivet bottom-left"></div>
                        <div class="sp-rivet bottom-right"></div>
                        <div class="sp-crate-content">
                            <p class="sp-crate-label">UNKNOWN</p>
                            <p class="sp-crate-label">FORMAT</p>
                            <span class="sp-crate-ext">${escapeHtml(extDisplay)}</span>
                        </div>
                        <div class="sp-card-footer" style="position:relative;z-index:5;">
                            <div class="sp-tags-viewport">
                                <div class="sp-tags-content">${spTagsHtml}</div>
                            </div>
                            <button class="sp-gimmick-btn" data-action="open-parent-folder" title="格納フォルダをエクスプローラで開く" onmousedown="event.stopPropagation()">
                                <svg viewBox="0 0 24 24" width="10" height="10" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
                                    <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
                                    <polyline points="16 17 21 12 16 7"/>
                                    <line x1="21" y1="12" x2="9" y2="12"/>
                                </svg>
                            </button>
                        </div>
                    </div>
                </div>
            `;
        }
    }

    const SP_CARD_TYPES = new Set(['card-type-url', 'card-type-memo', 'card-type-task', 'card-type-folder', 'card-type-image', 'card-type-text', 'card-type-other']);
    const html = (customClass && SP_CARD_TYPES.has(customClass))
        ? previewHtml
        : `
            <div class="card-indicator"></div>
            <div class="card-header">
                <div class="card-title" title="${escapeHtml(res.title)}">${escapeHtml(res.title)}</div>
            </div>
            ${previewHtml}
            <div class="card-tags"><div class="tags-inner">${tagsHtml}</div></div>
        `;
    
    const card = window.BinderManager.addCard(html, x, y, { skipCollision: true, customClass: customClass, width: width, height: height, rotation: rotation });
    card.dataset.id = res.id;

    card.dataset.pinned = pinned ? 'true' : 'false';
    if (pinned) card.classList.add('is-pinned');

    if (res.locations && res.locations.length > 0) {
        const otherLocations = res.locations.filter(loc => loc.id !== state.currentBinder.id);
        if (otherLocations.length > 0) {
            card.classList.add('is-linked');
            const locNames = otherLocations.map(l => l.title || l.id).join(', ');
            card.title = `他の配置先バインダー:\n${locNames}`;

            const twineTag = document.createElement('div');
            twineTag.className = 'sp-binder-link-twine-tag';
            twineTag.innerHTML = '<div class="sp-binder-link-twine"></div><div class="sp-binder-link-tag-label"></div>';
            const spCardFront = card.querySelector('.sp-card-front');
            if (spCardFront) {
                spCardFront.appendChild(twineTag);
            } else {
                card.appendChild(twineTag);
            }
        }
    }
    
    if (customClass === 'card-type-memo') {
        const btn = card.querySelector('.sp-gimmick-btn');
        if (btn && _openResourceModal) {
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                _openResourceModal(res);
            });
        }
    }
    
    if (customClass === 'card-type-image') {
        const btn = card.querySelector('.sp-gimmick-btn[data-action="fullscreen"]');
        if (btn && res.file_path) {
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                const modal = document.getElementById('imagePreviewModal');
                const img = document.getElementById('imagePreviewTarget');
                if (modal && img) {
                    img.src = `/api/file/preview?path=${encodeURIComponent(res.file_path)}&original=1&_cb=${Date.now()}`;
                    modal.classList.add('active');
                }
            });
        }
    }

    if (customClass === 'card-type-folder') {
        const btn = card.querySelector('.sp-gimmick-btn[data-action="open-folder"]');
        if (btn && res.file_path) {
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                openExplorer(res.file_path)
                    .then(() => showToast('エクスプローラを開きました（背面に隠れている場合はタスクバーをご確認ください）', 'success'))
                    .catch(err => showToast(err.message, 'error'));
            });
        }
    }

    if (customClass === 'card-type-other') {
        const btn = card.querySelector('.sp-gimmick-btn[data-action="open-parent-folder"]');
        if (btn && res.file_path) {
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                openExplorer(res.file_path)
                    .then(() => showToast('エクスプローラを開きました（背面に隠れている場合はタスクバーをご確認ください）', 'success'))
                    .catch(err => showToast(err.message, 'error'));
            });
        }
    }

    if (customClass === 'card-type-task') {
        const btn = card.querySelector('.sp-gimmick-btn');
        if (btn && _openResourceModal) {
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                _openResourceModal(res);
            });
        }
    }

    card.addEventListener('dblclick', (e) => {
        if (!card.classList.contains('binder-link-card')) {
            if (customClass === 'card-type-image' && res.file_path) {
                const modal = document.getElementById('imagePreviewModal');
                const img = document.getElementById('imagePreviewTarget');
                if (modal && img) {
                    img.src = `/api/file/preview?path=${encodeURIComponent(res.file_path)}&original=1&_cb=${Date.now()}`;
                    modal.classList.add('active');
                }
            } else if (customClass === 'card-type-memo') {
                if (_openResourceModal) _openResourceModal(res);
            } else {
                if (_openResourceModal) _openResourceModal(res);
            }
        }
    });

    card.classList.add('is-booting');
    card.style.opacity = '0';
    card.style.transform = buildCardTransform(x, y - 40, card.dataset.rotation, 0.95);
    card.style.transition = 'transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s ease';

    setTimeout(() => {
        card.style.opacity = '1';
        card.style.transform = buildCardTransform(card.dataset.x, card.dataset.y, card.dataset.rotation, 1);

        setTimeout(() => {
            card.classList.remove('is-booting');
            card.classList.add('is-ignited');

            setTimeout(() => {
                card.classList.remove('is-ignited');
                card.style.transition = '';
            }, 600);
        }, 300);

        const tagsContainer = card.querySelector('.card-tags');
        const tagsInner = card.querySelector('.tags-inner');
        if (tagsContainer && tagsInner) {
            const cs = window.getComputedStyle(tagsContainer);
            const pl = parseFloat(cs.paddingLeft) || 0;
            const pr = parseFloat(cs.paddingRight) || 0;
            const visibleW = tagsContainer.clientWidth - pl - pr;
            if (tagsInner.scrollWidth > visibleW) {
                const overflow = tagsInner.scrollWidth - visibleW + 10;
                tagsInner.style.setProperty('--scroll-distance', `-${overflow}px`);
                tagsInner.classList.add('is-scrolling');
            }
        }
        
        const spViewport = card.querySelector('.sp-tags-viewport');
        const spContent = card.querySelector('.sp-tags-content');
        if (spViewport && spContent) {
            if (spContent.scrollWidth > spViewport.clientWidth) {
                const overflowDistance = spContent.scrollWidth - spViewport.clientWidth;
                spContent.style.setProperty('--scroll-distance', `-${overflowDistance + 12}px`);
                spContent.classList.add('is-scrolling');
            }
        }
        
        if (customClass === 'card-type-url' && typeof QRCode !== 'undefined') {
            const qrContainer = card.querySelector('.qrcode-container');
            if (qrContainer && !qrContainer.hasChildNodes()) {
                const url = qrContainer.dataset.url;
                new QRCode(qrContainer, {
                    text: url,
                    width: 100,
                    height: 100,
                    colorDark : "#000000",
                    colorLight : "#ffffff",
                    correctLevel : QRCode.CorrectLevel.L
                });
            }
        }
    }, index * 80);

    if (isTextFile && res.file_path) {
        fetch(`/api/file/text-preview?path=${encodeURIComponent(res.file_path)}&_cb=${Date.now()}`)
            .then(r => r.json())
            .then(data => {
                const previewEl = card.querySelector(`#preview-${res.id}`);
                if (previewEl) {
                    if (data.preview) {
                        previewEl.textContent = data.preview;
                        setTimeout(() => {
                            const viewport = previewEl.parentElement;
                            const viewportHeight = viewport ? viewport.clientHeight : previewEl.clientHeight;
                            const contentHeight = previewEl.scrollHeight;
                            const startY = viewportHeight;
                            const endY = -contentHeight;

                            const speed = 20;
                            const moveDist = startY - endY; // 窓の外から窓の外までの実移動距離
                            const scrollDur = Math.max(moveDist / speed, 5); // 最低5秒はかける
                            const pauseDur = 1.5; // スクロール完了後に止まる時間（秒）
                            const totalDur = scrollDur + pauseDur;
                            const scrollOffset = scrollDur / totalDur;

                            previewEl.style.animation = 'none';
                            const scrollAnim = previewEl.animate(
                                [
                                    { transform: `translateY(${startY}px)`, offset: 0 },
                                    { transform: `translateY(${endY}px)`, offset: scrollOffset },
                                    { transform: `translateY(${endY}px)`, offset: 1 }
                                ],
                                {
                                    duration: totalDur * 1000,
                                    iterations: Infinity,
                                    easing: 'linear',
                                    delay: -(Math.random() * totalDur * 1000)
                                }
                            );

                            card.addEventListener('mouseenter', () => scrollAnim.pause());
                            card.addEventListener('mouseleave', () => scrollAnim.play());
                        }, 0);
                    } else if (data.error) {
                        previewEl.textContent = 'Preview not available';
                    }
                }
            })
            .catch(e => {
                console.error('Failed to load text preview for:', res.file_path, e);
                try {
                    const previewEl = document.getElementById(`preview-${res.id}`);
                    if (previewEl) previewEl.textContent = 'Preview error';
                } catch (domErr) {}
            });
    }
}

export function renderBinderLinkCard(binderInfo, x, y, index, width, height, pinned, rotation) {
    const binderTitle = escapeHtml(binderInfo.title || binderInfo.id);
    const html = `
        <div class="sp-vault-face">
            <div class="card-led-indicator"></div>
            <div class="sp-rivet top-left"></div>
            <div class="sp-rivet top-right"></div>
            <div class="sp-rivet bottom-left"></div>
            <div class="sp-rivet bottom-right"></div>
            <div class="sp-vault-keyhole-area">
                <svg viewBox="0 0 28 38" width="28" height="38" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <rect x="0" y="0" width="28" height="38" rx="4" fill="#252e3a"/>
                    <rect x="1" y="1" width="26" height="36" rx="3" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
                    <rect x="0" y="0" width="28" height="38" rx="4" fill="none" stroke="#7a8ea6" stroke-width="1.5"/>
                    <circle cx="14" cy="16" r="6" fill="#080c14"/>
                    <rect x="11" y="20" width="6" height="13" rx="2.5" fill="#080c14"/>
                </svg>
            </div>
            <div class="sp-vault-plate">
                <span class="sp-vault-plate-text">${binderTitle}</span>
            </div>
            <div class="sp-card-footer" style="position:relative;z-index:5;">
                <div class="sp-tags-viewport">
                    <div class="sp-tags-content">
                        <span class="sp-tag">BINDER LINK</span>
                    </div>
                </div>
                <button class="sp-gimmick-btn" title="このバインダーへ移動" onmousedown="event.stopPropagation()">
                    <svg viewBox="0 0 24 24" width="10" height="10" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
                        <circle cx="7.5" cy="15.5" r="5.5"/>
                        <path d="M21 2l-9.6 9.6M15.5 7.5l2 -2M12.5 10.5l2 -2"/>
                    </svg>
                </button>
            </div>
        </div>
    `;
    const card = window.BinderManager.addCard(html, x, y, {
        isPortal: true,
        isBinderLink: true,
        targetBinderId: binderInfo.id,
        skipCollision: true,
        customClass: 'card-type-binder-link',
        width: width,
        height: height,
        rotation: rotation
    });
    card.dataset.id = binderInfo.id;
    card.dataset.type = 'binder';

    const btn = card.querySelector('.sp-gimmick-btn');
    if (btn) {
        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            if (window.App && window.App.loadBinder) {
                window.App.loadBinder(binderInfo.id);
            }
        });
    }

    card.dataset.pinned = pinned ? 'true' : 'false';
    if (pinned) card.classList.add('is-pinned');

    card.classList.add('is-booting');
    card.style.opacity = '0';
    card.style.transform = buildCardTransform(x, y - 40, card.dataset.rotation, 0.95);
    card.style.transition = 'transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s ease';

    setTimeout(() => {
        card.style.opacity = '1';
        card.style.transform = buildCardTransform(card.dataset.x, card.dataset.y, card.dataset.rotation, 1);

        setTimeout(() => {
            card.classList.remove('is-booting');
            card.classList.add('is-ignited');

            setTimeout(() => {
                card.classList.remove('is-ignited');
                card.style.transition = '';
            }, 600);
        }, 300);
    }, index * 80);
}
