/**
 * exif.js — JPEG EXIF 提取 / TIFF 重建 / Apple MakerNote(ContentIdentifier) 注入
 *
 * 用途：
 * 1. 从华为动态照片的 JPEG 部分提取原始 EXIF（保留拍摄时间、GPS 等）；
 * 2. 重建 EXIF TIFF，把 Apple MakerNotes 块（含 ContentIdentifier、PhotoIdentifier、
 *    LivePhotoVideoIndex、AE/AF/HDR 等 29 个 Apple 私有字段）写入 Exif IFD 的
 *    0x927C MakerNote 字段 —— Synology Photos 等第三方识别 Live Photo 配对的关键元数据。
 *
 * MakerNotes 模板来源：iPhone 15 Pro Max 原生 HEIC (apple.HEIC) 实测提取，
 * 见 js/apple_makernotes.js。
 */

import { buildAppleMakerNote as buildAppleMakerNoteFull } from './apple_makernotes.js';

const TYPE_SIZE = { 1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 6: 1, 7: 1, 8: 2, 9: 4, 10: 8, 11: 4, 12: 8 };

/* ---------------- JPEG APP1 / EXIF 提取 ---------------- */

/** 从 JPEG 字节中提取 EXIF TIFF 字节（不含 "Exif\0\0" 头） */
export function extractExifFromJpeg(jpeg) {
  if (!jpeg || jpeg.length < 4 || jpeg[0] !== 0xff || jpeg[1] !== 0xd8) return null;
  let p = 2;
  while (p + 4 <= jpeg.length) {
    if (jpeg[p] !== 0xff) break;
    const marker = jpeg[p + 1];
    if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0x01) { p += 2; continue; }
    if (marker === 0xda) break; // SOS 之后是压缩数据
    const segLen = (jpeg[p + 2] << 8) | jpeg[p + 3];
    if (marker === 0xe1 && segLen > 8) {
      // "Exif\0\0"
      if (jpeg[p + 4] === 0x45 && jpeg[p + 5] === 0x78 && jpeg[p + 6] === 0x69 &&
          jpeg[p + 7] === 0x66 && jpeg[p + 8] === 0 && jpeg[p + 9] === 0) {
        const tiff = jpeg.slice(p + 10, p + 2 + segLen);
        if (tiff.length >= 8) return new Uint8Array(tiff);
      }
    }
    p += 2 + segLen;
    if (segLen <= 0) break;
  }
  return null;
}

/* ---------------- TIFF 解析 ---------------- */

class Reader {
  constructor(bytes, little) { this.b = bytes; this.le = little; }
  u16(o) { return this.le ? this.b[o] | (this.b[o + 1] << 8) : (this.b[o] << 8) | this.b[o + 1]; }
  u32(o) { return this.le
    ? (this.b[o] | (this.b[o + 1] << 8) | (this.b[o + 2] << 16) | (this.b[o + 3] << 24)) >>> 0
    : ((this.b[o] << 24) | (this.b[o + 1] << 16) | (this.b[o + 2] << 8) | this.b[o + 3]) >>> 0; }
}

/**
 * 解析 TIFF：返回 IFD0 与 Exif IFD 的条目列表
 * 条目: { tag, type, count, value: Uint8Array }
 */
export function parseTiff(tiff) {
  if (tiff.length < 8) return null;
  const order = String.fromCharCode(tiff[0], tiff[1]);
  if (order !== 'II' && order !== 'MM') return null;
  const r = new Reader(tiff, order === 'II');
  if (r.u16(2) !== 42) return null;
  const ifd0Offset = r.u32(4);

  function readIfd(offset) {
    if (offset < 8 || offset + 2 > tiff.length) return [];
    const count = r.u16(offset);
    const entries = [];
    for (let i = 0; i < count; i++) {
      const eOff = offset + 2 + i * 12;
      if (eOff + 12 > tiff.length) break;
      const tag = r.u16(eOff);
      const type = r.u16(eOff + 2);
      const cnt = r.u32(eOff + 4);
      const typeSize = TYPE_SIZE[type] || 1;
      const total = typeSize * cnt;
      let value;
      if (total <= 4) {
        value = tiff.slice(eOff + 8, eOff + 8 + total);
      } else {
        const vOff = r.u32(eOff + 8);
        if (vOff + total <= tiff.length) value = tiff.slice(vOff, vOff + total);
        else value = null;
      }
      entries.push({ tag, type, count: cnt, value: value ? new Uint8Array(value) : null });
    }
    return entries;
  }

  function ptrOffset(e) {
    return order === 'II'
      ? (e.value[0] | (e.value[1] << 8) | (e.value[2] << 16) | (e.value[3] << 24)) >>> 0
      : ((e.value[0] << 24) | (e.value[1] << 16) | (e.value[2] << 8) | e.value[3]) >>> 0;
  }

  const ifd0 = readIfd(ifd0Offset);
  let exifIfd = null;
  const exifPtr = ifd0.find(e => e.tag === 0x8769 && e.value && e.value.length >= 4);
  if (exifPtr) exifIfd = readIfd(ptrOffset(exifPtr));
  let gpsIfd = null;
  const gpsPtr = ifd0.find(e => e.tag === 0x8825 && e.value && e.value.length >= 4);
  if (gpsPtr) gpsIfd = readIfd(ptrOffset(gpsPtr));
  return { order, ifd0, exifIfd, gpsIfd };
}

/* ---------------- Apple MakerNote ---------------- */

