// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Immutable graph input types for layout engines.
///
/// Engines must treat their inputs as read-only. To enforce this at the type
/// level, the engine-facing graph type (`GraphInput`) and all nested types must
/// not contain `mut` fields.

///|
pub struct GraphInput {
  name : String
  root : ObjectInput
  objects : Array[ObjectInput]
  edges : Array[EdgeInput]
  sequence_fragments : Array[SequenceFragmentInput]
  activation_boxes : Array[(String, Box)]
  sequence_notes : Array[(String, String, Box)]
  sequence_fragments_layout : Array[SequenceFragmentLayout]
  layers : Array[LayerInput]
  scenarios : Array[ScenarioInput]
  steps : Array[StepInput]
  legend : LegendInput?
  is_folder_only : Bool
  data : Map[String, GraphDataValue]
} derive(Debug)

///|
pub fn GraphInput::new(name : String) -> GraphInput {
  let root = ObjectInput::new("root")
  {
    name,
    root,
    objects: [],
    edges: [],
    sequence_fragments: [],
    activation_boxes: [],
    sequence_notes: [],
    sequence_fragments_layout: [],
    layers: [],
    scenarios: [],
    steps: [],
    legend: None,
    is_folder_only: false,
    data: Map([]),
  }
}

///|
pub fn GraphInput::from_parts(
  name : String,
  root : ObjectInput,
  objects : Array[ObjectInput],
  edges : Array[EdgeInput],
  sequence_fragments : Array[SequenceFragmentInput],
  activation_boxes : Array[(String, Box)],
  sequence_notes : Array[(String, String, Box)],
  sequence_fragments_layout : Array[SequenceFragmentLayout],
  layers : Array[LayerInput],
  scenarios : Array[ScenarioInput],
  steps : Array[StepInput],
  legend : LegendInput?,
  is_folder_only? : Bool = false,
  data? : Map[String, GraphDataValue] = Map([]),
) -> GraphInput {
  {
    name,
    root,
    objects,
    edges,
    sequence_fragments,
    activation_boxes,
    sequence_notes,
    sequence_fragments_layout,
    layers,
    scenarios,
    steps,
    legend,
    is_folder_only,
    data,
  }
}

///|
pub fn GraphInput::find_object(self : GraphInput, id : String) -> ObjectInput? {
  if self.root.abs_id_syntax == id {
    return Some(self.root)
  }
  for obj in self.objects {
    if obj.abs_id_syntax == id {
      return Some(obj)
    }
  }
  if self.root.id == id {
    return Some(self.root)
  }
  for obj in self.objects {
    if obj.id == id {
      return Some(obj)
    }
  }
  None
}

///|
pub fn GraphInput::children_of(
  self : GraphInput,
  obj : ObjectInput,
) -> Array[ObjectInput] {
  let children : Array[ObjectInput] = []
  for child_id in obj.child_ids {
    match self.find_object(child_id) {
      Some(child) => children.push(child)
      None => ()
    }
  }
  children
}

///|
pub fn GraphInput::get_objects(self : GraphInput) -> Array[ObjectInput] {
  self.objects
}

///|
pub fn GraphInput::get_edges(self : GraphInput) -> Array[EdgeInput] {
  self.edges
}

///|
/// Whether this graph or one of its child boards contains a LaTeX label.
pub fn GraphInput::uses_latex_labels(self : GraphInput) -> Bool {
  if self.root.language == Some("latex") {
    return true
  }
  for obj in self.objects {
    if obj.language == Some("latex") {
      return true
    }
  }
  for layer in self.layers {
    if layer.graph.uses_latex_labels() {
      return true
    }
  }
  for scenario in self.scenarios {
    if scenario.graph.uses_latex_labels() {
      return true
    }
  }
  for step in self.steps {
    if step.graph.uses_latex_labels() {
      return true
    }
  }
  false
}

///|
fn StyleInput::enables_sketch(self : StyleInput) -> Bool {
  match self.sketch {
    Some(value) => value.as_bool().unwrap_or(false)
    None => false
  }
}

///|
/// Whether this graph or one of its child boards explicitly enables sketch
/// rendering on an object or edge.
pub fn GraphInput::uses_sketch_styles(self : GraphInput) -> Bool {
  if self.root.style.enables_sketch() {
    return true
  }
  for obj in self.objects {
    if obj.style.enables_sketch() {
      return true
    }
  }
  for edge in self.edges {
    if edge.style.enables_sketch() {
      return true
    }
  }
  for layer in self.layers {
    if layer.graph.uses_sketch_styles() {
      return true
    }
  }
  for scenario in self.scenarios {
    if scenario.graph.uses_sketch_styles() {
      return true
    }
  }
  for step in self.steps {
    if step.graph.uses_sketch_styles() {
      return true
    }
  }
  false
}

