#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
UPSTREAM_RS274="$ROOT_DIR/../linuxcnc/bin/rs274"
GCODE_FIXTURE_DIR="$ROOT_DIR/tests/fixtures/gcode"
CANON_FIXTURE_DIR="$ROOT_DIR/tests/fixtures/canon"
GCODE_ERROR_FIXTURE_DIR="$ROOT_DIR/tests/fixtures/gcode_errors"
CANON_ERROR_FIXTURE_DIR="$ROOT_DIR/tests/fixtures/canon_errors"
BUILD_DIR="$ROOT_DIR/build/native"
CACHE_KEY_FILE="$BUILD_DIR/native-linuxcnc-fixture-baseline.input.sha256"
CACHE_OK_FILE="$BUILD_DIR/native-linuxcnc-fixture-baseline.ok"

FIXTURES=(
  minimal_linear
  arc_semantics
  canned_cycles
  canon_runtime_edges
  comment_logging
  coordinate_offsets
  cutter_comp_motion
  feed_control_modes
  file_open_reset
  g53_machine_coordinates
  length_units
  plane_selection
  modal_incremental
  namedparam_ini_semantics
  numbered_params
  nurbs_g5_semantics
  nurbs_g6_semantics
  oword_subroutine
  percent_file_finish
  position_params
  probe_semantics
  program_end_modal_reset
  spindle_orient
  state_tag_motion
  threading_sync
  tool_semantics
  tool_table_setup
  tool_reload
)

ERROR_FIXTURES=(
  arc_radius_mismatch
  arc_zero_radius
  cutter_comp_plane_change
  g1_zero_feed
  g53_incremental
  namedparam_readonly
  numbered_param_readonly
  tool_length_offset_not_found
  tool_not_found
)

if [[ ! -x "$UPSTREAM_RS274" ]]; then
  echo "missing upstream rs274 executable: $UPSTREAM_RS274" >&2
  exit 1
fi

mkdir -p "$BUILD_DIR"

hash_tree() {
  find "$@" -type f -print0 | sort -z | xargs -0 -r sha256sum
}

fixture_baseline_fingerprint() {
  {
    sha256sum \
      "$ROOT_DIR/tools/verify_native_linuxcnc_fixture_baseline.sh" \
      "$UPSTREAM_RS274"
    "$UPSTREAM_RS274" --version 2>/dev/null || true
    hash_tree \
      "$GCODE_FIXTURE_DIR" \
      "$CANON_FIXTURE_DIR" \
      "$GCODE_ERROR_FIXTURE_DIR" \
      "$CANON_ERROR_FIXTURE_DIR" \
      "$ROOT_DIR/tests/fixtures/ini"
  } | sha256sum | awk '{ print $1 }'
}

FIXTURE_BASELINE_FINGERPRINT="$(fixture_baseline_fingerprint)"
if [[ -f "$CACHE_KEY_FILE" && -f "$CACHE_OK_FILE" ]] &&
   [[ "$(tr -d '[:space:]' < "$CACHE_KEY_FILE")" == "$FIXTURE_BASELINE_FINGERPRINT" ]] &&
   [[ "$(tr -d '[:space:]' < "$CACHE_OK_FILE")" == "$FIXTURE_BASELINE_FINGERPRINT" ]]; then
  echo "native LinuxCNC fixture baseline up to date"
  exit 0
fi

TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
TOOL_TABLE_BASELINE="$TMP_DIR/tool_table_setup.tbl"
cat > "$TOOL_TABLE_BASELINE" <<'TOOL_TABLE'
T0 P0 ; no tool
T2 P2 Z1.25 D0.25 ; standalone fixture tool 2
TOOL_TABLE
TOOL_TABLE_INI="$TMP_DIR/tool_table_setup.ini"
cat > "$TOOL_TABLE_INI" <<'TOOL_INI'
[TRAJ]
LINEAR_UNITS = mm
TOOL_INI