/**
 * 构建完整 Apple MakerNotes（含 ContentIdentifier + PhotoIdentifier +
 * LivePhotoVideoIndex 等 29 个 Apple 私有字段），依据 iPhone 15 Pro Max
 * 原生 HEIC 实测模板生成。详见 js/apple_makernotes.js。
 *
 * @param {string} uuid         ContentIdentifier 36 字符 UUID 大写
 * @param {string} [photoUuid]  PhotoIdentifier；缺省则同 ContentIdentifier
 * @param {Date}   [date]       拍摄时间；缺省用当前时间
 * @returns {Uint8Array} 1788 字节 MakerNotes 二进制
 */
export function buildAppleMakerNote(uuid, photoUuid, date) {
  return buildAppleMakerNoteFull(uuid, photoUuid || uuid, date || new Date());
}

/* ---------------- 极简 Apple MakerNote（Synology 配对实测可用） ---------------- */

/**
 * 极简 Apple MakerNote 模板：从 Synology 实测可识别参照 `1.jpg` 逐字节提取。
 * 共 67 字节，仅含 1 个字段 ContentIdentifier (tag 0x0011, ASCII, 36 字符 UUID + '\0')。
 *
 * 模板 hex（固定 31 字节头 + 36 字节 UUID 数据）：
 *   4170706c6520694f530000014d4d0001  <- 'Apple iOS\0\x00\x01MM\x00\x01'（含版本+字节序+Apple magic=1）
 *   0011 0002 00000025 00000020       <- entry: tag=17, type=2, count=37, value offset=0x20
 *   00000000                           <- next IFD = 0
 *   [36 字节 UUID + '\0']             <- 数据区
 *
 * ⚠️ Apple MakerNote 是**私有格式**，非标准 TIFF：
 *   - magic = 0x0001（不是 TIFF 标准 0x002a）
 *   - 没有 IFD entry count 前缀，直接从 magic 后是 12 字节 entry
 *   - value offset 相对 blob 起点（0x00），不是相对 MM
 */
const MINIMAL_MAKERNOTE_TEMPLATE_HEX =
  '4170706c6520694f530000014d4d0001' +  // 'Apple iOS\0\x00\x01MM\x00\x01'（必须以 Apple iOS 开头，exiftool 正则 /^Apple iOS\0/ 匹配）
  '0011' +                              // tag = 0x0011 (ContentIdentifier)
  '0002' +                              // type = 2 (ASCII)
  '00000025' +                          // count = 37 (36 UUID + '\0')
  '00000020' +                          // value offset = 0x20 (相对 blob 起点)
  '00000000';                           // next IFD = 0

const MINIMAL_MAKERNOTE_HEADER = (() => {
  const hex = MINIMAL_MAKERNOTE_TEMPLATE_HEX;
  const out = new Uint8Array(hex.length / 2);
  for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16);
  return out;
})();

const OFFSET_MINIMAL_UUID = 0x20; // UUID 数据起点（相对 blob 起点）

/**
 * 构建极简 Apple MakerNote：仅含 ContentIdentifier 一项。
 * 与 Synology Photos 实测可识别参照文件 `1.jpg` 字节级同构（67 字节，**无 padding 前缀**）。
 *
 * ⚠️ 关键约束：blob 必须以 'Apple iOS\0' 开头，exiftool 用正则 /^Apple iOS\0/ 识别；
 *    若前面混入 padding 字节会被识别为 MakerNoteUnknown，ContentIdentifier 无法解析。
 *
 * @param {string} uuid 36 字符 ContentIdentifier 大写 UUID
 * @returns {Uint8Array} 67 字节极简 MakerNotes 二进制
 */
export function buildMinimalAppleMakerNote(uuid) {
  if (!/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/.test(uuid)) {
    throw new Error('uuid 必须是 36 字符 UUID 大写字符串');
  }
  const out = new Uint8Array(0x20 + 37);
  out.set(MINIMAL_MAKERNOTE_HEADER, 0);
  for (let i = 0; i < 36; i++) out[OFFSET_MINIMAL_UUID + i] = uuid.charCodeAt(i);
  out[OFFSET_MINIMAL_UUID + 36] = 0x00;
  return out;
}

/* ---------------- TIFF 重建 ---------------- */

/**
 * 重建 EXIF TIFF：保留原 IFD0/Exif IFD 条目（丢弃原 MakerNote），
 * 写入完整 Apple MakerNote（29 个 Apple 私有字段）。
 *
 * @param {Uint8Array|null} originalTiff 原 EXIF（可为 null）
 * @param {string} uuid         ContentIdentifier
 * @param {Date}   [photoDate]  拍摄时间；缺省从 originalTiff 的 DateTimeOriginal 提取；都没有则当前时间
 * @param {string} [photoUuid]  PhotoIdentifier；缺省则自动生成
 * @returns {Uint8Array} 完整 TIFF 二进制（含 Apple MakerNotes）
 */
