# Copyright (c) VOLVICON.
#
# This file is provided solely for educational purposes in connection with
# the Volvicon software and related documentation.
#
# You may use and modify this file for personal or internal purposes when
# working with Volvicon products. Any reproduction, distribution, or use
# of this file outside the Volvicon ecosystem is strictly prohibited
# without prior written authorization from VOLVICON.

"""
Advanced AI segmentation workflow.

This tutorial script demonstrates a complete post-segmentation pipeline that:
- runs TotalSegmentator or reuses existing masks,
- converts masks into printable surface meshes,
- cleans the meshes for downstream manufacturing workflows,
- computes wall thickness on each mesh,
- optionally performs gray value analysis against the source CT,
- exports STL meshes, color-mapped PLY meshes, screenshots, and reports.

Prerequisites
- The Volvicon application must be running
- At least one multilabel segmentation mask must be available in the project, or TotalSegmentator must be installed and configured in the AI Segmentation module.
"""

# =============================================================================
# Scripting Api Initialization
# =============================================================================

import ScriptingApi as api
import os
from datetime import datetime

# Create Application instance
app = api.Application()

ai_segmentation = app.get_ai_segmentation()
volume_operations = app.get_volume_operations()
mask_operations = app.get_mask_operations()
surface_operations = app.get_surface_operations()
analysis_operations = app.get_analysis_operations()
surface_render_properties_operations = surface_operations.get_render_properties_operations()

# =============================================================================
# Workflow Configuration
# =============================================================================
# Set REUSE_EXISTING_MASKS to True if you want to skip AI segmentation and run
# the downstream mesh workflow on masks that already exist in the project.
REUSE_EXISTING_MASKS = True

# TotalSegmentator configuration.
TOTAL_SEGMENTATOR_TASK = "total"
TOTAL_SEGMENTATOR_DEVICE = "gpu"
TOTAL_SEGMENTATOR_FASTEST = True
MAKE_IMAGE_ORIENTATION_COMPATIBLE = True

# When the workflow receives a single multilabel mask, it can split only the
# largest structures to keep the tutorial focused on printable anatomy.
SEPARATE_ONLY_LARGEST_LABELS = True
NUM_LARGEST_LABELS = 1

# Optional advanced step for reporting volume intensities on each surface.
ENABLE_GRAY_VALUE_ANALYSIS = True

# Surface preparation and wall-thickness display settings.
SMOOTH_ITERATIONS = 40
SMOOTH_FACTOR = 0.02
REDUCE_PERCENTAGE = 90.0
WALL_THICKNESS_SEARCH_ANGLE_DEG = 20.0
WALL_THICKNESS_DISPLAY_RANGE_MM = [2.0, 20.0]

OUTPUT_PREFIX = "Volvicon_AI_Segmentation_Workflow"

def print_step(step_number, title):
    print("\n" + "=" * 80)
    print(f"STEP {step_number}: {title}")
    print("=" * 80)


def print_info(message):
    print(f"[INFO] {message}")


def print_warning(message):
    print(f"[WARN] {message}")


def print_error(message):
    print(f"[ERROR] {message}")


def safe_name(name):
    invalid_chars = '<>:"/\\|?*'
    sanitized = "".join("_" if character in invalid_chars else character for character in name)
    sanitized = sanitized.strip().replace(" ", "_")
    return sanitized or "object"


def format_value(value, decimals=3, suffix=""):
    if value is None:
        return "n/a"

    if isinstance(value, str):
        return value

    return f"{float(value):.{decimals}f}{suffix}"


def ensure_directory(*parts):
    directory = os.path.join(*parts)
    os.makedirs(directory, exist_ok=True)
    return directory