normalize_standalone_events() {
  awk '
    function norm(value) {
      return sprintf("%.12g", value + 0)
    }

    function norm_arc(value, numeric) {
      numeric = value + 0
      if (numeric > -0.00005 && numeric < 0.00005) {
        numeric = 0
      }
      return sprintf("%.4f", numeric)
    }

    /^canon_event=/ || /^(STRAIGHT_(TRAVERSE|FEED)|SET_FEED_RATE) / {
      line = $0
      sub(/^canon_event=/, "", line)

      if (line == "ON_RESET") {
        print line
        next
      }

      if (line ~ /^COMMENT: /) {
        sub(/^COMMENT: /, "", line)
        print "COMMENT|" line
        next
      }

      if (line ~ /^MESSAGE: /) {
        sub(/^MESSAGE: /, "", line)
        print "MESSAGE|" line
        next
      }

      if (line ~ /^LOG(OPEN|APPEND)?: /) {
        op = line
        sub(/:.*/, "", op)
        sub(/^[^:]+: /, "", line)
        print op "|" line
        next
      }

      if (line == "LOGCLOSE" || line == "FINISH" || line == "RELOAD_TOOLDATA") {
        print line
        next
      }

      if (line == "CHANGE_TOOL") {
        print line
        next
      }

      if (line ~ /^NURBS_G5_FEED /) {
        print "NURBS_G5_FEED"
        next
      }

      if (line ~ /^NURBS_G6_FEED /) {
        print "NURBS_G6_FEED"
        next
      }

      if (line == "MIST_ON" || line == "MIST_OFF" || line == "FLOOD_ON" || line == "FLOOD_OFF" ||
          line == "ENABLE_FEED_OVERRIDE" || line == "DISABLE_FEED_OVERRIDE" ||
          line == "DISABLE_ADAPTIVE_FEED" || line == "ENABLE_ADAPTIVE_FEED" ||
          line == "DISABLE_FEED_HOLD" || line == "ENABLE_FEED_HOLD" ||
          line == "PROGRAM_STOP" || line == "PROGRAM_END") {
        print line
        next
      }

      if (line ~ /^SELECT_PLANE plane=/) {
        value = line
        sub(/^SELECT_PLANE plane=/, "", value)
        if (value == "1") {
          print "SELECT_PLANE|XY"
        } else if (value == "2") {
          print "SELECT_PLANE|YZ"
        } else if (value == "3") {
          print "SELECT_PLANE|XZ"
        }
        next
      }

      if (line ~ /^USE_LENGTH_UNITS units=/) {
        value = line
        sub(/^USE_LENGTH_UNITS units=/, "", value)
        if (value == "1") {
          print "USE_LENGTH_UNITS|INCHES"
        } else if (value == "2") {
          print "USE_LENGTH_UNITS|MM"
        }
        next
      }

      if (line ~ /^SET_G5X_OFFSET index=/) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "SET_G5X_OFFSET|" values["index"] "|" norm(values["x"]) "|" \
          norm(values["y"]) "|" norm(values["z"]) "|" norm(values["a"]) "|" \
          norm(values["b"]) "|" norm(values["c"])
        delete values
        next
      }

      if (line ~ /^SET_G92_OFFSET /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "SET_G92_OFFSET|" norm(values["x"]) "|" norm(values["y"]) "|" \
          norm(values["z"]) "|" norm(values["a"]) "|" norm(values["b"]) "|" \
          norm(values["c"])
        delete values
        next
      }

      if (line ~ /^SET_XY_ROTATION rotation=/) {
        value = line
        sub(/^SET_XY_ROTATION rotation=/, "", value)
        print "SET_XY_ROTATION|" norm(value)
        next
      }

      if (line ~ /^SET_FEED_RATE rate=/) {
        value = line
        sub(/^SET_FEED_RATE rate=/, "", value)
        print "SET_FEED_RATE|" norm(value)
        next
      }

      if (line ~ /^SET_SPINDLE_SPEED /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "SET_SPINDLE_SPEED|" values["spindle"] "|" norm(values["speed"])
        delete values
        next
      }

      if (line ~ /^START_SPINDLE_(CLOCKWISE|COUNTERCLOCKWISE) /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print fields[1] "|" values["spindle"]
        delete values
        next
      }

      if (line ~ /^STOP_SPINDLE_TURNING /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "STOP_SPINDLE_TURNING|" values["spindle"]
        delete values
        next
      }

      if (line ~ /^SELECT_TOOL /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "SELECT_TOOL|" values["tool"]
        delete values
        next
      }

      if (line ~ /^CHANGE_TOOL_NUMBER /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "CHANGE_TOOL_NUMBER|" values["pocket"]
        delete values
        next
      }

      if (line ~ /^USE_TOOL_LENGTH_OFFSET /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "USE_TOOL_LENGTH_OFFSET|" norm(values["x"]) "|" norm(values["y"]) "|" norm(values["z"])
        delete values
        next
      }

      if (line ~ /^(ENABLE|DISABLE)_SPEED_OVERRIDE /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print fields[1] "|" values["spindle"]
        delete values
        next
      }

      if (line ~ /^(SET|CLEAR)_(MOTION|AUX)_OUTPUT_BIT /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print fields[1] "|" values["index"]
        delete values
        next
      }

      if (line ~ /^SET_(MOTION|AUX)_OUTPUT_VALUE /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print fields[1] "|" values["index"] "|" norm(values["value"])
        delete values
        next
      }

      if (line ~ /^SET_SPINDLE_MODE /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "SET_SPINDLE_MODE|" values["spindle"] "|" norm(values["css_max"])
        delete values
        next
      }

      if (line ~ /^SET_TOOL_TABLE_ENTRY /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "SET_TOOL_TABLE_ENTRY|" values["toolno"] "|" norm(values["x"]) "|" \
          norm(values["y"]) "|" norm(values["z"]) "|" \
          norm(values["frontangle"]) "|" norm(values["backangle"]) "|" values["orientation"]
        delete values
        next
      }

      if (line ~ /^START_SPEED_FEED_SYNCH /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "START_SPEED_FEED_SYNCH|" norm(values["feed_per_revolution"]) "|" values["velocity_mode"]
        delete values
        next
      }

      if (line == "STOP_SPEED_FEED_SYNCH") {
        print line
        next
      }

      if (line ~ /^RIGID_TAP /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "RIGID_TAP|" norm(values["x"]) "|" norm(values["y"]) "|" norm(values["z"])
        delete values
        next
      }

      if (line ~ /^ARC_FEED /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "ARC_FEED|" norm_arc(values["first_end"]) "|" norm_arc(values["second_end"]) "|" \
          norm_arc(values["first_axis"]) "|" norm_arc(values["second_axis"]) "|" values["rotation"] "|" \
          norm_arc(values["axis_end_point"]) "|" norm_arc(values["a"]) "|" norm_arc(values["b"]) "|" \
          norm_arc(values["c"])
        delete values
        next
      }

      # Upstream rs274 does not emit saicanon WAIT for the M66 case.
      if (line ~ /^WAIT /) {
        next
      }

      if (line ~ /^SET_FEED_MODE /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "SET_FEED_MODE|" values["spindle"] "|" values["mode"]
        delete values
        next
      }

      if (line ~ /^SET_MOTION_CONTROL_MODE /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "SET_MOTION_CONTROL_MODE|" values["mode"] "|" norm(values["tolerance"])
        delete values
        next
      }

      if (line ~ /^SET_NAIVECAM_TOLERANCE tolerance=/) {
        value = line
        sub(/^SET_NAIVECAM_TOLERANCE tolerance=/, "", value)
        print "SET_NAIVECAM_TOLERANCE|" norm(value)
        next
      }

      if (line ~ /^DWELL seconds=/) {
        value = line
        sub(/^DWELL seconds=/, "", value)
        print "DWELL|" norm(value)
        next
      }

      if (line ~ /^ORIENT_SPINDLE /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "ORIENT_SPINDLE|" values["spindle"] "|" norm(values["orientation"]) "|" values["mode"]
        delete values
        next
      }

      if (line ~ /^WAIT_SPINDLE_ORIENT_COMPLETE /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        print "WAIT_SPINDLE_ORIENT_COMPLETE|" values["spindle"] "|" norm(values["timeout"])
        delete values
        next
      }

      if (line == "TURN_PROBE_ON" || line == "TURN_PROBE_OFF") {
        print line
        next
      }

      if (line ~ /^STRAIGHT_PROBE /) {
        print "STRAIGHT_PROBE"
        next
      }

      if (line ~ /^STRAIGHT_(TRAVERSE|FEED) /) {
        split(line, fields, /[[:space:]]+/)
        for (i in fields) {
          split(fields[i], pair, "=")
          values[pair[1]] = pair[2]
        }
        if (!("x" in values)) {
          print fields[1]
          delete values
          next
        }
        print fields[1] "|" norm(values["x"]) "|" norm(values["y"]) "|" \
          norm(values["z"]) "|" norm(values["a"]) "|" norm(values["b"]) "|" \
          norm(values["c"])
        delete values
        next
      }
    }
  '
}

