///|
/// Select ordinary channels, preserving all annotation channels and record timing.
pub fn Recording::select(
  self : Recording,
  indices : Array[Int],
) -> Recording raise EdfError {
  if indices.is_empty() {
    raise Invalid("select at least one ordinary channel")
  }
  let selected = indices.copy()
  let seen : Map[Int, Bool] = Map([])
  for s in indices {
    if s < 0 ||
      s >= self.header.signals.length() ||
      self.header.signals[s].is_annotation() {
      raise Invalid("selection requires ordinary signal indices")
    }
    if seen.contains(s) {
      raise Invalid("duplicate selected signal")
    }
    seen[s] = true
  }
  for s = 0; s < self.header.signals.length(); s = s + 1 {
    if self.header.signals[s].is_annotation() {
      selected.push(s)
    }
  }
  let n = selected.length()
  let oldn = self.header.signals.length()
  let hs = 256 + n * 256
  let mut rs = 0
  for s in selected {
    rs += self.header.signals[s].samples_per_record * self.header.format.width()
  }
  let out = Array::make(hs + rs * self.records, b'\x00')
  copy_bytes(out, 0, self.data, 0, 256)
  put_text(out, 184, 8, hs.to_string())
  put_text(out, 252, 4, n.to_string())
  put_text(out, 236, 8, self.records.to_string())
  let mut cumulative = 0
  for width in [16, 80, 8, 8, 8, 8, 8, 80, 8, 32] {
    for i = 0; i < n; i = i + 1 {
      copy_bytes(
        out,
        256 + n * cumulative + i * width,
        self.data,
        256 + oldn * cumulative + selected[i] * width,
        width,
      )
    }
    cumulative += width
  }
  for r = 0; r < self.records; r = r + 1 {
    let mut at = hs + r * rs
    for s in selected {
      let size = self.header.signals[s].samples_per_record *
        self.header.format.width()
      copy_bytes(
        out,
        at,
        self.data,
        self.header.bytes +
        r * self.header.record_bytes +
        self.header.offsets[s],
        size,
      )
      at += size
    }
  }
  decode(Bytes::from_array(out))
}

///|
fn checked_time_value(text : String, onset : Bool) -> Double raise EdfError {
  let mut after_dot = -1
  for c in text.iter() {
    if c == '.' {
      after_dot = 0
    } else if after_dot >= 0 {
      after_dot += 1
      if after_dot > 7 && c != '0' {
        raise Unsupported("time transform would lose sub-100ns precision")
      }
    }
  }
  tal_number(text, onset)
}

///|
/// Crop whole records. EDF+ timestamps are rebased; dates advance across midnight.
pub fn Recording::crop(
  self : Recording,
  first : Int,
  count : Int,
) -> Recording raise EdfError {
  if first < 0 ||
    count < 1 ||
    first >= self.records ||
    count > self.records - first {
    raise Invalid("crop record range out of bounds")
  }
  let h = self.header
  let start = self.starts[first]
  let shift = start.floor().to_int()
  if h.continuity == Plain && start != shift.to_double() {
    raise Unsupported(
      "plain EDF/BDF cannot represent a fractional cropped origin; convert to plus before cropping",
    )
  }
  let date = parse_datetime(h.date, h.time).advance(shift)
  let out = Array::make(h.bytes + count * h.record_bytes, b'\x00')
  copy_bytes(out, 0, self.data, 0, h.bytes)
  copy_bytes(
    out,
    h.bytes,
    self.data,
    h.bytes + first * h.record_bytes,
    count * h.record_bytes,
  )
  put_text(out, 168, 8, date.date_field())
  put_text(out, 176, 8, date.time_field())
  put_text(out, 236, 8, count.to_string())
  if h.continuity != Plain {
    let tokens = h.recording
      .split(" ")
      .filter(x => !x.is_empty())
      .map(x => x.to_owned())
      .collect()
    if tokens.length() < 5 || tokens[0] != "Startdate" {
      raise Invalid("cannot update malformed plus recording identification")
    }
    tokens[1] = date.long_date()
    put_text(out, 88, 80, tokens.join(" "))
    for r = 0; r < count; r = r + 1 {
      for s = 0; s < h.signals.length(); s = s + 1 {
        let signal = h.signals[s]
        if !signal.is_annotation() {
          continue
        }
        let capacity = signal.samples_per_record * h.format.width()
        let at = h.bytes + r * h.record_bytes + h.offsets[s]
        let tals = parse_tals(
          part(
            self.data,
            h.bytes + (first + r) * h.record_bytes + h.offsets[s],
            capacity,
          ),
        )
        let changed = tals.map(t => {
          (
            {
              onset: time_text(
                checked_time_value(t.onset, true) - shift.to_double(),
                true,
              ),
              duration: t.duration,
              texts: t.texts,
            } : Tal)
        })
        let b = encode_tals(changed, capacity)
        copy_bytes(out, at, b, 0, capacity)
      }
    }
  }
  decode(Bytes::from_array(out))
}

