///|
/// An outgoing presence, also usable as the initial Identify presence.
pub(all) struct UpdatePresence {
  status : PresenceStatus
  activities : Array[BotActivity]
  afk : Bool
  since : Int64?
} derive(Debug)

///|
/// Construct a presence; an absent `since` is encoded as null.
pub fn UpdatePresence::UpdatePresence(
  status~ : PresenceStatus = Online,
  activities~ : Array[BotActivity] = [],
  afk~ : Bool = false,
  since? : Int64,
) -> UpdatePresence {
  { status, activities, afk, since, }
}

///|
/// Encode the four required Gateway Presence Update fields.
pub impl ToJson for UpdatePresence with fn to_json(self) {
  {
    "status": self.status.to_json(),
    "activities": Json(self.activities),
    "afk": Json(self.afk),
    "since": match self.since {
      Some(value) => Json::number(value.to_double(), repr=value.to_string())
      None => Json::null()
    },
  }
}

///|
/// The activity fields bot users may send, separate from received activities.
pub(all) struct BotActivity {
  name : String
  type_ : ActivityType
  state : String?
  url : String?
} derive(ToJson(fields(type_(rename="type"))), Debug)

///|
/// Show a playing activity.
pub fn BotActivity::playing(name : String) -> BotActivity {
  { name, type_: Playing, state: None, url: None, }
}

///|
/// Show a streaming activity with a Twitch or YouTube URL.
pub fn BotActivity::streaming(name : String, url : String) -> BotActivity {
  { name, type_: Streaming, state: None, url: Some(url), }
}

///|
/// Show a listening activity.
pub fn BotActivity::listening(name : String) -> BotActivity {
  { name, type_: Listening, state: None, url: None, }
}

///|
/// Show a watching activity.
pub fn BotActivity::watching(name : String) -> BotActivity {
  { name, type_: Watching, state: None, url: None, }
}

///|
/// Show a competing activity.
pub fn BotActivity::competing(name : String) -> BotActivity {
  { name, type_: Competing, state: None, url: None, }
}

///|
/// Show custom status text using the conventional name `Custom Status`.
pub fn BotActivity::custom(state : String) -> BotActivity {
  { name: "Custom Status", type_: Custom, state: Some(state), url: None, }
}

///|
/// Select all members, a nonempty username prefix, or 1 to 100 user ids.
pub(all) enum MemberSelect {
  All
  Prefix(String, limit~ : Int32)
  Ids(Array[UserId])
} derive(Debug)

///|
/// An invalid outgoing Gateway command, with the offending field and reason.
pub(all) suberror GatewayCommandError {
  InvalidCommand(LimitViolation)
} derive(Debug)

///|
/// Request members for one guild. `validate` is called by typed send paths.
pub(all) struct RequestGuildMembers {
  guild_id : GuildId
  select : MemberSelect
  presences : Bool
  nonce : String?
} derive(Debug)

///|
/// Check selection bounds and the nonce's UTF-8 byte length before sending.
pub fn RequestGuildMembers::validate(
  self : RequestGuildMembers,
) -> Unit raise GatewayCommandError {
  let checker = LimitChecker::new()
  match self.select {
    All => ()
    Prefix(query, limit~) => {
      if query.is_empty() {
        checker.report("query", "prefix must be nonempty")
      }
      checker.count("limit", Some(limit), min=1, max=100)
    }
    Ids(ids) => checker.count("user_ids", Some(ids.length()), min=1, max=100)
  }
  if self.nonce is Some(nonce) {
    // Count UTF-8 bytes, rather than the module's usual character limits.
    let mut bytes = 0
    for c in nonce.iter() {
      let code = c.to_int()
      bytes += if code < 0x80 {
        1
      } else if code < 0x800 {
        2
      } else if code < 0x10000 {
        3
      } else {
        4
      }
    }
    if bytes > 32 {
      checker.report("nonce", "\{bytes} UTF-8 bytes (at most 32)")
    }
  }
  if checker.violations.get(0) is Some(violation) {
    raise InvalidCommand(violation)
  }
}

///|
/// Encode exactly one of `query`/`limit` and `user_ids`.
pub impl ToJson for RequestGuildMembers with fn to_json(self) {
  let fields : Map[String, Json] = {
    "guild_id": self.guild_id.to_json(),
    "presences": Json(self.presences),
  }
  match self.select {
    All => {
      fields["query"] = ""
      fields["limit"] = 0
    }
    Prefix(query, limit~) => {
      fields["query"] = Json(query)
      fields["limit"] = Json(limit)
    }
    Ids(ids) => fields["user_ids"] = Json(ids)
  }
  if self.nonce is Some(nonce) {
    fields["nonce"] = Json(nonce)
  }
  Json::object(fields)
}

///|
/// Request soundboard sounds for each listed guild.
pub(all) struct RequestSoundboardSounds {
  guild_ids : Array[GuildId]
} derive(ToJson, Debug)

///|
/// An ephemeral channel field; `Other` preserves future field names.
#non_exhaustive
pub(all) enum ChannelInfoField {
  Status
  VoiceStartTime
  Other(String)
} derive(Eq, Debug)

///|
/// Encode a requested channel field name.
pub impl ToJson for ChannelInfoField with fn to_json(self) {
  match self {
    Status => "status"
    VoiceStartTime => "voice_start_time"
    Other(value) => Json(value)
  }
}

///|
/// Request ephemeral channel data for a guild.
pub(all) struct RequestChannelInfo {
  guild_id : GuildId
  fields : Array[ChannelInfoField]
} derive(ToJson, Debug)