/*
  StreamGoose main server/UI launcher — Go reference implementation
 
  Created by Luxferre in 2024, released into public domain
*/
package main

import (
  "os"
  "fmt"
  "log"
  "time"
  "strings"
  "slices"
  "encoding/json"
  "net/http"
  "io/ioutil"
  "path/filepath"
)

/* main chat message structure */
type ChatMessage struct {
  Id uint64        `json:"id"`        /* message id (auto-filled) */
  NativeId string  `json:"native_id"` /* message id native to the platform */
  AuthorId string  `json:"author_id"` /* author id native to the platform */
  Type string      `json:"type"`      /* message type string */
  Timestamp string `json:"timestamp"` /* timestamp string (formatted) */
  Username string  `json:"username"`  /* author username (as displayed) */
  Text string      `json:"text"`      /* message text */
  Icon string      `json:"icon"`      /* optional message icon URI */
}

var global_msg_id uint64 = 0 /* track global message ID */
var message_transport chan ChatMessage  /* global message channel */
var sse_transports []chan string /* SSE output channels */
var APP_ROOT_DIR string /* global application root directory */

/* headless behaviour by default */
var goose_start_gui = func(config map[string]interface{}) {
  /* target server listening address */
  const fmt_str = "[headless] Connect to http://%s:%d in any browser to see the chat window"
  log.Printf(fmt_str, config["server_listen_ip"].(string), int(config["server_port"].(float64)))
  /* just do nothing except sleep in the main thread */
  for {
    time.Sleep(time.Duration(3600) * time.Second)
  }
}

/* escape HTML entities in JS parameters */
func esc(s string) string {
  s = strings.ReplaceAll(s, "&", "&amp;")
  s = strings.ReplaceAll(s, "'", "&apos;")
  s = strings.ReplaceAll(s, "\"", "&quot;")
  s = strings.ReplaceAll(s, "<", "&lt;")
  s = strings.ReplaceAll(s, ">", "&gt;")
  return s
}

/* chat message appender thread */
func appendChatMessages() {
  var msg ChatMessage
  for {
    msg = <-message_transport /* receive it from the channel */
    /* populate the running id */
    global_msg_id += 1
    msg.Id = global_msg_id

    /* TODO any additional message logging should be done at this point! */
    
    /* serialize the message and send it to the SSE channel */
    json_msg, err := json.Marshal(msg)
    if err != nil {
      log.Println("Error serializing the message:", err)
    }

    /* broadcast the message across the clients */
    for _, transport := range sse_transports {
      if transport != nil {
        transport <- string(json_msg)
      }
    }
  }
}

/* POST /new handler (for external backends) */
func msgAddHandler(w http.ResponseWriter, r *http.Request) {
  decoder := json.NewDecoder(r.Body)
  var msg ChatMessage         /* create a new message */
  err := decoder.Decode(&msg) /* fill in the message structure */
  if err != nil {
    log.Println("Error decoding POST request:", err)
    return
  }
  /* post it to the transport channel */
  message_transport <- msg
  w.Write([]byte("{\"status\": 1}")) /* respond with a status */
}

/* GET /sse handler */
func sseHandler(w http.ResponseWriter, r *http.Request) {
  /* prepare the headers */
  w.Header().Set("Content-Type", "text/event-stream")
  w.Header().Set("Cache-Control", "no-cache")
  w.Header().Set("Connection", "keep-alive")
  w.Header().Set("Access-Control-Allow-Origin", "*")

  /* create the channel */
  transport := make(chan string)
  /* append it to the broadcast list */
  sse_transports = append(sse_transports, transport)

  /* close the channel after exiting the function */
  defer func() {
    close(transport)
    idx := slices.IndexFunc(sse_transports, func(c chan string) bool { return c == transport })
    sse_transports[idx] = nil
    transport = nil
  }()

  /* use HTTP response flusher */
  flusher, _ := w.(http.Flusher)

  /* message loop */
  for {
    select {
      case message := <-transport:
        fmt.Fprintf(w, "data: %s\n\n", message)
        flusher.Flush()
      case <-r.Context().Done(): /* handle disconnects */
        return
    }
  }
}

/* entry point */
func main() {
  /* detect current executable directory and load the config from there */
  ex, err := os.Executable()
  if err != nil {
    panic(err)
  }
  ex, err = filepath.EvalSymlinks(ex)
  if err != nil {
    panic(err)
  }
  APP_ROOT_DIR = filepath.Dir(ex)
  config_path := filepath.Join(APP_ROOT_DIR, "config.json")
  if len(os.Args) > 1 {
    config_path = os.Args[1]
  }
  content, err := ioutil.ReadFile(config_path)
  if err != nil {
    log.Fatal("Error when opening config file: ", err)
  }
  var config map[string]interface{}
  err = json.Unmarshal(content, &config)
  if err != nil {
    log.Fatal("Error during Unmarshal(): ", err)
  }

  /* create the message transport channel */
  message_transport = make(chan ChatMessage)

  /* target server listening address */
  target_addr := fmt.Sprintf("%s:%d", config["server_listen_ip"].(string), int(config["server_port"].(float64)))

  go appendChatMessages() /* start the appender thread */

  go func() { /* start the server thread */
    /* serve the POST /new */
    http.HandleFunc("/new", msgAddHandler)
    /* serve the GET /sse */
    http.HandleFunc("/sse", sseHandler)
    /* serve the GET /badge/{platform}/{badgename} (call to the badge provider) */
    http.HandleFunc("/badge/{platform}/{badgename}", bp_handle_request)
    /* serve the webroot directory */
    fs := http.FileServer(http.Dir(filepath.Join(APP_ROOT_DIR, "webroot")))
    http.Handle("/", fs)
    log.Println(fmt.Sprintf("Listening on %s\n", target_addr))
    err = http.ListenAndServe(target_addr, nil)
    if err != nil {
      log.Fatal(err)
    }
  }()
  
  if _, ok := config["tw_backend_enabled"]; ok && config["tw_backend_enabled"].(bool) == true {
    go twitch_run(config)  /* start the Twitch backend */
  }

  if _, ok := config["yt_backend_enabled"]; ok && config["yt_backend_enabled"].(bool) == true {
    go youtube_run(config) /* start the YouTube backend */
  }

  if _, ok := config["oc_backend_enabled"]; ok && config["oc_backend_enabled"].(bool) == true {
    go owncast_run(config) /* start the Owncast backend */
  }

  if _, ok := config["kk_backend_enabled"]; ok && config["kk_backend_enabled"].(bool) == true {
    go kick_run(config) /* start the Kick backend */
  }

  /* start the GUI from gui.go file */
  goose_start_gui(config)

}
