/*
  StreamGoose Twitch backend — Go reference implementation
  
  Created by Luxferre in 2024, released into public domain
*/

package main

import (
  "fmt"
  "log"
  "strings"
  "strconv"
  "time"
  "net"
  "io/ioutil"
  "path/filepath"
  "unicode/utf8"
)

/* helper function to write a string to the socket */
func tcpwritestr(conn net.Conn, str string) int {
  i, err := conn.Write([]byte(str))
  if err != nil {
    log.Fatal("Error writing to socket: ", err)
  }
  return i
}

/* helper function to read a string from the socket */
func tcpreadstr(conn net.Conn) string {
  reply := make([]byte, 2048)
  _, err := conn.Read(reply)
  if err != nil {
    log.Fatal("Error reading from socket: ", err)
  }
  return string(reply)
}

/* emote handling function */
func tw_handle_emotes(text string, emote_data string) string {
  replacements := make(map[string]string) /* future replacement string cache */
  parts := strings.Split(emote_data, "/")
  textlen := utf8.RuneCountInString(text)
  for _, emotedesc := range parts { /* iterate over emote descriptors */
    emote_id, emote_range, _ := strings.Cut(emotedesc, ":")
    emote_ranges := strings.Split(emote_range, ",") /* there can be several ranges */
    for _, r := range emote_ranges {
      estart, eend, _ := strings.Cut(r, "-")
      estart_int, _ := strconv.Atoi(estart)
      eend_int, _ := strconv.Atoi(eend)
      if estart_int < 0 {
        estart_int = 0
      }
      if eend_int >= textlen {
        eend_int = textlen - 1
      }
      /* use rune conversion for correct offsets */
      replacements[string([]rune(text)[estart_int:eend_int + 1])] = "#TWEMOTE-" + emote_id + "#"
    }
  }
  for from, to := range replacements { /* iterate over replacements */
    text = strings.ReplaceAll(text, from, to)
  }
  return text  
}

/* main message procesing function */
func tw_process_message(rawmsg string) {
  /* if the message came from a real user, this is the one we should process */
  if strings.HasPrefix(rawmsg, "@badge-info=") {
    parts := strings.Split(rawmsg, " :") /* get raw message parts */
    /* first part is metadata, second is a full IRC username,
        the rest is the message ending with \r\n */
    metadata_str := strings.TrimSpace(parts[0])
    msg_text := strings.Join(parts[2:], " :")
    msg_text = strings.ReplaceAll(msg_text, "\u0000", "") /* remove excess null bytes */

    metadata := make(map[string]string) /* store parsed metadata here */
    parts = strings.Split(metadata_str, ";")
    for _, field := range parts { /* iterate over metadata fields */
      fieldkey, fieldval, _ := strings.Cut(field, "=")
      metadata[fieldkey] = fieldval
    }

    /* convert the timestamp */
    ts, err := strconv.ParseInt(metadata["tmi-sent-ts"], 10, 0)
    if err != nil { /* fallback to current time if anything */
      ts = time.Now().UnixMilli()
    }
    sent_dt := time.UnixMilli(ts)
    
    /* process Twitch emotes before any positional changes can occur */
    if _, ok := metadata["emotes"]; ok && len(metadata["emotes"]) > 0 {
      msg_text = tw_handle_emotes(msg_text, metadata["emotes"])
    }

    /* sanitize \x01ACTION... \x01 in the text*/
    msg_text = strings.ReplaceAll(msg_text, "\u0001ACTION", "/me")
    msg_text = strings.ReplaceAll(msg_text, "\u0001", "")

    /* prepare output message */
    out_message := ChatMessage {
      Type: "tw",
      Username: metadata["display-name"],
      Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
      Text: strings.TrimSpace(msg_text),
      Icon: "",
    }

    /* process username badges */
    if _, ok := metadata["badges"]; ok {
      badges := strings.Split(metadata["badges"], ",") /* they are comma-delimited */
      if len(badges) > 0 { /* process badge names */
        for i, badge := range badges { /* extract the badge name */
          bname, _, _ := strings.Cut(badge, "/")
          badges[i] = bname /* reassign */
        }
        out_message.Type += "-" + strings.Join(badges, "-")
      }
    }

    /* process username color preference */
    if _, ok := metadata["color"]; ok && len(metadata["color"]) > 0 {
      out_message.Type += "-color" + metadata["color"]
    }

    /* process third-party action */
    if strings.HasPrefix(out_message.Text, "/me") {
      out_message.Type += "-me"
      out_message.Text = strings.TrimSpace(out_message.Text[4:])
    }

    /* process system messages (e.g. in USERNOTICE event) */
    if _, ok := metadata["system-msg"]; ok && len(metadata["system-msg"]) > 0 {
      sysmsg := strings.ReplaceAll(metadata["system-msg"], "\\s", " ")
      oldtext := out_message.Text
      out_message.Text = strings.Join(strings.Fields(sysmsg), " ")
      if len(oldtext) > 0 {
        out_message.Text += ": " + oldtext
      }
    }

    /* finally, send it to the message broker via the message_transport channel */
    message_transport <- out_message
  }
}

/* entry point */
func twitch_run(config map[string]interface{}) {
  token_path := filepath.Join(APP_ROOT_DIR, "auth", "twitch-token.txt")
  token, err := ioutil.ReadFile(token_path)
  token_err_msg := "[twitch] No Twitch token file provided or it is empty. "
  token_err_msg += "Please visit https://twitchapps.com/tmi/, generate a token there "
  token_err_msg += "and paste it into the " + token_path +" file!"
  if err != nil {
    log.Fatal(token_err_msg + "\nDetailed error: ", err)
  }
  config["tw_token"] = strings.TrimSpace(string(token))
  if len(config["tw_token"].(string)) < 1 {
    log.Fatal(token_err_msg)
  }

  /* Connect to the Twitch IRC server */
  conn, err := net.Dial("tcp", "irc.chat.twitch.tv:6667")
  if err != nil {
    log.Fatal("[twitch] Error connecting to Twitch: ", err)
  }

  tcpwritestr(conn, fmt.Sprintf("PASS %s\n", config["tw_token"]))
  tcpwritestr(conn, fmt.Sprintf("NICK %s\n", config["tw_nickname"]))
  tcpwritestr(conn, fmt.Sprintf("JOIN %s\n", config["tw_channel"]))
  /* request advanced chat message capabilities */
  tcpwritestr(conn, "CAP REQ :twitch.tv/membership twitch.tv/tags twitch.tv/commands\n")

  log.Println("StreamGoose Twitch Go backend connected")

  var rawmsg string

  for { /* main loop */
    rawmsg = tcpreadstr(conn)
    if _, ok := config["debug"]; ok && config["debug"].(bool) == true {
      log.Println(rawmsg)
    }
    if strings.HasPrefix(rawmsg, "PING") { /* respond to the PING message */
      tcpwritestr(conn, "PONG\n")
    } else { /* run main procesing */
      tw_process_message(rawmsg)
    }
  }
}
