///|
/// Structured render payload for `render_frame_state` clients. The server
/// diffs terminal state against what each client already holds, so `lines`
/// carries only the rows to repaint unless `full` is set. Runs keep explicit
/// `col` and `width` so the client can map text back to grid cells for
/// selection without replaying a terminal emulator.
pub(all) struct Frame {
  /// Whether `lines` covers every viewport row. A diff frame repaints only
  /// the listed rows; an empty `runs` array clears its row.
  full : Bool
  /// Frame-scoped style dictionary referenced by `Run::style`.
  styles : Array[RunStyle]
  lines : Array[Line]
  /// Absent while the client views scrollback or the cursor is hidden.
  cursor : Cursor?
} derive(Eq, Debug)

///|
pub(all) struct Line {
  y : Int
  runs : Array[Run]
} derive(Eq, Debug)

///|
/// One styled run of cells. An ASCII run holds one cell per character, so
/// `width` equals the character count. A non-ASCII glyph always forms its own
/// run whose `width` (1 or 2) is its grid-cell span; the client renders it as
/// a fixed-width centered span so glyph advances cannot push later columns
/// off the grid.
pub(all) struct Run {
  col : Int
  width : Int
  text : String
  /// Index into `Frame::styles`; `None` is the default style.
  style : Int?
} derive(Eq, Debug)

///|
const RunBold : Int = 0x1

///|
const RunFaint : Int = 0x2

///|
const RunItalic : Int = 0x4

///|
const RunStrikethrough : Int = 0x8

///|
const RunOverline : Int = 0x10

///|
const RunBlink : Int = 0x20

///|
const RunInverse : Int = 0x40

///|
const RunUnderlineShift : Int = 7

///|
/// SGR 8: the run's glyphs ship for selection and copy but must not paint,
/// including their decorations. Bits 7-9 hold the underline kind.
const RunConceal : Int = 0x400

///|
/// Colors are `#rrggbb`; `None` means the client's default foreground or
/// background. The server resolves palette indices (including bold
/// promotion) but never substitutes its own default colors, so client themes
/// keep owning the defaults — including under `inverse`, which the client
/// applies by swapping resolved colors with its theme defaults.
pub(all) struct RunStyle {
  fg : String?
  bg : String?
  underline_color : String?
  flags : Int
} derive(Eq, Debug)

///|
pub(all) enum Underline {
  None
  Single
  Double
  Curly
  Dotted
  Dashed
} derive(Eq, Debug)

///|
fn Underline::to_bits(self : Underline) -> Int {
  match self {
    None => 0
    Single => 1
    Double => 2
    Curly => 3
    Dotted => 4
    Dashed => 5
  }
}

///|
fn Underline::from_bits(bits : Int) -> Underline {
  match bits {
    1 => Single
    2 => Double
    3 => Curly
    4 => Dotted
    5 => Dashed
    _ => None
  }
}

///|
pub fn RunStyle::new(
  fg? : String,
  bg? : String,
  underline_color? : String,
  bold? : Bool = false,
  faint? : Bool = false,
  italic? : Bool = false,
  strikethrough? : Bool = false,
  overline? : Bool = false,
  blink? : Bool = false,
  inverse? : Bool = false,
  conceal? : Bool = false,
  underline? : Underline = Underline::None,
) -> RunStyle {
  let mut flags = underline.to_bits() << RunUnderlineShift
  if bold {
    flags = flags | RunBold
  }
  if faint {
    flags = flags | RunFaint
  }
  if italic {
    flags = flags | RunItalic
  }
  if strikethrough {
    flags = flags | RunStrikethrough
  }
  if overline {
    flags = flags | RunOverline
  }
  if blink {
    flags = flags | RunBlink
  }
  if inverse {
    flags = flags | RunInverse
  }
  if conceal {
    flags = flags | RunConceal
  }
  { fg, bg, underline_color, flags }
}

///|
pub fn RunStyle::bold(self : RunStyle) -> Bool {
  (self.flags & RunBold) != 0
}

///|
pub fn RunStyle::faint(self : RunStyle) -> Bool {
  (self.flags & RunFaint) != 0
}

///|
pub fn RunStyle::italic(self : RunStyle) -> Bool {
  (self.flags & RunItalic) != 0
}

///|
pub fn RunStyle::strikethrough(self : RunStyle) -> Bool {
  (self.flags & RunStrikethrough) != 0
}

///|
pub fn RunStyle::overline(self : RunStyle) -> Bool {
  (self.flags & RunOverline) != 0
}

///|
pub fn RunStyle::blink(self : RunStyle) -> Bool {
  (self.flags & RunBlink) != 0
}

///|
pub fn RunStyle::inverse(self : RunStyle) -> Bool {
  (self.flags & RunInverse) != 0
}

///|
pub fn RunStyle::conceal(self : RunStyle) -> Bool {
  (self.flags & RunConceal) != 0
}

