// Text.render (upstream `Text.render`) and a fused
// `Text("\n").join(lines).render(...)` used by `Text.__rich_console__`.

///|
/// Render events `(offset, leaving, seq)` packed in a UInt64 (biased offset,
/// leaving flag, sequence number) so that sorting the integers sorts by
/// `(offset, leaving)` and keeps equal keys in sequence order, like
/// upstream's stable `spans.sort(key=itemgetter(0, 1))`.
fn pack_event(offset : Int, leaving : Bool, seq : Int) -> UInt64 {
  ((offset.to_int64() + 0x8000_0000L).reinterpret_as_uint64() << 32) |
  (if leaving { 0x8000_0000UL } else { 0UL }) |
  seq.to_uint64()
}

///|
fn event_offset(e : UInt64) -> Int {
  ((e >> 32).reinterpret_as_int64() - 0x8000_0000L).to_int()
}

///|
fn event_leaving(e : UInt64) -> Bool {
  (e & 0x8000_0000UL) != 0
}

///|
fn event_seq(e : UInt64) -> Int {
  (e & 0x7FFF_FFFFUL).to_int()
}

///|
/// The open styles of a render (upstream's `stack` and `style_cache` in
/// `Text.render`): style ids are kept sorted, and the current style is
/// the combination of their styles in id order.
priv struct ActiveStyles {
  style_map : FixedArray[Style]
  stack : Array[Int]
  mut cache : Map[Array[Int], Style]?
}

///|
fn ActiveStyles::new(style_map : FixedArray[Style]) -> ActiveStyles {
  { style_map, stack: [], cache: None, }
}

///|
fn ActiveStyles::enter(self : ActiveStyles, id : Int) -> Unit {
  match self.stack.binary_search(id) {
    Ok(idx) | Err(idx) => self.stack.insert(idx, id)
  }
}

///|
fn ActiveStyles::leave(self : ActiveStyles, id : Int) -> Unit {
  match self.stack.binary_search(id) {
    Ok(idx) => self.stack.remove(idx) |> ignore
    Err(_) => ()
  }
}

