#!/usr/bin/env bash
# Re-cuts the committed vendor/ snapshots when upstream i2pd/Boost/zlib move. Not part of `cargo
# build`: a submodule or build-time fetch can't survive `cargo package`, hence committed sources.
#
# Source this and call the functions individually -- each needs a human to review the diff before
# committing. No `set -euo pipefail` at file scope, since errexit in the sourcing shell would kill
# the session on the next non-zero command; each function body is a `( ... )` subshell that sets it
# for that invocation only.

SCRATCH="${SCRATCH:-$(mktemp -d)}"
# One level up from scripts/, whatever the checkout is named -- not "two up + i2pd-sys/", which
# only worked when the directory happened to be named that and otherwise created a stray tree.
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VENDOR="$REPO_ROOT/vendor"

if ! grep -q '^name = "i2pd-sys"' "$REPO_ROOT/Cargo.toml" 2>/dev/null; then
  echo "error: expected $REPO_ROOT/Cargo.toml to be i2pd-sys's own manifest -- is this script" >&2
  echo "still at <i2pd-sys checkout>/scripts/update-vendor.sh?" >&2
  # `return` when sourced, `exit` when run directly -- a bare `exit` would close the shell.
  # shellcheck disable=SC2317 # the `exit` is reachable: `return` fails outside a sourced script.
  return 1 2>/dev/null || exit 1
fi

echo "Scratch directory: $SCRATCH"
echo "Vendor directory:  $VENDOR"