def build_output_layout(output_root):
    analysis_root = ensure_directory(output_root, "analysis")
    mesh_root = ensure_directory(output_root, "meshes")

    return {
        "root": output_root,
        "reports": ensure_directory(output_root, "reports"),
        "screenshots": ensure_directory(output_root, "screenshots"),
        "wall_thickness": ensure_directory(analysis_root, "wall_thickness"),
        "gray_value": ensure_directory(analysis_root, "gray_value"),
        "stl_meshes": ensure_directory(mesh_root, "stl"),
        "colored_meshes": ensure_directory(mesh_root, "analysis_color_ply"),
    }


def to_rgb8(color_components):
    rgb = [0.0, 0.0, 0.0]
    for index in range(min(3, len(color_components))):
        rgb[index] = float(color_components[index])

    return [max(0, min(255, int(round(component * 255.0)))) for component in rgb]


def create_or_reuse_masks(volume_name):
    existing_masks = app.get_all_mask_names()
    if REUSE_EXISTING_MASKS and existing_masks:
        print_info(f"Reusing {len(existing_masks)} existing mask(s): {existing_masks}")
        return existing_masks, "existing"

    ai_segmentation.set_model_type(api.AiSegmentationModelType.TotalSegmentator)

    if not ai_segmentation.get_installation_status():
        print_info("Installing the active TotalSegmentator environment. This may take several minutes.")
        if not ai_segmentation.install_model(True):
            raise RuntimeError("TotalSegmentator installation failed.")

    total_segmentator_params = ai_segmentation.get_default_total_segmentator_params()
    total_segmentator_params.task = TOTAL_SEGMENTATOR_TASK
    total_segmentator_params.fastest = TOTAL_SEGMENTATOR_FASTEST
    total_segmentator_params.device = TOTAL_SEGMENTATOR_DEVICE

    mask_names = ai_segmentation.run_total_segmentator(
        [volume_name],
        total_segmentator_params,
        True,
        True,
        [],
        MAKE_IMAGE_ORIENTATION_COMPATIBLE,
    )

    if not mask_names:
        raise RuntimeError("Segmentation finished without creating any masks.")

    print_info(f"Generated {len(mask_names)} segmentation mask(s): {mask_names}")
    return mask_names, "totalsegmentator"


def prepare_structure_masks(volume_name, initial_masks):
    if len(initial_masks) > 1:
        print_info("Multiple masks are already available. The workflow will process them directly.")
        return initial_masks, None

    candidate_mask = initial_masks[0]
    try:
        split_masks = mask_operations.split_multi_label_mask(
            volume_name,
            candidate_mask,
            SEPARATE_ONLY_LARGEST_LABELS,
            max(1, NUM_LARGEST_LABELS),
        )
    except Exception as exception:
        print_warning(f"Mask splitting failed for '{candidate_mask}'. Continuing with the original mask. Details: {exception}")
        return [candidate_mask], candidate_mask

    if not split_masks:
        print_warning(f"Mask splitting produced no child masks for '{candidate_mask}'. Continuing with the original mask.")
        return [candidate_mask], candidate_mask

    print_info(f"Prepared {len(split_masks)} structure mask(s) from '{candidate_mask}'.")
    return split_masks, candidate_mask


def convert_masks_to_surfaces(mask_names):
    mask_to_surface_params = api.MaskToSurfaceParams()
    surface_records = []

    for index, mask_name in enumerate(mask_names, start=1):
        try:
            created_surfaces = mask_operations.convert_to_surface_objects([mask_name], mask_to_surface_params)
            if not created_surfaces:
                print_warning(f"No surface was created for mask '{mask_name}'.")
                continue

            surface_name = created_surfaces[0]
            surface_records.append(
                {
                    "index": index,
                    "mask_name": mask_name,
                    "surface_name": surface_name,
                }
            )
            print_info(f"Converted mask '{mask_name}' to surface '{surface_name}'.")
        except Exception as exception:
            print_warning(f"Failed to convert mask '{mask_name}' to a surface. Details: {exception}")

    return surface_records


