///|
/// Twitch IRC から読んだ 1 行の解釈結果。
pub(all) enum IrcMessage {
  /// サーバからの PING。payload をそのまま PONG で返す
  Ping(String)
  /// チャット発言
  Chat(Comment)
  /// それ以外(接続時の挨拶など)
  Other(String)
} derive(Debug, Eq)

///|
/// 匿名ユーザーで読み取り専用接続するためのログイン行。
/// `twitch.tv/tags` を要求して user-id・表示名・エモートの位置・送信時刻を受け取る。
pub fn twitch_login_lines(nick : String, channel : String) -> Array[String] {
  ["CAP REQ :twitch.tv/tags", "NICK \{nick}", "JOIN #\{channel.to_lower()}"]
}

///|
/// PING への返答行。
pub fn pong_line(payload : String) -> String {
  "PONG :\{payload}"
}

///|
/// IRC の 1 行(`[@tags] [:prefix] COMMAND [params] [:trailing]`)を解釈する。
pub fn parse_irc_line(raw : String) -> IrcMessage {
  let line = raw.trim(chars="\r\n").to_owned()
  let mut rest = line.view()
  let tags = if rest is ['@', .. after_at] {
    let (tag_part, after) = split_once(after_at, ' ')
    rest = after
    parse_tags(tag_part)
  } else {
    Map([])
  }
  let prefix = if rest is [':', .. after_colon] {
    let (p, after) = split_once(after_colon, ' ')
    rest = after
    p.to_owned()
  } else {
    ""
  }
  let (command, params) = split_once(rest, ' ')
  match command {
    "PING" => Ping(trailing_of(params))
    "PRIVMSG" => {
      let text = trailing_of(params)
      let (target, _) = split_once(params, ' ')
      let channel = target.trim_start(chars="#").to_owned().to_lower()
      let id = match tags.get("user-id") {
        Some(user_id) if !user_id.is_empty() => user_id
        _ => nick_of(prefix)
      }
      let at = match tags.get("tmi-sent-ts") {
        Some(ts) => parse_int64(ts)
        None => 0
      }
      let author = match tags.get("display-name") {
        Some(name) if !name.is_empty() => name
        _ => nick_of(prefix)
      }
      let emotes = match tags.get("emotes") {
        Some(tag) => emote_names(text, tag)
        None => []
      }
      Chat({
        participant_id: participant_id("twitch", id),
        author,
        text,
        at,
        emotes,
        channel,
      })
    }
    _ => Other(line)
  }
}

///|
/// `sep` で最初の 1 回だけ分割する。見つからなければ残りは空。
fn split_once(s : StringView, sep : Char) -> (StringView, StringView) {
  match s.find(sep.to_string()) {
    Some(i) => (s[:i], s[i + 1:])
    None => (s, "")
  }
}

///|
/// params の `:` 以降(trailing)を取り出す。無ければ params 全体。
fn trailing_of(params : StringView) -> String {
  match params.find(":") {
    Some(i) => params[i + 1:].to_owned()
    None => params.to_owned()
  }
}

///|
/// `nick!user@host` から nick を取り出す。
fn nick_of(prefix : String) -> String {
  match prefix.find("!") {
    Some(i) => prefix[:i].to_owned()
    None => prefix
  }
}

///|
fn parse_tags(part : StringView) -> Map[String, String] {
  let tags : Map[String, String] = Map([])
  for pair in part.split(";") {
    let (key, value) = split_once(pair, '=')
    tags[key.to_owned()] = value.to_owned()
  }
  tags
}

///|
fn parse_int64(s : String) -> Int64 {
  let mut n : Int64 = 0
  for c in s {
    guard c is ('0'..='9') else { return 0 }
    n = n * 10 + (c.to_int() - '0').to_int64()
  }
  n
}

///|
/// `emotes` タグ(`:<開始>-<終了>,.../:...`、位置は文字単位)から、本文中のエモート名を取り出す。
/// 重複は除く。壊れた範囲は無視する。
fn emote_names(text : String, tag : String) -> Array[String] {
  let chars = text.to_array()
  let names : Array[String] = []
  for emote in tag.split("/") {
    let (_, ranges) = split_once(emote, ':')
    for range in ranges.split(",") {
      let (start_text, end_text) = split_once(range, '-')
      guard parse_index(start_text) is Some(start) &&
        parse_index(end_text) is Some(end) &&
        start <= end &&
        end < chars.length() else {
        continue
      }
      let name = StringBuilder()
      for i in start..<=end {
        name.write_char(chars[i])
      }
      let name = name.to_string()
      if !names.contains(name) {
        names.push(name)
      }
    }
  }
  names
}

///|
fn parse_index(s : StringView) -> Int? {
  guard s.length() > 0 && s.length() <= 9 else { return None }
  let mut n = 0
  for c in s {
    guard c is ('0'..='9') else { return None }
    n = n * 10 + (c.to_int() - '0')
  }
  Some(n)
}