export function buildExifWithAppleMakerNote(originalTiff, uuid, photoDate, photoUuid) {
  const date = photoDate || (originalTiff ? readExifDate(originalTiff) : null) || new Date();
  const pUuid = photoUuid || (typeof crypto !== 'undefined' && crypto.randomUUID
    ? crypto.randomUUID().toUpperCase()
    : (() => { const b = new Uint8Array(16); crypto.getRandomValues(b);
      b[6] = (b[6] & 0x0f) | 0x40; b[8] = (b[8] & 0x3f) | 0x80;
      const h = [...b].map(x => x.toString(16).padStart(2, '0')).join('');
      return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`.toUpperCase();
    })());
  const makerNote = buildAppleMakerNote(uuid, pUuid, date);

  let ifd0Entries = [];
  let exifEntries = [];
  let little = true;
  if (originalTiff) {
    const parsed = parseTiff(originalTiff);
    if (parsed) {
      little = parsed.order === 'II';
      ifd0Entries = parsed.ifd0.filter(e => e.value && e.tag !== 0x8769);
      exifEntries = (parsed.exifIfd || []).filter(e => e.value && e.tag !== 0x927c); // 丢弃原 MakerNote
    }
  }
  if (ifd0Entries.length === 0) {
    // 最小 IFD0：Make
    ifd0Entries = [{ tag: 0x010f, type: 2, count: 6, value: new TextEncoder().encode('Apple\0') }];
  }
  exifEntries.push({ tag: 0x927c, type: 7, count: makerNote.length, value: makerNote });

  return serializeTiff(ifd0Entries, exifEntries, little);
}

/**
 * 拍摄参数白名单（ExifIFD 中保留的字段）。
 * 这些是标准的 EXIF 曝光/镜头参数，第三方相册与 iOS 都能正常显示，
 * 且不影响 ContentIdentifier 配对凭证。
 *   tag       名称                       类型
 *   0x829A    ExposureTime                RATIONAL
 *   0x829D    FNumber                     RATIONAL
 *   0x8827    ISOSpeedRatings             SHORT
 *   0x9201    ShutterSpeedValue           SRATIONAL
 *   0x9202    ApertureValue               RATIONAL
 *   0x9204    ExposureBiasValue           SRATIONAL
 *   0x9205    MaxApertureValue            RATIONAL
 *   0x9207    MeteringMode                SHORT
 *   0x9209    Flash                       SHORT
 *   0x920A    FocalLength                 RATIONAL
 *   0xA402    ExposureMode                SHORT
 *   0xA403    WhiteBalance                SHORT
 *   0xA405    FocalLengthIn35mmFormat     SHORT
 *   0xA406    SceneCaptureType            SHORT
 */
const EXIF_PARAM_TAGS = new Set([
  0x829a, 0x829d, 0x8827, 0x9201, 0x9202, 0x9204, 0x9205, 0x9207,
  0x9209, 0x920a, 0xa402, 0xa403, 0xa405, 0xa406,
]);

/**
 * 设备字段白名单（IFD0 中保留，ASCII 类型）。
 * 注意：不含 0x010F Make（厂商）——Make 字段直接删除（既不伪装 Apple，
 * 也不保留原 HUAWEI 厂商名，用户明确要求）。Model 保留原图实际值。
 */
const IFD0_DEVICE_TAGS = [0x0110 /* Model */, 0x0131 /* Software */];

/**
 * 重建极简 EXIF TIFF — 在 Synology 实测可识别参照 `1.jpg` 基础上扩展：
 *   - IFD0：`Orientation` + 原图 `Model/Software`（原值保留）+ `DateTime`（拍摄时间）
 *   - **不写 Make 字段**（既不伪装 Apple，也不保留原 HUAWEI 厂商名）
 *   - ExifIFD：`ExifVersion = 0232` + 拍摄参数 + `MakerNote（极简，仅 ContentIdentifier）`
 *   - **Apple MakerNote 只有 1 个 ContentIdentifier 字段**（非 25 字段完整版）
 *
 * 用途：极简配对模式（minimal），与 makelivephoto.com / AssassinJY/live_motion_photos_convert 同路线。
 * 参照实测：Synology Photos 只需 JPG 含 ContentIdentifier（与 MOV 侧共享 UUID）即可配对，
 *          完整 25 字段反而可能干扰其检测器。
 *
 * @param {Uint8Array|null} originalTiff 原 EXIF（仅用于提取 Orientation；可为 null）
 * @param {string} uuid  ContentIdentifier 36 字符大写 UUID
 * @returns {Uint8Array} 极简 TIFF 二进制
 */
export function buildExifMinimalAppleMakerNote(originalTiff, uuid) {
  const makerNote = buildMinimalAppleMakerNote(uuid);

  // IFD0：Make=Apple + Orientation +（设备 Model/Software）+（拍摄时间 DateTime）
  let orientation = 1;
  const deviceEntries = [];  // Model / Software（ASCII，字节序无关）
  const paramEntries = [];   // 拍摄参数（ExifIFD，数值类型需转 MM）
  let dateStr = null;
  let gpsEntries = null;

  if (originalTiff) {
    const parsed = parseTiff(originalTiff);
    if (parsed) {
      const littleIn = parsed.order === 'II';

      // Orientation
      const o = parsed.ifd0.find(e => e.tag === 0x0112 && e.value);
      if (o && o.value && o.value.length >= 2) {
        orientation = (o.value[0] << 8) | o.value[1];
      }

      // 设备字段（IFD0）：Model / Software；Make 厂商不保留（保持 Apple）
      for (const tag of IFD0_DEVICE_TAGS) {
        const e = parsed.ifd0.find(x => x.tag === tag && x.value);
        if (e) {
          deviceEntries.push({ tag: e.tag, type: 2, count: e.count, value: new Uint8Array(e.value) });
        }
      }

      // 拍摄参数（从 IFD0 与 ExifIFD 两个池提取，去重后转 MM 字节序）。
      // 部分相机（尤其测试样本/早期固件）把 ExposureTime/ISO 直接写在 IFD0，
      // 标准相机写在 ExifIFD，故两处都要扫。
      const seenParamTags = new Set();
      for (const pool of [(parsed.exifIfd || []), parsed.ifd0]) {
        for (const e of pool) {
          if (EXIF_PARAM_TAGS.has(e.tag) && e.value && !seenParamTags.has(e.tag)) {
            seenParamTags.add(e.tag);
            paramEntries.push({ tag: e.tag, type: e.type, count: e.count, value: toMM(e.value, e.type, e.count, littleIn) });
          }
        }
      }

      // 拍摄时间：优先 DateTimeOriginal(0x9003)，其次 CreateDate(0x9004)，再 ModifyDate(0x0132)
      const find = (list, tag) => list && list.find(e => e.tag === tag && e.value);
      const dto = find(parsed.exifIfd, 0x9003) || find(parsed.ifd0, 0x9003)
        || find(parsed.exifIfd, 0x9004) || find(parsed.ifd0, 0x9004)
        || find(parsed.ifd0, 0x0132);
      if (dto) {
        const s = new TextDecoder().decode(dto.value).replace(/\0+$/, '').trim();
        if (s.length >= 19) dateStr = s;
      }

      // 拍摄地点：GPS IFD 全量保留（转 MM 字节序）
      if (parsed.gpsIfd && parsed.gpsIfd.length) {
        gpsEntries = parsed.gpsIfd
          .filter(e => e.value)
          .map(e => ({ tag: e.tag, type: e.type, count: e.count, value: toMM(e.value, e.type, e.count, littleIn) }));
      }
    }
  }

  const ifd0Entries = [
    { tag: 0x0112, type: 3, count: 1, value: new Uint8Array([0, orientation]) },     // Orientation (SHORT, MM 内联)
    ...deviceEntries,                                                                 // Model / Software（原值保留，不含 Make）
  ];
  if (dateStr) {
    // DateTime ASCII：固定 "YYYY:MM:DD HH:MM:SS\0"（无则补齐）
    const d = new TextEncoder().encode(dateStr.replace(/-/g, ':').slice(0, 19) + '\0');
    ifd0Entries.push({ tag: 0x0132, type: 2, count: d.length, value: d });
  }

  const exifEntries = [
    { tag: 0x9000, type: 7, count: 4, value: new Uint8Array([0x30, 0x32, 0x33, 0x32]) }, // ExifVersion "0232"
    ...paramEntries,                                                                      // 拍摄参数（曝光/光圈/ISO/焦距…）
    { tag: 0x927c, type: 7, count: makerNote.length, value: makerNote },                  // MakerNote
  ];
  return serializeTiff(ifd0Entries, exifEntries, false /* big-endian MM，与 REF JPG 一致 */, gpsEntries);
}

/**
 * 把 TIFF 条目数值从源字节序转成 MM big-endian（仅用于 GPS 数值类型）。
 * type 1/2/7（BYTE/ASCII/UNDEFINED）字节序无关直接拷贝；
 * type 3/8(SHORT)、4/9(LONG)、5/10(RATIONAL) 按元素宽度翻转。
 */
function toMM(value, type, count, littleIn) {
  const n = count;
  const out = new Uint8Array(value.length);
  if (!littleIn) { out.set(value); return out; }
  const dvIn = new DataView(value.buffer, value.byteOffset, value.byteLength);
  const dvOut = new DataView(out.buffer);
  try {
    if (type === 3 || type === 8) {         // SHORT / SSHORT
      for (let i = 0; i < n; i++) dvOut.setUint16(i * 2, dvIn.getUint16(i * 2, true), false);
    } else if (type === 4 || type === 9) {  // LONG / SLONG
      for (let i = 0; i < n; i++) dvOut.setUint32(i * 4, dvIn.getUint32(i * 4, true), false);
    } else if (type === 5 || type === 10) { // RATIONAL / SRATIONAL（每个值 8 字节 = 2 个 uint32）
      for (let i = 0; i < n * 2; i++) dvOut.setUint32(i * 4, dvIn.getUint32(i * 4, true), false);
    } else {
      out.set(value);
    }
  } catch { out.set(value); }
  return out;
}

/** 内部辅助：序列化 IFD0 + ExifIFD +（可选）GPS IFD 为完整 TIFF（MM big-endian 或 II little-endian） */
function serializeTiff(ifd0Entries, exifEntries, little, gpsEntries) {
  const w16 = (arr, v) => { if (little) { arr.push(v & 0xff, (v >> 8) & 0xff); } else { arr.push((v >> 8) & 0xff, v & 0xff); } };
  const w32 = (arr, v) => {
    v = v >>> 0;
    if (little) arr.push(v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0xff, (v >>> 24) & 0xff);
    else arr.push((v >>> 24) & 0xff, (v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff);
  };

  function serializeIfd(entries) {
    // 返回 { ifdBytes, dataBytes }，dataBytes 中的值偏移由调用方修正
    const ifd = [];
    const data = [];
    const valueOffsets = []; // data 区内每个外部值的起始
    w16(ifd, entries.length);
    for (const e of entries) {
      w16(ifd, e.tag);
      w16(ifd, e.type);
      w32(ifd, e.count);
      const typeSize = TYPE_SIZE[e.type] || 1;
      const total = typeSize * e.count;
      if (total <= 4) {
        // 内联值（≤4 字节）：按 type 处理字节序
        //   type 1/2/7 (BYTE/ASCII/UNDEFINED)：字节序无关，直接 copy
        //   type 3 (SHORT) count=1：MM 序应为 [hi, lo, 0, 0]；II 序应为 [lo, hi, 0, 0]
        //   type 3 (SHORT) count=2：按 SHORT 序分别打包
        //   type 4 (LONG) count=1：按 32 位字节序打包
        const inline = new Uint8Array(4);
        if (e.type === 2 || e.type === 7 || e.type === 1) {
          // 字节序无关：直接 copy value 到 inline 前 total 字节
          inline.set(e.value.subarray(0, total), 0);
        } else if (e.type === 3) {
          // SHORT 数组：每 2 字节一组按字节序处理
          for (let i = 0; i < e.count && i < 2; i++) {
            const hi = e.value[i * 2] ?? 0;
            const lo = e.value[i * 2 + 1] ?? 0;
            if (little) { inline[i * 2] = lo; inline[i * 2 + 1] = hi; }
            else { inline[i * 2] = hi; inline[i * 2 + 1] = lo; }
          }
        } else if (e.type === 4) {
          // LONG：4 字节按字节序处理
          const b0 = e.value[0] ?? 0, b1 = e.value[1] ?? 0, b2 = e.value[2] ?? 0, b3 = e.value[3] ?? 0;
          if (little) { inline[0] = b3; inline[1] = b2; inline[2] = b1; inline[3] = b0; }
          else { inline[0] = b0; inline[1] = b1; inline[2] = b2; inline[3] = b3; }
        } else {
          inline.set(e.value.subarray(0, 4), 0);
        }
        ifd.push(...inline);
      } else {
        valueOffsets.push({ pos: ifd.length, start: data.length, len: total });
        w32(ifd, 0); // 占位，稍后回填
        for (let i = 0; i < total; i++) data.push(e.value[i] ?? 0);
        // 对齐到 2 字节
        if (data.length % 2) data.push(0);
      }
    }
    w32(ifd, 0); // next IFD
    return { ifd, data, valueOffsets };
  }

  const i0 = serializeIfd(ifd0Entries);
  const exifIfdOffset = 8 + i0.ifd.length + i0.data.length;
  const i1 = serializeIfd(exifEntries);
  const hasGps = gpsEntries && gpsEntries.length > 0;
  const iG = hasGps ? serializeIfd(gpsEntries) : null;

  // IFD0 中加入 0x8769 指针（以及可选 0x8825 GPS 指针）：
  // 需要重新序列化 IFD0（多一个/两个条目）
  // 简单做法：把指针条目并入 ifd0Entries 后再算偏移 —— 由于指针条目本身占 12 字节，
  // 先用固定偏移计算：ExifIFD 起始 = 8 + (2 + (n+1)*12 + 4) + ifd0DataLen（2 字节对齐后）
  const ptrs = [{ tag: 0x8769, type: 4, count: 1, value: null }];
  if (hasGps) ptrs.push({ tag: 0x8825, type: 4, count: 1, value: null });
  const ifd0WithPtr = [...ifd0Entries, ...ptrs];
  const i0b = serializeIfd(ifd0WithPtr.map(e =>
    (e.tag === 0x8769 || e.tag === 0x8825) ? { tag: e.tag, type: 4, count: 1, value: new Uint8Array([0, 0, 0, 0]) } : e));
  const exifIfdOffsetFinal = 8 + i0b.ifd.length + i0b.data.length;

  // GPS IFD 偏移 = ExifIFD 结束后
  const gpsIfdOffsetFinal = exifIfdOffsetFinal + i1.ifd.length + i1.data.length;

  const totalLen = exifIfdOffsetFinal + i1.ifd.length + i1.data.length
    + (hasGps ? iG.ifd.length + iG.data.length : 0);
  const out = new Uint8Array(totalLen);
  const dv = new DataView(out.buffer);

  // header
  out[0] = little ? 0x49 : 0x4d; out[1] = little ? 0x49 : 0x4d;
  dv.setUint16(2, 42, little);
  dv.setUint32(4, 8, little);

  let p = 8;
  // IFD0（含指针，指针值回填）
  // 重新手动序列化 IFD0，以便知道指针条目的位置
  {
    const ifd = [];
    const data = [];
    const fixups = [];
    w16(ifd, ifd0WithPtr.length);
    for (const e of ifd0WithPtr) {
      w16(ifd, e.tag);
      w16(ifd, e.type);
      w32(ifd, e.count);
      if (e.tag === 0x8769) {
        w32(ifd, exifIfdOffsetFinal);
      } else if (e.tag === 0x8825) {
        w32(ifd, gpsIfdOffsetFinal);
      } else {
        const typeSize = TYPE_SIZE[e.type] || 1;
        const total = typeSize * e.count;
        if (total <= 4) {
          // 内联值（≤4 字节）：按 type 处理字节序（与 serializeIfd 同步逻辑）
          const inline = new Uint8Array(4);
          if (e.type === 2 || e.type === 7 || e.type === 1) {
            inline.set(e.value.subarray(0, total), 0);
          } else if (e.type === 3) {
            for (let i = 0; i < e.count && i < 2; i++) {
              const hi = e.value[i * 2] ?? 0;
              const lo = e.value[i * 2 + 1] ?? 0;
              if (little) { inline[i * 2] = lo; inline[i * 2 + 1] = hi; }
              else { inline[i * 2] = hi; inline[i * 2 + 1] = lo; }
            }
          } else if (e.type === 4) {
            const b0 = e.value[0] ?? 0, b1 = e.value[1] ?? 0, b2 = e.value[2] ?? 0, b3 = e.value[3] ?? 0;
            if (little) { inline[0] = b3; inline[1] = b2; inline[2] = b1; inline[3] = b0; }
            else { inline[0] = b0; inline[1] = b1; inline[2] = b2; inline[3] = b3; }
          } else {
            inline.set(e.value.subarray(0, 4), 0);
          }
          ifd.push(...inline);
        } else {
          fixups.push({ pos: ifd.length, start: data.length, len: total });
          w32(ifd, 0);
          for (let i = 0; i < total; i++) data.push(e.value[i] ?? 0);
          if (data.length % 2) data.push(0);
        }
      }
    }
    w32(ifd, 0);
    // 回填外部值偏移（相对 TIFF 起点 = 当前 p + ifd.length + start），先补丁再写入
    const ifdArr = new Uint8Array(ifd);
    const ifdDv0 = new DataView(ifdArr.buffer);
    for (const f of fixups) {
      ifdDv0.setUint32(f.pos, p + ifd.length + f.start, little);
    }
    out.set(ifdArr, p); p += ifd.length;
    out.set(new Uint8Array(data), p); p += data.length;
  }
  // Exif IFD
  {
    const ifd = i1.ifd;
    const data = i1.data;
    // 回填偏移：绝对 = p + ifd.length + start
    const ifdArr = new Uint8Array(ifd);
    const ifdDv = new DataView(ifdArr.buffer);
    for (const f of i1.valueOffsets) {
      ifdDv.setUint32(f.pos, p + ifd.length + f.start, little);
    }
    out.set(ifdArr, p); p += ifd.length;
    out.set(new Uint8Array(data), p); p += data.length;
  }
  // GPS IFD（可选）
  if (hasGps) {
    const ifd = iG.ifd;
    const data = iG.data;
    const ifdArr = new Uint8Array(ifd);
    const ifdDv = new DataView(ifdArr.buffer);
    for (const f of iG.valueOffsets) {
      ifdDv.setUint32(f.pos, p + ifd.length + f.start, little);
    }
    out.set(ifdArr, p); p += ifd.length;
    out.set(new Uint8Array(data), p); p += data.length;
  }

  return out;
}

/**
 * 给 JPEG 原地注入 Apple MakerNote（ContentIdentifier），不重新编码图像数据。
 *
 * Apple 的实况照片配对机制对静态图是 JPEG 还是 HEIC 都适用——只要 JPEG 的
 * EXIF 里带同样的 MakerNote tag 17，就能与 MOV 配对。以 JPEG 形式导出可以
 * 做到零重编码、完全无损、速度极快，且对第三方照片库（如 Synology Photos）
 * 的兼容性优于 HEIC。
 *
 * @param {Uint8Array} jpeg 原始 JPEG 字节
 * @param {string} uuid 配对标识
 * @returns {Uint8Array|null} 新的 JPEG；若未找到 Exif APP1 段则返回 null
 */
export function patchJpegWithAppleMakerNote(jpeg, uuid) {
  const EXIF_SIG = [0x45, 0x78, 0x69, 0x66, 0x00, 0x00]; // 'Exif\0\0'
  let p = 2; // 跳过 SOI (FFD8)
  while (p + 4 <= jpeg.length) {
    if (jpeg[p] !== 0xFF) { p++; continue; }
    const marker = jpeg[p + 1];
    // 无长度字段的独立标记
    if (marker === 0x01 || (marker >= 0xD0 && marker <= 0xD9)) { p += 2; continue; }
    if (marker === 0xDA) break; // SOS：其后为熵编码数据，不再有段结构
    const segLen = (jpeg[p + 2] << 8) | jpeg[p + 3];
    if (segLen < 2) break;

    let isExif = marker === 0xE1;
    if (isExif) {
      for (let i = 0; i < 6; i++) {
        if (jpeg[p + 4 + i] !== EXIF_SIG[i]) { isExif = false; break; }
      }
    }
    if (isExif) {
      const tiffStart = p + 10;          // FF E1 + len(2) + 'Exif\0\0'(6)
      const segEnd = p + 2 + segLen;
      const tiff = jpeg.slice(tiffStart, segEnd);
      const newTiff = buildExifWithAppleMakerNote(tiff, uuid);
      const newSegLen = 2 + 6 + newTiff.length; // len 字段自身 + 'Exif\0\0' + TIFF
      if (newSegLen > 0xFFFF) throw new Error('EXIF 段超过 64KB，无法写入 JPEG');

      const out = new Uint8Array(jpeg.length - segLen + newSegLen);
      out.set(jpeg.subarray(0, p), 0);
      let o = p;
      out[o++] = 0xFF; out[o++] = 0xE1;
      out[o++] = (newSegLen >> 8) & 255; out[o++] = newSegLen & 255;
      out.set(EXIF_SIG, o); o += 6;
      out.set(newTiff, o); o += newTiff.length;
      out.set(jpeg.subarray(segEnd), o);
      return out;
    }
    p += 2 + segLen;
  }
  return null;
}

/** 从 EXIF 中读取 DateTimeOriginal (tag 0x9003) 返回 Date 或 null（兼容 IFD0/ExifIFD 两种存放位置） */
export function readExifDate(tiff) {
  if (!tiff) return null;
  const parsed = parseTiff(tiff);
  if (!parsed) return null;
  const pools = [parsed.exifIfd, parsed.ifd0];
  for (const pool of pools) {
    if (!pool) continue;
    const e = pool.find(x => x.tag === 0x9003 && x.value);
    if (!e) continue;
    const s = new TextDecoder().decode(e.value).replace(/\0.*$/, '').trim();
    const m = s.match(/^(\d{4}):(\d{2}):(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/);
    if (m) {
      const d = new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
      if (!isNaN(d)) return d;
    }
  }
  return null;
}

/**
 * 读取 EXIF 中的相机参数（FocalLength / Aperture / FocalLengthIn35mmFormat），
 * 用于写入视频轨内 meta box 的 Apple 相机 QuickTime Keys：
 *   com.apple.quicktime.camera.lens_model
 *   com.apple.quicktime.camera.focal_length.35mm_equivalent
 *   com.apple.quicktime.camera.lens_irisfnumber
 *
 * 返回值通过 lensModel/focal35mmEquivalent/lensIrisFNumber 字段携带，由
 * 调用方传给 mp4ToLivePhotoMov(opts.camera)。当 EXIF 不含相机参数时，
 * 字段为 null，由 mov.js 退回到 iPhone 通用占位值。
 *
 * @param {Uint8Array|null} tiff 原始 EXIF TIFF 字节
 * @returns {{ lensModel: string|null, focal35mmEquivalent: string|null,
 *            lensIrisFNumber: string|null, makerNote74: number|null, makerNote97: number|null,
 *            realFocalLength: number|null, fNumber: number|null }}
 */
export function extractCameraMeta(tiff) {
  const empty = {
    lensModel: null, focal35mmEquivalent: null, lensIrisFNumber: null,
    makerNote74: null, makerNote97: null,
    realFocalLength: null, fNumber: null,
  };
  if (!tiff) return empty;
  const parsed = parseTiff(tiff);
  if (!parsed || !parsed.exifIfd) return empty;
  const r = new Reader(tiff, parsed.order === 'II');

  function readRational(v) {
    if (!v || v.length < 8) return null;
    const le = r.le;
    const num = le
      ? (v[0] | (v[1] << 8) | (v[2] << 16) | (v[3] << 24)) >>> 0
      : ((v[0] << 24) | (v[1] << 16) | (v[2] << 8) | v[3]) >>> 0;
    const den = le
      ? (v[4] | (v[5] << 8) | (v[6] << 16) | (v[7] << 24)) >>> 0
      : ((v[4] << 24) | (v[5] << 16) | (v[6] << 8) | v[7]) >>> 0;
    return den === 0 ? null : num / den;
  }

  // 0x920A FocalLength (mm, rational)
  const realFL = parsed.exifIfd.find(e => e.tag === 0x920A);
  const realFocalLength = realFL ? readRational(realFL.value) : null;
  // 0xA432 FocalLengthIn35mmFormat (mm, int32)
  const focal35 = parsed.exifIfd.find(e => e.tag === 0xa432);
  let focal35mmEquivalent = null;
  if (focal35 && focal35.value && focal35.value.length >= 4) {
    focal35mmEquivalent = String(r.u32(0));
  }
  // 0x829D FNumber (rational)
  const fNum = parsed.exifIfd.find(e => e.tag === 0x829d);
  const fNumber = fNum ? readRational(fNum.value) : null;

  // lensModel: 拼接 "Make Model back camera FocalLength mm f/Aperture"
  // 若用户原图非 iPhone（如华为/小米），仍能给出可识别的 iPhone 风格字符串，
  // Synology 仅依此字段判断"是否 iPhone 拍摄的视频元数据"。
  return {
    lensModel: null, // 留给 mov.js 的默认占位；这里不强行构造以保留 iPhone 风味
    focal35mmEquivalent: focal35mmEquivalent ? String(parseInt(focal35mmEquivalent, 10)) : null,
    lensIrisFNumber: fNumber ? `F${fNumber.toFixed(2)}` : null,
    makerNote74: 2,
    makerNote97: 24,
    realFocalLength: realFocalLength,
    fNumber,
  };
}


/**
 * 读取 EXIF 中的 Make/Model/Software/GPS 等元数据，用于写入 MOV 侧的
 * QuickTime Keys（com.apple.quicktime.{make,model,software,location.ISO6709}）。
 *
 * @param {Uint8Array|null} tiff 原始 EXIF TIFF 字节
 * @returns {{ make: string, model: string, software: string, iso6709: string|null,
 *           hasGps: boolean, lat: number|null, lon: number|null, alt: number|null }}
 */
export function extractExifMeta(tiff) {
  const empty = { make: '', model: '', software: '', iso6709: null, hasGps: false, lat: null, lon: null, alt: null };
  if (!tiff) return empty;
  const parsed = parseTiff(tiff);
  if (!parsed) return empty;
  const r = new Reader(tiff, parsed.order === 'II');
  const le = r.le;

  function ascii(value) {
    if (!value) return '';
    return new TextDecoder().decode(value).replace(/\0.*$/, '').trim();
  }
  function readRational(val) {
    // type=5: 8 字节 = [num 4][den 4]，按字节序读
    if (!val || val.length < 8) return 0;
    const num = le ? (val[0] | (val[1] << 8) | (val[2] << 16) | (val[3] << 24)) >>> 0
                   : ((val[0] << 24) | (val[1] << 16) | (val[2] << 8) | val[3]) >>> 0;
    const den = le ? (val[4] | (val[5] << 8) | (val[6] << 16) | (val[7] << 24)) >>> 0
                   : ((val[4] << 24) | (val[5] << 16) | (val[6] << 8) | val[7]) >>> 0;
    return den === 0 ? 0 : num / den;
  }
  function readRationals(val, n) {
    // n 组 8 字节 rational
    const out = [];
    for (let i = 0; i < n && (i * 8 + 8) <= val.length; i++) {
      out.push(readRational(val.subarray(i * 8, i * 8 + 8)));
    }
    return out;
  }

  const ifd0 = parsed.ifd0 || [];
  const make = ascii(ifd0.find(e => e.tag === 0x010f)?.value);
  const model = ascii(ifd0.find(e => e.tag === 0x0110)?.value);
  const software = ascii(ifd0.find(e => e.tag === 0x0131)?.value);

  // GPS IFD（IFD0 → tag 0x8825）
  let lat = null, lon = null, alt = null;
  let latRef = 'N', lonRef = 'E', altRef = 0;
  const gpsPtr = ifd0.find(e => e.tag === 0x8825 && e.value && e.value.length >= 4);
  if (gpsPtr) {
    const off = le
      ? (gpsPtr.value[0] | (gpsPtr.value[1] << 8) | (gpsPtr.value[2] << 16) | (gpsPtr.value[3] << 24)) >>> 0
      : ((gpsPtr.value[0] << 24) | (gpsPtr.value[1] << 16) | (gpsPtr.value[2] << 8) | gpsPtr.value[3]) >>> 0;
    if (off >= 8 && off + 2 <= tiff.length) {
      const cnt = r.u16(off);
      for (let i = 0; i < cnt; i++) {
        const eOff = off + 2 + i * 12;
        if (eOff + 12 > tiff.length) break;
        const tag = r.u16(eOff);
        const type = r.u16(eOff + 2);
        const count = r.u32(eOff + 4);
        const typeSize = TYPE_SIZE[type] || 1;
        const total = typeSize * count;
        let val;
        if (total <= 4) val = tiff.slice(eOff + 8, eOff + 8 + total);
        else {
          const vOff = r.u32(eOff + 8);
          if (vOff + total <= tiff.length) val = tiff.slice(vOff, vOff + total);
          else val = null;
        }
        if (!val) continue;
        if (tag === 0x0001 && val.length >= 1) latRef = String.fromCharCode(val[0]);
        else if (tag === 0x0003 && val.length >= 1) lonRef = String.fromCharCode(val[0]);
        else if (tag === 0x0005 && val.length >= 1) altRef = val[0];
        else if (tag === 0x0002 && type === 5 && count === 3) lat = readRationals(val, 3);
        else if (tag === 0x0004 && type === 5 && count === 3) lon = readRationals(val, 3);
        else if (tag === 0x0006 && type === 5 && count === 1) alt = readRational(val);
      }
    }
  }

  // ISO 6709：参照 iPhone 原生格式 ±DD.dddd±DDD.dddd±AAA.aaa/
  let iso6709 = null;
  if (lat && lon) {
    const latDeg = lat[0] + lat[1] / 60 + lat[2] / 3600;
    const lonDeg = lon[0] + lon[1] / 60 + lon[2] / 3600;
    const signedLat = latRef === 'S' ? -latDeg : latDeg;
    const signedLon = lonRef === 'W' ? -lonDeg : lonDeg;
    const signChar = v => (v < 0 ? '-' : '+');
    // 纬度绝对值填 7 字符（含符号共 8：±DD.dddd），经度填 8 字符（±DDD.dddd）
    // 海拔填 7 字符（±AAA.aaa），参照 iPhone 15 Pro Max 原生：+37.7890+175.2995+035.332/
    const latStr = signChar(signedLat) + Math.abs(signedLat).toFixed(4).padStart(7, '0');
    const lonStr = signChar(signedLon) + Math.abs(signedLon).toFixed(4).padStart(8, '0');
    const altPart = alt !== null
      ? (altRef === 1 ? '-' : '+') + Math.abs(alt).toFixed(3).padStart(7, '0') + '/'
      : '/';
    iso6709 = `${latStr}${lonStr}${altPart}`;
  }

  return {
    make, model, software,
    iso6709, hasGps: !!iso6709,
    lat: lat ? (latRef === 'S' ? -(lat[0] + lat[1] / 60 + lat[2] / 3600) : (lat[0] + lat[1] / 60 + lat[2] / 3600)) : null,
    lon: lon ? (lonRef === 'W' ? -(lon[0] + lon[1] / 60 + lon[2] / 3600) : (lon[0] + lon[1] / 60 + lon[2] / 3600)) : null,
    alt,
  };
}

/**
 * 把 Date 格式化为 Apple QuickTime creationdate 字符串：
 *   YYYY-MM-DDTHH:MM:SS±HHMM（参照 iPhone 15 Pro Max 原生：2025-10-12T23:36:14+1300）。
 * 浏览器中 Date 没显式时区，使用本地时区偏移；无 photoDate 则用 new Date()。
 */
export function formatAppleCreationDate(date) {
  const d = date instanceof Date ? date : new Date();
  const yyyy = d.getFullYear();
  const mm = String(d.getMonth() + 1).padStart(2, '0');
  const dd = String(d.getDate()).padStart(2, '0');
  const HH = String(d.getHours()).padStart(2, '0');
  const MM = String(d.getMinutes()).padStart(2, '0');
  const SS = String(d.getSeconds()).padStart(2, '0');
  const off = -d.getTimezoneOffset(); // 分钟，东为正
  const sign = off >= 0 ? '+' : '-';
  const abs = Math.abs(off);
  const tzH = String(Math.floor(abs / 60)).padStart(2, '0');
  const tzM = String(abs % 60).padStart(2, '0');
  return `${yyyy}-${mm}-${dd}T${HH}:${MM}:${SS}${sign}${tzH}${tzM}`;
}