def optimize_surface_for_printing(surface_name):
    # Remove disconnected surfaces and retain only the largest one
    shell_filter_params = api.SurfaceFilterShellsParams()
    shell_filter_params.largest_shells = 1
    surface_operations.filter_shells(surface_name, api.SurfaceFilterShellsMethod.LargestShells, shell_filter_params)

    # Apply smoothing to improve surface quality for 3D printing.
    surface_operations.smooth_smart([surface_name], SMOOTH_ITERATIONS, SMOOTH_FACTOR)

    # Reduce or decimate the surface to optimize for 3D printing.
    surface_operations.reduce([surface_name], REDUCE_PERCENTAGE)

    # Remesh the surface to create a more uniform triangle distribution.
    surface_operations.remesh([surface_name], api.SurfaceRemeshMethod.Regular)

    # Run diagnostics_checks and fix any issues that could impact 3D printability, such as non-manifold edges or holes.
    diagnostics_checks = api.SurfaceDiagnosticsChecks()
    surface_operations.fix_surface(surface_name, diagnostics_checks)


def run_wall_thickness_analysis(surface_record, output_layout):
    surface_name = surface_record["surface_name"]
    file_stem = f"{surface_record['index']:02d}_{safe_name(surface_name)}"

    # Restore the default visualization settings for all analyses on the surface to ensure a
    # clean starting point for the wall-thickness analysis display.
    for analysis in app.get_all_analysis_names():
        analysis_operations.restore_visualizations(analysis)

    # Configure the wall-thickness analysis parameters.
    wall_thickness_params = api.WallThicknessParams()
    wall_thickness_params.method = api.WallThicknessMethod.RayCasting
    wall_thickness_params.max_wall_thickness = WALL_THICKNESS_DISPLAY_RANGE_MM[1]
    wall_thickness_params.search_angle = WALL_THICKNESS_SEARCH_ANGLE_DEG

    # Create the wall-thickness analysis object for the surface.
    analysis_name = analysis_operations.create_wall_thickness_analysis_surface("", surface_name, wall_thickness_params)
    if not analysis_name:
        raise RuntimeError("Failed to create the wall-thickness analysis object.")

    # Run the analysis and wait for it to complete. The results will be stored in the analysis object and can be retrieved after completion.
    if not analysis_operations.run_analysis(analysis_name):
        raise RuntimeError(f"run_analysis returned False for '{analysis_name}'.")

    if not analysis_operations.has_analysis_results(analysis_name):
        raise RuntimeError(f"No wall-thickness results are available for '{analysis_name}'.")

    # Retrieve the results and statistics objects from the analysis.
    results : api.WallThicknessAnalysisResults = analysis_operations.get_wall_thickness_analysis_results(analysis_name)
    stats : api.WallThicknessAnalysisStatistics = results.statistics

    # Write the wall-thickness results to a text file for reporting and record-keeping.
    output_path = os.path.join(output_layout["wall_thickness"], f"{file_stem}_wall_thickness.txt")
    analysis_operations.write_wall_thickness_results_to_disk(results, output_path)

    # Configure the display settings for the wall-thickness analysis to use a color map that highlights thin and thick areas,
    # and set the display range to focus on the most relevant thickness values for 3D printing.
    display_settings = analysis_operations.get_display_settings(analysis_name)
    display_settings.lookup_table_type = api.LookupTableType.ReverseRainbow
    display_settings.range = list(WALL_THICKNESS_DISPLAY_RANGE_MM)
    analysis_operations.set_display_settings(analysis_name, display_settings)
    analysis_operations.update_analysis(analysis_name)

    print_info(
        " | ".join(
            [
                f"Wall thickness for '{surface_name}'",
                f"min={format_value(stats.min_value, suffix=' mm')}",
                f"mean={format_value(stats.mean_value, suffix=' mm')}",
                f"max={format_value(stats.max_value, suffix=' mm')}",
                f"within range={format_value(stats.percentage_within_range, suffix='%')}",
            ]
        )
    )

    return {
        "analysis_name": analysis_name,
        "report_path": output_path,
        "statistics": {
            "min_value": stats.min_value,
            "max_value": stats.max_value,
            "mean_value": stats.mean_value,
            "std_deviation": stats.std_deviation,
            "percentage_within_range": stats.percentage_within_range,
            "percentage_below_range": stats.percentage_below_range,
            "percentage_above_range": stats.percentage_above_range,
            "total_area": stats.total_area,
        },
    }