///|
fn StyleInput::with_default_font(
  self : StyleInput,
  font : String,
) -> StyleInput {
  if self.font is Some(_) {
    self
  } else {
    { ..self, font: Some(StyleValue::from_string(font)) }
  }
}

///|
/// Apply a layout-time fallback font without overriding explicit object or edge
/// font styles. Theme rules use this before invoking any layout backend so text
/// measurement remains backend-independent.
pub fn GraphInput::with_default_font(
  self : GraphInput,
  font : String,
) -> GraphInput {
  let objects = self.objects.map(fn(obj) {
    { ..obj, style: obj.style.with_default_font(font) }
  })
  let edges = self.edges.map(fn(edge) {
    { ..edge, style: edge.style.with_default_font(font) }
  })
  let layers = self.layers.map(fn(layer) {
    LayerInput::new(layer.name, layer.graph.with_default_font(font))
  })
  let scenarios = self.scenarios.map(fn(scenario) {
    ScenarioInput::new(scenario.name, scenario.graph.with_default_font(font))
  })
  let steps = self.steps.map(fn(step) {
    StepInput::new(step.name, step.graph.with_default_font(font))
  })
  {
    ..self,
    root: { ..self.root, style: self.root.style.with_default_font(font) },
    objects,
    edges,
    layers,
    scenarios,
    steps,
  }
}

///|
fn StyleInput::disables_text_transform(self : StyleInput) -> Bool {
  match self.text_transform {
    Some(value) => value.get_value().to_lower() == "none"
    None => false
  }
}

///|
fn ObjectInput::with_caps_lock(self : ObjectInput) -> ObjectInput {
  if self.shape_type == Code ||
    self.language == Some("latex") ||
    self.style.disables_text_transform() {
    self
  } else {
    { ..self, label: self.label.to_upper() }
  }
}

///|
fn EdgeInput::with_caps_lock(self : EdgeInput) -> EdgeInput {
  if self.style.disables_text_transform() {
    self
  } else {
    { ..self, label: self.label.to_upper() }
  }
}

///|
/// Apply a theme's caps-lock rule before layout so measured labels and rendered
/// labels use the same text. Explicit `text-transform: none`, code blocks, and
/// LaTeX labels retain their source text, matching D2's theme semantics.
pub fn GraphInput::with_caps_lock(self : GraphInput) -> GraphInput {
  let objects = self.objects.map(fn(obj) { obj.with_caps_lock() })
  let edges = self.edges.map(fn(edge) { edge.with_caps_lock() })
  let layers = self.layers.map(fn(layer) {
    LayerInput::new(layer.name, layer.graph.with_caps_lock())
  })
  let scenarios = self.scenarios.map(fn(scenario) {
    ScenarioInput::new(scenario.name, scenario.graph.with_caps_lock())
  })
  let steps = self.steps.map(fn(step) {
    StepInput::new(step.name, step.graph.with_caps_lock())
  })
  {
    ..self,
    root: self.root.with_caps_lock(),
    objects,
    edges,
    layers,
    scenarios,
    steps,
  }
}

///|
fn copy_graph_data_value(value : GraphDataValue) -> GraphDataValue {
  match value {
    Text(text) => Text(text)
    StringArray(values) => {
      let copied : Array[String] = []
      for item in values {
        copied.push(item)
      }
      StringArray(copied)
    }
  }
}

///|
pub fn split_syntax_path(id : String) -> Array[String] {
  if id.length() == 0 {
    return []
  }
  let segments : Array[String] = []
  let current = StringBuilder::new()
  let mut quote : Char? = None
  let mut escaped = false
  for ch in id {
    if escaped {
      current.write_char(ch)
      escaped = false
      continue
    }
    match quote {
      Some(q) => {
        current.write_char(ch)
        if ch == '\\' {
          escaped = true
        } else if ch == q {
          quote = None
        }
      }
      None => {
        if ch == '.' {
          segments.push(current.to_string())
          current.reset()
          continue
        }
        current.write_char(ch)
        if ch == '"' || ch == '\'' {
          quote = Some(ch)
        }
      }
    }
  }
  segments.push(current.to_string())
  segments
}

///|
pub fn syntax_path_depth(id : String) -> Int {
  split_syntax_path(id).length()
}

