package main

import (
	"bytes"
	"io"
	"log"
  "fmt"
	"net/http"
	"os"
  "encoding/base64"
  "encoding/json"
	"sync"
	"time"
  "path/filepath"
  "github.com/dhowden/tag"
  "github.com/tcolgate/mp3"
  "math/rand"
  "math"
)

const (
	BUFFERSIZE = 8192
)

var artworkAsUrl = ""
var title = ""
var url = ""

type Connection struct {
	bufferChannel chan []byte
	buffer        []byte
}

type ConnectionPool struct {
	ConnectionMap map[*Connection]struct{}
	mu            sync.Mutex
}

type MetadataConnection struct {
  metadataSent bool
}

type MetadataConnectionPool struct {
	MetadataConnectionMap map[*MetadataConnection]struct{}
	mu            sync.Mutex
}

// Audio connection pool
func (cp *ConnectionPool) AddConnection(connection *Connection) {
	defer cp.mu.Unlock()
	cp.mu.Lock()
	cp.ConnectionMap[connection] = struct{}{}
}

func (cp *ConnectionPool) DeleteConnection(connection *Connection) {
	defer cp.mu.Unlock()
	cp.mu.Lock()
	delete(cp.ConnectionMap, connection)
}

func NewConnectionPool() *ConnectionPool {
	connectionMap := make(map[*Connection]struct{})
	return &ConnectionPool{ConnectionMap: connectionMap}
}

func (cp *ConnectionPool) Broadcast(buffer []byte) {
	defer cp.mu.Unlock()
	cp.mu.Lock()

	for connection := range cp.ConnectionMap {
		copy(connection.buffer, buffer)
		select {
		case connection.bufferChannel <- connection.buffer:
		default:
		}
	}
}

// Metadata connection pool
func (cp *MetadataConnectionPool) AddConnection(connection *MetadataConnection) {
	defer cp.mu.Unlock()
	cp.mu.Lock()
	cp.MetadataConnectionMap[connection] = struct{}{}
}

func (cp *MetadataConnectionPool) DeleteConnection(connection *MetadataConnection) {
	defer cp.mu.Unlock()
	cp.mu.Lock()
	delete(cp.MetadataConnectionMap, connection)
}

func NewMetadataConnectionPool() *MetadataConnectionPool {
	metadataConnectionMap := make(map[*MetadataConnection]struct{})
	return &MetadataConnectionPool{MetadataConnectionMap: metadataConnectionMap}
}

func (cp *MetadataConnectionPool) Broadcast() {
	defer cp.mu.Unlock()
	cp.mu.Lock()

	for connection := range cp.MetadataConnectionMap {
    connection.metadataSent = false
	}
}

func getFileDelay(mp3FilePath string) int64 {
  t := 0.0
  size := 0

  file, err := os.Open(mp3FilePath)
  if err != nil {
    log.Fatal(err)
  }

  d := mp3.NewDecoder(file)
  var f mp3.Frame
  skipped := 0

  for {
      if err := d.Decode(&f, &skipped); err != nil {
          if err == io.EOF {
              break
          }
          log.Println(err)
          return 0
      }
      t = t + f.Duration().Seconds()
      size = size + f.Size()
  }

  track_duration := 1000 * t

  delayVal := int64(math.Floor(track_duration)) * BUFFERSIZE / int64(size)
  log.Print(delayVal)

  return delayVal
}

func streamFolder(connectionPool *ConnectionPool, metadataConnectionPool *MetadataConnectionPool, list []string) {

	buffer := make([]byte, BUFFERSIZE)

  for _, music := range list {

    file, err := os.Open(filepath.Join("./music/", music))
    if err != nil {
      log.Fatal(err)
    }

    m, err := tag.ReadFrom(file)
    if err != nil {
      log.Fatal(err)
    }
    
    title = m.Title()
    log.Print(title)

    rawData := m.Raw()

    for _, v := range rawData {
      if _, isTagPicture := v.(*tag.Picture); isTagPicture {
        artworkMIMEType :=  v.(*tag.Picture).MIMEType
        artworkAsUrl = fmt.Sprintf("data:%s;base64,%s",artworkMIMEType, base64.StdEncoding.EncodeToString(m.Picture().Data))
      }
      
      if _, isTagComm := v.(*tag.Comm); isTagComm {
        entryDescription := v.(*tag.Comm).Description
        if entryDescription == "purl" {
          url = v.(*tag.Comm).Text
        }
      }
    }
    log.Print(url)

    metadataConnectionPool.Broadcast()

    ctn, err := io.ReadAll(file)
    if err != nil {
      log.Fatal(err)
    }

		// clear() is a new builtin function introduced in go 1.21. Just reinitialize the buffer if on a lower version.
		clear(buffer)
		tempfile := bytes.NewReader(ctn)
    delay := getFileDelay(filepath.Join("./music/", music))
		ticker := time.NewTicker(time.Millisecond * time.Duration(delay))

    // Send one buffer in advance to avoid client choking

		for range ticker.C {

			_, err := tempfile.Read(buffer)

			if err == io.EOF {

				ticker.Stop()
				break

			}

			connectionPool.Broadcast(buffer)

		}

	}

}

func main() {
	connPool := NewConnectionPool()
  metadataConnPool := NewMetadataConnectionPool()

  music_files := []string{}

  entries, err := os.ReadDir("./music")
  if err != nil {
      log.Fatal(err)
  }

  for _, e := range entries {
    if filepath.Ext(e.Name()) == ".mp3" {
      music_files = append(music_files, e.Name())
    }
  }

  log.Printf("%d Music file found.", len(music_files))

  rand.Seed(time.Now().UnixNano())
  rand.Shuffle(len(music_files), func(i, j int) { music_files[i], music_files[j] = music_files[j], music_files[i] })

	go streamFolder(connPool, metadataConnPool, music_files)

  http.Handle("/", http.FileServer(http.Dir("./dist")))

	http.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {

		w.Header().Add("Content-Type", "audio/mp3")
		w.Header().Add("Connection", "keep-alive")

		flusher, ok := w.(http.Flusher)
		if !ok {
			log.Println("Could not create flusher")
		}

		connection := &Connection{bufferChannel: make(chan []byte), buffer: make([]byte, BUFFERSIZE)}
		connPool.AddConnection(connection)
		log.Printf("%s has connected to the audio stream\n", r.Host)

		for {
			buf := <-connection.bufferChannel
			if _, err := w.Write(buf); err != nil {
				connPool.DeleteConnection(connection)
				log.Printf("%s's connection to the audio stream has been closed\n", r.Host)
				return
			}
			flusher.Flush()
			clear(connection.buffer)
		}
	})

  http.HandleFunc("/metadata", func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Access-Control-Allow-Origin", "*")
    w.Header().Set("Access-Control-Expose-Headers", "Content-Type")

    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    connection := &MetadataConnection{metadataSent: false}
		metadataConnPool.AddConnection(connection)
		log.Printf("%s has connected to the metadata stream\n", r.Host)
    // Simulate sending events (you can replace this with real data)
    for {
      if(connection.metadataSent == false) {
        data := map[string]string{ "title":"", "url":"", "artwork":"" }
        data["title"] = title
        data["url"] = url 
        data["artwork"] = artworkAsUrl

        finalData, err := json.Marshal(data)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Fprintf(w, "data: %s\n\n", fmt.Sprintf("%s", finalData))
        w.(http.Flusher).Flush()
        connection.metadataSent = true
      }
      time.Sleep(time.Second * 1)
    }
  })

	log.Println("Listening on port 8080...")
	log.Fatal(http.ListenAndServe(":8080", nil))

}