# i2pd: trim a fresh checkout of the pinned tag to what the CMake build needs.
i2pd_snapshot() (
  set -euo pipefail
  local tag="${1:?usage: i2pd_snapshot <tag, e.g. 2.61.0>}"
  local src="$SCRATCH/i2pd"
  rm -rf "$src"
  git clone --depth 1 --branch "$tag" https://github.com/PurpleI2P/i2pd.git "$src"

  local dst="$VENDOR/i2pd-src"
  rm -rf "$dst"
  mkdir -p "$dst"

  cp -r "$src/libi2pd" "$dst/libi2pd"
  cp -r "$src/build" "$dst/build"

  # GOST R 34.10/34.11 -- see patch 0007. Deleted here so the patch needn't carry ~1300 lines.
  rm -f "$dst/libi2pd/Gost.cpp" "$dst/libi2pd/Gost.h"

  cp "$src/LICENSE" "$dst/LICENSE"
  cp "$src/README.md" "$dst/README.md"
  git -C "$src" rev-parse HEAD > "$dst/SNAPSHOT_COMMIT.txt"

  # Applied automatically so a re-run can't silently ship without them.
  local patch
  for patch in "$REPO_ROOT/scripts/patches"/*.patch; do
    echo "applying $(basename "$patch")..."
    (cd "$dst" && git apply "$patch")
  done

  echo "i2pd $tag snapshotted and trimmed to $dst, with all scripts/patches/*.patch applied --"
  echo "review the diff against a pristine checkout, then re-verify with:"
  echo "  cargo test -p i2pd-sys --test roundtrip -- --ignored"
)

# zlib: just the sources needed to compile libz via cc::Build.
zlib_snapshot() (
  set -euo pipefail
  local version="${1:?usage: zlib_snapshot <version, e.g. 1.3.1>}"
  local tarball="$SCRATCH/zlib-$version.tar.gz"
  # zlib.net's own download links have moved/404'd before; the GitHub release asset is reliable.
  curl -fsSL -o "$tarball" \
    "https://github.com/madler/zlib/releases/download/v$version/zlib-$version.tar.gz"
  tar -xzf "$tarball" -C "$SCRATCH"
  local src="$SCRATCH/zlib-$version"

  local dst="$VENDOR/zlib-src"
  rm -rf "$dst"
  mkdir -p "$dst"
  local f files=(
    adler32.c compress.c crc32.c deflate.c gzclose.c gzlib.c gzread.c gzwrite.c infback.c
    inflate.c inftrees.c inffast.c trees.c uncompr.c zutil.c
    zlib.h zconf.h.in crc32.h deflate.h gzguts.h inffast.h inffixed.h inflate.h inftrees.h
    trees.h zutil.h LICENSE README
  )
  for f in "${files[@]}"; do cp "$src/$f" "$dst/$f"; done
  # No template substitution needed on any platform this crate supports -- only an `@(#)` SCCS
  # comment references the `@` marker autotools would process.
  cp "$dst/zconf.h.in" "$dst/zconf.h"
  # build.rs invalidates on this marker, not the tree -- re-cutting must rewrite it.
  echo "$version" > "$dst/SNAPSHOT_VERSION.txt"

  echo "zlib $version snapshotted to $dst"
)

# Boost: a minimal CMake-buildable subset via `bcp`.
boost_snapshot() (
  set -euo pipefail
  local tag="${1:?usage: boost_snapshot <tag, e.g. boost-1.86.0>}"
  local super="$SCRATCH/boost-superproject"
  # A reused $SCRATCH would otherwise silently reuse a checkout of a different tag.
  if [ -d "$super" ] && [ "$(git -C "$super" describe --tags --exact-match 2>/dev/null)" != "$tag" ]; then
    echo "existing checkout at $super is not tag $tag -- removing and re-cloning"
    rm -rf "$super"
  fi
  if [ ! -d "$super" ]; then
    git clone --branch "$tag" --recurse-submodules --shallow-submodules --depth 1 \
      https://github.com/boostorg/boost.git "$super"
  fi

  # bcp doesn't build against the Boost it ships with: it still calls Filesystem's removed v2 API
  # and misses an explicit <boost/filesystem/directory.hpp>. Known upstream bit-rot; these seds
  # become harmless no-ops if it is ever fixed.
  local bcp_dir="$super/tools/bcp"
  sed -i 's/\.leaf()/.filename()/g; s/\.branch_path()/.parent_path()/g' \
    "$bcp_dir"/add_dependent_lib.cpp "$bcp_dir"/add_path.cpp "$bcp_dir"/copy_path.cpp \
    "$bcp_dir"/file_types.cpp
  sed -i 's/normalized_path\.normalize();/normalized_path = normalized_path.lexically_normal();/' \
    "$bcp_dir"/add_path.cpp
  sed -i 's|#include <boost/filesystem/operations.hpp>|#include <boost/filesystem/operations.hpp>\n#include <boost/filesystem/directory.hpp>|' \
    "$bcp_dir"/add_dependent_lib.cpp "$bcp_dir"/add_path.cpp

  ( cd "$super" && ./bootstrap.sh && ./b2 headers -j"$(nproc)" && ./b2 tools/bcp -j"$(nproc)" )

  local out="$SCRATCH/boost-bcp-out"
  rm -rf "$out"
  mkdir -p "$out"
  "$super/dist/bin/bcp" --boost="$super" --scan \
    "$VENDOR/i2pd-src"/libi2pd/*.cpp "$VENDOR/i2pd-src"/libi2pd/*.h \
    "$VENDOR/i2pd-src"/libi2pd/*.hpp \
    "$out"

  local name sub lib direct_deps=(filesystem program_options atomic system asio property_tree)
  local header_only_deps=(
    algorithm align any assert bind concept_check container container_hash core describe detail
    function functional integer intrusive io iterator lexical_cast move mp11 mpl optional
    preprocessor range scope smart_ptr tuple type_index type_traits utility variant2
    winapi static_assert throw_exception function_types tokenizer array unordered fusion
    conversion typeof variant pool multi_index
    compat # new in Boost 1.90+; Boost::system links it unconditionally
  )
  for name in "${direct_deps[@]}" "${header_only_deps[@]}"; do
    mkdir -p "$out/libs/$name"
    for sub in include src CMakeLists.txt cmake; do
      # rm -rf first: bcp may have partially populated this, and cp -r into an existing directory
      # nests (.../src/src) instead of replacing. The copy from $super is authoritative.
      if [ -e "$super/libs/$name/$sub" ]; then
        rm -rf "$out/libs/$name/$sub"
        cp -r "$super/libs/$name/$sub" "$out/libs/$name/$sub"
      fi
    done
  done
  mkdir -p "$out/libs/numeric/conversion"
  for sub in include src CMakeLists.txt cmake; do
    # An `if`, not `[ ... ] && cp`: as the last statement in the body, a false test returns
    # non-zero and aborts the snapshot under errexit whenever an optional path is absent.
    if [ -e "$super/libs/numeric/conversion/$sub" ]; then
      cp -r "$super/libs/numeric/conversion/$sub" "$out/libs/numeric/conversion/$sub"
    fi
  done
  # Libraries bcp populated with CMakeLists.txt but no include/. Deliberately not `test`: nothing
  # in the vendored subset references boost/test, so the multi-MB framework stays out.
  for name in config exception predef regex timer; do
    # mkdir -p first: whether bcp leaves a stub here at all varies run to run.
    mkdir -p "$out/libs/$name"
    for sub in include src CMakeLists.txt cmake; do
      if [ -e "$super/libs/$name/$sub" ] && [ ! -e "$out/libs/$name/$sub" ]; then
        cp -r "$super/libs/$name/$sub" "$out/libs/$name/$sub"
      fi
    done
  done
  mkdir -p "$out/libs" "$out/tools"
  cp -r "$super/libs/headers" "$out/libs/headers"
  cp "$super/CMakeLists.txt" "$out/CMakeLists.txt"
  cp -r "$super/tools/cmake" "$out/tools/cmake"
  cp "$super/LICENSE_1_0.txt" "$out/LICENSE_1_0.txt"
  cp "$super/README.md" "$out/README.md"

  local all_expected_libs=(
    "${direct_deps[@]}" "${header_only_deps[@]}" numeric/conversion config exception predef regex
    timer headers
  )
  local missing_libs=()
  for lib in "${all_expected_libs[@]}"; do
    if [ ! -e "$out/libs/$lib/CMakeLists.txt" ] && [ ! -d "$out/libs/$lib/include" ]; then
      missing_libs+=("$lib")
    fi
  done
  if [ "${#missing_libs[@]}" -gt 0 ]; then
    echo "error: bcp's output has no usable libs/<name>/ for: ${missing_libs[*]}" >&2
    echo "(only flattened into the throwaway $out/boost/<name> tree instead, if even that --" >&2
    echo "inspect $out/boost/<name>, then copy the real libs/<name>/{include,src,CMakeLists.txt}" >&2
    echo "in from \$super by hand, the same way the header_only_deps loop above does)" >&2
    echo "NOT touching \$VENDOR/boost-src -- the previous snapshot there is untouched." >&2
    return 1
  fi

  # property_tree declares Boost::serialization unconditionally, for a ptree_serialization.hpp
  # feature i2pd never touches (zero includes of boost/serialization or boost/archive). That one
  # edge drags in serialization -> spirit -> {thread -> chrono -> ratio, proto, phoenix}: ~26 MiB
  # and ~2000 files of dead weight.
  sed -i '/Boost::serialization/d' "$out/libs/property_tree/CMakeLists.txt"

  # Second edge, from Boost 1.91 on: the umbrella asio target links asio_spawn (stackful
  # coroutines), which needs Boost::context, a compiled library carrying per-arch assembly.
  # Verified with:
  #   grep -rn 'asio/spawn\|asio::spawn\|yield_context' vendor/i2pd-src/libi2pd shim
  sed -i '/Boost::asio_spawn/d' "$out/libs/asio/CMakeLists.txt"

  # Third edge: asio_deadline_timer is the only thing wanting Boost::date_time, also compiled, and
  # i2pd uses steady_timer exclusively (33 uses, zero deadline_timer). Verified with:
  #   grep -rn 'deadline_timer\|posix_time' vendor/i2pd-src/libi2pd shim
  sed -i '/Boost::date_time/d' "$out/libs/asio/CMakeLists.txt"
  rm -rf "$out/libs"/{serialization,spirit,phoenix,proto,thread,chrono,ratio}
  # Boost's internal dependency graph is not stable across versions -- re-check against a new tag:
  #   for l in serialization spirit phoenix proto thread chrono ratio; do
  #     grep -l "Boost::$l\b" "$out"/libs/*/CMakeLists.txt; done
  # should print nothing for every name still safe to omit.

  # Drop what isn't needed to compile: tests, docs, examples, the b2 Jamfiles, and the flat
  # top-level boost/ tree, which BoostRoot.cmake never adds to any include path.
  find "$out/libs" -maxdepth 2 -type d \
    \( -name test -o -name doc -o -name example -o -name examples -o -name xmldoc -o -name data \
       -o -name build -o -name meta -o -name check \) -exec rm -rf {} +
  find "$out" -iname "Jamfile*" -delete
  find "$out" -name "*.jam" -not -path "*/tools/cmake/*" -delete
  rm -rf "$out/boost" "$out/boost.png" "$out/Jamroot"
  # These were copied wholesale, so they carry a test/ suite and a dangling `.git` gitlink into
  # this function's own $super checkout.
  rm -rf "$out/tools/cmake/test"
  find "$out" -name ".git" -delete

  rm -rf "$VENDOR/boost-src"
  cp -r "$out" "$VENDOR/boost-src"
  # build.rs invalidates on this marker, not the directory.
  echo "$tag" > "$VENDOR/boost-src/SNAPSHOT_TAG.txt"

  echo "Boost $tag subset snapshotted to $VENDOR/boost-src -- re-verify with:"
  echo "  cmake -S $VENDOR/boost-src -B /tmp/boost-verify -DBOOST_INCLUDE_LIBRARIES=filesystem\;program_options\;atomic\;system\;asio\;property_tree -DBUILD_TESTING=OFF"
  echo "  cmake --build /tmp/boost-verify -j\$(nproc)"
)

echo "This script defines functions (i2pd_snapshot, zlib_snapshot, boost_snapshot) -- source it"
echo "and call them individually, e.g.:"
echo "  source i2pd-sys/scripts/update-vendor.sh"
echo "  i2pd_snapshot 2.61.0   # applies scripts/patches/*.patch automatically, review the diff"
echo "  zlib_snapshot 1.3.1"
echo "  boost_snapshot boost-1.86.0   # NOTE: verify its output carefully -- see the warning"
echo "                                # printed by boost_snapshot itself before trusting it"