///|
pub fn syntax_path_last(id : String) -> String {
  let segments = split_syntax_path(id)
  if segments.is_empty() {
    id
  } else {
    segments[segments.length() - 1]
  }
}

///|
pub fn display_key_segment(segment : String) -> String {
  if segment.length() < 2 {
    return segment
  }
  let quote = segment[0]
  if (quote != '"' && quote != '\'') || segment[segment.length() - 1] != quote {
    return segment
  }
  let out = StringBuilder::new()
  let mut escaped = false
  for i = 1; i < segment.length() - 1; i = i + 1 {
    match segment[i].to_char() {
      Some(ch) =>
        if escaped {
          if quote == '"' {
            match ch {
              'a' => out.write_char('\u{7}')
              'b' => out.write_char('\u{8}')
              'f' => out.write_char('\u{c}')
              'n' => out.write_char('\n')
              'r' => out.write_char('\r')
              't' => out.write_char('\t')
              'v' => out.write_char('\u{b}')
              _ => out.write_char(ch)
            }
          } else {
            out.write_char(ch)
          }
          escaped = false
        } else if ch == '\\' {
          escaped = true
        } else {
          out.write_char(ch)
        }
      None => ()
    }
  }
  if escaped {
    out.write_char('\\')
  }
  out.to_string()
}

///|
pub fn syntax_path_last_display(id : String) -> String {
  display_key_segment(syntax_path_last(id))
}

///|
pub struct LayerInput {
  name : String
  graph : GraphInput
} derive(Debug)

///|
pub fn LayerInput::new(name : String, graph : GraphInput) -> LayerInput {
  { name, graph }
}

///|
pub struct ScenarioInput {
  name : String
  graph : GraphInput
} derive(Debug)

///|
pub fn ScenarioInput::new(name : String, graph : GraphInput) -> ScenarioInput {
  { name, graph }
}

///|
pub struct StepInput {
  name : String
  graph : GraphInput
} derive(Debug)

///|
pub fn StepInput::new(name : String, graph : GraphInput) -> StepInput {
  { name, graph }
}

///|
pub struct SequenceFragmentInput {
  fragment_type : String
  condition : String?
  start_index : Int
  end_index : Int
  operands : Array[FragmentOperandInput]
} derive(Debug)

///|
pub fn SequenceFragmentInput::new(
  fragment_type : String,
  condition : String?,
  start_index : Int,
  end_index : Int,
  operands : Array[FragmentOperandInput],
) -> SequenceFragmentInput {
  { fragment_type, condition, start_index, end_index, operands }
}

///|
pub struct FragmentOperandInput {
  condition : String?
} derive(Debug)

///|
pub fn FragmentOperandInput::new(condition : String?) -> FragmentOperandInput {
  { condition, }
}

///|
pub fn ObjectReference::contained_by(
  self : ObjectReference,
  abs_id_syntax : String,
) -> Bool {
  match self.scope_abs_id_syntax {
    Some(scope) =>
      scope == abs_id_syntax || scope.has_prefix(abs_id_syntax + ".")
    None => false
  }
}

///|
pub fn EdgeReference::contained_by(
  self : EdgeReference,
  abs_id_syntax : String,
) -> Bool {
  match self.scope_abs_id_syntax {
    Some(scope) =>
      scope == abs_id_syntax || scope.has_prefix(abs_id_syntax + ".")
    None => false
  }
}