normalize_upstream_rs274_output() {
  awk '
    function norm(value) {
      return sprintf("%.12g", value + 0)
    }

    function norm_arc(value, numeric) {
      numeric = value + 0
      if (numeric > -0.00005 && numeric < 0.00005) {
        numeric = 0
      }
      return sprintf("%.4f", numeric)
    }

    function trim(value) {
      sub(/^[[:space:]]+/, "", value)
      sub(/[[:space:]]+$/, "", value)
      return value
    }

    function args_of(call, args) {
      args = call
      sub(/^[^(]+\(/, "", args)
      sub(/\).*$/, "", args)
      return args
    }

    /^[[:space:]]*[0-9]+[[:space:]]+N\.\.\.\.\./ {
      line = $0
      sub(/^[[:space:]]*[0-9]+[[:space:]]+N\.\.\.\.\.[[:space:]]+/, "", line)

      if (line == "ON_RESET()") {
        if (!seen_reset) {
          print "ON_RESET"
        }
        seen_reset = 1
        next
      }

      if (!seen_reset) {
        next
      }

      if (line ~ /^NURBS_G5_FEED\(/) {
        print "NURBS_G5_FEED"
        next
      }

      if (line ~ /^COMMENT\("/) {
        value = args_of(line)
        sub(/^"/, "", value)
        sub(/"$/, "", value)
        print "COMMENT|" value
        next
      }

      if (line ~ /^MESSAGE\("/) {
        value = args_of(line)
        sub(/^"/, "", value)
        sub(/"$/, "", value)
        print "MESSAGE|" value
        next
      }

      if (line ~ /^LOG(OPEN|APPEND)?\("/) {
        op = line
        sub(/\(.*/, "", op)
        value = args_of(line)
        sub(/^"/, "", value)
        sub(/"$/, "", value)
        print op "|" value
        next
      }

      if (line == "LOGCLOSE()" || line == "FINISH()" || line == "RELOAD_TOOLDATA()") {
        value = line
        sub(/\(\)$/, "", value)
        print value
        next
      }

      if (line == "CHANGE_TOOL()") {
        print "CHANGE_TOOL"
        next
      }

      if (line ~ /^(MIST_ON|MIST_OFF|FLOOD_ON|FLOOD_OFF|ENABLE_FEED_OVERRIDE|DISABLE_FEED_OVERRIDE|DISABLE_ADAPTIVE_FEED|ENABLE_ADAPTIVE_FEED|DISABLE_FEED_HOLD|ENABLE_FEED_HOLD|PROGRAM_STOP|PROGRAM_END)\(\)$/) {
        value = line
        sub(/\(\)$/, "", value)
        print value
        next
      }

      if (line ~ /^SELECT_PLANE\(CANON_PLANE_/) {
        value = line
        sub(/^SELECT_PLANE\(CANON_PLANE_/, "", value)
        sub(/\)$/, "", value)
        print "SELECT_PLANE|" value
        next
      }

      if (line ~ /^USE_LENGTH_UNITS\(CANON_UNITS_/) {
        value = line
        sub(/^USE_LENGTH_UNITS\(CANON_UNITS_/, "", value)
        sub(/\)$/, "", value)
        print "USE_LENGTH_UNITS|" value
        next
      }

      if (line ~ /^SET_G5X_OFFSET\(/) {
        args = args_of(line)
        split(args, values, ",")
        print "SET_G5X_OFFSET|" trim(values[1]) "|" norm(values[2]) "|" norm(values[3]) "|" \
          norm(values[4]) "|" norm(values[5]) "|" norm(values[6]) "|" norm(values[7])
        next
      }

      if (line ~ /^SET_G92_OFFSET\(/) {
        args = args_of(line)
        split(args, values, ",")
        print "SET_G92_OFFSET|" norm(values[1]) "|" norm(values[2]) "|" norm(values[3]) "|" \
          norm(values[4]) "|" norm(values[5]) "|" norm(values[6])
        next
      }

      if (line ~ /^SET_XY_ROTATION\(/) {
        args = args_of(line)
        print "SET_XY_ROTATION|" norm(args)
        next
      }

      if (line ~ /^SET_FEED_RATE\(/) {
        args = args_of(line)
        print "SET_FEED_RATE|" norm(args)
        next
      }

      if (line ~ /^SET_SPINDLE_SPEED\(/) {
        args = args_of(line)
        split(args, values, ",")
        print "SET_SPINDLE_SPEED|" trim(values[1]) "|" norm(values[2])
        next
      }

      if (line ~ /^START_SPINDLE_(CLOCKWISE|COUNTERCLOCKWISE)\(/) {
        op = line
        sub(/\(.*/, "", op)
        args = args_of(line)
        print op "|" trim(args)
        next
      }

      if (line ~ /^STOP_SPINDLE_TURNING\(/) {
        args = args_of(line)
        print "STOP_SPINDLE_TURNING|" trim(args)
        next
      }

      if (line ~ /^SELECT_TOOL\(/) {
        args = args_of(line)
        print "SELECT_TOOL|" trim(args)
        next
      }

      if (line ~ /^CHANGE_TOOL_NUMBER\(/) {
        args = args_of(line)
        print "CHANGE_TOOL_NUMBER|" trim(args)
        next
      }

      if (line ~ /^USE_TOOL_LENGTH_OFFSET\(/) {
        args = args_of(line)
        split(args, groups, ",")
        gsub(/[[:space:]]+/, " ", groups[1])
        split(trim(groups[1]), offset, " ")
        print "USE_TOOL_LENGTH_OFFSET|" norm(offset[1]) "|" norm(offset[2]) "|" norm(offset[3])
        next
      }

      if (line ~ /^(ENABLE|DISABLE)_SPEED_OVERRIDE\(/) {
        op = line
        sub(/\(.*/, "", op)
        args = args_of(line)
        print op "|" trim(args)
        next
      }

      if (line ~ /^(SET|CLEAR)_(MOTION|AUX)_OUTPUT_BIT\(/) {
        op = line
        sub(/\(.*/, "", op)
        args = args_of(line)
        print op "|" trim(args)
        next
      }

      if (line ~ /^SET_(MOTION|AUX)_OUTPUT_VALUE\(/) {
        op = line
        sub(/\(.*/, "", op)
        args = args_of(line)
        split(args, values, ",")
        print op "|" trim(values[1]) "|" norm(values[2])
        next
      }

      if (line ~ /^SET_SPINDLE_MODE\(/) {
        args = args_of(line)
        gsub(/[[:space:]]+/, " ", args)
        split(trim(args), values, " ")
        print "SET_SPINDLE_MODE|" values[1] "|" norm(values[2])
        next
      }

      if (line ~ /^SET_TOOL_TABLE_ENTRY\(/) {
        args = args_of(line)
        split(args, values, ",")
        gsub(/[[:space:]]+/, " ", values[3])
        split(trim(values[3]), offset, " ")
        print "SET_TOOL_TABLE_ENTRY|" trim(values[2]) "|" norm(offset[1]) "|" \
          norm(offset[2]) "|" norm(offset[3]) "|" \
          norm(values[4]) "|" norm(values[5]) "|" trim(values[6])
        next
      }

      if (line ~ /^START_SPEED_FEED_SYNC\(/) {
        args = args_of(line)
        split(args, values, ",")
        print "START_SPEED_FEED_SYNCH|" norm(values[1]) "|" trim(values[2])
        next
      }

      if (line == "STOP_SPEED_FEED_SYNCH()") {
        print "STOP_SPEED_FEED_SYNCH"
        next
      }

      if (line ~ /^RIGID_TAP\(/) {
        args = args_of(line)
        split(args, values, ",")
        print "RIGID_TAP|" norm(values[1]) "|" norm(values[2]) "|" norm(values[3])
        next
      }

      if (line ~ /^ARC_FEED\(/) {
        args = args_of(line)
        split(args, values, ",")
        print "ARC_FEED|" norm_arc(values[1]) "|" norm_arc(values[2]) "|" norm_arc(values[3]) "|" \
          norm_arc(values[4]) "|" trim(values[5]) "|" norm_arc(values[6]) "|" norm_arc(values[7]) "|" \
          norm_arc(values[8]) "|" norm_arc(values[9])
        next
      }

      if (line ~ /^SET_FEED_MODE\(/) {
        args = args_of(line)
        split(args, values, ",")
        print "SET_FEED_MODE|" trim(values[1]) "|" trim(values[2])
        next
      }

      if (line ~ /^SET_MOTION_CONTROL_MODE\(/) {
        args = args_of(line)
        split(args, values, ",")
        mode_name = trim(values[1])
        if (mode_name == "CANON_EXACT_STOP") {
          mode = 1
        } else if (mode_name == "CANON_EXACT_PATH") {
          mode = 2
        } else if (mode_name == "CANON_CONTINUOUS") {
          mode = 3
        } else {
          next
        }
        print "SET_MOTION_CONTROL_MODE|" mode "|" norm(values[2])
        next
      }

      if (line ~ /^SET_NAIVECAM_TOLERANCE\(/) {
        args = args_of(line)
        print "SET_NAIVECAM_TOLERANCE|" norm(args)
        next
      }

      if (line ~ /^DWELL\(/) {
        args = args_of(line)
        print "DWELL|" norm(args)
        next
      }

      if (line ~ /^ORIENT_SPINDLE\(/) {
        args = args_of(line)
        split(args, values, ",")
        print "ORIENT_SPINDLE|" trim(values[1]) "|" norm(values[2]) "|" trim(values[3])
        next
      }

      if (line ~ /^SPINDLE\.[0-9]+\.WAIT_ORIENT_COMPLETE\(/) {
        spindle = line
        sub(/^SPINDLE\./, "", spindle)
        sub(/\.WAIT_ORIENT_COMPLETE.*/, "", spindle)
        args = args_of(line)
        print "WAIT_SPINDLE_ORIENT_COMPLETE|" spindle "|" norm(args)
        next
      }

      if (line == "TURN_PROBE_ON()" || line == "TURN_PROBE_OFF()") {
        value = line
        sub(/\(\)$/, "", value)
        print value
        next
      }

      if (line ~ /^STRAIGHT_PROBE\(/) {
        print "STRAIGHT_PROBE"
        next
      }

      if (line ~ /^STRAIGHT_(TRAVERSE|FEED)\(/) {
        op = line
        sub(/\(.*/, "", op)
        args = args_of(line)
        split(args, values, ",")
        if (op in sparse_ops) {
          print op
          next
        }
        print op "|" norm(values[1]) "|" norm(values[2]) "|" norm(values[3]) "|" \
          norm(values[4]) "|" norm(values[5]) "|" norm(values[6])
        next
      }
    }

    /^N\.\.\.\.\. saicanon NURBS_G6_FEED_/ && seen_reset {
      print "NURBS_G6_FEED"
      next
    }
  '
}

filter_to_expected_event_keys() {
  local expected_norm="$1"

  awk '
    NR == FNR {
      key = $0
      sub(/\|.*/, "", key)
      keys[key] = 1
      next
    }

    {
      key = $0
      sub(/\|.*/, "", key)
      if (key in keys) {
        print
      }
    }
  ' "$expected_norm" -
}

normalize_absent_standalone_event() {
  local absent_line="$1"
  local event="${absent_line#absent=canon_event=}"

  printf 'canon_event=%s\n' "$event" | normalize_standalone_events
}

for name in "${FIXTURES[@]}"; do
  gcode_file="$GCODE_FIXTURE_DIR/$name.ngc"
  expected_file="$CANON_FIXTURE_DIR/$name.events"
  native_raw="$TMP_DIR/$name.rs274.raw"
  native_norm="$TMP_DIR/$name.rs274.normalized"
  native_filtered="$TMP_DIR/$name.rs274.filtered"
  expected_norm="$TMP_DIR/$name.expected.normalized"

  if [[ ! -f "$gcode_file" ]]; then
    echo "missing G-code fixture: $gcode_file" >&2
    exit 1
  fi

  if [[ ! -f "$expected_file" ]]; then
    echo "missing canonical fixture: $expected_file" >&2
    exit 1
  fi

  set +e
  if [[ "$name" == "tool_table_setup" || "$name" == "tool_semantics" ]]; then
    "$UPSTREAM_RS274" -g -i "$TOOL_TABLE_INI" -t "$TOOL_TABLE_BASELINE" "$gcode_file" > "$native_raw" 2>&1
  elif [[ "$name" == "namedparam_ini_semantics" ]]; then
    "$UPSTREAM_RS274" -g -i "$ROOT_DIR/tests/fixtures/ini/namedparams.ini" "$gcode_file" > "$native_raw" 2>&1
  else
    "$UPSTREAM_RS274" -g "$gcode_file" > "$native_raw" 2>&1
  fi
  rs274_rc=$?
  set -e

  if [[ "$rs274_rc" != "0" ]] && ! grep -Fq "File ended with no percent sign (%) or program end (M2)" "$native_raw"; then
    echo "upstream rs274 failed for fixture: $name" >&2
    sed -n '1,160p' "$native_raw" >&2
    exit 1
  fi

  normalize_upstream_rs274_output < "$native_raw" > "$native_norm"
  normalize_standalone_events < "$expected_file" > "$expected_norm"
  if [[ "$name" == "arc_semantics" ]]; then
    grep -v '^COMMENT|' "$native_norm" > "$TMP_DIR/$name.rs274.no-comments"
    grep -v '^COMMENT|' "$expected_norm" > "$TMP_DIR/$name.expected.no-comments"
    mv "$TMP_DIR/$name.rs274.no-comments" "$native_norm"
    mv "$TMP_DIR/$name.expected.no-comments" "$expected_norm"
  fi
  if grep -Eq '^STRAIGHT_(TRAVERSE|FEED)$' "$expected_norm"; then
    sparse_ops_file="$TMP_DIR/$name.sparse_ops"
    grep -E '^STRAIGHT_(TRAVERSE|FEED)$' "$expected_norm" > "$sparse_ops_file"
    awk '
      NR == FNR {
        sparse_ops[$0] = 1
        next
      }

      {
        key = $0
        sub(/\|.*/, "", key)
        if (key in sparse_ops) {
          print key
        } else {
          print
        }
      }
    ' "$sparse_ops_file" "$native_norm" > "$TMP_DIR/$name.rs274.sparse"
    mv "$TMP_DIR/$name.rs274.sparse" "$native_norm"
  fi
  if [[ "$name" == "program_end_modal_reset" ]]; then
    awk '
      /^STRAIGHT_FEED$/ {
        seen_motion = 1
      }

      seen_motion {
        print
      }
    ' "$native_norm" > "$TMP_DIR/$name.rs274.program-end"
    mv "$TMP_DIR/$name.rs274.program-end" "$native_norm"
  fi
  filter_to_expected_event_keys "$expected_norm" < "$native_norm" > "$native_filtered"

  if [[ ! -s "$native_filtered" ]]; then
    echo "no normalized upstream rs274 events for fixture: $name" >&2
    sed -n '1,160p' "$native_raw" >&2
    exit 1
  fi

  if ! diff -u "$expected_norm" "$native_filtered"; then
    echo "native LinuxCNC rs274 baseline mismatch for fixture: $name" >&2
    exit 1
  fi
done

for name in "${ERROR_FIXTURES[@]}"; do
  gcode_file="$GCODE_ERROR_FIXTURE_DIR/$name.ngc"
  expected_file="$CANON_ERROR_FIXTURE_DIR/$name.expected"
  native_raw="$TMP_DIR/$name.error.rs274.raw"
  native_norm="$TMP_DIR/$name.error.rs274.normalized"

  if [[ ! -f "$gcode_file" ]]; then
    echo "missing negative G-code fixture: $gcode_file" >&2
    exit 1
  fi

  if [[ ! -f "$expected_file" ]]; then
    echo "missing negative expected fixture: $expected_file" >&2
    exit 1
  fi

  set +e
  "$UPSTREAM_RS274" -g "$gcode_file" > "$native_raw" 2>&1
  rs274_rc=$?
  set -e

  if [[ "$rs274_rc" == "0" ]]; then
    echo "upstream rs274 unexpectedly succeeded for negative fixture: $name" >&2
    sed -n '1,160p' "$native_raw" >&2
    exit 1
  fi

  normalize_upstream_rs274_output < "$native_raw" > "$native_norm"

  while IFS= read -r expected_line; do
    [[ -z "$expected_line" ]] && continue
    if [[ "$expected_line" == error_text=* ]]; then
      expected_error="${expected_line#error_text=}"
      if ! grep -Fq "$expected_error" "$native_raw"; then
        echo "missing upstream rs274 error text for fixture $name: $expected_error" >&2
        sed -n '1,160p' "$native_raw" >&2
        exit 1
      fi
    elif [[ "$expected_line" == absent=canon_event=* ]]; then
      absent_norm="$(normalize_absent_standalone_event "$expected_line")"
      if grep -Fxq "$absent_norm" "$native_norm"; then
        echo "unexpected upstream rs274 event for fixture $name: $absent_norm" >&2
        sed -n '1,160p' "$native_raw" >&2
        exit 1
      fi
    fi
  done < "$expected_file"
done

printf '%s\n' "$FIXTURE_BASELINE_FINGERPRINT" > "$CACHE_KEY_FILE"
printf '%s\n' "$FIXTURE_BASELINE_FINGERPRINT" > "$CACHE_OK_FILE"
echo "native LinuxCNC fixture baseline validation complete"
