#!/bin/bash
#
# Refresh the tutorial art in docs/tutorials/assets/ from Kenney.nl.
#
# Those PNGs are tracked on purpose. They are what the two tutorial games in
# examples/ draw, what docs/tutorials/PROVENANCE.md accounts for, and what a
# reader copies into their own project -- so the tree has to build and the
# tutorials have to run with no network access at all. This script therefore
# has the same rule mkcontrollermappings.sh was fixed into for 0.5.0: **it must
# never replace a good tracked asset with a worse one**. Everything is fetched
# and repacked in a temporary directory, every step is checked, and the staged
# files are moved into place only once all of them have come out right. Any
# failure leaves the tracked copies exactly as they were and exits non-zero.
#
# Nothing in the build runs this. It is a deliberate, standalone refresh, and
# its output should land in its own commit so the diff is reviewable.
#
# Two of the eight images are verbatim copies of a Kenney tileset. The other six
# are repacked: 16x16 source art composited bottom-centre into 32x32 cells, one
# row, left to right, which is the layout akgl_spritesheet_coords_for_frame
# indexes. The repack lives here rather than in a second script so that the
# tracked bytes are reproducible from one command.
#
# The JSON (sprites, characters) and the TMJ maps are hand-authored source, not
# generated. This script does not touch them.
#
# Usage: scripts/fetch_tutorial_assets.sh [repository-root]

set -euo pipefail

AKGL_KENNEY_ASSET_URL="${AKGL_KENNEY_ASSET_URL:-https://kenney.nl/assets}"
# A floor, not a target. Each pack page carries its own licence line; a page
# that does not say CC0 is either the wrong page or a relicensed pack, and
# either way its bytes must not land in this repository.
AKGL_KENNEY_LICENSE_TEXT="${AKGL_KENNEY_LICENSE_TEXT:-Creative Commons CC0}"

function die()
{
    echo "fetch_tutorial_assets: $*" >&2
    exit 1
}

function note()
{
    echo "fetch_tutorial_assets: $*"
}

rootdir="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
assetdir="${rootdir}/docs/tutorials/assets"

if [[ ! -d "${assetdir}/sidescroller" ]] || [[ ! -d "${assetdir}/jrpg" ]]; then
    die "no such directory: ${assetdir}/{sidescroller,jrpg}"
fi

command -v curl    >/dev/null 2>&1 || die "curl is not available"
command -v unzip   >/dev/null 2>&1 || die "unzip is not available"
command -v convert >/dev/null 2>&1 || die "ImageMagick 'convert' is not available"
command -v identify >/dev/null 2>&1 || die "ImageMagick 'identify' is not available"

workdir="$(mktemp -d)"

function cleanup()
{
    rm -rf "${workdir}"
}
trap cleanup EXIT

staging="${workdir}/staging"
mkdir -p "${staging}/sidescroller" "${staging}/jrpg"

##
## Fetching
##

# Download one pack, check its page says CC0, and unpack it under ${workdir}/<slug>.
function fetch_pack()
{
    local slug="$1"
    local page="${workdir}/${slug}.html"
    local zip="${workdir}/${slug}.zip"
    local dest="${workdir}/${slug}"
    local url=""

    # --fail so an HTTP error status is an exit status rather than an error page
    # in the body. Not a pipeline: the status being checked is curl's, and a
    # pipeline reports the last command's.
    if ! curl --fail --silent --show-error --location "${AKGL_KENNEY_ASSET_URL}/${slug}" --output "${page}"; then
        die "could not fetch the pack page for ${slug}; tracked assets left as they were"
    fi

    if ! grep -q "${AKGL_KENNEY_LICENSE_TEXT}" "${page}"; then
        die "the ${slug} page does not say '${AKGL_KENNEY_LICENSE_TEXT}'; refusing to vendor it"
    fi

    url="$(grep -o "https://[^'\"]*${slug}\.zip" "${page}" | head -1)"
    if [[ -z "${url}" ]]; then
        die "no download link found on the ${slug} page; tracked assets left as they were"
    fi

    if ! curl --fail --silent --show-error --location "${url}" --output "${zip}"; then
        die "download failed from ${url}; tracked assets left as they were"
    fi

    if ! unzip -tq "${zip}" >/dev/null 2>&1; then
        die "${slug}.zip did not survive an integrity check; tracked assets left as they were"
    fi

    mkdir -p "${dest}"
    if ! unzip -qo "${zip}" -d "${dest}"; then
        die "could not unpack ${slug}.zip; tracked assets left as they were"
    fi

    if ! grep -q "Creative Commons Zero" "${dest}/License.txt"; then
        die "${slug} does not carry a CC0 License.txt; refusing to vendor it"
    fi

    note "fetched ${slug} from ${url}"
}