///|
/// Immutable representation of a graph object for layout engines.
///
/// Fields:
///
/// * `id` : Compatibility absolute identifier using semantic path values.
/// * `id_val` : Semantic value of the local object identifier.
/// * `id_syntax` : Canonical source syntax for the local object identifier.
/// * `abs_id_syntax` : Canonical source syntax for the absolute object path.
/// * `references` : Source references that compiled into this object.
/// * `label` : Display text for the object.
/// * `shape_type` : Visual shape of the object.
/// * `style` : Visual styling properties.
/// * `box` : Optional precomputed bounding box.
/// * `label_box` : Optional precomputed label bounding box.
/// * `child_ids` : Array of child object absolute syntax identifiers.
/// * `z_index` : Layer ordering for visual stacking.
/// * `icon` : Optional icon identifier.
/// * `tooltip` : Optional tooltip text.
/// * `link` : Optional hyperlink URL.
/// * `classes` : Array of CSS class names for styling.
/// * `language` : Optional language tag preserved for code/text handling.
/// * `sql_constraints` : Optional SQL constraints preserved for class/sql_table children.
/// * `grid_rows` : Optional number of grid rows for layout.
/// * `grid_columns` : Optional number of grid columns for layout.
/// * `grid_gap` : Optional spacing between grid items.
/// * `horizontal_gap` : Optional horizontal spacing between children.
/// * `vertical_gap` : Optional vertical spacing between children.
/// * `grid_column_span` : Optional number of columns this object spans.
/// * `grid_row_span` : Optional number of rows this object spans.
/// * `near` : Optional identifier of object to position near.
/// * `top` : Optional fixed top coordinate in pixels.
/// * `left` : Optional fixed left coordinate in pixels.
/// * `direction` : Optional layout direction override (`up|down|left|right`).
///
pub struct ObjectInput {
  id : String
  id_val : String
  id_syntax : String
  abs_id_syntax : String
  references : Array[ObjectReference]
  label : String
  shape_type : ShapeType
  style : StyleInput
  box : Box?
  label_box : Box?
  child_ids : Array[String]
  z_index : Int
  icon : String?
  icon_position : String?
  tooltip : String?
  tooltip_position : String?
  link : String?
  label_position : String?
  classes : Array[String]
  language : String?
  sql_constraints : Array[String]
  grid_rows : Int?
  grid_columns : Int?
  grid_row_directed : Bool?
  grid_gap : Double?
  horizontal_gap : Double?
  vertical_gap : Double?
  grid_column_span : Int?
  grid_row_span : Int?
  near : String?
  top : Int?
  left : Int?
  direction : String?
} derive(Debug)

///|
pub fn ObjectInput::new(
  id : String,
  id_val? : String = id,
  id_syntax? : String = id,
  abs_id_syntax? : String = id,
) -> ObjectInput {
  {
    id,
    id_val,
    id_syntax,
    abs_id_syntax,
    references: [],
    label: id_val,
    shape_type: Rectangle,
    style: StyleInput::new(),
    box: None,
    label_box: None,
    child_ids: [],
    z_index: 0,
    icon: None,
    icon_position: None,
    tooltip: None,
    tooltip_position: None,
    link: None,
    label_position: None,
    classes: [],
    language: None,
    sql_constraints: [],
    grid_rows: None,
    grid_columns: None,
    grid_row_directed: None,
    grid_gap: None,
    horizontal_gap: None,
    vertical_gap: None,
    grid_column_span: None,
    grid_row_span: None,
    near: None,
    top: None,
    left: None,
    direction: None,
  }
}

///|
pub fn ObjectInput::from_parts(
  id : String,
  label : String,
  shape_type : ShapeType,
  style : StyleInput,
  box : Box?,
  label_box : Box?,
  child_ids : Array[String],
  z_index : Int,
  icon : String?,
  tooltip : String?,
  link : String?,
  classes : Array[String],
  grid_rows : Int?,
  grid_columns : Int?,
  grid_gap : Double?,
  horizontal_gap : Double?,
  vertical_gap : Double?,
  grid_column_span : Int?,
  grid_row_span : Int?,
  near : String?,
  top : Int?,
  left : Int?,
  direction? : String? = None,
  language? : String? = None,
  sql_constraints? : Array[String] = [],
  id_val? : String = id,
  id_syntax? : String = id,
  abs_id_syntax? : String = id,
  references? : Array[ObjectReference] = [],
  icon_position? : String? = None,
  tooltip_position? : String? = None,
  label_position? : String? = None,
  grid_row_directed? : Bool? = None,
) -> ObjectInput {
  {
    id,
    id_val,
    id_syntax,
    abs_id_syntax,
    references,
    label,
    shape_type,
    style,
    box,
    label_box,
    child_ids,
    z_index,
    icon,
    tooltip,
    link,
    icon_position,
    tooltip_position,
    label_position,
    classes,
    language,
    sql_constraints,
    grid_rows,
    grid_columns,
    grid_row_directed,
    grid_gap,
    horizontal_gap,
    vertical_gap,
    grid_column_span,
    grid_row_span,
    near,
    top,
    left,
    direction,
  }
}

///|
pub fn ObjectInput::with_direction(
  self : ObjectInput,
  direction : String?,
) -> ObjectInput {
  { ..self, direction, }
}

///|
pub fn ObjectInput::lookup_id(self : ObjectInput) -> String {
  self.abs_id_syntax
}

///|
pub fn ObjectInput::get_box(self : ObjectInput) -> Box? {
  self.box
}

///|
pub fn ObjectInput::get_label_box(self : ObjectInput) -> Box? {
  self.label_box
}

