package main

import (
	"fmt"
	"log"
	"net"
	"time"

	"github.com/g3n/engine/core"
	"github.com/g3n/engine/geometry"
	"github.com/g3n/engine/graphic"
	"github.com/g3n/engine/material"
	"github.com/g3n/engine/math32"
)

var (
	dials []Dial
)

type Dial struct {
	Connection *net.TCPConn       // the real deal underlying TCP connection
	Geo        *geometry.Geometry // the 3D geometry
	Mat        *material.Standard // the material / color
	Mesh       *graphic.Mesh      // the combined geometry and material
	Node       *core.Node         // pointer to node in scene, required for removing the object if connection goes down
	Status     string             // String to control what's displayed in the scene.  Can be Disconnected, Connected, Leased Out

}

// TBD: Make less destructive when downsizing (prioritize dials without connections, then dials that aren't leased out, then what's left)
func setConnPoolSize(maxPoolSize int) {

	for _, dial := range dials {
		scene.Remove(dial.Mesh)
		dial.Geo = nil
		dial.Mat = nil
		dial.Mesh = nil
		dial.Node = nil
	}

	dials = make([]Dial, 0)

	// Update the position of the connections on the scene
	padding := float32(200 / (maxPoolSize + 1)) // space between the connection objects in 3D space
	currX := float32(maxPoolSize/2) * -padding  // variable gets updated, this is the initial starting position (left most connection in 3D space)
	if maxPoolSize%2 == 0 {
		currX += padding / 2
	}
	for i := 0; i < maxPoolSize; i++ {
		dial := Dial{Geo: geometry.NewBox(1, 1, 1), Mat: material.NewStandard(math32.NewColor("Grey")), Status: "Disconnected"}
		dial.Mesh = graphic.NewMesh(dial.Geo, dial.Mat)
		dial.Mesh.SetPositionX(currX)
		currX += padding
		dial.Node = scene.Add(dial.Mesh)
		dials = append(dials, dial)
	}
	updateSceneConnections()
}

// Keeps the pool full, replaces stale connections, and at the same time creates the objects in the 3D scenes representing physical connections
// should be ran in its own goroutine
func connPoolWatchdog(serverAddress string) {
	tcpAddr, err := net.ResolveTCPAddr("tcp4", serverAddress)
	if err != nil {
		log.Fatalf("failed to resolve serverAddress '%s': %v", serverAddress, err)
	}

	for {

		stateChange := false // set to true if there's been any state changes since the last polling

		// Check status of existing open connections in pool
		for i := range dials {

			// if there is no existing connection
			if dials[i].Connection == nil {
				log.Printf("dialing new connection")
				var err error
				dials[i].Connection, err = net.DialTCP("tcp", nil, tcpAddr)
				if err != nil {
					log.Printf("failed to dial TCP connection: %v", err)
					break
				}

				dials[i].Connection.SetKeepAlive(true)
				dials[i].Connection.SetKeepAlivePeriod(time.Second * 5)
				dials[i].Status = "Connected"
				stateChange = true

				continue
			}

			// test if the connection is still working
			if !isConnUp(dials[i].Connection) { // TBD: this should be moved to check before first write to the socket file descriptor instead of polling here
				log.Printf("closing bad idle connection and removing from pool - %s", dials[i].Connection.LocalAddr().String())
				dials[i].Connection.Close()
				dials[i].Connection = nil
				dials[i].Status = "Disconnected"
				stateChange = true

				continue
			}

		}

		if stateChange {
			updateSceneConnections()
		}

		time.Sleep(time.Second * 5) // random sleep, AKA evidence this shouldn't be a watchdog,
		// this whole function should be event based instead of polling
	}
}

// TBD: Refactor to buffered channel instead of finding one on the fly?
func requestLease() (*net.TCPConn, error) {
	inPoolConns := make(map[int]Dial)
	for i, dial := range dials {
		if dial.Status == "Connected" {
			inPoolConns[i] = dial
		}
	}

	if len(inPoolConns) == 0 {
		return nil, fmt.Errorf("pool exhausted")
	}

	// Selects a random connection from the pool
	var i int
	for i = range inPoolConns {
		break
	}

	dials[i].Status = "Leased Out"
	updateSceneConnections()

	return dials[i].Connection, nil
}

func releaseConnection(conn *net.TCPConn) {
	for i := range dials {
		if dials[i].Connection.LocalAddr().String() != conn.LocalAddr().String() {
			continue
		}
		dials[i].Status = "Connected"
		updateSceneConnections()
		return
	}
}

// Checks if connection is still up by reading the heartbeat data from the server
// returns err if connection is not active
func isConnUp(conn *net.TCPConn) bool {
	conn.SetReadDeadline(time.Now().Add(time.Millisecond * 300))

	buf := make([]byte, 128)
	_, err := conn.Read(buf)
	if err != nil {
		log.Printf("connection error detected: %v", err)
		return false
	}

	var zero time.Time
	conn.SetReadDeadline(zero)

	return true
}
