// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// How a record becomes one line.
///
/// A layout knows nothing about where the line goes; a sink is handed the line
/// its slot's layout made of the record.
pub(open) trait Layout {
  /// The record as one line of text, without the line break.
  fn render(Self, Record, Scene) -> String
}

///|
pub impl Layout for Pattern with fn render(self, record, scene) {
  self.render_line(record, scene)
}

///|
pub extend Pattern with Layout::{render}

///|
/// One `key=value` line per record: `time=… level=… target=… msg=… key=value`.
///
/// Values are quoted the logfmt way, so a message with a space in it cannot be
/// mistaken for two fields. This is the shape a shell pipeline reads with
/// `cut` and `awk`.
pub struct Logfmt {}

///|
/// The layout; it has nothing to configure.
pub fn Logfmt::new() -> Logfmt {
  Logfmt::{ }
}

///|
pub impl Layout for Logfmt with fn render(_self, record, scene) {
  let out = StringBuilder()
  out.write_string("time=")
  Clock::Utc.write(out, record.time, 0)
  out.write_string(" level=")
  out.write_string(record.level.name())
  out.write_string(" target=")
  write_logfmt(out, record.target)
  out.write_string(" msg=")
  write_logfmt(out, record.msg)
  write_fields(out, record, true, false)
  let _ = scene
  out.to_string()
}

///|
pub extend Logfmt with Layout::{render}

///|
/// A first line with the time, level, target and message, then one aligned
/// `key = value` line per field. What a person reads in a terminal when the
/// fields matter more than the bytes.
pub struct Pretty {}

///|
/// The layout; it has nothing to configure.
pub fn Pretty::new() -> Pretty {
  Pretty::{ }
}

///|
pub impl Layout for Pretty with fn render(_self, record, scene) {
  let out = StringBuilder()
  let clock = StringBuilder()
  Clock::Local.write(clock, record.time, (scene.identity.offset)(record.time))
  paint(out, scene.colour, Colour::Grey, clock.to_string())
  out.write_char(' ')
  paint(
    out,
    scene.colour,
    record.level.colour,
    pad(record.level.label(scene.profile), 5, '<'),
  )
  out.write_char(' ')
  paint(out, scene.colour, Colour::White, record.target)
  out.write_char(' ')
  out.write_string(record.msg)
  let rows : Array[(String, String)] = []
  for pair in record.fields {
    flat_rows(rows, pair.0, pair.1)
  }
  let mut width = 0
  for (key, _) in rows {
    if key.char_length() > width {
      width = key.char_length()
    }
  }
  for (key, value) in rows {
    out.write_string("\n    ")
    paint(out, scene.colour, Colour::Cyan, pad(key, width, '<'))
    out.write_string(" = ")
    out.write_string(value)
  }
  out.to_string()
}

///|
fn flat_rows(
  rows : Array[(String, String)],
  key : String,
  value : Value,
) -> Unit {
  match value {
    Map(pairs) =>
      for (k, v) in pairs {
        flat_rows(rows, key + "." + k, v)
      }
    _ => rows.push((key, value.to_text()))
  }
}

///|
pub extend Pretty with Layout::{render}

///|
/// Wraps `text` in `colour` when the scene takes colour.
fn paint(
  out : StringBuilder,
  on : Bool,
  colour : Colour,
  text : String,
) -> Unit {
  if on && colour != Colour::Plain {
    out.write_string(colour.ansi())
    out.write_string(text)
    out.write_string(reset)
  } else {
    out.write_string(text)
  }
}