///|
pub struct EdgeInput {
  index : Int
  src_id : String
  dst_id : String
  src_id_syntax : String
  dst_id_syntax : String
  references : Array[EdgeReference]
  classes : Array[String]
  src_arrow : Bool
  dst_arrow : Bool
  src_arrowhead : ArrowheadType
  dst_arrowhead : ArrowheadType
  src_arrowhead_label : String?
  dst_arrowhead_label : String?
  src_arrowhead_label_color : String?
  dst_arrowhead_label_color : String?
  src_anchor : String?
  dst_anchor : String?
  label : String
  icon : String?
  icon_position : String?
  icon_border_radius : Double?
  link : String?
  style : StyleInput
  route : Array[Point]
  bend_points : Array[Point]?
  is_curve : Bool
  z_index : Int
  reference_count : Int
  label_box : Box?
  src_column_index : Int?
  dst_column_index : Int?
} derive(Debug)

///|
pub fn EdgeInput::new(
  index : Int,
  src_id : String,
  dst_id : String,
  src_arrow : Bool,
  dst_arrow : Bool,
  src_id_syntax? : String = src_id,
  dst_id_syntax? : String = dst_id,
) -> EdgeInput {
  {
    index,
    src_id,
    dst_id,
    src_id_syntax,
    dst_id_syntax,
    references: [],
    classes: [],
    src_arrow,
    dst_arrow,
    src_arrowhead: if src_arrow {
      Triangle
    } else {
      None
    },
    dst_arrowhead: if dst_arrow {
      Triangle
    } else {
      None
    },
    src_arrowhead_label: None,
    dst_arrowhead_label: None,
    src_arrowhead_label_color: None,
    dst_arrowhead_label_color: None,
    src_anchor: None,
    dst_anchor: None,
    label: "",
    icon: None,
    icon_position: None,
    icon_border_radius: None,
    link: None,
    style: StyleInput::new(),
    route: [],
    bend_points: None,
    is_curve: false,
    z_index: 0,
    reference_count: 1,
    label_box: None,
    src_column_index: None,
    dst_column_index: None,
  }
}

///|
pub fn EdgeInput::from_parts(
  index : Int,
  src_id : String,
  dst_id : String,
  src_arrow : Bool,
  dst_arrow : Bool,
  src_arrowhead : ArrowheadType,
  dst_arrowhead : ArrowheadType,
  src_arrowhead_label : String?,
  dst_arrowhead_label : String?,
  src_arrowhead_label_color : String?,
  dst_arrowhead_label_color : String?,
  src_anchor : String?,
  dst_anchor : String?,
  label : String,
  style : StyleInput,
  route : Array[Point],
  bend_points : Array[Point]?,
  is_curve : Bool,
  z_index : Int,
  reference_count : Int,
  label_box : Box?,
  src_column_index : Int?,
  dst_column_index : Int?,
  src_id_syntax? : String = src_id,
  dst_id_syntax? : String = dst_id,
  references? : Array[EdgeReference] = [],
  icon? : String? = None,
  icon_position? : String? = None,
  icon_border_radius? : Double? = None,
  link? : String? = None,
  classes? : Array[String] = [],
) -> EdgeInput {
  {
    index,
    src_id,
    dst_id,
    src_id_syntax,
    dst_id_syntax,
    references,
    classes,
    src_arrow,
    dst_arrow,
    src_arrowhead,
    dst_arrowhead,
    src_arrowhead_label,
    dst_arrowhead_label,
    src_arrowhead_label_color,
    dst_arrowhead_label_color,
    src_anchor,
    dst_anchor,
    label,
    icon,
    icon_position,
    icon_border_radius,
    link,
    style,
    route,
    bend_points,
    is_curve,
    z_index,
    reference_count,
    label_box,
    src_column_index,
    dst_column_index,
  }
}

///|
pub fn EdgeInput::get_src_id(self : EdgeInput) -> String {
  self.src_id
}

///|
pub fn EdgeInput::lookup_src_id(self : EdgeInput) -> String {
  self.src_id_syntax
}

///|
pub fn EdgeInput::get_dst_id(self : EdgeInput) -> String {
  self.dst_id
}

///|
pub fn EdgeInput::lookup_dst_id(self : EdgeInput) -> String {
  self.dst_id_syntax
}

///|
pub fn EdgeInput::get_label_box(self : EdgeInput) -> Box? {
  self.label_box
}

///|
/// A style value with its source range.
///
/// The value is preserved as a string. Consumers may parse it as needed.
pub struct StyleValue {
  value : String
  range : @lexer.Range
} derive(Eq, Debug)

