// ==UserScript==
// @name         X/Twitter 原寸画像一括DLボタン
// @namespace    https://greasyfork.org/users/local
// @version      1.0.0
// @description  タイムライン上の各ツイートに、原寸画像(最大4枚)を個別ファイルとして順次ダウンロードするボタンを追加します
// @author       you
// @match        https://twitter.com/*
// @match        https://x.com/*
// @grant        GM_download
// @connect      pbs.twimg.com
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const BUTTON_MARK = 'origDlBtnInjected';
  const ICON_SVG = `<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 16l-6-6h4V4h4v6h4l-6 6zM4 18h16v2H4z"/></svg>`;
  const COLOR_DEFAULT = '#87CEEB'; // 水色
  const COLOR_HOVER = '#3EA8DC';

  // ツイートURLから userId / tweetId を取得
  function getTweetIds(article) {
    const link = article.querySelector('a[href*="/status/"]');
    if (!link) return null;
    const m = link.getAttribute('href').match(/^\/([^/]+)\/status\/(\d+)/);
    if (!m) return null;
    return { userId: m[1], tweetId: m[2] };
  }

  // ツイート内の画像要素から原寸URL(最大4枚・重複除去)を取得
  function getOrigImageUrls(article) {
    const imgs = article.querySelectorAll('img[src*="pbs.twimg.com/media"]');
    const seen = new Set();
    const urls = [];
    for (const img of imgs) {
      const base = img.src.split('?')[0];
      if (seen.has(base)) continue;
      seen.add(base);
      // format を維持しつつ name=orig に変換
      const u = new URL(img.src);
      const format = u.searchParams.get('format') || 'jpg';
      urls.push(`${base}?format=${format}&name=orig`);
      if (urls.length >= 4) break;
    }
    return urls;
  }

  function extFromFormat(url) {
    const m = url.match(/format=([a-zA-Z0-9]+)/);
    return m ? m[1] : 'jpg';
  }

  function downloadImages(article, ids) {
    const urls = getOrigImageUrls(article);
    if (urls.length === 0) return;
    urls.forEach((url, i) => {
      const ext = extFromFormat(url);
      const filename = `${ids.userId}_${ids.tweetId}_${i + 1}.${ext}`;
      GM_download({
        url,
        name: filename,
        onerror: (err) => console.error('[原寸DL] ダウンロード失敗:', filename, err),
      });
    });
  }

  // ツイート内の画像グリッド全体を包む要素を取得
  function getMediaContainer(article) {
    const photoCell = article.querySelector('div[data-testid="tweetPhoto"]');
    if (!photoCell) return null;
    const grid = photoCell.parentElement || photoCell;
    if (getComputedStyle(grid).position === 'static') {
      grid.style.position = 'relative';
    }
    return grid;
  }

  function processArticle(article) {
    if (article.dataset[BUTTON_MARK]) return;
    if (article.querySelectorAll('img[src*="pbs.twimg.com/media"]').length === 0) return;

    const mediaContainer = getMediaContainer(article);
    if (!mediaContainer) {
      console.log('[原寸DL] mediaContainer not found', article);
      return;
    }

    const ids = getTweetIds(article);
    if (!ids) {
      console.log('[原寸DL] ids not found', article);
      return;
    }

    article.dataset[BUTTON_MARK] = '1';
    const btn = createFixedButton(article, ids, 36);
    mediaContainer.appendChild(btn);
    console.log('[原寸DL] button injected', ids);
  }

  // --- 写真/動画拡大表示(モーダル)対応 ---

  function isPhotoOrVideoUrl() {
    return /\/status\/\d+\/(photo|video)\/\d+/.test(location.pathname);
  }

  function getIdsFromLocation() {
    const m = location.pathname.match(/^\/([^/]+)\/status\/(\d+)/);
    if (!m) return null;
    return { userId: m[1], tweetId: m[2] };
  }

  function createFixedButton(container, ids, size) {
    const s = size || 48;
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.title = '原寸画像を一括ダウンロード';
    btn.innerHTML = ICON_SVG;
    btn.style.cssText = `
      position:absolute;right:8px;top:8px;z-index:100;
      display:flex;align-items:center;justify-content:center;
      width:${s}px;height:${s}px;border:none;border-radius:9999px;
      background:rgba(0,0,0,0.6);color:${COLOR_DEFAULT};cursor:pointer;
      box-shadow:0 2px 8px rgba(0,0,0,0.4);
    `;
    btn.addEventListener('mouseenter', () => {
      btn.style.color = COLOR_HOVER;
      btn.style.background = 'rgba(0,0,0,0.75)';
    });
    btn.addEventListener('mouseleave', () => {
      btn.style.color = COLOR_DEFAULT;
      btn.style.background = 'rgba(0,0,0,0.6)';
    });
    btn.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      downloadImages(container, ids);
    });
    return btn;
  }

  // 表示中の画像そのものを包んでいる要素を、位置基準として使う
  function getPrimaryImageContainer(modal) {
    const img = modal.querySelector('img[src*="pbs.twimg.com/media"]');
    if (!img) return null;
    const wrapper = img.parentElement;
    if (!wrapper) return null;
    if (getComputedStyle(wrapper).position === 'static') {
      wrapper.style.position = 'relative';
    }
    return wrapper;
  }

  let modalBtnEl = null;
  let modalBtnKey = null;

  function updatePhotoModalButton() {
    const modal = document.querySelector('div[role="dialog"][aria-modal="true"]');
    const shouldShow =
      isPhotoOrVideoUrl() &&
      modal &&
      modal.querySelectorAll('img[src*="pbs.twimg.com/media"]').length > 0;

    if (!shouldShow) {
      if (modalBtnEl) {
        modalBtnEl.remove();
        modalBtnEl = null;
        modalBtnKey = null;
      }
      return;
    }

    const ids = getIdsFromLocation();
    if (!ids) {
      console.log('[原寸DL] modal ids not found');
      return;
    }

    const key = `${ids.userId}_${ids.tweetId}_${location.pathname}`;
    if (modalBtnEl && modalBtnKey === key && modalBtnEl.isConnected) return;

    if (modalBtnEl) modalBtnEl.remove();

    const imgContainer = getPrimaryImageContainer(modal);
    if (!imgContainer) {
      console.log('[原寸DL] image container not found');
      return;
    }

    modalBtnEl = createFixedButton(modal, ids, 48);
    modalBtnKey = key;
    imgContainer.appendChild(modalBtnEl);
    console.log('[原寸DL] modal button injected', ids);
  }

  function scan() {
    document.querySelectorAll('article[data-testid="tweet"]').forEach(processArticle);
    updatePhotoModalButton();
  }

  let scanTimer = null;
  function scheduleScan() {
    if (scanTimer) return;
    scanTimer = setTimeout(() => {
      scanTimer = null;
      scan();
    }, 300);
  }

  const observer = new MutationObserver(() => scheduleScan());
  observer.observe(document.body, { childList: true, subtree: true });
  scan();
})();