const path = require('path');
const fs = require('fs');
const { spawnSync } = require('child_process');

const vips = path.resolve(__dirname, 'vips-dev-8.8/bin/vips.exe');
const tilesetsPath = path.resolve(__dirname, 'source_zoomify');
const outPath = path.resolve(__dirname, 'restitched_zoomify');

// Invoke vips directly via spawnSync — avoids cmd.exe and its ~8191-char limit.
function runVips(args) {
  const result = spawnSync(vips, args, { stdio: 'inherit' });
  if (result.error) throw result.error;
  if (result.status !== 0) throw new Error(`vips exited with code ${result.status}`);
}

// Parse a Zoomify tile filename "Z-X-Y.jpg" into { z, x, y }.
// Handles any number of digits — unlike the original charAt(0)/charAt(4) approach
// which breaks when zoom >= 10 or column >= 10.
function parseTileName(filename) {
  const base = path.basename(filename, path.extname(filename));
  const parts = base.split('-').map(Number);
  if (parts.length !== 3 || parts.some(isNaN)) return null;
  const [z, x, y] = parts;
  return { z, x, y };
}

function stitchSpecimen(specimenName, srcPath) {
  console.log(`\nstitching ${specimenName}`);

  // Collect every tile path from all TileGroup* sub-directories.
  const allTiles = [];
  for (const entry of fs.readdirSync(srcPath)) {
    const groupPath = path.join(srcPath, entry);
    if (!fs.lstatSync(groupPath).isDirectory()) continue;
    for (const file of fs.readdirSync(groupPath)) {
      if (parseTileName(file)) allTiles.push(path.join(groupPath, file));
    }
  }

  if (allTiles.length === 0) {
    console.log('  no tiles found, skipping');
    return;
  }

  // Determine highest zoom level and keep only those tiles.
  const maxZ = Math.max(...allTiles.map(p => parseTileName(path.basename(p)).z));
  const tiles = allTiles
    .map(p => ({ path: p, ...parseTileName(path.basename(p)) }))
    .filter(t => t.z === maxZ);

  // Group tiles by row (y), sort each row by column (x).
  const rowMap = new Map();
  for (const t of tiles) {
    if (!rowMap.has(t.y)) rowMap.set(t.y, []);
    rowMap.get(t.y).push(t);
  }
  for (const row of rowMap.values()) row.sort((a, b) => a.x - b.x);
  const sortedYs = Array.from(rowMap.keys()).sort((a, b) => a - b);

  console.log(`  zoom level ${maxZ}: ${tiles.length} tiles, ${rowMap.size} rows of ${rowMap.get(sortedYs[0]).length}`);

  // Temp directory for intermediate row images (cleaned up in finally).
  const tmpDir = path.join(outPath, `_tmp_${specimenName}`);
  if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });

  try {
    const rowFiles = [];

    // Pass 1: join each row of tiles horizontally into a temp image.
    // Each command touches at most (image-width / tile-size) paths — well within limits.
    for (const y of sortedYs) {
      const rowTiles = rowMap.get(y);
      const rowOut = path.join(tmpDir, `row_${y}.jpg`);
      const tileList = rowTiles.map(t => t.path).join(' ');
      process.stdout.write(`  row ${y}/${sortedYs[sortedYs.length - 1]} (${rowTiles.length} tiles)...\r`);
      runVips(['arrayjoin', tileList, rowOut, '--across', String(rowTiles.length)]);
      rowFiles.push(rowOut);
    }

    // Pass 2: stack all row images vertically into the final output.
    // --across 1 = one image wide = vertical stack.
    const finalOut = path.join(outPath, `${specimenName}.jpg`);
    console.log(`\n  joining ${rowFiles.length} rows → ${finalOut}`);
    runVips(['arrayjoin', rowFiles.join(' '), finalOut, '--across', '1']);

    console.log(`  done → ${finalOut}`);
  } finally {
    // Always remove temp files even if vips fails mid-way.
    for (const f of fs.readdirSync(tmpDir)) fs.unlinkSync(path.join(tmpDir, f));
    fs.rmdirSync(tmpDir);
  }
}

if (!fs.existsSync(outPath)) fs.mkdirSync(outPath, { recursive: true });

// Single-specimen mode: node restitch.js <path-to-zoomify-folder>
// Batch mode (no argument): process every folder inside source_zoomify/
if (process.argv[2]) {
  const srcArg = path.resolve(process.argv[2]);
  stitchSpecimen(path.basename(srcArg), srcArg);
} else {
  for (const specimenName of fs.readdirSync(tilesetsPath)) {
    const srcPath = path.join(tilesetsPath, specimenName);
    if (!fs.lstatSync(srcPath).isDirectory()) continue;
    stitchSpecimen(specimenName, srcPath);
  }
}