///|
/// Promote a plain recording to plus without altering its samples/calibration text.
/// Existing plus files are returned unchanged. Plain identification strings are
/// preserved as annotations; unknown plus patient subfields are explicitly X.
pub fn Recording::to_plus(self : Recording) -> Recording raise EdfError {
  let h = self.header
  if h.continuity != Plain {
    return self
  }
  let ns = h.signals.length()
  if ns >= 4096 {
    raise Limit("no room for annotation channel")
  }
  let dt = parse_datetime(h.date, h.time)
  let label = if h.format == Edf {
    "EDF Annotations"
  } else {
    "BDF Annotations"
  }
  let capacity = 256
  let samples = (capacity + h.format.width() - 1) / h.format.width()
  let width = samples * h.format.width()
  let hs = 256 + (ns + 1) * 256
  let rs = h.record_bytes + width
  if rs > 8388608 || self.records > (268435456 - hs) / rs {
    raise Limit("promoted recording exceeds resource limit")
  }
  let out = Array::make(hs + self.records * rs, b'\x00')
  copy_bytes(out, 0, self.data, 0, 256)
  put_text(out, 184, 8, hs.to_string())
  put_text(out, 192, 44, if h.format == Edf { "EDF+C" } else { "BDF+C" })
  put_text(out, 252, 4, (ns + 1).to_string())
  put_text(out, 236, 8, self.records.to_string())
  put_text(out, 88, 80, "Startdate " + dt.long_date() + " X X X")
  // A plain patient field may be empty or free text. It is not a structured
  // plus patient ID: do not infer sex, birthdate or administrative identity.
  put_text(out, 8, 80, "X X X X")
  let vals = [
    label,
    "",
    "",
    "-1",
    "1",
    if h.format == Edf {
      "-32768"
    } else {
      "-8388608"
    },
    if h.format == Edf {
      "32767"
    } else {
      "8388607"
    },
    "",
    samples.to_string(),
    "",
  ]
  let mut cumulative = 0
  let widths = [16, 80, 8, 8, 8, 8, 8, 80, 8, 32]
  for i = 0; i < widths.length(); i = i + 1 {
    let w = widths[i]
    copy_bytes(
      out,
      256 + (ns + 1) * cumulative,
      self.data,
      256 + ns * cumulative,
      ns * w,
    )
    put_text(out, 256 + (ns + 1) * cumulative + ns * w, w, vals[i])
    cumulative += w
  }
  for r = 0; r < self.records; r = r + 1 {
    copy_bytes(
      out,
      hs + r * rs,
      self.data,
      h.bytes + r * h.record_bytes,
      h.record_bytes,
    )
    let tals : Array[Tal] = [
      { onset: time_text(self.starts[r], true), duration: None, texts: [""], },
    ]
    if r == 0 && !h.recording.is_empty() {
      tals.push({
        onset: "+0",
        duration: None,
        texts: ["Original recording ID: " + h.recording],
      })
    }
    if r == 0 && !h.patient.is_empty() {
      tals.push({
        onset: "+0",
        duration: None,
        texts: ["Original patient ID: " + h.patient],
      })
    }
    let b = encode_tals(tals, width)
    copy_bytes(out, hs + r * rs + h.record_bytes, b, 0, width)
  }
  decode(Bytes::from_array(out))
}