def export_surface_meshes(surface_record, output_layout):
    surface_name = surface_record["surface_name"]
    wall_thickness_name = surface_record["wall_thickness"]["analysis_name"]
    file_stem = f"{surface_record['index']:02d}_{safe_name(surface_name)}"

    stl_path = os.path.join(output_layout["stl_meshes"], f"{file_stem}.stl")
    if not surface_operations.export_surface_to_disk(surface_name, stl_path, False):
        raise RuntimeError(f"Failed to export STL for '{surface_name}'.")

    analysis_operations.update_analysis(wall_thickness_name)

    # Ensure that the surface per-vertex colors are visible in the render view before exporting the color-mapped PLY,
    # otherwise the exported geometry data may not contain the color arrays.
    surface_render_properties_operations.set_scalar_visibility([surface_name], True)

    # Export a color-mapped PLY mesh that encodes the wall-thickness values as vertex colors.
    colored_mesh_path = os.path.join(output_layout["colored_meshes"], f"{file_stem}_wall_thickness.ply")   
    ok = surface_operations.export_surface_to_disk(surface_name, colored_mesh_path)

    if not ok:
        print_warning(f"'{surface_name}' failed to export.")
    else:
        print_info(f"Exported color-mapped PLY mesh to '{colored_mesh_path}'.")

    return {
        "stl_path": stl_path,
        "colored_mesh_path": colored_mesh_path,
    }


def capture_surface_screenshot(surface_record, output_layout):
    surface_name = surface_record["surface_name"]
    wall_thickness_name = surface_record["wall_thickness"]["analysis_name"]
    file_stem = f"{surface_record['index']:02d}_{safe_name(surface_name)}"

    app.set_volumes_visible(app.get_all_volume_names(), False)
    app.set_masks_visible(app.get_all_mask_names(), False)
    app.isolate_surfaces([surface_name])
    surface_render_properties_operations.set_opacity([surface_name], 1.0)
    surface_render_properties_operations.set_scalar_visibility([surface_name], True)
    analysis_operations.update_analysis(wall_thickness_name)

    screenshot_path = os.path.join(output_layout["screenshots"], f"{file_stem}_wall_thickness.png")
    if not app.save_snapshot_to_disk(api.SnapshotType.Scene, screenshot_path, "PNG"):
        raise RuntimeError(f"Failed to save the screenshot for '{surface_name}'.")

    print_info(f"Saved wall-thickness screenshot to '{screenshot_path}'.")
    return screenshot_path


