///|
fn csv_cell(value : String) -> String {
  if value.contains(",") || value.contains("\"") || value.contains("\n") {
    "\"\{value.replace_all(old="\"", new="\"\"")}\""
  } else {
    value
  }
}

///|
fn ics_escape(value : String) -> String {
  value
  .replace_all(old="\\", new="\\\\")
  .replace_all(old=";", new="\\;")
  .replace_all(old=",", new="\\,")
  .replace_all(old="\n", new="\\n")
}

///|
fn ics_datetime(jd : Double, tz : Double) -> String {
  let (y, m, d, h) = jd_to_calendar(jd + tz / 24.0)
  let hh = @math.floor(h).to_int()
  let mm = @math.floor((h - hh.to_double()) * 60.0).to_int()
  let ss = @math.floor(((h - hh.to_double()) * 60.0 - mm.to_double()) * 60.0).to_int()
  let zz = if tz < 0.0 { "-" } else { "+" }
  let abs_tz = if tz < 0.0 { -tz } else { tz }
  let th = @math.floor(abs_tz).to_int()
  let tm = @math.round((abs_tz - th.to_double()) * 60.0).to_int()
  "\{y}\{pad2_export(m)}\{pad2_export(d)}T\{pad2_export(hh)}\{pad2_export(mm)}\{pad2_export(ss)}\{zz}\{pad2_export(th)}\{pad2_export(tm)}"
}

///|
fn pad2_export(n : Int32) -> String {
  if n < 10 {
    "0\{n}"
  } else {
    n.to_string()
  }
}

///|
/// Export a sampled tide curve as CSV.
pub fn samples_to_csv(samples : Array[TideSample], tz : Double) -> String {
  let lines = Array(capacity=samples.length() + 1)
  lines.push("time,height_m,rate_m_per_hour,state")
  for sample in samples {
    lines.push(
      "\{csv_cell(format_local_datetime(sample.jd, tz))},\{format_meters(sample.height)},\{format_meters(sample.rate)},\{csv_cell(state_label(sample.state))}",
    )
  }
  lines.join("\n")
}

///|
/// Export high/low water events as CSV.
pub fn extremes_to_csv(extremes : Array[TideExtreme], tz : Double) -> String {
  let lines = Array(capacity=extremes.length() + 1)
  lines.push("time,kind,height_m")
  for extreme in extremes {
    let kind = if extreme.kind == ExtremeKind::High { "high" } else { "low" }
    lines.push(
      "\{csv_cell(format_local_datetime(extreme.time_jd, tz))},\{kind},\{format_meters(extreme.height)}",
    )
  }
  lines.join("\n")
}

///|
/// Export high/low water events as an iCalendar feed.
pub fn extremes_to_ics(
  station : Station,
  extremes : Array[TideExtreme],
  tz : Double,
) -> String {
  let lines = Array(capacity=extremes.length() * 8 + 10)
  lines.push("BEGIN:VCALENDAR")
  lines.push("VERSION:2.0")
  lines.push("PRODID:-//MoonTide//Tide Events//CN")
  lines.push("CALSCALE:GREGORIAN")
  lines.push("METHOD:PUBLISH")
  for i = 0; i < extremes.length(); i = i + 1 {
    let extreme = extremes[i]
    let kind = if extreme.kind == ExtremeKind::High {
      "高潮"
    } else {
      "低潮"
    }
    lines.push("BEGIN:VEVENT")
    lines.push("UID:moontide-\{station.id}-\{i}@local")
    lines.push("DTSTAMP:\{ics_datetime(extremes[0].time_jd, tz)}")
    lines.push("DTSTART:\{ics_datetime(extreme.time_jd, tz)}")
    lines.push("SUMMARY:\{ics_escape("\{station.name} \{kind}")}")
    lines.push(
      "DESCRIPTION:\{ics_escape("潮高 \{format_meters(extreme.height)} m")}",
    )
    lines.push("END:VEVENT")
  }
  lines.push("END:VCALENDAR")
  lines.join("\r\n")
}

///|
/// Render one activity plan as Markdown.
pub fn plan_to_markdown(plan : ActivityPlan, tz : Double) -> String {
  let lines = Array(capacity=plan.windows.length() * 4 + 5)
  lines.push("## \{plan.activity}")
  lines.push("")
  lines.push(plan.note)
  lines.push("")
  if plan.windows.is_empty() {
    lines.push("暂无可推荐时段。")
  } else {
    for i = 0; i < plan.windows.length(); i = i + 1 {
      let item = plan.windows[i]
      lines.push(
        "\{i + 1}. \{format_local_datetime(item.window.start_jd, tz)} 至 \{format_local_datetime(item.window.end_jd, tz)},评分 \{format_meters(item.score)}。\{item.reason}",
      )
    }
  }
  lines.join("\n")
}

///|
/// Render all activity plans as Markdown.
pub fn plans_to_markdown(plans : Array[ActivityPlan], tz : Double) -> String {
  let sections = Array(capacity=plans.length())
  for plan in plans {
    sections.push(plan_to_markdown(plan, tz))
  }
  sections.join("\n\n")
}

///|
/// Render descriptive statistics as Markdown.
pub fn stats_to_markdown(stats : TideStats, coefficient : Double) -> String {
  let lines = Array(capacity=12)
  lines.push("## 潮汐统计")
  lines.push("")
  lines.push("- 最低潮高:\{format_meters(stats.min_height)} m")
  lines.push("- 最高潮高:\{format_meters(stats.max_height)} m")
  lines.push("- 平均潮高:\{format_meters(stats.mean_height)} m")
  lines.push("- 潮差:\{format_meters(stats.range)} m")
  lines.push(
    "- 最大涨潮速率:\{format_meters(stats.max_flood_rate)} m/h",
  )
  lines.push("- 最大落潮速率:\{format_meters(stats.max_ebb_rate)} m/h")
  lines.push(
    "- 涨潮时间占比:\{format_meters(stats.rising_fraction * 100.0)}%",
  )
  lines.push("- 潮汐系数:\{format_meters(coefficient)}")
  lines.join("\n")
}

///|
/// Render a compact station inventory as Markdown.
pub fn stations_to_markdown(stations : Array[Station]) -> String {
  let lines = Array(capacity=stations.length() * 4 + 4)
  lines.push("# MoonTide 站点列表")
  lines.push("")
  lines.push("| ID | 名称 | 纬度 | 经度 | 平均潮差 | 数据来源 |")
  lines.push("| --- | --- | --- | --- | --- | --- |")
  for station in stations {
    lines.push(
      "| \{station.id} | \{station.name} | \{format_meters(station.lat)} | \{format_meters(station.lon)} | \{format_meters(mean_tidal_range(station))} m | \{station.source} |",
    )
  }
  lines.join("\n")
}

///|
/// Render a validation report as Markdown.
pub fn validation_to_markdown(issues : Array[ValidationIssue]) -> String {
  let lines = Array(capacity=issues.length() * 2 + 5)
  lines.push("## 站点校验")
  lines.push("")
  if issues.is_empty() {
    lines.push("未发现问题。")
  } else {
    for issue in issues {
      let level = match issue.level {
        ValidationLevel::Error => "错误"
        ValidationLevel::Warning => "警告"
        ValidationLevel::Info => "信息"
      }
      lines.push("- [\{level}] \{issue.message}")
    }
  }
  lines.join("\n")
}