///|
pub fn RunStyle::underline(self : RunStyle) -> Underline {
  Underline::from_bits((self.flags >> RunUnderlineShift) & 7)
}

///|
/// Whether this is the default style a `Run` encodes as `style : None`.
pub fn RunStyle::is_default(self : RunStyle) -> Bool {
  self.fg is None &&
  self.bg is None &&
  self.underline_color is None &&
  self.flags == 0
}

///|
pub(all) enum CursorShape {
  Block
  Bar
  Underline
} derive(Eq, Debug)

///|
/// The visible cursor. Coordinates are zero-based viewport cells; a cursor
/// on a wide glyph's trailing cell is already snapped to the glyph head.
/// `color` is set only when the child application overrides the cursor
/// color, so the client theme keeps the default. The glyph under a block
/// cursor, its cell span, and its inverted color are not on the wire: the
/// client derives them from the run grid it already holds.
pub(all) struct Cursor {
  x : Int
  y : Int
  shape : CursorShape
  blink : Bool
  color : String?
} derive(Eq, Debug)

///|
pub impl ToJson for RunStyle with fn to_json(self) {
  let obj : Map[String, Json] = Map([])
  if self.fg is Some(value) {
    obj["fg"] = Json::string(value)
  }
  if self.bg is Some(value) {
    obj["bg"] = Json::string(value)
  }
  if self.underline_color is Some(value) {
    obj["uc"] = Json::string(value)
  }
  if self.flags != 0 {
    obj["fl"] = self.flags.to_json()
  }
  Json::object(obj)
}

///|
pub impl @json.FromJson for RunStyle with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected run style object"))
  }
  let fg = if fields.get("fg") is Some(String(value)) {
    Some(value)
  } else {
    None
  }
  let bg = if fields.get("bg") is Some(String(value)) {
    Some(value)
  } else {
    None
  }
  let underline_color = if fields.get("uc") is Some(String(value)) {
    Some(value)
  } else {
    None
  }
  let flags = if fields.get("fl") is Some(Number(value, ..)) {
    value.to_int()
  } else {
    0
  }
  { fg, bg, underline_color, flags }
}

///|
pub impl ToJson for Run with fn to_json(self) {
  let obj : Map[String, Json] = {
    "c": self.col.to_json(),
    "w": self.width.to_json(),
    "t": Json::string(self.text),
  }
  if self.style is Some(index) {
    obj["s"] = index.to_json()
  }
  Json::object(obj)
}

///|
pub impl @json.FromJson for Run with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected run object"))
  }
  let col = if fields.get("c") is Some(Number(value, ..)) {
    value.to_int()
  } else {
    0
  }
  let width = if fields.get("w") is Some(Number(value, ..)) {
    value.to_int()
  } else {
    0
  }
  let text = if fields.get("t") is Some(String(value)) { value } else { "" }
  let style = if fields.get("s") is Some(Number(value, ..)) {
    Some(value.to_int())
  } else {
    None
  }
  { col, width, text, style }
}

///|
pub impl ToJson for Line with fn to_json(self) {
  { "y": self.y.to_json(), "runs": self.runs.to_json() }
}

///|
pub impl @json.FromJson for Line with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected line object"))
  }
  let y = if fields.get("y") is Some(Number(value, ..)) {
    value.to_int()
  } else {
    0
  }
  let runs : Array[Run] = match fields.get("runs") {
    Some(runs_json) => @json.from_json(runs_json, path~)
    _ => []
  }
  { y, runs }
}

///|
fn CursorShape::to_wire(self : CursorShape) -> String {
  match self {
    Block => "block"
    Bar => "bar"
    Underline => "underline"
  }
}

///|
fn CursorShape::from_wire(value : String) -> CursorShape {
  match value {
    "bar" => Bar
    "underline" => Underline
    _ => Block
  }
}

///|
pub impl ToJson for Cursor with fn to_json(self) {
  let obj : Map[String, Json] = {
    "x": self.x.to_json(),
    "y": self.y.to_json(),
    "shape": Json::string(self.shape.to_wire()),
  }
  if self.blink {
    obj["blink"] = self.blink.to_json()
  }
  if self.color is Some(value) {
    obj["color"] = Json::string(value)
  }
  Json::object(obj)
}

///|
pub impl @json.FromJson for Cursor with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected cursor object"))
  }
  let x = if fields.get("x") is Some(Number(value, ..)) {
    value.to_int()
  } else {
    0
  }
  let y = if fields.get("y") is Some(Number(value, ..)) {
    value.to_int()
  } else {
    0
  }
  let shape = if fields.get("shape") is Some(String(value)) {
    CursorShape::from_wire(value)
  } else {
    Block
  }
  let blink = if fields.get("blink") is Some(True) { true } else { false }
  let color = if fields.get("color") is Some(String(value)) {
    Some(value)
  } else {
    None
  }
  { x, y, shape, blink, color }
}