///|
fn checksum_status_name(status : ChecksumStatus) -> String {
  match status {
    PresentValid => "present_valid"
    PresentInvalid => "present_invalid"
    MissingChecksum => "missing"
    NotChecked => "not_checked"
  }
}

///|
fn severity_name(severity : Severity) -> String {
  match severity {
    Info => "info"
    Warning => "warning"
    Error => "error"
  }
}

///|
fn finding_kind_name(kind : TrackFindingKind) -> String {
  match kind {
    NoFixFinding => "no_fix"
    LowSatellitesFinding => "low_satellites"
    LowFixQualityFinding => "low_fix_quality"
    NonMonotonicTimeFinding => "non_monotonic_time"
    LongGapFinding => "long_gap"
    CoordinateJumpFinding => "coordinate_jump"
    ExcessiveSpeedFinding => "excessive_speed"
    SpeedMismatchFinding => "speed_mismatch"
  }
}

///|
fn int_array_text(values : Array[Int]) -> String {
  let mut result = ""
  for index = 0; index < values.length(); index = index + 1 {
    if index > 0 {
      result = result + ","
    }
    result = result + values[index].to_string()
  }
  result
}

///|
fn int_array_json(values : Array[Int]) -> Json {
  let items : Array[Json] = []
  for value in values {
    items.push(Json::number(value.to_double()))
  }
  Json::array(items)
}

///|
fn string_count_json(
  entries : Array[StringCountEntry],
  key_name : String,
) -> Json {
  let items : Array[Json] = []
  for entry in entries {
    let object : Map[String, Json] = {
      "count": Json::number(entry.count_value.to_double()),
    }
    object.set(key_name, Json::string(entry.key_value))
    items.push(Json::object(object))
  }
  Json::array(items)
}

///|
pub fn render_batch_text(batch : NmeaBatch) -> String {
  let summary = batch.summary_value
  let mut text = "NMEA batch summary\n" +
    "total: " +
    summary.total_value.to_string() +
    "\n" +
    "successes: " +
    summary.successes_value.to_string() +
    "\n" +
    "failures: " +
    summary.failures_value.to_string() +
    "\n" +
    "formatters:\n"
  for entry in summary.formatter_entries_value {
    text = text +
      "- " +
      entry.key_value +
      ": " +
      entry.count_value.to_string() +
      "\n"
  }
  text = text + "talkers:\n"
  for entry in summary.talker_entries_value {
    text = text +
      "- " +
      entry.key_value +
      ": " +
      entry.count_value.to_string() +
      "\n"
  }
  text = text + "errors:\n"
  for entry in summary.error_entries_value {
    text = text +
      "- " +
      entry.key_value +
      ": " +
      entry.count_value.to_string() +
      "\n"
  }
  text
}

///|
pub fn render_batch_json(batch : NmeaBatch) -> String {
  let summary = batch.summary_value
  let checksums : Array[Json] = []
  for entry in summary.checksum_entries_value {
    checksums.push(
      Json::object({
        "status": Json::string(checksum_status_name(entry.status_value)),
        "count": Json::number(entry.count_value.to_double()),
      }),
    )
  }
  Json::object({
    "type": Json::string("nmea_batch"),
    "total": Json::number(summary.total_value.to_double()),
    "successes": Json::number(summary.successes_value.to_double()),
    "failures": Json::number(summary.failures_value.to_double()),
    "formatters": string_count_json(
      summary.formatter_entries_value,
      "formatter",
    ),
    "talkers": string_count_json(summary.talker_entries_value, "talker"),
    "checksums": Json::array(checksums),
    "errors": string_count_json(summary.error_entries_value, "code"),
  }).stringify()
}

///|
pub fn render_gsv_text(assembly : GsvAssembly) -> String {
  let signal_id = match assembly.signal_id_value {
    Some(value) => value
    None => ""
  }
  "GSV assembly\n" +
  "talker: " +
  assembly.talker_value +
  "\n" +
  "signal_id: " +
  signal_id +
  "\n" +
  "total_satellites: " +
  assembly.total_satellites_value.to_string() +
  "\n" +
  "observations: " +
  assembly.observations_value.length().to_string() +
  "\n" +
  "source_records: " +
  int_array_text(assembly.source_record_indexes_value) +
  "\n"
}

///|
fn optional_int_json(value : Int?) -> Json {
  match value {
    Some(integer) => Json::number(integer.to_double())
    None => Json::null()
  }
}

