///|
fn flowsom_demo_ints(values : Array[Int32]) -> String {
  values.map(fn(value) { value.to_string() }).join(", ")
}

///|
fn flowsom_demo_optional(value : Double?) -> String {
  match value {
    Some(actual) => actual.to_string()
    None => "NA"
  }
}

///|
fn flowsom_demo_bool_count(values : Array[Bool]) -> Int32 {
  let mut total = 0
  for value in values {
    if value {
      total = total + 1
    }
  }
  total
}

///|
fn main {
  println("=== Bioconductor FlowSOM 2.21.0 Demo ===")

  // FlowSOM uses cell x marker matrices.
  let cells = [
    [8.0, 1.0, 5.0],
    [8.2, 1.1, 5.1],
    [7.8, 0.9, 4.9],
    [8.1, 1.2, 5.0],
    [1.0, 8.0, 5.0],
    [1.1, 8.2, 5.2],
    [0.9, 7.8, 4.8],
    [1.2, 8.1, 5.1],
    [4.0, 4.0, 9.0],
    [4.2, 4.1, 9.2],
    [3.8, 3.9, 8.8],
    [4.1, 4.2, 9.1],
  ]
  let config = @bio.FlowSomConfig::create(
    xdim=3,
    ydim=2,
    rlen=5,
    mst_runs=2,
    alpha_start=0.08,
    alpha_end=0.01,
    radius_start=2.0,
    radius_end=0.0,
    importance=[1.0, 1.0, 0.75],
    metaclusters=3,
    meta_starts=6,
    outlier_mad=3.0,
    seed=101,
  ) catch {
    _ => abort("failed to create the FlowSOM configuration")
  }
  let model = @bio.flowsom_train(cells, config, marker_names=[
    "CD3", "CD19", "CD45",
  ]) catch {
    _ => abort("FlowSOM training failed")
  }

  println("\n1. Topology-aware SOM training")
  println(@bio.flowsom_summary(model))
  println("  node counts: " + flowsom_demo_ints(model.node_counts))
  println(
    "  cell meta-clusters: " +
    flowsom_demo_ints(model.cell_meta_clusters.map(fn(value) { value + 1 })),
  )

  println("\n2. Minimum spanning tree")
  for edge in model.mst_edges {
    println(
      "  node " +
      (edge.from + 1).to_string() +
      " -> " +
      (edge.to + 1).to_string() +
      ", weight=" +
      edge.weight.to_string(),
    )
  }

  let mut occupied = 0
  while occupied < model.node_counts.length() &&
        model.node_counts[occupied] == 0 {
    occupied = occupied + 1
  }
  println("\n3. Node statistics on unweighted marker values")
  println("  first occupied node: " + (occupied + 1).to_string())
  println(
    "  CD3 median fluorescence: " +
    flowsom_demo_optional(model.node_medians[occupied][0]),
  )
  println(
    "  CD3 coefficient of variation: " +
    flowsom_demo_optional(model.node_cvs[occupied][0]),
  )
  let positive = @bio.flowsom_node_positive_percentages(model, [5.0, 5.0, 7.0]) catch {
    _ => abort("node positivity calculation failed")
  }
  println(
    "  CD3-positive fraction: " + flowsom_demo_optional(positive[occupied][0]),
  )
  println(
    "  training outliers: " +
    flowsom_demo_bool_count(model.outliers.per_cell).to_string(),
  )

  let projected = @bio.flowsom_map_new(model, [
    [model.codes[0][0], model.codes[0][1], model.codes[0][2] / 0.75],
    [20.0, 20.0, 20.0],
  ]) catch {
    _ => abort("new data projection failed")
  }
  println("\n4. New-data projection and MAD outliers")
  println(
    "  mapped nodes: " +
    flowsom_demo_ints(projected.mapping.clusters.map(fn(value) { value + 1 })),
  )
  println(
    "  mapped meta-clusters: " +
    flowsom_demo_ints(projected.meta_clusters.map(fn(value) { value + 1 })),
  )
  println(
    "  outlier flags: " +
    projected.outliers.map(fn(value) { value.to_string() }).join(", "),
  )

  let frame = @bio.FlowFrame::new(cells, [
    @bio.ParameterDescription::new("CD3", 25.0, 0.0, 25.0),
    @bio.ParameterDescription::new("CD19", 25.0, 0.0, 25.0),
    @bio.ParameterDescription::new("CD45", 25.0, 0.0, 25.0),
  ])
  let frame_config = @bio.FlowSomConfig::create(
    xdim=3,
    ydim=2,
    rlen=5,
    mst_runs=2,
    importance=[1.0, 0.75],
    metaclusters=3,
    meta_starts=6,
    seed=103,
  ) catch {
    _ => abort("failed to create the FlowFrame configuration")
  }
  let frame_model = @bio.flowsom_train_flow_frame(frame, frame_config, marker_indices=[
    0, 2,
  ]) catch {
    _ => abort("FlowFrame integration failed")
  }
  println("\n5. flowCore FlowFrame integration")
  println("  selected markers: " + frame_model.marker_names.join(", "))
  println(
    "  mapped events: " + frame_model.mapping.clusters.length().to_string(),
  )

  // SingleCellExperiment assays use marker x cell orientation.
  let experiment = @bio.SingleCellExperiment::new(
    [
      [8.0, 8.2, 7.8, 8.1, 1.0, 1.1, 0.9, 1.2, 4.0, 4.2, 3.8, 4.1],
      [1.0, 1.1, 0.9, 1.2, 8.0, 8.2, 7.8, 8.1, 4.0, 4.1, 3.9, 4.2],
      [5.0, 5.1, 4.9, 5.0, 5.0, 5.2, 4.8, 5.1, 9.0, 9.2, 8.8, 9.1],
    ],
    ["CD3", "CD19", "CD45"],
    ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "C10", "C11", "C12"],
  )
  experiment.metadata["source"] = "flowsom_demo"
  let clustered = @bio.flowsom_cluster_sce(experiment, config) catch {
    _ => abort("SingleCellExperiment integration failed")
  }
  println("\n6. Immutable SingleCellExperiment integration")
  println(
    "  SOM labels: " +
    clustered.experiment.col_data["FlowSOM.cluster"].join(", "),
  )
  println(
    "  meta-cluster labels: " +
    clustered.experiment.col_data["FlowSOM.metacluster"].join(", "),
  )
  println(
    "  original unchanged: " +
    (!experiment.col_data.contains("FlowSOM.cluster")).to_string(),
  )
  println("\nFlowSOM demo completed.")
}