def run_gray_value_analysis(surface_record, volume_name, output_layout):
    surface_name = surface_record["surface_name"]
    file_stem = f"{surface_record['index']:02d}_{safe_name(surface_name)}"

    # If the workflow has already run a wall-thickness analysis, there may be existing visualization
    # settings that could interfere with the gray-value analysis display. To ensure a clean starting
    # point, we can restore the default visualization settings for all analyses on the surface before
    # creating the new gray-value analysis.
    for analysis in app.get_all_analysis_names():
        analysis_operations.restore_visualizations(analysis)

    # Configure the gray-value analysis parameters.
    gray_value_params = api.GrayValueParams()
    analysis_name = analysis_operations.create_gray_value_analysis_surface("", surface_name, volume_name, gray_value_params)
    if not analysis_name:
        raise RuntimeError("Failed to create the gray-value analysis object.")

    # Run the analysis and wait for it to complete. The results will be stored in the analysis object and can be retrieved after completion.
    if not analysis_operations.run_analysis(analysis_name):
        raise RuntimeError(f"run_analysis returned False for '{analysis_name}'.")

    if not analysis_operations.has_analysis_results(analysis_name):
        raise RuntimeError(f"No gray-value results are available for '{analysis_name}'.")

    # Retrieve the results and statistics objects from the analysis.
    results : api.GrayValueAnalysisResults = analysis_operations.get_gray_value_analysis_results(analysis_name)
    stats : api.GrayValueAnalysisStatistics = results.statistics

    # Write the gray-value results to a text file for reporting and record-keeping.
    output_path = os.path.join(output_layout["gray_value"], f"{file_stem}_gray_value.txt")
    analysis_operations.write_gray_value_results_to_disk(results, output_path)

    print_info(
        " | ".join(
            [
                f"Gray values for '{surface_name}'",
                f"min={format_value(stats.min_value)}",
                f"mean={format_value(stats.mean_value)}",
                f"max={format_value(stats.max_value)}",
                f"std={format_value(stats.std_deviation)}",
            ]
        )
    )

    # Configure the display settings for the gray-value analysis to use a color map that
    # highlights different intensity ranges.
    display_settings = analysis_operations.get_display_settings(analysis_name)
    display_settings.lookup_table_type = api.LookupTableType.ReverseRainbow
    display_settings.range = volume_operations.get_scalar_range(app.get_active_volume_name())
    analysis_operations.set_display_settings(analysis_name, display_settings)
    analysis_operations.update_analysis(analysis_name)

    # Export a color-mapped PLY mesh that encodes the gray-value analysis results as vertex/face colors.
    colored_mesh_path = os.path.join(output_layout["colored_meshes"], f"{file_stem}_gray_value.ply") 
    ok = surface_operations.export_surface_to_disk(surface_name, colored_mesh_path)
    if not ok:
        print_warning(f"'{surface_name}' failed to export.")
    else:
        print_info(f"Exported color-mapped PLY mesh to '{colored_mesh_path}'.")

    return {
        "analysis_name": analysis_name,
        "report_path": output_path,
        "statistics": {
            "min_value": stats.min_value,
            "max_value": stats.max_value,
            "mean_value": stats.mean_value,
            "std_deviation": stats.std_deviation,
        },
    }