# Refuse to publish an audio file that is not actually Ogg. `file(1)` is not
# guaranteed to be installed; the container magic is four bytes at offset zero.
function require_ogg()
{
    local file="$1"
    local minbytes="$2"

    [[ -f "${file}" ]] || die "expected file is missing: ${file}"
    if [[ "$(head -c 4 "${file}")" != "OggS" ]]; then
        die "${file} is not an Ogg stream; tracked assets left as they were"
    fi
    if [[ "$(wc -c < "${file}")" -lt "${minbytes}" ]]; then
        die "${file} is under ${minbytes} bytes; tracked assets left as they were"
    fi
}

# Refuse to work from an image that is not the size the frame arithmetic assumes.
function require_size()
{
    local file="$1"
    local want="$2"
    local got=""

    [[ -f "${file}" ]] || die "expected file is missing: ${file}"
    got="$(identify -format '%wx%h' "${file}")"
    if [[ "${got}" != "${want}" ]]; then
        die "${file} is ${got}, expected ${want}; the pack layout has changed and the frame offsets no longer hold"
    fi
}

##
## Repacking
##
## A sheet is one row of 32x32 cells. Each cell holds one 16x16 source tile
## composited at (8, 16) -- horizontally centred, sitting on the cell's bottom
## edge -- so an actor drawn into a 32x32 destination rectangle has its feet on
## the bottom of that rectangle whichever sheet it came from.
##

AKGL_SHEET_CELL=32
AKGL_SOURCE_TILE=16
AKGL_CELL_OFFSET_X=8
AKGL_CELL_OFFSET_Y=16

# build_sheet <source-png> <output-png> <spec>...
#
# Each spec is "X,Y" or "X,Y,flop": the source pixel offset of a 16x16 tile, and
# whether to mirror it horizontally. Specs are laid into cells left to right, so
# the position of a spec in the argument list *is* its frame id.
function build_sheet()
{
    local src="$1"
    local out="$2"
    shift 2

    local -a args=()
    local count=$#
    local idx=0
    local spec=""
    local sx=""
    local sy=""
    local flop=""

    args+=( -size "$((count * AKGL_SHEET_CELL))x${AKGL_SHEET_CELL}" xc:none )
    for spec in "$@"; do
        IFS=',' read -r sx sy flop <<< "${spec}"
        args+=( '(' "${src}" -crop "${AKGL_SOURCE_TILE}x${AKGL_SOURCE_TILE}+${sx}+${sy}" +repage )
        if [[ "${flop:-}" == "flop" ]]; then
            args+=( -flop )
        fi
        args+=( ')' -geometry "+$(( (idx * AKGL_SHEET_CELL) + AKGL_CELL_OFFSET_X ))+${AKGL_CELL_OFFSET_Y}" -composite )
        idx=$((idx + 1))
    done

    # -strip, and the two date properties by name, because ImageMagick writes
    # date:create/date:modify tEXt chunks from the wall clock. Without this a
    # refresh that changed nothing at all still produced eight different files,
    # and the diff would say the art had changed when it had not.
    args+=( -strip +set date:create +set date:modify +set date:timestamp )
    if ! convert "${args[@]}" "${out}"; then
        die "could not repack ${out}; tracked assets left as they were"
    fi
}

# rpg_urban_character <character-index> <output-png>
#
# The RPG Urban Pack lays each character out as a 4-column by 3-row block in the
# last four columns of the tileset: columns are left, down, up, right; rows are
# stand, step A, step B. This regroups one block into the twelve-frame order the
# tutorial sprite JSON expects -- down, left, right, up, three frames each.
function rpg_urban_character()
{
    local charidx="$1"
    local out="$2"
    local src="${workdir}/rpg-urban-pack/Tilemap/tilemap_packed.png"
    local left=368
    local down=384
    local up=400
    local right=416
    local y0=$(( charidx * 3 * 16 ))
    local y1=$(( y0 + 16 ))
    local y2=$(( y0 + 32 ))

    build_sheet "${src}" "${out}" \
        "${down},${y0}"  "${down},${y1}"  "${down},${y2}"  \
        "${left},${y0}"  "${left},${y1}"  "${left},${y2}"  \
        "${right},${y0}" "${right},${y1}" "${right},${y2}" \
        "${up},${y0}"    "${up},${y1}"    "${up},${y2}"
}

##
## Do the work
##

fetch_pack "pixel-line-platformer"
fetch_pack "rpg-urban-pack"
fetch_pack "music-jingles"

plp="${workdir}/pixel-line-platformer/Tilemap/tilemap_packed.png"
rup="${workdir}/rpg-urban-pack/Tilemap/tilemap_packed.png"

require_size "${plp}" "160x96"
require_size "${rup}" "432x288"

# Tilesets are copied verbatim. Both are already 16x16 with zero spacing and
# zero margin, which is the only geometry akgl_tilemap_compute_tileset_offsets
# gets right: it adds `spacing` to the tile pitch but sets the first row's y
# offset to `spacing` rather than zero, and it ignores `margin` entirely.
cp "${plp}" "${staging}/sidescroller/tiles.png"
cp "${rup}" "${staging}/jrpg/tiles.png"