///|
fn ActiveStyles::current(self : ActiveStyles) -> Style {
  let stack = self.stack
  let style_map = self.style_map
  // Fast paths: one style, or two plain styles (`+` is memoized and
  // without links/meta the result does not depend on the cache).
  if stack.length() == 1 {
    return style_map[stack[0]]
  }
  if stack.length() == 2 {
    let a = style_map[stack[0]]
    let b = style_map[stack[1]]
    if a.is_plain() && b.is_plain() {
      return a + b
    }
  }
  let cache = match self.cache {
    Some(cache) => cache
    None => {
      let cache = Map([])
      self.cache = Some(cache)
      cache
    }
  }
  match cache.get(stack) {
    Some(s) => s
    None => {
      let mut s = style_map[stack[0]]
      for k in 1.. Unit raise {
  let text = self.plain()
  if self.spans_.is_empty() {
    result.push(Segment::new(text))
    if end != "" {
      result.push(Segment::new(end))
    }
    return
  }
  let null = Def("")
  let n = self.spans_.length()
  let style_map : FixedArray[Style] = FixedArray::make(n + 1, null_style)
  for i, span in self.spans_ {
    style_map[i + 1] = console.get_style(span.style, default=null)
  }
  style_map[0] = console.get_style(self.style, default=null)
  // sequence numbers: 0 enters the base style, 1..=n enter the spans,
  // n+1..=2n leave them and 2n+1 leaves the base style
  let events : Array[UInt64] = Array(capacity=2 * n + 2)
  events.push(pack_event(0, false, 0))
  for i, span in self.spans_ {
    events.push(pack_event(span.start, false, i + 1))
  }
  for i, span in self.spans_ {
    events.push(pack_event(span.end, true, n + i + 1))
  }
  events.push(pack_event(text.length(), true, 2 * n + 1))
  events.sort()
  let active = ActiveStyles::new(style_map)
  let text_length = text.length()
  for i in 0..<(events.length() - 1) {
    let e = events[i]
    let offset = event_offset(e)
    let seq = event_seq(e)
    let next_offset = event_offset(events[i + 1])
    if event_leaving(e) {
      active.leave(if seq > n { seq - n } else { 0 })
    } else {
      active.enter(seq)
    }
    if next_offset > offset {
      let segment_text = if offset == 0 && next_offset == text_length {
        text
      } else {
        text.view(start_offset=offset, end_offset=next_offset).to_owned()
      }
      result.push(Segment::new(segment_text, style=active.current()))
    }
  }
  if end != "" {
    result.push(Segment::new(end))
  }
}

///|
/// `Text::new(text="\n").join(lines).render_into(console, end, out)` without
/// building the joined Text: the same segments (the same style lookups in
/// the same order, the same segment boundaries) from the lines themselves.
///
/// In the joined text every line contributes a span for its style (when
/// set) and its own spans, all within the line, so the open styles never
/// cross a newline: events can be generated and sorted line by line, and a
/// segment only crosses lines where there is no event at either side of a
/// newline. Lines with spans outside `0..=length` (which would cross into
/// the following lines) use the general path.
fn render_joined_lines(
  lines : Array[Text],
  console : Console,
  end : String,
  out : Array[Segment],
) -> Unit raise {
  let line_count = lines.length()
  let mut span_count = 0
  for line in lines {
    let length = line.length_
    let general = length != line.plain().length() ||
      line.spans_
      .iter()
      .any(span => span.start < 0 || span.start > span.end || span.end > length)
    if general {
      Text::new(text="\n").join(lines).render_into(console, end, out)
      return
    }
    if line.style.to_bool() {
      span_count += 1
    }
    span_count += line.spans_.length()
  }
  if span_count == 0 {
    // the joined text has no spans: a single segment
    out.push(Segment::new(lines.map(line => line.plain()).join("\n")))
    if end != "" {
      out.push(Segment::new(end))
    }
    return
  }
  // style ids in the order of the joined text's spans; 0 is its own style
  let null = Def("")
  let style_map : FixedArray[Style] = FixedArray::make(
    span_count + 1,
    null_style,
  )
  let first_ids : FixedArray[Int] = FixedArray::make(line_count, 0)
  let mut id = 0
  for i, line in lines {
    first_ids[i] = id + 1
    if line.style.to_bool() {
      id += 1
      style_map[id] = console.get_style(line.style, default=null)
    }
    for span in line.spans_ {
      id += 1
      style_map[id] = console.get_style(span.style, default=null)
    }
  }
  style_map[0] = console.get_style(null, default=null)
  // offset of each line in the joined text
  let starts : FixedArray[Int] = FixedArray::make(line_count + 1, 0)
  for i, line in lines {
    starts[i + 1] = starts[i] + line.length_ + 1
  }
  let total_length = starts[line_count] - 1
  // text of the joined text between `a` and `b`, with `a` in line `cursor`
  let mut cursor = 0
  fn slice(a : Int, b : Int) -> String {
    while cursor + 1 < line_count && starts[cursor + 1] <= a {
      cursor += 1
    }
    let plain = lines[cursor].plain()
    let base = starts[cursor]
    let line_end = base + plain.length()
    if b <= line_end {
      return if a == base && b == line_end {
        plain
      } else {
        plain.view(start_offset=a - base, end_offset=b - base).to_owned()
      }
    }
    let buf = StringBuilder(size_hint=(b - a) * 2)
    buf.write_view(plain.view(start_offset=a - base))
    let mut pos = line_end
    let mut index = cursor
    while pos < b {
      buf.write_char('\n')
      pos += 1
      index += 1
      if pos >= b {
        break
      }
      let next = lines[index].plain()
      let take = @cmp.minimum(b - pos, next.length())
      buf.write_view(next.view(end_offset=take))
      pos += take
    }
    buf.to_string()
  }

  let active = ActiveStyles::new(style_map)
  active.enter(0)
  let mut group_offset = 0
  let events : Array[UInt64] = []
  for i, line in lines {
    // events of this line, in local offsets; sequence numbers: entering
    // `k` is `k`, leaving it is `n + k` (`k` counts the style span first)
    let has_style = line.style.to_bool()
    let length = line.length_
    let n = line.spans_.length() + (if has_style { 1 } else { 0 })
    if n == 0 {
      continue
    }
    events.clear()
    if has_style {
      events.push(pack_event(0, false, 0))
    }
    let shift = if has_style { 1 } else { 0 }
    for k, span in line.spans_ {
      events.push(pack_event(span.start, false, k + shift))
    }
    if has_style {
      events.push(pack_event(length, true, n))
    }
    for k, span in line.spans_ {
      events.push(pack_event(span.end, true, n + k + shift))
    }
    events.sort()
    let base = starts[i]
    let first_id = first_ids[i]
    for e in events {
      let offset = base + event_offset(e)
      if offset > group_offset {
        out.push(
          Segment::new(slice(group_offset, offset), style=active.current()),
        )
        group_offset = offset
      }
      let seq = event_seq(e)
      if event_leaving(e) {
        active.leave(first_id + seq - n)
      } else {
        active.enter(first_id + seq)
      }
    }
  }
  if total_length > group_offset {
    out.push(
      Segment::new(slice(group_offset, total_length), style=active.current()),
    )
  }
  if end != "" {
    out.push(Segment::new(end))
  }
}