///|
pub fn StyleValue::new(value : String, range : @lexer.Range) -> StyleValue {
  { value, range }
}

///|
/// Create a style value without a concrete source location.
pub fn StyleValue::synthetic(value : String) -> StyleValue {
  let p = @lexer.Position::zero()
  { value, range: @lexer.Range::new(p, p) }
}

///|
pub fn StyleValue::as_bool(self : StyleValue) -> Bool? {
  match self.value.to_lower() {
    "true" => Some(true)
    "false" => Some(false)
    _ => None
  }
}

///|
pub fn StyleValue::as_int(self : StyleValue) -> Int? {
  Some(@string.parse_int(self.value)) catch {
    _ => None
  }
}

///|
pub fn StyleValue::as_double(self : StyleValue) -> Double? {
  Some(@string.parse_double(self.value)) catch {
    _ => None
  }
}

///|
pub fn StyleValue::get_value(self : StyleValue) -> String {
  self.value
}

///|
pub fn StyleValue::get_range(self : StyleValue) -> @lexer.Range {
  self.range
}

///|
pub fn StyleValue::from_string(value : String) -> StyleValue {
  StyleValue::synthetic(value)
}

///|
pub fn StyleValue::from_bool(value : Bool) -> StyleValue {
  let s = if value { "true" } else { "false" }
  StyleValue::synthetic(s)
}

///|
pub fn StyleValue::from_int(value : Int) -> StyleValue {
  StyleValue::synthetic("\{value}")
}

///|
pub fn StyleValue::from_double(value : Double) -> StyleValue {
  StyleValue::synthetic("\{value}")
}

///|
pub fn StyleValue::from_string_opt(value : String?) -> StyleValue? {
  match value {
    Some(v) => Some(StyleValue::from_string(v))
    None => None
  }
}

///|
pub fn StyleValue::from_bool_opt(value : Bool?) -> StyleValue? {
  match value {
    Some(v) => Some(StyleValue::from_bool(v))
    None => None
  }
}

///|
pub fn StyleValue::from_int_opt(value : Int?) -> StyleValue? {
  match value {
    Some(v) => Some(StyleValue::from_int(v))
    None => None
  }
}

///|
pub fn StyleValue::from_double_opt(value : Double?) -> StyleValue? {
  match value {
    Some(v) => Some(StyleValue::from_double(v))
    None => None
  }
}

///|
pub struct StyleInput {
  opacity : StyleValue?
  stroke : StyleValue?
  fill : StyleValue?
  stroke_width : StyleValue?
  stroke_dash : StyleValue?
  border_radius : StyleValue?
  shadow : StyleValue?
  three_d : StyleValue?
  multiple : StyleValue?
  font : StyleValue?
  font_size : StyleValue?
  font_color : StyleValue?
  bold : StyleValue?
  italic : StyleValue?
  underline : StyleValue?
  animated : StyleValue?
  filled : StyleValue?
  double_border : StyleValue?
  text_transform : StyleValue?
  font_family : StyleValue?
  fill_pattern : StyleValue?
  sketch : StyleValue?
  curved : StyleValue?
} derive(Eq, Debug)

///|
pub fn StyleInput::new() -> StyleInput {
  {
    opacity: None,
    stroke: None,
    fill: None,
    stroke_width: None,
    stroke_dash: None,
    border_radius: None,
    shadow: None,
    three_d: None,
    multiple: None,
    font: None,
    font_size: None,
    font_color: None,
    bold: None,
    italic: None,
    underline: None,
    animated: None,
    filled: None,
    double_border: None,
    text_transform: None,
    font_family: None,
    fill_pattern: None,
    sketch: None,
    curved: None,
  }
}

///|
pub fn StyleInput::from_parts(
  opacity : StyleValue?,
  stroke : StyleValue?,
  fill : StyleValue?,
  stroke_width : StyleValue?,
  stroke_dash : StyleValue?,
  border_radius : StyleValue?,
  shadow : StyleValue?,
  three_d : StyleValue?,
  multiple : StyleValue?,
  font : StyleValue?,
  font_size : StyleValue?,
  font_color : StyleValue?,
  bold : StyleValue?,
  italic : StyleValue?,
  underline : StyleValue?,
  animated : StyleValue?,
  filled : StyleValue?,
  double_border : StyleValue?,
  text_transform : StyleValue?,
  font_family : StyleValue?,
  fill_pattern : StyleValue?,
  sketch : StyleValue?,
  curved : StyleValue?,
) -> StyleInput {
  {
    opacity,
    stroke,
    fill,
    stroke_width,
    stroke_dash,
    border_radius,
    shadow,
    three_d,
    multiple,
    font,
    font_size,
    font_color,
    bold,
    italic,
    underline,
    animated,
    filled,
    double_border,
    text_transform,
    font_family,
    fill_pattern,
    sketch,
    curved,
  }
}

