package main

/* a wrapper around bitmask that exposes status to a QtQml gui */

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"reflect"
	"sync"
	"unsafe"

	"0xacab.org/leap/bitmask-vpn/pkg/bitmask"
	"0xacab.org/leap/bitmask-vpn/pkg/systray2"
	"github.com/jmshal/go-locale"
	"golang.org/x/text/message"
)

// typedef void (*cb)();
// inline void _do_callback(cb f) {
// 	f();
// }
import "C"

/* callbacks into C-land */

var mut sync.Mutex
var stmut sync.Mutex
var cbs = make(map[string](*[0]byte))
var initOnce sync.Once

// Events are just a enumeration of all the posible events that C functions can
// be interested in subscribing to. You cannot subscribe to an event that is
// not listed here.
type Events struct {
	OnStatusChanged string
}

const OnStatusChanged string = "OnStatusChanged"

// subscribe registers a callback from C-land.
// This callback needs to be passed as a void* C function pointer.
func subscribe(event string, fp unsafe.Pointer) {
	mut.Lock()
	defer mut.Unlock()
	e := &Events{}
	v := reflect.Indirect(reflect.ValueOf(&e))
	hf := v.Elem().FieldByName(event)
	if reflect.ValueOf(hf).IsZero() {
		fmt.Println("ERROR: not a valid event:", event)
	} else {
		cbs[event] = (*[0]byte)(fp)
	}
}

// trigger fires a callback from C-land.
func trigger(event string) {
	mut.Lock()
	defer mut.Unlock()
	cb := cbs[event]
	if cb != nil {
		C._do_callback(cb)
	} else {
		fmt.Println("ERROR: this event does not have subscribers:", event)
	}
}

/* connection status */

const logFile = "systray.log"

const (
	offStr      = "off"
	startingStr = "starting"
	onStr       = "on"
	stoppingStr = "stopping"
	failedStr   = "failed"
)

// status reflects the current VPN status. Go code is responsible for updating
// it; C-land just watches its changes and pulls its updates via the serialized
// context object.
type status int

const (
	off status = iota
	starting
	on
	stopping
	failed
	unknown
)

func (s status) String() string {
	return [...]string{offStr, startingStr, onStr, stoppingStr, failedStr}[s]
}

func (s status) MarshalJSON() ([]byte, error) {
	b := bytes.NewBufferString(`"`)
	b.WriteString(s.String())
	b.WriteString(`"`)
	return b.Bytes(), nil
}

func (s status) fromString(st string) status {
	switch st {
	case offStr:
		return off
	case startingStr:
		return starting
	case onStr:
		return on
	case stoppingStr:
		return stopping
	case failedStr:
		return failed
	default:
		return unknown
	}
}

// An action is originated in the UI. These represent requests coming from the
// frontend via the C code. VPN code needs to watch them and fullfill their
// requests as soon as possible.
type actions int

const (
	switchOn actions = iota
	switchOff
	unblock
)

func (a actions) String() string {
	return [...]string{"switchOn", "switchOff", "unblock"}[a]
}

func (a actions) MarshalJSON() ([]byte, error) {
	b := bytes.NewBufferString(`"`)
	b.WriteString(a.String())
	b.WriteString(`"`)
	return b.Bytes(), nil
}

// The connectionCtx keeps the global state that is passed around to C-land. It
// also serves as the primary way of passing requests from the frontend to the
// Go-core, by letting the UI write some of these variables and processing
// them.
type connectionCtx struct {
	AppName  string    `json:"appName"`
	Provider string    `json:"provider"`
	Status   status    `json:"status"`
	Actions  []actions `json:"actions,omitempty"`
	bm       bitmask.Bitmask
}

func (c connectionCtx) toJson() ([]byte, error) {
	stmut.Lock()
	defer stmut.Unlock()
	b, err := json.Marshal(c)
	if err != nil {
		log.Println(err)
		return nil, err
	}
	return b, nil
}

