office-automation 0.3.4

Windows CLI tool that automates PowerPoint and Excel via COM
//! Step 5: Update chart data links via COM.
//!
//! Unlike OLE links (where we skip Update), charts NEED LinkFormat.Update()
//! to refresh their data series. This is slower but necessary for correct output.
//!
//! GOTCHA #25: Some charts have IsLinked=True but SourceFullName="NULL" —
//! these are broken links that we skip gracefully.

use crate::com::variant::Variant;
use crate::error::OaResult;
use crate::office::constants::PpUpdateOption;
use crate::shapes::inventory::SlideInventory;

/// Update all linked chart shapes to point to the new Excel file.
///
/// If `skip_refresh` is true, skips the expensive `LinkFormat.Update()` call because
/// chart data was already pre-updated in the PPTX ZIP (Tier 1 optimization).
/// Still sets AutoUpdate=Manual on each chart.
///
/// Returns the count of charts successfully updated.
pub fn update_charts(
    inventory: &SlideInventory,
    excel_path: &str,
    skip_refresh: bool,
) -> OaResult<usize> {
    if inventory.charts.is_empty() {
        return Ok(0);
    }

    let mut updated = 0;

    for chart_ref in &inventory.charts {
        let mut shape = chart_ref.dispatch.clone();

        let mut link_format = match shape.nav("LinkFormat") {
            Ok(lf) => lf,
            Err(_) => continue,
        };

        // GOTCHA #25: Check for broken links (SourceFullName = "NULL")
        let current_source = link_format.get("SourceFullName")
            .and_then(|v| v.as_string())
            .unwrap_or_default();

        if current_source == "NULL" || current_source.is_empty() {
            // Broken link — skip gracefully
            continue;
        }

        if skip_refresh {
            // ZIP pre-update already wrote correct data — just set AutoUpdate
            let _ = link_format.put("AutoUpdate", Variant::from(PpUpdateOption::Manual as i32));
            updated += 1;
            super::verbose::detail(chart_ref.slide_index, &chart_ref.name, "AutoUpdate=Manual (pre-updated)");
        } else {
            // Full COM update: set source + refresh data
            if let Err(e) = link_format.put("SourceFullName", Variant::from(excel_path)) {
                super::verbose::warn(&format!(
                    "Slide {:>2}{} · failed to set chart source: {e}",
                    chart_ref.slide_index, chart_ref.name));
                continue;
            }
            let refreshed = match link_format.call0("Update") {
                Ok(_) => true,
                Err(e) => {
                    super::verbose::warn(&format!(
                        "Slide {:>2}{} · chart refresh failed: {e}",
                        chart_ref.slide_index, chart_ref.name));
                    false
                }
            };
            let _ = link_format.put("AutoUpdate", Variant::from(PpUpdateOption::Manual as i32));
            if refreshed {
                updated += 1;
                super::verbose::detail(chart_ref.slide_index, &chart_ref.name, "linked + refreshed");
            } else {
                super::verbose::detail(chart_ref.slide_index, &chart_ref.name, "refresh FAILED");
            }
        }
    }

    Ok(updated)
}