///|
pub enum LegendEntryInput {
  ShapeEntry(
    id~ : String,
    shape~ : ShapeType,
    label~ : String,
    style~ : StyleInput?
  )
  ConnectionEntry(
    src_id~ : String,
    dst_id~ : String,
    index~ : Int,
    src_arrow_enabled~ : Bool,
    dst_arrow_enabled~ : Bool,
    src_arrow~ : ArrowheadType,
    dst_arrow~ : ArrowheadType,
    label~ : String,
    style~ : StyleInput?
  )
  CustomEntry(icon~ : String, label~ : String)
  Separator
} derive(Debug)

///|
pub fn LegendEntryInput::shape(
  shape : ShapeType,
  label : String,
  style : StyleInput?,
  id? : String = label,
) -> LegendEntryInput {
  ShapeEntry(id~, shape~, label~, style~)
}

///|
pub fn LegendEntryInput::connection(
  src_arrow : ArrowheadType,
  dst_arrow : ArrowheadType,
  label : String,
  style : StyleInput?,
  src_id? : String = "src",
  dst_id? : String = "dst",
  index? : Int = 0,
  src_arrow_enabled? : Bool = src_arrow != None,
  dst_arrow_enabled? : Bool = dst_arrow != None,
) -> LegendEntryInput {
  ConnectionEntry(
    src_id~,
    dst_id~,
    index~,
    src_arrow_enabled~,
    dst_arrow_enabled~,
    src_arrow~,
    dst_arrow~,
    label~,
    style~,
  )
}

///|
pub fn LegendEntryInput::custom(
  icon : String,
  label : String,
) -> LegendEntryInput {
  CustomEntry(icon~, label~)
}

///|
pub fn LegendEntryInput::separator() -> LegendEntryInput {
  Separator
}

///|
pub struct LegendInput {
  title : String?
  position : LegendPosition
  entries : Array[LegendEntryInput]
  fill : String?
  stroke : String?
  padding : Double
  entry_gap : Double
  icon_size : Double
} derive(Debug)

///|
pub fn LegendInput::from_parts(
  title : String?,
  position : LegendPosition,
  entries : Array[LegendEntryInput],
  fill : String?,
  stroke : String?,
  padding : Double,
  entry_gap : Double,
  icon_size : Double,
) -> LegendInput {
  { title, position, entries, fill, stroke, padding, entry_gap, icon_size }
}

///|
pub fn LegendInput::is_empty(self : LegendInput) -> Bool {
  self.entries.is_empty()
}

///|
pub fn LegendInput::entry_count(self : LegendInput) -> Int {
  let mut count = 0
  for entry in self.entries {
    match entry {
      Separator => ()
      _ => count += 1
    }
  }
  count
}

///|
pub fn GraphInput::from_model(graph : GraphModel) -> GraphInput {
  let objects : Array[ObjectInput] = []
  for obj in graph.objects {
    objects.push(ObjectInput::from_model(obj))
  }
  let edges : Array[EdgeInput] = []
  for e in graph.edges {
    edges.push(EdgeInput::from_model(e))
  }
  let fragments : Array[SequenceFragmentInput] = []
  for f in graph.sequence_fragments {
    fragments.push(SequenceFragmentInput::from_model(f))
  }
  let layers : Array[LayerInput] = []
  for layer in graph.layers {
    layers.push(
      LayerInput::new(layer.name, GraphInput::from_model(layer.graph)),
    )
  }
  let scenarios : Array[ScenarioInput] = []
  for scenario in graph.scenarios {
    scenarios.push(
      ScenarioInput::new(scenario.name, GraphInput::from_model(scenario.graph)),
    )
  }
  let steps : Array[StepInput] = []
  for step in graph.steps {
    steps.push(StepInput::new(step.name, GraphInput::from_model(step.graph)))
  }
  let data : Map[String, GraphDataValue] = Map([])
  for key, value in graph.data {
    data[key] = copy_graph_data_value(value)
  }
  {
    name: graph.name,
    root: ObjectInput::from_model(graph.root),
    objects,
    edges,
    sequence_fragments: fragments,
    activation_boxes: [],
    sequence_notes: [],
    sequence_fragments_layout: [],
    layers,
    scenarios,
    steps,
    legend: match graph.legend {
      Some(l) => Some(LegendInput::from_model(l))
      None => None
    },
    is_folder_only: graph.is_folder_only,
    data,
  }
}