def write_summary_report(report_path, timestamp, volume_name, output_layout, segmentation_source, multilabel_mask_name, surface_records):
    with open(report_path, "w", encoding="utf-8", newline="\n") as stream:
        stream.write("=" * 80 + "\n")
        stream.write("VOLVICON AI SEGMENTATION AND MESH PREPARATION WORKFLOW REPORT\n")
        stream.write("=" * 80 + "\n\n")

        stream.write(f"Timestamp: {timestamp}\n")
        stream.write(f"Source volume: {volume_name}\n")
        stream.write(f"Segmentation source: {segmentation_source}\n")
        stream.write(f"Primary multilabel mask: {multilabel_mask_name or 'n/a'}\n")
        stream.write(f"Output root: {output_layout['root']}\n\n")

        stream.write("CONFIGURATION\n")
        stream.write("-" * 80 + "\n")
        stream.write(f"TotalSegmentator task: {TOTAL_SEGMENTATOR_TASK}\n")
        stream.write(f"TotalSegmentator device: {TOTAL_SEGMENTATOR_DEVICE}\n")
        stream.write(f"Reuse existing masks: {REUSE_EXISTING_MASKS}\n")
        stream.write(f"Largest-label splitting: {SEPARATE_ONLY_LARGEST_LABELS}\n")
        stream.write(f"Requested largest labels: {NUM_LARGEST_LABELS}\n")
        stream.write(f"Gray-value analysis enabled: {ENABLE_GRAY_VALUE_ANALYSIS}\n")
        stream.write(
            f"Wall-thickness display range (mm): {WALL_THICKNESS_DISPLAY_RANGE_MM[0]} to {WALL_THICKNESS_DISPLAY_RANGE_MM[1]}\n\n"
        )

        stream.write("STRUCTURE RESULTS\n")
        stream.write("-" * 80 + "\n")
        for surface_record in surface_records:
            stream.write(
                f"\n[{surface_record['index']:02d}] Mask '{surface_record['mask_name']}' -> Surface '{surface_record['surface_name']}'\n"
            )

            if surface_record.get("wall_thickness"):
                wall_thickness = surface_record["wall_thickness"]
                stream.write("  Wall thickness:\n")
                stream.write(f"    Analysis: {wall_thickness['analysis_name']}\n")
                stream.write(
                    f"    Min / Mean / Max (mm): {format_value(wall_thickness['statistics']['min_value'])} / "
                    f"{format_value(wall_thickness['statistics']['mean_value'])} / "
                    f"{format_value(wall_thickness['statistics']['max_value'])}\n"
                )
                stream.write(f"    Std. deviation (mm): {format_value(wall_thickness['statistics']['std_deviation'])}\n")
                stream.write(
                    f"    Area within range (%): {format_value(wall_thickness['statistics']['percentage_within_range'])}\n"
                )
                stream.write(f"    Text report: {wall_thickness['report_path']}\n")
            else:
                stream.write("  Wall thickness: not available\n")

            if surface_record.get("exports"):
                exports = surface_record["exports"]
                stream.write("  Mesh exports:\n")
                stream.write(f"    STL: {exports['stl_path']}\n")
                stream.write(f"    Wall-thickness PLY: {exports['colored_mesh_path']}\n")

            if surface_record.get("screenshot_path"):
                stream.write(f"  Screenshot: {surface_record['screenshot_path']}\n")

            if surface_record.get("gray_value"):
                gray_value = surface_record["gray_value"]
                stream.write("  Gray-value analysis:\n")
                stream.write(f"    Analysis: {gray_value['analysis_name']}\n")
                stream.write(
                    f"    Min / Mean / Max: {format_value(gray_value['statistics']['min_value'])} / "
                    f"{format_value(gray_value['statistics']['mean_value'])} / "
                    f"{format_value(gray_value['statistics']['max_value'])}\n"
                )
                stream.write(f"    Std. deviation: {format_value(gray_value['statistics']['std_deviation'])}\n")
                stream.write(f"    Text report: {gray_value['report_path']}\n")
            elif ENABLE_GRAY_VALUE_ANALYSIS:
                stream.write("  Gray-value analysis: not available\n")
            else:
                stream.write("  Gray-value analysis: disabled by configuration\n")

        stream.write("\nEXPORT DIRECTORIES\n")
        stream.write("-" * 80 + "\n")
        stream.write(f"STL meshes: {output_layout['stl_meshes']}\n")
        stream.write(f"Colored PLY meshes: {output_layout['colored_meshes']}\n")
        stream.write(f"Wall-thickness reports: {output_layout['wall_thickness']}\n")
        stream.write(f"Gray-value reports: {output_layout['gray_value']}\n")
        stream.write(f"Screenshots: {output_layout['screenshots']}\n")


