package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"strings"

	"code.ivysaur.me/libnmdc"
	"github.com/googollee/go-socket.io"
)

type ActiveConnection struct {
	s *socketio.Socket
	h *libnmdc.HubConnection
}

type App struct {
	cfg *Config
}

func NewApp(ConfigFilePath string) (*App, error) {

	cfgData, err := ioutil.ReadFile(ConfigFilePath)
	if err != nil {
		return nil, err
	}

	cfg := Config{}
	err = json.Unmarshal(cfgData, &cfg)
	if err != nil {
		return nil, err
	}

	return &App{cfg: &cfg}, nil
}

type UserMessageStruct struct {
	User    string `json:"user"`
	Message string `json:"message,omitempty"`
}

func (this *App) HubWorker(Nick, Pass string, so socketio.Socket, done chan struct{}) {

	log.Printf("[%s] Connecting to hub\n", so.Id())

	selfUser := libnmdc.NewUserInfo(Nick)
	selfUser.ClientTag = this.cfg.Hub.Tag
	selfUser.ClientVersion = libnmdc.DEFAULT_CLIENT_VERSION

	hco := libnmdc.HubConnectionOptions{
		Address:           libnmdc.HubAddress(fmt.Sprintf("%s:%d", this.cfg.Hub.Address, this.cfg.Hub.Port)),
		Self:              *selfUser,
		NickPassword:      Pass,
		NumEventsToBuffer: 0,
	}

	hub := hco.Connect()

	defer func() {
		hub.Disconnect()
		so.Emit("disconnect") // necessary? https://github.com/googollee/go-socket.io/issues/117
		log.Printf("[%s] Leaving worker\n", so.Id())
	}()

	// Register after-connected SIO handlers

	so.On("pub", func(data map[string]string) {
		hub.SayPublic(data["message"])
	})

	so.On("priv", func(data map[string]string) {
		hub.SayPrivate(data["user"], data["message"])
	})

	so.On("raw", func(data map[string]string) {
		hub.SayRaw(data["message"])
	})

	// Loop hub connection

	for {
		select {

		case hev, ok := <-hub.OnEvent:
			if !ok {
				log.Printf("[%s] hub chan closed\n", so.Id())
				return // abandon
			}

			switch hev.EventType {
			case libnmdc.EVENT_SYSTEM_MESSAGE_FROM_CONN, libnmdc.EVENT_SYSTEM_MESSAGE_FROM_HUB:
				so.Emit("sys", hev.Message)

			case libnmdc.EVENT_PUBLIC:
				so.Emit("pub", UserMessageStruct{User: hev.Nick, Message: hev.Message})

			case libnmdc.EVENT_PRIVATE:
				so.Emit("priv", UserMessageStruct{User: hev.Nick, Message: hev.Message})

			case libnmdc.EVENT_USER_JOINED:
				so.Emit("join", UserMessageStruct{User: hev.Nick})

			case libnmdc.EVENT_USER_PART:
				so.Emit("part", UserMessageStruct{User: hev.Nick})

			case libnmdc.EVENT_CONNECTION_STATE_CHANGED:
				if hev.StateChange == libnmdc.CONNECTIONSTATE_CONNECTED {
					so.Emit("hello")
				} else if hev.StateChange == libnmdc.CONNECTIONSTATE_DISCONNECTED {
					so.Emit("close")
				} else {
					so.Emit("sys", hev.StateChange.String())
				}

			case libnmdc.EVENT_HUBNAME_CHANGED:
				so.Emit("hubname", hev.Nick)

			case libnmdc.EVENT_USERCOMMAND:
				so.Emit("usercommand", map[string]interface{}{
					"type":    hev.UserCommand.Type,
					"context": hev.UserCommand.Context,
					"title":   hev.UserCommand.Message,
					"raw":     hev.UserCommand.Command,
				})

			default:
				if this.cfg.App.Debug {
					log.Printf("[%s] %v\n", so.Id(), hev)
				}

			}

		case <-done:
			log.Printf("[%s] done chan closed\n", so.Id())
			return // abandon

		}
	}

}

func (this *App) SocketIOServer(so socketio.Socket) {
	log.Printf("[%s] Client connected", so.Id())

	so.Emit("cls")
	so.Emit("raw", this.cfg.App.MotdHTML+"<br>")
	so.Emit("sys", "Enter a name to connect as (or name:pass for a registered nick)")

	doneChan := make(chan struct{}, 0)

	so.On("hello", func(data map[string]string) {
		log.Printf("[%s] Connection request", so.Id())
		go this.HubWorker(data["nick"], data["pass"], so, doneChan)
	})

	so.On("disconnection", func() { // n.b. not 'disconnect' (??)
		log.Printf("[%s] Client dropped", so.Id())
		close(doneChan)
	})

}

func (this *App) ConfigRequestHandler(w http.ResponseWriter, r *http.Request) {

	confStruct := struct {
		Extern string `json:"extern"`
		Title  string `json:"title"`
	}{
		Extern: this.cfg.Web.Extern,
		Title:  this.cfg.Web.Title,
	}
	confBytes, _ := json.Marshal(confStruct)

	//

	w.Header().Set("Content-Type", "text/javascript")
	w.WriteHeader(200)
	fmt.Fprintf(w, "var DCWEBUI_CONF = %s;\n", string(confBytes))
}

func (this *App) StaticRequestHandler(w http.ResponseWriter, r *http.Request) {
	fileName := r.URL.Path[1:]
	if fileName == "" {
		fileName = "index.htm"
	}

	data, err := Asset(fileName)
	if err != nil {
		w.WriteHeader(404)
		return
	}

	knownContentTypes := map[string]string{
		".htm": "text/html",
		".css": "text/css",
		".js":  "application/javascript",
		".png": "image/png",
	}

	foundMime := false
	for ext, mimeType := range knownContentTypes {
		if strings.HasSuffix(fileName, ext) {
			w.Header().Set("Content-Type", mimeType)
			foundMime = true
			break
		}
	}
	if !foundMime {
		w.Header().Set("Content-Type", "application/x-octet-stream")
	}
	dataInfo, _ := AssetInfo(fileName)
	w.Header().Set("Content-Length", fmt.Sprintf("%d", dataInfo.Size()))

	w.Write(data)
}

func (this *App) RunServer() {

	// Socket.io handler
	server, err := socketio.NewServer(nil)
	if err != nil {
		log.Fatal(err)
	}
	server.On("connection", this.SocketIOServer)
	server.On("error", func(so socketio.Socket, err error) {
		log.Println("error:", err)
	})
	http.Handle("/socket.io/", server)

	// Configuration handler
	http.HandleFunc("/conf", this.ConfigRequestHandler)

	// Other files: asset handler
	http.HandleFunc("/", this.StaticRequestHandler)

	// Listen and serve
	bindAddr := fmt.Sprintf("%s:%d", this.cfg.Web.BindTo, this.cfg.Web.Port)
	log.Printf("Serving at %s...", bindAddr)
	log.Fatal(http.ListenAndServe(bindAddr, nil))
}

func main() {

	a, err := NewApp("nmdc-webfrontend.conf")
	if err != nil {
		log.Fatal(err.Error())
		return
	}

	a.RunServer()
}