# Pixel Line Platformer tile ids, ten columns: id -> ((id % 10) * 16, (id / 10) * 16)
#   40, 41, 42  the armed rabbit: contact, passing (also the jump pose), contact
#   44          the gold pickup
#   51, 52      the moth, wings up and down
#   55, 56      the red blob, two frames
#
# akgl_actor_render always draws with SDL_FLIP_NONE, so a left-facing run has to
# exist in the sheet as its own frames rather than being mirrored at draw time.
build_sheet "${plp}" "${staging}/sidescroller/player.png" \
    "0,64" "16,64" "32,64" \
    "0,64,flop" "16,64,flop" "32,64,flop"
build_sheet "${plp}" "${staging}/sidescroller/coin.png" \
    "64,64"
build_sheet "${plp}" "${staging}/sidescroller/hazard.png" \
    "80,80" "96,80" "16,80" "32,80"

rpg_urban_character 0 "${staging}/jrpg/player.png"
rpg_urban_character 3 "${staging}/jrpg/npc_shopkeeper.png"
rpg_urban_character 2 "${staging}/jrpg/npc_elder.png"

# One jingle per game, copied unchanged. akgl_load_start_bgm() is the library's
# only file-audio entry point -- akgl_audio_* is a synthesiser, and there is no
# sound-effect loader at all -- so a jingle is what a tutorial can actually
# play. Both are stings of under two seconds, and the loop request in
# akgl_load_start_bgm does not take effect, so each plays once.
cp "${workdir}/music-jingles/Audio/8-Bit jingles/jingles_NES00.ogg" \
   "${staging}/sidescroller/jingle_start.ogg"
cp "${workdir}/music-jingles/Audio/Pizzicato jingles/jingles_PIZZI07.ogg" \
   "${staging}/jrpg/jingle_start.ogg"

cp "${workdir}/pixel-line-platformer/License.txt" "${staging}/LICENSE.kenney_pixel-line-platformer.txt"
cp "${workdir}/rpg-urban-pack/License.txt"        "${staging}/LICENSE.kenney_rpg-urban-pack.txt"
cp "${workdir}/music-jingles/License.txt"         "${staging}/LICENSE.kenney_music-jingles.txt"

##
## Check the staging area before anything is allowed near the tracked copies
##

# name                                   expected size
AKGL_TUTORIAL_ASSETS=(
    "sidescroller/tiles.png              160x96"
    "sidescroller/player.png             192x32"
    "sidescroller/coin.png               32x32"
    "sidescroller/hazard.png             128x32"
    "jrpg/tiles.png                      432x288"
    "jrpg/player.png                     384x32"
    "jrpg/npc_shopkeeper.png             384x32"
    "jrpg/npc_elder.png                  384x32"
)

staged=0
for entry in "${AKGL_TUTORIAL_ASSETS[@]}"; do
    read -r name want <<< "${entry}"
    require_size "${staging}/${name}" "${want}"
    if [[ ! -s "${staging}/${name}" ]]; then
        die "staged ${name} is empty; tracked assets left as they were"
    fi
    staged=$((staged + 1))
done

if [[ "${staged}" -ne "${#AKGL_TUTORIAL_ASSETS[@]}" ]]; then
    die "staged ${staged} images, expected ${#AKGL_TUTORIAL_ASSETS[@]}; tracked assets left as they were"
fi

AKGL_TUTORIAL_AUDIO=(
    "sidescroller/jingle_start.ogg"
    "jrpg/jingle_start.ogg"
)

for name in "${AKGL_TUTORIAL_AUDIO[@]}"; do
    require_ogg "${staging}/${name}" 4096
done

AKGL_TUTORIAL_LICENSES=(
    "LICENSE.kenney_pixel-line-platformer.txt"
    "LICENSE.kenney_rpg-urban-pack.txt"
    "LICENSE.kenney_music-jingles.txt"
)

for name in "${AKGL_TUTORIAL_LICENSES[@]}"; do
    if [[ ! -s "${staging}/${name}" ]]; then
        die "staged ${name} is empty; tracked assets left as they were"
    fi
done

##
## Publish
##

for entry in "${AKGL_TUTORIAL_ASSETS[@]}"; do
    read -r name want <<< "${entry}"
    mv "${staging}/${name}" "${assetdir}/${name}"
done
for name in "${AKGL_TUTORIAL_AUDIO[@]}" "${AKGL_TUTORIAL_LICENSES[@]}"; do
    mv "${staging}/${name}" "${assetdir}/${name}"
done

note "wrote ${staged} images, ${#AKGL_TUTORIAL_AUDIO[@]} jingles and ${#AKGL_TUTORIAL_LICENSES[@]} upstream licence files to ${assetdir}"
