#!/bin/bash
#
# Regenerate every figure in the documentation from the listing shown beside it.
#
# A chapter that says "CIRCLE draws ellipses" and shows a listing wants a picture
# of what that listing draws, and the failure mode of a picture is that it stops
# being of the code next to it. Nothing tells you: a screenshot taken by hand in
# 2026 still looks like a screenshot in 2027, long after the verb it illustrates
# has changed.
#
# So the figure is not an asset, it is *output*. A block tagged
#
#     ```basic requires=akgl screenshot=circle
#
# is run by tools/screenshot.c and written to docs/images/circle.png, and the
# chapter shows that file. Regenerate and the picture follows the code. The
# generated PNGs are checked in on purpose -- a reader on the forge has no build
# tree -- and tests/docs_examples.sh fails if a tagged block has no image, so a
# new figure cannot be forgotten.
#
# **This is not run by the build.** `cmake --build build-akgl --target
# docs_screenshots` is deliberate; see the target's comment in CMakeLists.txt.
#
# Exit status is the number of figures that failed, the house convention.

set -u

ROOT=""
TOOL=""
CHECK=0
FAILURES=0

usage()
{
    cat >&2 <<'EOF'
usage: docs_screenshots.sh --root DIR --tool PATH [--check] [FILE...]

  --root DIR    repository root; images are written to DIR/docs/images
  --tool PATH   the built akbasic_screenshot
  --check       render to a scratch directory and compare, changing nothing
  FILE...       which documents to regenerate (default: docs/*.md)
EOF
    exit 2
}

while [ $# -gt 0 ]; do
    case "$1" in
    --root) ROOT="$2"; shift 2 ;;
    --tool) TOOL="$2"; shift 2 ;;
    --check) CHECK=1; shift ;;
    --help|-h) usage ;;
    --*) echo "unknown option $1" >&2; usage ;;
    *) break ;;
    esac
done

[ -n "${ROOT}" ] || usage
[ -n "${TOOL}" ] || usage
[ -x "${TOOL}" ] || { echo "FAIL: no screenshot tool at ${TOOL}" >&2; exit 2; }

# Absolute, because the loop below cd's into a sandbox to run each program --
# a listing that loads an asset resolves it relative to its own directory.
case "${TOOL}" in
/*) ;;
*)  TOOL="${PWD}/${TOOL}" ;;
esac

cd "${ROOT}" || exit 2
ROOT="${PWD}"

DOCS=("$@")
if [ ${#DOCS[@]} -eq 0 ]; then
    DOCS=(docs/*.md)
fi

IMAGES="${ROOT}/docs/images"
mkdir -p "${IMAGES}" || exit 2

WORK="$(mktemp -d)"
trap 'rm -rf "${WORK}"' EXIT

# --check renders somewhere else entirely and compares, so a run that finds a
# stale figure does not also fix it. A test that repairs what it is measuring
# passes the second time for the wrong reason.
if [ "${CHECK}" -eq 1 ]; then
    OUTDIR="${WORK}/rendered"
    mkdir -p "${OUTDIR}" || exit 2
else
    OUTDIR="${IMAGES}"
fi

# The value of attribute $1 in an info string $2, or empty. Same shape as the
# `attr` in tests/docs_examples.sh, and deliberately so -- the two read the same
# fence tags and a second dialect of them would be a bug waiting to happen.
attr()
{
    local name="$1" info="$2" word
    for word in ${info}; do
	case "${word}" in
	"${name}"=*) echo "${word#*=}"; return 0 ;;
	esac
    done
    echo ""
}

# The font a `text=1` figure is drawn with. The bundled one, at the size the
# standalone frontend opens it at, so a figure of a text-grid program has the
# same cell size the reader's own window will have.
FONT="${ROOT}/assets/fonts/C64_Pro_Mono-STYLE.ttf"

# One figure: write the listing to a file, run it, keep the PNG.
render()
{
    local name="$1" body="$2" size="$3" where="$4" setup="$5" text="$6" ui="$7"
    local w=320 h=200 out="${OUTDIR}/${name}.png" log="" font="" grid=""

    if [ -n "${size}" ]; then
	w="${size%x*}"
	h="${size#*x}"
    fi

    # Two separate questions, and they were one until the UI verbs arrived.
    # `text=1` says this program's output is characters rather than drawing, so
    # the tool has to render the grid. `ui=1` says it draws widgets, which need a
    # font but must not have the grid -- the text layer owns every pixel of the
    # rows it covers and would black the picture out underneath them. Both need
    # the font; only `text=1` asks for the layer.
    if [ -n "${text}" ] || [ -n "${ui}" ]; then
	if [ ! -r "${FONT}" ]; then
	    echo "FAIL ${where}: text=1 and ui=1 need a font at ${FONT}" >&2
	    FAILURES=$((FAILURES + 1))
	    return
	fi
	font="${FONT}"
    fi
    if [ -n "${text}" ]; then
	grid="1"
    fi

    # The same setup scripts tests/docs_examples.sh uses, run in the same place
    # relative to the program. A listing that loads `ship.png` needs one whether
    # it is being checked or being photographed.
    if [ -n "${setup}" ]; then
	if [ ! -x "${ROOT}/tests/docs_setups/${setup}.sh" ]; then
	    echo "FAIL ${where}: no setup script ${setup}.sh" >&2
	    FAILURES=$((FAILURES + 1))
	    return
	fi
	( cd "${WORK}" && "${ROOT}/tests/docs_setups/${setup}.sh" ) || {
	    echo "FAIL ${where}: setup ${setup} failed" >&2
	    FAILURES=$((FAILURES + 1))
	    return
	}
    fi

    printf '%s' "${body}" > "${WORK}/${name}.bas"
    # **stdout only.** The tool puts the interpreter's sink on stdout and libakgl
    # logs its registry chatter -- "Actor akbasic:actor:1 initialized" -- to
    # stderr, so merging the two would make every sprite figure look like a
    # failing program. stderr is shown when the tool itself refuses, which is
    # when it is worth reading.
    if ! log="$(cd "${WORK}" && "${TOOL}" "${name}.bas" "${out}" "${w}" "${h}" "${font}" "${grid}" \
			  2>"${WORK}/${name}.err")"; then
	echo "FAIL ${where}: ${name} did not render" >&2
	cat "${WORK}/${name}.err" >&2
	FAILURES=$((FAILURES + 1))
	return
    fi
    # A program that raised prints its error line and still exits zero, because
    # a BASIC error is the script's and not the host's. For a figure that is
    # still a failure: an image of a blank screen is worse than no image.
    if [ -n "${log}" ]; then
	echo "FAIL ${where}: ${name} rendered, but the program reported:" >&2
	echo "${log}" >&2
	FAILURES=$((FAILURES + 1))
	return
    fi

    if [ "${CHECK}" -eq 1 ]; then
	if [ ! -r "${IMAGES}/${name}.png" ]; then
	    echo "FAIL ${where}: docs/images/${name}.png does not exist" >&2
	    FAILURES=$((FAILURES + 1))
	    return
	fi
	if ! cmp -s "${out}" "${IMAGES}/${name}.png"; then
	    echo "FAIL ${where}: docs/images/${name}.png is not what that listing draws" >&2
	    echo "       regenerate with: cmake --build <akgl build> --target docs_screenshots" >&2
	    FAILURES=$((FAILURES + 1))
	    return
	fi
	echo "  ${name}.png matches"
	return
    fi
    echo "  ${name}.png (${w}x${h})"
}

for DOC in "${DOCS[@]}"; do
    [ -r "${DOC}" ] || { echo "FAIL: no document at \"${DOC}\"" >&2; exit 2; }

    # Walk the file collecting fenced blocks. Only `screenshot=` ones are kept;
    # everything else is somebody else's problem, and docs_examples.sh is that
    # somebody.
    IN_BLOCK=0
    INFO=""
    BODY=""
    START=0
    LINENO=0
    while IFS= read -r LINE; do
	LINENO=$((LINENO + 1))
	case "${LINE}" in
	'```'*)
	    if [ "${IN_BLOCK}" -eq 0 ]; then
		IN_BLOCK=1
		INFO="${LINE#'```'}"
		BODY=""
		START="${LINENO}"
	    else
		IN_BLOCK=0
		NAME="$(attr screenshot "${INFO}")"
		if [ -n "${NAME}" ]; then
		    render "${NAME}" "${BODY}" "$(attr size "${INFO}")" \
			   "${DOC}:${START}" "$(attr setup "${INFO}")" \
			   "$(attr text "${INFO}")" "$(attr ui "${INFO}")"
		fi
	    fi
	    ;;
	*)
	    if [ "${IN_BLOCK}" -eq 1 ]; then
		BODY="${BODY}${LINE}"$'\n'
	    fi
	    ;;
	esac
    done < "${DOC}"
done

if [ "${FAILURES}" -ne 0 ]; then
    echo "${FAILURES} figure(s) failed" >&2
fi
exit "${FAILURES}"