///|
pub fn render_gsv_json(assembly : GsvAssembly) -> String {
  let observations : Array[Json] = []
  for observation in assembly.observations_value {
    observations.push(
      Json::object({
        "satellite_id": Json::string(observation.satellite_id_value),
        "elevation_degrees": optional_int_json(
          observation.elevation_degrees_value,
        ),
        "azimuth_degrees": optional_int_json(observation.azimuth_degrees_value),
        "snr": optional_int_json(observation.snr_value),
      }),
    )
  }
  Json::object({
    "type": Json::string("gsv_assembly"),
    "talker": Json::string(assembly.talker_value),
    "signal_id": match assembly.signal_id_value {
      Some(value) => Json::string(value)
      None => Json::null()
    },
    "total_messages": Json::number(assembly.total_messages_value.to_double()),
    "total_satellites": Json::number(
      assembly.total_satellites_value.to_double(),
    ),
    "observations": Json::array(observations),
    "source_records": int_array_json(assembly.source_record_indexes_value),
  }).stringify()
}

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

///|
fn optional_double_json(value : Double?) -> Json {
  match value {
    Some(number) => Json::number(number)
    None => Json::null()
  }
}

///|
fn optional_decimal_json(value : ExactDecimal?) -> Json {
  match value {
    Some(decimal) => Json::string(decimal.to_string())
    None => Json::null()
  }
}

///|
fn date_text(value : UtcDate?) -> String {
  match value {
    Some(date) =>
      date.year_value.to_string() +
      "-" +
      pad2(date.month_value) +
      "-" +
      pad2(date.day_value)
    None => ""
  }
}

///|
pub fn render_fused_fix_text(fix : FusedFix) -> String {
  "Fused fix\n" +
  "date: " +
  date_text(fix.date_value) +
  "\n" +
  "time: " +
  utc_time_wire(fix.time_value) +
  "\n" +
  "latitude: " +
  optional_double_text(fix.latitude_value) +
  "\n" +
  "longitude: " +
  optional_double_text(fix.longitude_value) +
  "\n" +
  "usable: " +
  fix.usable_value.to_string() +
  "\n" +
  "diagnostics: " +
  fix.diagnostics_value.length().to_string() +
  "\n" +
  "source_records: " +
  int_array_text(fix.source_record_indexes_value) +
  "\n"
}

///|
pub fn render_fused_fix_json(fix : FusedFix) -> String {
  let provenance : Array[Json] = []
  for item in fix.provenance_value {
    provenance.push(
      Json::object({
        "field": Json::string(item.field_value),
        "source": Json::string(
          match item.source_value {
            GgaSource => "GGA"
            RmcSource => "RMC"
            VtgSource => "VTG"
          },
        ),
        "record_index": Json::number(item.record_index_value.to_double()),
      }),
    )
  }
  Json::object({
    "type": Json::string("fused_fix"),
    "date": if fix.date_value is Some(_) {
      Json::string(date_text(fix.date_value))
    } else {
      Json::null()
    },
    "time": Json::string(utc_time_wire(fix.time_value)),
    "latitude": optional_double_json(fix.latitude_value),
    "longitude": optional_double_json(fix.longitude_value),
    "altitude": optional_decimal_json(fix.altitude_value),
    "speed_knots": optional_decimal_json(fix.speed_knots_value),
    "course": optional_decimal_json(fix.course_value),
    "usable": Json::boolean(fix.usable_value),
    "provenance": Json::array(provenance),
    "source_records": int_array_json(fix.source_record_indexes_value),
  }).stringify()
}

///|
pub fn render_track_quality_text(report : TrackQualityReport) -> String {
  let mut text = "Track quality report\nfindings: " +
    report.findings_value.length().to_string() +
    "\n"
  for finding in report.findings_value {
    text = text +
      "- " +
      finding_kind_name(finding.kind_value) +
      " [" +
      severity_name(finding.severity_value) +
      "]: " +
      finding.diagnostic_value.message() +
      "\n"
  }
  text
}

///|
pub fn render_track_quality_json(report : TrackQualityReport) -> String {
  let findings : Array[Json] = []
  for finding in report.findings_value {
    findings.push(
      Json::object({
        "kind": Json::string(finding_kind_name(finding.kind_value)),
        "severity": Json::string(severity_name(finding.severity_value)),
        "point_index": optional_int_json(finding.point_index_value),
        "segment_index": optional_int_json(finding.segment_index_value),
        "code": Json::string(finding.diagnostic_value.code()),
        "message": Json::string(finding.diagnostic_value.message()),
      }),
    )
  }
  Json::object({
    "type": Json::string("track_quality"),
    "finding_count": Json::number(report.findings_value.length().to_double()),
    "findings": Json::array(findings),
  }).stringify()
}