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

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
FREECAD_SOURCE_DIR="${ROOT_DIR}/.cache/freecad/FreeCAD"
OVERLAY_DIR="${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk/source-occt8"
OCCT_MIGRATION="${ROOT_DIR}/.cache/occt/occt/adm/scripts/migration_800/migrate_raise_to_throw.py"
OCCT_TYPEDEF_MIGRATION="${ROOT_DIR}/.cache/occt/occt/adm/scripts/migration_800/replace_typedefs.py"
OCCT_TYPEDEF_MAP="${ROOT_DIR}/.cache/occt/occt/adm/scripts/migration_800/collected_typedefs.json"
OCCT_TYPEDEF_RESULTS="adm/scripts/migration_800/replacement_results.json"
LOCKED_COMMIT="0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"

[[ -f "${FREECAD_SOURCE_DIR}/CMakeLists.txt" ]] || { echo "FreeCAD source is missing: ${FREECAD_SOURCE_DIR}" >&2; exit 1; }
[[ -f "${OCCT_MIGRATION}" ]] || { echo "OCCT 8 migration script is missing: ${OCCT_MIGRATION}" >&2; exit 1; }
[[ -f "${OCCT_TYPEDEF_MIGRATION}" && -f "${OCCT_TYPEDEF_MAP}" ]] || { echo "OCCT 8 typedef migration inputs are missing" >&2; exit 1; }
git -C "${ROOT_DIR}/.cache/occt/occt" diff --quiet -- "${OCCT_TYPEDEF_RESULTS}" || { echo "OCCT migration result file is already modified" >&2; exit 1; }
cleanup_migration_result() {
  git -C "${ROOT_DIR}/.cache/occt/occt" restore -- "${OCCT_TYPEDEF_RESULTS}"
}
trap cleanup_migration_result EXIT
[[ "$(git -C "${FREECAD_SOURCE_DIR}" rev-parse HEAD)" == "${LOCKED_COMMIT}" ]] || { echo "FreeCAD source commit is not locked" >&2; exit 1; }
git -C "${FREECAD_SOURCE_DIR}" diff --quiet
git -C "${FREECAD_SOURCE_DIR}" diff --cached --quiet

if [[ -e "${OVERLAY_DIR}" && ! -f "${OVERLAY_DIR}/.git" ]]; then
  echo "Refusing to replace non-worktree overlay: ${OVERLAY_DIR}" >&2
  exit 1
fi
if [[ -f "${OVERLAY_DIR}/.git" ]]; then
  git -C "${FREECAD_SOURCE_DIR}" worktree remove --force "${OVERLAY_DIR}"
fi
git -C "${FREECAD_SOURCE_DIR}" worktree prune
git -C "${FREECAD_SOURCE_DIR}" worktree add --detach "${OVERLAY_DIR}" "${LOCKED_COMMIT}"

# The OCCT 8 script locates its own source root, so import it as a module and
# apply the documented Raise-to-throw phase only to FreeCAD's source overlay.
PYTHONDONTWRITEBYTECODE=1 python3 - "${OCCT_MIGRATION}" "${OVERLAY_DIR}/src" <<'PY'
import importlib.util
import pathlib
import sys

script = pathlib.Path(sys.argv[1])
source = pathlib.Path(sys.argv[2])
spec = importlib.util.spec_from_file_location("occt8_raise_migration", script)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
files = module.find_files_with_raise(source)
for path in files:
    module.process_file(path)
print(f"Applied OCCT 8 Raise-to-throw migration to {len(files)} FreeCAD source files")
PY
PYTHONDONTWRITEBYTECODE=1 python3 "${OCCT_TYPEDEF_MIGRATION}" \
  "${OVERLAY_DIR}/src" --input "${OCCT_TYPEDEF_MAP}" --jobs "${JOBS:-4}"
# replace_typedefs.py always writes a diagnostic beside the pinned tool. Its
# content is not an SDK input, so restore it immediately after the overlay run.
cleanup_migration_result