func (c connectionCtx) updateStatus() {
	if stStr, err := c.bm.GetStatus(); err != nil {
		log.Printf("Error getting status: %v", err)
	} else {
		setStatusFromStr(stStr)
	}

	statusCh := c.bm.GetStatusCh()
	for {
		select {
		case stStr := <-statusCh:
			setStatusFromStr(stStr)
		}
	}
}

var ctx *connectionCtx

func setStatus(st status) {
	stmut.Lock()
	defer stmut.Unlock()
	ctx.Status = st
	go trigger(OnStatusChanged)
}

func setStatusFromStr(stStr string) {
	log.Println("status:", stStr)
	setStatus(unknown.fromString(stStr))
}

func initPrinter() *message.Printer {
	locale, err := go_locale.DetectLocale()
	if err != nil {
		log.Println("Error detecting the system locale: ", err)
	}

	return message.NewPrinter(message.MatchLanguage(locale, "en"))
}

// initializeBitmask instantiates a bitmask connection
func initializeBitmask() {
	if ctx == nil {
		log.Println("error: cannot initialize bitmask, ctx is nil")
		os.Exit(1)
	}
	conf := systray.ParseConfig()
	conf.Version = "unknown"
	conf.Printer = initPrinter()
	b, err := bitmask.Init(conf.Printer)
	if err != nil {
		log.Fatal(err)
	}
	ctx.bm = b
}

func startVPN() {
	err := ctx.bm.StartVPN(ctx.Provider)
	if err != nil {
		log.Println(err)
		os.Exit(1)
	}
}

func stopVPN() {
	err := ctx.bm.StopVPN()
	if err != nil {
		log.Println(err)
	}
}

// initializeContext initializes an empty connStatus and assigns it to the
// global ctx holder. This is expected to be called only once, so the public
// api uses the sync.Once primitive to call this.
func initializeContext(provider, appName string) {
	var st status = off
	ctx = &connectionCtx{
		AppName:  appName,
		Provider: provider,
		Status:   st,
	}
	go trigger(OnStatusChanged)
	initializeBitmask()
}

/* mock http server: easy way to mocking vpn behavior on ui interaction. This
* should also show a good way of writing functionality tests just for the Qml
* layer */

func mockUIOn(w http.ResponseWriter, r *http.Request) {
	log.Println("changing status: on")
	setStatus(on)
}

func mockUIOff(w http.ResponseWriter, r *http.Request) {
	log.Println("changing status: off")
	setStatus(off)
}

func mockUIFailed(w http.ResponseWriter, r *http.Request) {
	log.Println("changing status: failed")
	setStatus(failed)
}

func mockUI() {
	http.HandleFunc("/on", mockUIOn)
	http.HandleFunc("/off", mockUIOff)
	http.HandleFunc("/failed", mockUIFailed)
	http.ListenAndServe(":8080", nil)
}

/*

  exported C api

*/

//export SwitchOn
func SwitchOn() {
	setStatus(starting)
	startVPN()
}

//export SwitchOff
func SwitchOff() {
	setStatus(stopping)
	stopVPN()
}

//export Quit
func Quit() {
	if ctx.Status != off {
		setStatus(stopping)
		stopVPN()
	}
}

//export Unblock
func Unblock() {
	fmt.Println("unblock... [not implemented]")
}

//export SubscribeToEvent
func SubscribeToEvent(event string, f unsafe.Pointer) {
	subscribe(event, f)
}

//export InitializeBitmaskContext
func InitializeBitmaskContext() {
	provider := "black.riseup.net"
	appName := "RiseupVPN"
	initOnce.Do(func() {
		initializeContext(provider, appName)
	})
	go ctx.updateStatus()
}

//export RefreshContext
func RefreshContext() *C.char {
	c, _ := ctx.toJson()
	return C.CString(string(c))
}

/* end of the exposed api */

/* we could enable this one optionally for the qt tests */

/* uncomment: export MockUIInteraction */
func MockUIInteraction() {
	log.Println("mocking ui interaction on port 8080. \nTry 'curl localhost:8080/{on|off|failed}' to toggle status.")
	go mockUI()
}

func main() {}