// ==UserScript==
// @name         X/Twitter 複数画像を縦並びに変換
// @namespace    https://example.local/x-vertical-images
// @version      6.0
// @description  タイムライン上のツイート内で複数画像がある場合、常に縦1列で1枚ずつ見やすく表示する(DLボタン位置修正版)
// @author       you
// @match        https://twitter.com/*
// @match        https://x.com/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  // ── CSS ──────────────────────────────────────────────────────────────────
  const style = document.createElement('style');
  style.textContent = `
    /* ツイート記事自体の overflow:hidden を解除する。
       これをやらないと、後から追加した縦並び画像が記事の元の高さで
       クリップされ、下のツイートに隠れてしまう。 */
    [data-testid="primaryColumn"] article[data-testid="tweet"] {
      overflow: visible !important;
    }

    /* 元のグリッド表示は隠す */
    [data-vimg-branch="1"] { display: none !important; }

    /* 差し替え後の縦並びコンテナ: 常に1カラムで縦に積む */
    .vimg-stack {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 6px;
      width: 100%;
      margin-top: 4px;
    }

    /* 画像1枚ごとの箱。position:relativeにして、DLボタンの位置基準を
       コンテナ全体ではなく「実際の画像の右上」にする */
    .vimg-item {
      position: relative;
      display: inline-block;
      max-width: 100%;
    }

    .vimg-item > img {
      width: auto;
      height: auto;
      max-width: 100%;
      max-height: 45vh; /* 縦長画像が画面をはみ出すほど巨大にならないよう上限を設定 */
      display: block;
      border-radius: 12px;
      object-fit: contain;
      cursor: zoom-in;
    }
  `;
  (document.head || document.documentElement).appendChild(style);

  // ── ヘルパー ─────────────────────────────────────────────────────────────

  // name=small 等を name=large に置換して高解像度を取得
  function toHiRes(src) {
    return src.replace(/([?&]name=)[^&]+/, '$1large');
  }

  function countPhotos(el) {
    return el.querySelectorAll('[data-testid="tweetPhoto"]').length;
  }

  // ツイート本文(tweetText)や引用ツイートなどを巻き込まない、
  // 画像だけを含む最小の共通祖先(元のグリッド全体)を探す
  function findMediaBranch(tweet, photos) {
    if (photos.length === 0) return null;

    const SAFETY_SELECTORS = [
      '[data-testid="tweetText"]',
      '[data-testid="User-Name"]',
      '[data-testid="reply"]',
      '[data-testid="like"]',
      '[data-testid="retweet"]',
      '[data-testid="videoPlayer"]',
    ].join(',');

    const branchIsSafe = (el) =>
      photos.every((p) => el.contains(p)) &&
      el.querySelectorAll(SAFETY_SELECTORS).length === 0;

    let el = photos[0].parentElement;
    let highestSafe = null;
    while (el && el !== tweet) {
      if (branchIsSafe(el)) {
        highestSafe = el;
      } else if (highestSafe) {
        break;
      }
      el = el.parentElement;
    }
    return highestSafe;
  }

  // 非表示にした元グリッドの中に、他スクリプトが追加した一括DLボタン等が
  // 巻き込まれて消えていないかチェックし、見つかったら縦並びコンテナ側に
  // 移設する(元のボタンが後から非同期で追加されるケースにも対応するため、
  // 毎tickごとに確認する)
  function rescueOrphanedButtons() {
    document
      .querySelectorAll('[data-vimg-branch="1"] button:not([data-vimg-moved])')
      .forEach((btn) => {
        btn.setAttribute('data-vimg-moved', '1');
        const branch = btn.closest('[data-vimg-branch="1"]');
        const stack = branch && branch.nextElementSibling;
        if (stack && stack.classList.contains('vimg-stack')) {
          // コンテナ全体ではなく「1枚目の画像の箱」を基準にすることで、
          // 画像が縮小されていてもボタンが画像の右上にちゃんと乗るようにする
          const firstItem = stack.querySelector('.vimg-item');
          if (firstItem) firstItem.appendChild(btn);
        }
      });
  }

  // ── メイン処理 ────────────────────────────────────────────────────────────

  function processTweets() {
    const unprocessed = document.querySelectorAll('[data-testid="tweetPhoto"]:not([data-vimg])');
    if (unprocessed.length === 0) return;

    const tweetMap = new Map();
    unprocessed.forEach((photo) => {
      const tweet = photo.closest('article[data-testid="tweet"]');
      if (!tweet) return;
      // 画像がまだ読み込まれていない場合は処理済みにせず、次回に回す
      const img = photo.querySelector('img');
      if (!img || !img.src) return;
      photo.setAttribute('data-vimg', '1');
      if (!tweetMap.has(tweet)) tweetMap.set(tweet, []);
      tweetMap.get(tweet).push(photo);
    });

    tweetMap.forEach((photos, tweet) => {
      if (photos.length < 2) return; // 画像1枚以下は対象外

      const mediaBranch = findMediaBranch(tweet, photos);
      if (!mediaBranch || mediaBranch.hasAttribute('data-vimg-branch') || mediaBranch === tweet) return;

      const items = photos
        .map((photo) => {
          const img = photo.querySelector('img');
          return img ? toHiRes(img.src) : null;
        })
        .filter(Boolean);

      if (items.length < 2) return;

      mediaBranch.setAttribute('data-vimg-branch', '1');

      const stack = document.createElement('div');
      stack.className = 'vimg-stack';
      const seen = new Set();
      items.forEach((src) => {
        if (seen.has(src)) return;
        seen.add(src);
        const item = document.createElement('div');
        item.className = 'vimg-item';
        const img = document.createElement('img');
        img.src = src;
        img.loading = 'lazy';
        img.onclick = (e) => {
          e.stopPropagation();
          window.open(src, '_blank');
        };
        item.appendChild(img);
        stack.appendChild(item);
      });

      mediaBranch.insertAdjacentElement('afterend', stack);
    });
  }

  // ── 監視・初期化 ──────────────────────────────────────────────────────────

  let debounce = null;
  const tick = () => {
    processTweets();
    rescueOrphanedButtons();
  };
  const observer = new MutationObserver(() => {
    clearTimeout(debounce);
    debounce = setTimeout(tick, 150);
  });

  function startObserving() {
    observer.observe(document.body, { childList: true, subtree: true });
    tick();
    // Reactが少しずつツイートをマウントする最初の数秒は、デバウンス付き
    // observerだけだと取りこぼすことがあるので、短時間だけ高頻度で再実行する
    let burstsLeft = 25;
    const burst = setInterval(() => {
      tick();
      if (--burstsLeft <= 0) clearInterval(burst);
    }, 200);
  }

  if (document.body) {
    startObserving();
  } else {
    const bootstrap = new MutationObserver(() => {
      if (document.body) {
        bootstrap.disconnect();
        startObserving();
      }
    });
    bootstrap.observe(document.documentElement, { childList: true });
  }
})();