# OCCT 8 moved iterator typedefs into their collection headers. The official
# typedef migration replaces usages but intentionally leaves include-only
# references untouched, so map the removed OCCT 7 header names explicitly.
while IFS='|' read -r removed_header collection_header; do
  mapfile -d '' include_files < <(
    rg -l -0 -F "#include <${removed_header}>" "${OVERLAY_DIR}/src" || true
  )
  if (( ${#include_files[@]} > 0 )); then
    sed -i "s|#include <${removed_header}>|#include <${collection_header}>|g" "${include_files[@]}"
  fi
done <<'EOF'
BRepCheck_ListIteratorOfListOfStatus.hxx|BRepCheck_ListOfStatus.hxx
TColStd_ListIteratorOfListOfTransient.hxx|TColStd_ListOfTransient.hxx
TColStd_MapIteratorOfMapOfTransient.hxx|TColStd_MapOfTransient.hxx
TopTools_DataMapIteratorOfDataMapOfIntegerListOfShape.hxx|TopTools_DataMapOfIntegerListOfShape.hxx
TopTools_DataMapIteratorOfDataMapOfShapeShape.hxx|TopTools_DataMapOfShapeShape.hxx
TopTools_ListIteratorOfListOfShape.hxx|TopTools_ListOfShape.hxx
EOF

# Standard_Failure no longer participates in OCCT RTTI. OCCT 8 documents
# ExceptionType() as the source-compatible exception class-name API.
while IFS='|' read -r legacy_expression replacement_expression; do
  mapfile -d '' exception_files < <(
    rg -l -0 -F "${legacy_expression}" "${OVERLAY_DIR}/src" || true
  )
  if (( ${#exception_files[@]} > 0 )); then
    sed -i "s|${legacy_expression}|${replacement_expression}|g" "${exception_files[@]}"
  fi
done <<'EOF'
e.DynamicType()->get_type_name()|e.ExceptionType()
e.DynamicType()->Name()|e.ExceptionType()
ex.DynamicType()->Name()|ex.ExceptionType()
EOF

# BRepAdaptor_Curve exposes these inherited parameter accessors directly in
# OCCT 8; the removed BRepLProp_CurveTool only forwarded these calls here.
while IFS='|' read -r legacy_expression replacement_expression; do
  mapfile -d '' curve_tool_files < <(
    rg -l -0 -F "${legacy_expression}" "${OVERLAY_DIR}/src" || true
  )
  if (( ${#curve_tool_files[@]} > 0 )); then
    sed -i "s|${legacy_expression}|${replacement_expression}|g" "${curve_tool_files[@]}"
  fi
done <<'EOF'
BRepLProp_CurveTool::FirstParameter(adapt)|adapt.FirstParameter()
BRepLProp_CurveTool::LastParameter(adapt)|adapt.LastParameter()
EOF
mapfile -d '' curve_tool_include_files < <(
  rg -l -0 '^[[:space:]]*#[[:space:]]*include[[:space:]]*<BRepLProp_CurveTool\.hxx>' \
    "${OVERLAY_DIR}/src" || true
)
if (( ${#curve_tool_include_files[@]} > 0 )); then
  sed -i -E '\|^[[:space:]]*#[[:space:]]*include[[:space:]]*<BRepLProp_CurveTool\.hxx>|d' \
    "${curve_tool_include_files[@]}"
fi

# OCCT 8 consolidates Geom2dLProp into GeomLProp while preserving the 2D
# specialization's constructor and methods under the unified class name.
mapfile -d '' geom2d_lprop_files < <(
  rg -l -0 -F 'Geom2dLProp_CLProps2d' "${OVERLAY_DIR}/src" || true
)
if (( ${#geom2d_lprop_files[@]} > 0 )); then
  sed -i \
    -e 's|#include <Geom2dLProp_CLProps2d.hxx>|#include <GeomLProp_CLProps.hxx>|g' \
    -e 's|\bGeom2dLProp_CLProps2d\b|GeomLProp_CLProps2d|g' \
    "${geom2d_lprop_files[@]}"
fi

# OCCT 8 adds an initializer-list constructor to NCollection_List, making
# these calls ambiguous with FreeCAD's std::vector<TopoShape> overload. The
# latter was the only viable overload with the pinned FreeCAD/OCCT 7 sources.
TOPO_SHAPE_EXPANSION="${OVERLAY_DIR}/src/Mod/Part/App/TopoShapeExpansion.cpp"
sed -i \
  -e 's|mapper.populate(MappingStatus::Modified, e, {e1, e2, e3, e4});|mapper.populate(MappingStatus::Modified, e, std::vector<TopoShape>{e1, e2, e3, e4});|' \
  -e 's|mapper.populate(MappingStatus::Generated, v, {TopExp::FirstVertex(e1)});|mapper.populate(MappingStatus::Generated, v, std::vector<TopoShape>{TopExp::FirstVertex(e1)});|' \
  -e 's|mapper.populate(MappingStatus::Generated, v, {TopExp::LastVertex(e4)});|mapper.populate(MappingStatus::Generated, v, std::vector<TopoShape>{TopExp::LastVertex(e4)});|' \
  "${TOPO_SHAPE_EXPANSION}"

# Standard_Failure inherits std::exception in OCCT 8 and what() returns the
# same message as the deprecated GetMessageString(). Remove the three now
# unreachable duplicate fallbacks that immediately follow an identical
# std::exception handler.
TOPO_SHAPE_PY="${OVERLAY_DIR}/src/Mod/Part/App/TopoShapePyImp.cpp"
perl -0pi -e '
  s|(    catch \(const std::exception& e\) \{\n        PyErr_SetString\(PartExceptionOCCError, e\.what\(\)\);\n        return nullptr;\n    \})\n    catch \(Standard_Failure& e\) \{\n(?:\n)?        PyErr_SetString\(PartExceptionOCCError, e\.GetMessageString\(\)\);\n        return nullptr;\n    \}|$1|g
' "${TOPO_SHAPE_PY}"

if git -C "${OVERLAY_DIR}" status --porcelain | awk '{print $2}' | grep -Ev '^src/' >/dev/null; then
  echo "OCCT 8 migration changed files outside the FreeCAD src directory" >&2
  exit 1
fi
remaining="$( (rg -l 'Standard_[A-Za-z0-9_]+::Raise\s*\(' "${OVERLAY_DIR}/src" -g '*.{h,hpp,hxx,c,cpp,cxx,lxx,pxx}' || true) | wc -l)"
[[ "${remaining}" == "0" ]] || { echo "OCCT 8 migration left ${remaining} source files with legacy Raise calls" >&2; exit 1; }
removed_iterator_headers="$( (rg -l '#include <(BRepCheck_ListIteratorOfListOfStatus|TColStd_ListIteratorOfListOfTransient|TColStd_MapIteratorOfMapOfTransient|TopTools_DataMapIteratorOfDataMapOfIntegerListOfShape|TopTools_DataMapIteratorOfDataMapOfShapeShape|TopTools_ListIteratorOfListOfShape)\.hxx>' "${OVERLAY_DIR}/src" || true) | wc -l)"
[[ "${removed_iterator_headers}" == "0" ]] || { echo "OCCT 8 migration left ${removed_iterator_headers} files with removed iterator headers" >&2; exit 1; }
legacy_exception_rtti="$( (rg -l '\b(e|ex)\.DynamicType\(\)->(Name|get_type_name)\(\)' "${OVERLAY_DIR}/src" || true) | wc -l)"
[[ "${legacy_exception_rtti}" == "0" ]] || { echo "OCCT 8 migration left ${legacy_exception_rtti} files with legacy exception RTTI" >&2; exit 1; }
legacy_curve_tool="$( (rg -l '\bBRepLProp_CurveTool\b' "${OVERLAY_DIR}/src" || true) | wc -l)"
[[ "${legacy_curve_tool}" == "0" ]] || { echo "OCCT 8 migration left ${legacy_curve_tool} files using removed BRepLProp_CurveTool" >&2; exit 1; }
legacy_geom2d_lprop="$( (rg -l '\bGeom2dLProp_CLProps2d\b' "${OVERLAY_DIR}/src" || true) | wc -l)"
[[ "${legacy_geom2d_lprop}" == "0" ]] || { echo "OCCT 8 migration left ${legacy_geom2d_lprop} files using removed Geom2dLProp_CLProps2d" >&2; exit 1; }
ambiguous_shape_mapper_calls="$( (rg -n 'mapper\.populate\(MappingStatus::(Modified|Generated), (e|v), \{' "${TOPO_SHAPE_EXPANSION}" || true) | wc -l)"
[[ "${ambiguous_shape_mapper_calls}" == "0" ]] || { echo "OCCT 8 migration left ${ambiguous_shape_mapper_calls} ambiguous ShapeMapper calls" >&2; exit 1; }
unreachable_occt_exception_fallbacks="$( (rg -U -n 'catch \(const std::exception& e\) \{\n        PyErr_SetString\(PartExceptionOCCError, e\.what\(\)\);\n        return nullptr;\n    \}\n    catch \(Standard_Failure& e\)' "${TOPO_SHAPE_PY}" || true) | wc -l)"
[[ "${unreachable_occt_exception_fallbacks}" == "0" ]] || { echo "OCCT 8 migration left ${unreachable_occt_exception_fallbacks} unreachable Standard_Failure handlers" >&2; exit 1; }

git -C "${FREECAD_SOURCE_DIR}" diff --quiet
git -C "${FREECAD_SOURCE_DIR}" diff --cached --quiet
echo "FreeCAD OCCT 8 candidate overlay ready: ${OVERLAY_DIR}"