def main():
    volume_name = app.get_active_volume_name()
    if not volume_name:
        print_error("No active volume is available. Open a CT scan before running this workflow.")
        raise SystemExit(1)

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    output_root = os.path.abspath(os.path.expanduser(f"~/{OUTPUT_PREFIX}_{timestamp}"))
    output_layout = build_output_layout(output_root)

    print_info(f"Active volume: {volume_name}")
    print_info(f"Output root: {output_root}")

    print_step(1, "Create or reuse segmentation masks")
    try:
        initial_masks, segmentation_source = create_or_reuse_masks(volume_name)
    except Exception as exception:
        print_error(f"Segmentation step failed. Details: {exception}")
        raise SystemExit(1)

    print_step(2, "Prepare structure masks for downstream mesh generation")
    structure_masks, multilabel_mask_name = prepare_structure_masks(volume_name, initial_masks)
    if not structure_masks:
        print_error("No masks are available for surface generation.")
        raise SystemExit(1)

    print_step(3, "Convert masks to surfaces")
    surface_records = convert_masks_to_surfaces(structure_masks)
    if not surface_records:
        print_error("The workflow could not create any surfaces from the prepared masks.")
        raise SystemExit(1)

    print_step(4, "Optimize the surfaces for 3D-printing workflows")
    for surface_record in surface_records:
        surface_name = surface_record["surface_name"]
        try:
            optimize_surface_for_printing(surface_name)
            print_info(f"Optimized surface '{surface_name}'.")
        except Exception as exception:
            print_warning(f"Surface optimization failed for '{surface_name}'. Continuing with the current mesh. Details: {exception}")

    print_step(5, "Run wall-thickness analysis and save per-surface reports")
    wall_thickness_successes = 0
    for surface_record in surface_records:
        surface_name = surface_record["surface_name"]
        try:
            surface_record["wall_thickness"] = run_wall_thickness_analysis(surface_record, output_layout)
            wall_thickness_successes += 1
        except Exception as exception:
            surface_record["wall_thickness"] = None
            print_warning(f"Wall-thickness analysis failed for '{surface_name}'. Details: {exception}")

    print_step(6, "Export printable STL meshes and wall-thickness color-mapped PLY meshes")
    for surface_record in surface_records:
        surface_name = surface_record["surface_name"]
        if not surface_record.get("wall_thickness"):
            print_warning(f"Skipping color-mapped mesh export for '{surface_name}' because no wall-thickness analysis is available.")
            continue

        try:
            surface_record["exports"] = export_surface_meshes(surface_record, output_layout)
        except Exception as exception:
            print_warning(f"Mesh export failed for '{surface_name}'. Details: {exception}")

    print_step(7, "Capture wall-thickness screenshots")
    for surface_record in surface_records:
        surface_name = surface_record["surface_name"]
        if not surface_record.get("wall_thickness"):
            print_warning(f"Skipping screenshot for '{surface_name}' because no wall-thickness analysis is available.")
            continue

        try:
            surface_record["screenshot_path"] = capture_surface_screenshot(surface_record, output_layout)
        except Exception as exception:
            print_warning(f"Screenshot capture failed for '{surface_name}'. Details: {exception}")

    if ENABLE_GRAY_VALUE_ANALYSIS:
        print_step(8, "Optional gray-value analysis on the generated surfaces")
        for surface_record in surface_records:
            surface_name = surface_record["surface_name"]
            try:
                surface_record["gray_value"] = run_gray_value_analysis(surface_record, volume_name, output_layout)
            except Exception as exception:
                surface_record["gray_value"] = None
                print_warning(f"Gray-value analysis failed for '{surface_name}'. Details: {exception}")
    else:
        print_step(8, "Optional gray-value analysis on the generated surfaces")
        print_info("Gray-value analysis is disabled by configuration.")

    print_step(9, "Write the workflow summary report")
    report_path = os.path.join(output_layout["reports"], "workflow_summary.txt")
    try:
        write_summary_report(
            report_path,
            timestamp,
            volume_name,
            output_layout,
            segmentation_source,
            multilabel_mask_name,
            surface_records,
        )
        print_info(f"Summary report saved to '{report_path}'.")
    except Exception as exception:
        print_warning(f"Failed to write the summary report. Details: {exception}")

    print("\n" + "=" * 80)
    print("WORKFLOW COMPLETED")
    print("=" * 80)
    print(f"Processed surfaces: {len(surface_records)}")
    print(f"Wall-thickness analyses completed: {wall_thickness_successes}")
    print(f"Output root: {output_root}")
    print(f"STL meshes: {output_layout['stl_meshes']}")
    print(f"Color-mapped PLY meshes: {output_layout['colored_meshes']}")
    print(f"Screenshots: {output_layout['screenshots']}")
    print(f"Summary report: {report_path}")
    print("=" * 80)


if __name__ == "__main__":
    main()

