#!/usr/bin/env bash
#
# Package the CodeRunner syntax-highlighting extension as an installable .vsix.
#
# A .vsix is a plain zip with three things at its root: [Content_Types].xml, an
# extension.vsixmanifest, and an extension/ folder holding what the manifest
# points at. This builds it with zip and python3 rather than `vsce`, so it works
# offline and needs no node_modules.
#
# Usage: bash deploy/coderunner-extension/build.sh
#
set -euo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC="$HERE/src"

fail() { printf 'build: %s\n' "$1" >&2; exit 1; }

[ -f "$SRC/package.json" ] || fail "no src/package.json at $SRC"
command -v python3 >/dev/null || fail "python3 is required"

# --- validate the manifest, and read the identity out of it ------------------
# Every JSON file has to parse: VS Code drops an extension whose grammar or
# language configuration is malformed, and does it silently.
# Assigned, not piped or heredoc'd: `set -e` only sees the exit status of a
# command substitution when it is the whole right-hand side of an assignment,
# so a validation failure below has to abort the build here.
META="$(python3 - "$SRC" <<'PY'
import json, os, sys

src = sys.argv[1]
pkg = json.load(open(os.path.join(src, "package.json"), encoding="utf-8"))

errors = []
for key in ("name", "version", "publisher", "engines"):
    if not pkg.get(key):
        errors.append(f"package.json is missing '{key}'")

contributes = pkg.get("contributes", {})
languages = contributes.get("languages") or []
grammars = contributes.get("grammars") or []
if not languages:
    errors.append("package.json contributes no languages")
if not grammars:
    errors.append("package.json contributes no grammars")

# Every path the manifest references must exist and, if JSON, must parse.
referenced = [g.get("path") for g in grammars if g.get("path")]
referenced += [l.get("configuration") for l in languages if l.get("configuration")]
for rel in referenced:
    path = os.path.join(src, rel.lstrip("./"))
    if not os.path.isfile(path):
        errors.append(f"{rel} is referenced but missing")
        continue
    if path.endswith(".json"):
        try:
            json.load(open(path, encoding="utf-8"))
        except ValueError as exc:
            errors.append(f"{rel} is not valid JSON: {exc}")

# A grammar's scopeName must match what the contribution claims, or the editor
# loads the file and then never applies it.
for grammar in grammars:
    path = os.path.join(src, (grammar.get("path") or "").lstrip("./"))
    if os.path.isfile(path):
        declared = json.load(open(path, encoding="utf-8")).get("scopeName")
        if declared != grammar.get("scopeName"):
            errors.append(
                f"scopeName mismatch: package.json says {grammar.get('scopeName')!r}, "
                f"{grammar.get('path')} says {declared!r}"
            )

if errors:
    sys.exit("build: " + "\n       ".join(errors))

# One field per line: display names and descriptions contain spaces.
for value in (pkg["name"], pkg["version"], pkg["publisher"],
              pkg.get("displayName", pkg["name"]), pkg.get("description", "")):
    print(value)
PY
)"

mapfile -t META_FIELDS <<< "$META"
NAME="${META_FIELDS[0]}"
VERSION="${META_FIELDS[1]}"
PUBLISHER="${META_FIELDS[2]}"
DISPLAY="${META_FIELDS[3]}"
DESCRIPTION="${META_FIELDS[4]:-}"

OUT="$HERE/$PUBLISHER.$NAME-$VERSION.vsix"
STAGE="$(mktemp -d)"
trap 'rm -rf "$STAGE"' EXIT

mkdir -p "$STAGE/extension"
cp -r "$SRC"/. "$STAGE/extension/"

cat > "$STAGE/[Content_Types].xml" <<'XML'
<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="json" ContentType="application/json"/>
  <Default Extension="md" ContentType="text/markdown"/>
  <Default Extension="vsixmanifest" ContentType="text/xml"/>
</Types>
XML

python3 - "$STAGE" "$NAME" "$VERSION" "$PUBLISHER" "$DISPLAY" "$DESCRIPTION" <<'PY'
import sys, os
from xml.sax.saxutils import escape

stage, name, version, publisher, display, description = sys.argv[1:7]
manifest = f"""<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011">
  <Metadata>
    <Identity Language="en-US" Id="{escape(name)}" Version="{escape(version)}" Publisher="{escape(publisher)}" />
    <DisplayName>{escape(display)}</DisplayName>
    <Description xml:space="preserve">{escape(description)}</Description>
    <Categories>Programming Languages</Categories>
  </Metadata>
  <Installation>
    <InstallationTarget Id="Microsoft.VisualStudio.Code" />
  </Installation>
  <Dependencies/>
  <Assets>
    <Asset Type="Microsoft.VisualStudio.Code.Manifest" Path="extension/package.json" Addressable="true" />
  </Assets>
</PackageManifest>
"""
open(os.path.join(stage, "extension.vsixmanifest"), "w", encoding="utf-8").write(manifest)
PY

rm -f "$OUT"
python3 - "$STAGE" "$OUT" <<'PY'
import os, sys, zipfile

stage, out = sys.argv[1], sys.argv[2]
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as archive:
    for root, _dirs, files in os.walk(stage):
        for filename in sorted(files):
            path = os.path.join(root, filename)
            archive.write(path, os.path.relpath(path, stage))
PY

printf '\n  wrote %s (%s)\n\n' "$(basename "$OUT")" "$(du -h "$OUT" | cut -f1)"
python3 -c "
import sys, zipfile
for entry in zipfile.ZipFile(sys.argv[1]).namelist():
    print('   ', entry)
" "$OUT"