///|
pub fn ObjectInput::from_model(obj : ObjectModel) -> ObjectInput {
  let child_ids : Array[String] = []
  for child in obj.children {
    child_ids.push(child.abs_id_syntax)
  }
  let classes : Array[String] = []
  for c in obj.classes {
    classes.push(c)
  }
  let sql_constraints : Array[String] = []
  for constraint in obj.sql_constraints {
    sql_constraints.push(constraint)
  }
  let references : Array[ObjectReference] = []
  for reference in obj.references {
    references.push(reference)
  }
  ObjectInput::from_parts(
    obj.id,
    obj.label,
    obj.shape_type,
    StyleInput::from_model(obj.style),
    obj.box,
    obj.label_box,
    child_ids,
    obj.z_index,
    obj.icon,
    obj.tooltip,
    obj.link,
    classes,
    obj.grid_rows,
    obj.grid_columns,
    obj.grid_gap,
    obj.horizontal_gap,
    obj.vertical_gap,
    obj.grid_column_span,
    obj.grid_row_span,
    obj.near,
    obj.top,
    obj.left,
    direction=obj.direction,
    language=obj.language,
    sql_constraints~,
    id_val=obj.id_val,
    id_syntax=obj.id_syntax,
    abs_id_syntax=obj.abs_id_syntax,
    references~,
    icon_position=obj.icon_position,
    tooltip_position=obj.tooltip_position,
    label_position=obj.label_position,
  )
}

///|
pub fn EdgeInput::from_model(edge : EdgeModel) -> EdgeInput {
  let bend_points = match edge.bend_points {
    Some(ps) => {
      let copied : Array[Point] = []
      for p in ps {
        copied.push(p)
      }
      Some(copied)
    }
    None => None
  }
  let references : Array[EdgeReference] = []
  for reference in edge.references {
    references.push(reference)
  }
  EdgeInput::from_parts(
    edge.index,
    edge.src_id,
    edge.dst_id,
    edge.src_arrow,
    edge.dst_arrow,
    edge.src_arrowhead,
    edge.dst_arrowhead,
    edge.src_arrowhead_label,
    edge.dst_arrowhead_label,
    edge.src_arrowhead_label_color,
    edge.dst_arrowhead_label_color,
    edge.src_anchor,
    edge.dst_anchor,
    edge.label,
    StyleInput::from_model(edge.style),
    [],
    bend_points,
    edge.is_curve,
    edge.z_index,
    edge.reference_count,
    edge.label_box,
    edge.src_column_index,
    edge.dst_column_index,
    src_id_syntax=edge.src_id_syntax,
    dst_id_syntax=edge.dst_id_syntax,
    references~,
    icon=edge.icon,
    icon_position=edge.icon_position,
    icon_border_radius=edge.icon_border_radius,
    link=edge.link,
    classes=edge.classes,
  )
}

///|
pub fn StyleInput::from_model(style : StyleModel) -> StyleInput {
  StyleInput::from_parts(
    style.opacity,
    style.stroke,
    style.fill,
    style.stroke_width,
    style.stroke_dash,
    style.border_radius,
    style.shadow,
    style.three_d,
    style.multiple,
    style.font,
    style.font_size,
    style.font_color,
    style.bold,
    style.italic,
    style.underline,
    style.animated,
    style.filled,
    style.double_border,
    style.text_transform,
    style.font_family,
    style.fill_pattern,
    style.sketch,
    style.curved,
  )
}

///|
pub fn SequenceFragmentInput::from_model(
  fragment : SequenceFragmentModel,
) -> SequenceFragmentInput {
  let operands : Array[FragmentOperandInput] = []
  for op in fragment.operands {
    operands.push({ condition: op.condition })
  }
  {
    fragment_type: fragment.fragment_type,
    condition: fragment.condition,
    start_index: fragment.start_index,
    end_index: fragment.end_index,
    operands,
  }
}

///|
pub fn LegendInput::from_model(legend : LegendModel) -> LegendInput {
  let entries : Array[LegendEntryInput] = []
  for entry in legend.entries {
    entries.push(entry)
  }
  LegendInput::from_parts(
    legend.title,
    legend.position,
    entries,
    legend.fill,
    legend.stroke,
    legend.padding,
    legend.entry_gap,
    legend.icon_size,
  )
}