///|
/// A tide station: location, datum, harmonic components, and data provenance.
pub struct Station {
  id : String
  name : String
  lat : Double
  lon : Double
  datum : Double
  components : Array[Harmonic]
  source : String
} derive(Debug)

///|
fn harmonic_of(
  name : String,
  amplitude : Double,
  phase_lag : Double,
) -> Harmonic {
  match find_constituent(name) {
    Some(c) => { constituent: c, amplitude, phase_lag, }
    None => abort("unknown constituent: \{name}")
  }
}

///|
/// Stations bundled with MoonTide. Their constants are illustrative demo values.
pub fn builtin_stations() -> Array[Station] {
  [
    {
      id: "demo",
      name: "示例港(演示)",
      lat: 36.0,
      lon: 120.0,
      datum: 0.0,
      components: [
        harmonic_of("M2", 1.20, 300.0),
        harmonic_of("S2", 0.40, 340.0),
        harmonic_of("K1", 0.25, 180.0),
        harmonic_of("O1", 0.18, 220.0),
      ],
      source: "示例数据,用于演示算法,非官方调和常数",
    },
    {
      id: "qingdao",
      name: "青岛(示例常数)",
      lat: 36.06,
      lon: 120.31,
      datum: 0.0,
      components: [
        harmonic_of("M2", 1.35, 310.0),
        harmonic_of("S2", 0.45, 345.0),
        harmonic_of("N2", 0.20, 290.0),
        harmonic_of("K1", 0.28, 175.0),
        harmonic_of("O1", 0.20, 215.0),
      ],
      source: "示例常数(非官方),正式使用请以当地《潮汐表》为准",
    },
    {
      id: "xiamen",
      name: "厦门(示例常数)",
      lat: 24.45,
      lon: 118.07,
      datum: 0.0,
      components: [
        harmonic_of("M2", 1.80, 280.0),
        harmonic_of("S2", 0.60, 320.0),
        harmonic_of("K1", 0.35, 160.0),
        harmonic_of("O1", 0.28, 200.0),
      ],
      source: "示例常数(非官方),正式使用请以当地《潮汐表》为准",
    },
  ]
}

///|
/// Find a built-in station by id (case-insensitive).
pub fn find_station(id : String) -> Station? {
  let wanted = id.to_lower()
  for s in builtin_stations() {
    if s.id.to_lower() == wanted {
      return Some(s)
    }
  }
  None
}

///|
pub fn station_id(station : Station) -> String {
  station.id
}

///|
pub fn station_name(station : Station) -> String {
  station.name
}

///|
pub fn station_lat(station : Station) -> Double {
  station.lat
}

///|
pub fn station_lon(station : Station) -> Double {
  station.lon
}

///|
pub fn station_source(station : Station) -> String {
  station.source
}

///|
fn json_string(value : Json?) -> String? {
  match value {
    Some(Json::String(s)) => Some(s)
    _ => None
  }
}

///|
fn json_number(value : Json?) -> Double? {
  match value {
    Some(Json::Number(n, ..)) => Some(n)
    _ => None
  }
}

///|
/// Parse a station from MoonTide's JSON format.
pub fn station_from_json(root : Json) -> Station? {
  let object = match root {
    Json::Object(m) => m
    _ => return None
  }
  let id = match json_string(object.get("id")) {
    Some(v) => v
    None => return None
  }
  let name = match json_string(object.get("name")) {
    Some(v) => v
    None => return None
  }
  let lat = match json_number(object.get("lat")) {
    Some(v) => v
    None => return None
  }
  let lon = match json_number(object.get("lon")) {
    Some(v) => v
    None => return None
  }
  let datum = json_number(object.get("datum")).unwrap_or(0.0)
  let source = json_string(object.get("source")).unwrap_or("")
  let components_json = match object.get("components") {
    Some(Json::Array(arr)) => arr
    _ => return None
  }
  let components = Array(capacity=components_json.length())
  for item in components_json {
    let obj = match item {
      Json::Object(m) => m
      _ => return None
    }
    let cname = match json_string(obj.get("name")) {
      Some(v) => v
      None => return None
    }
    let amplitude = match json_number(obj.get("amplitude")) {
      Some(v) => v
      None => return None
    }
    let phase = match json_number(obj.get("phase")) {
      Some(v) => v
      None => return None
    }
    let c = match find_constituent(cname) {
      Some(v) => v
      None => return None
    }
    components.push({ constituent: c, amplitude, phase_lag: phase, })
  }
  Some({ id, name, lat, lon, datum, components, source, })
}

///|
/// Parse a station file containing a single JSON object.
pub fn parse_station_file(content : String) -> Station? {
  let json = @json.parse(content) catch { _ => return None }
  station_from_json(json)
}