// Text objects: the leaves of Block Kit.
//
// https://docs.slack.dev/reference/block-kit/composition-objects/text-object

///|
/// Which kind of text object this is.
///
/// `Other` exists so that an unrecognised text type is representable rather
/// than silently dropped. java-slack-sdk raises on one immediately; here it is
/// parsed, kept, and raised on by `validate` under `Strict` -- which round-trips
/// and still refuses when refusing is what you asked for.
pub(all) enum TextKind {
  PlainText
  Mrkdwn
  Other(String)
} derive(Eq, Debug)

///|
pub fn TextKind::type_name(self : Self) -> String {
  match self {
    PlainText => "plain_text"
    Mrkdwn => "mrkdwn"
    Other(t) => t
  }
}

///|
pub fn TextKind::of_name(name : String) -> TextKind {
  match name {
    "plain_text" => PlainText
    "mrkdwn" => Mrkdwn
    other => Other(other)
  }
}

///|
/// A `plain_text` or `mrkdwn` text object.
///
/// One struct with a kind rather than two variants: the two differ only in
/// which of `emoji` and `verbatim` is meaningful, the schema accepts either
/// wherever text is allowed, and code that handles "some text" would otherwise
/// have to match twice everywhere.
pub(all) struct TextObject {
  kind : TextKind
  text : String
  /// `plain_text` only: render `:emoji:` shortcodes as emoji.
  emoji : Bool?
  /// `mrkdwn` only: skip Slack's automatic link, channel and user detection.
  verbatim : Bool?
  extra : Map[String, Json]
} derive(Eq, Debug)

///|
pub fn TextObject::plain(text : String, emoji? : Bool) -> TextObject {
  { kind: PlainText, text, emoji, verbatim: None, extra: Map([]) }
}

///|
pub fn TextObject::mrkdwn(text : String, verbatim? : Bool) -> TextObject {
  { kind: Mrkdwn, text, emoji: None, verbatim, extra: Map([]) }
}

///|
pub fn TextObject::type_name(self : Self) -> String {
  self.kind.type_name()
}

///|
/// `None` when this is not a text object at all: not an object, or with no
/// `type` or no `text` string.
///
/// An unknown `type` is NOT a reason to refuse -- that is what `Other` is for.
/// An empty `type` or an empty `text` is not a reason either: Slack's own
/// scrubbed fixtures contain both, and a parser that refused them could not
/// read the corpus it is validated against.
pub fn TextObject::from_json(j : Json) -> TextObject? {
  guard j is Object(o) else { return None }
  let rest = fields_of(o)
  guard take_str(rest, "type") is Some(type_name) else { return None }
  guard take_str(rest, "text") is Some(text) else { return None }
  Some({
    kind: TextKind::of_name(type_name),
    text,
    emoji: take_bool(rest, "emoji"),
    verbatim: take_bool(rest, "verbatim"),
    extra: rest,
  })
}

///|
pub fn TextObject::to_json(self : Self) -> Json {
  let o = out_of(self.type_name())
  o["text"] = Json::string(self.text)
  put_bool(o, "emoji", self.emoji)
  put_bool(o, "verbatim", self.verbatim)
  merge_extra(o, self.extra)
}

///|
/// Raise if this text object's type is one Slack does not define.
pub fn TextObject::validate(self : Self) -> Unit raise BlockParseError {
  if self.kind is Other(t) {
    raise UnknownTextObject(t)
  }
}