// Package rtl8720dn implements TCP wireless communication over UART
// talking to a RealTek rtl8720dn module.
//
// 01/2023    sfeldma@gmail.com    Heavily modified to use netdev interface

package rtl8720dn // import "tinygo.org/x/drivers/rtl8720dn"

import (
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"machine"
	"net"
	"net/netip"
	"strings"
	"sync"
	"time"

	"tinygo.org/x/drivers"
	"tinygo.org/x/drivers/netdev"
	"tinygo.org/x/drivers/netlink"
)

var _debug debug = debugBasic

//var _debug debug = debugBasic | debugNetdev
//var _debug debug = debugBasic | debugNetdev | debugRpc

var (
	driverName = "Realtek rtl8720dn Wifi network device driver (rtl8720dn)"
)

const (
	O_NONBLOCK     = 1 // note: different value than syscall.O_NONBLOCK (0x800)
	RTW_MODE_STA   = 0x00000001
	defaultChannel = 6
)

type sock int32

type socket struct {
	protocol int
	inuse    bool
}

type Config struct {
	// Enable
	En machine.Pin

	// UART config
	Uart     *machine.UART
	Tx       machine.Pin
	Rx       machine.Pin
	Baudrate uint32
}

type rtl8720dn struct {
	cfg      *Config
	notifyCb func(netlink.Event)
	mu       sync.Mutex

	uart *machine.UART
	seq  uint64

	debug bool

	params *netlink.ConnectParams

	netConnected bool
	driverShown  bool
	deviceShown  bool

	killWatchdog chan bool

	// keyed by sock as returned by rpc_lwip_socket()
	sockets map[sock]*socket
}

func newSocket(protocol int) *socket {
	return &socket{protocol: protocol, inuse: true}
}

func New(cfg *Config) *rtl8720dn {
	return &rtl8720dn{
		debug:        (_debug & debugRpc) != 0,
		cfg:          cfg,
		sockets:      make(map[sock]*socket),
		killWatchdog: make(chan bool),
	}
}

func (r *rtl8720dn) startDhcpc() error {
	if result := r.rpc_tcpip_adapter_dhcpc_start(0); result == -1 {
		return netdev.ErrStartingDHCPClient
	}
	return nil
}

func (r *rtl8720dn) connectToAP() error {

	if len(r.params.Ssid) == 0 {
		return netlink.ErrMissingSSID
	}

	if len(r.params.Passphrase) != 0 && len(r.params.Passphrase) < 8 {
		return netlink.ErrShortPassphrase
	}

	if debugging(debugBasic) {
		fmt.Printf("Connecting to Wifi SSID '%s'...", r.params.Ssid)
	}

	// Start the connection process
	securityType := uint32(0) // RTW_SECURITY_OPEN
	if len(r.params.Passphrase) != 0 {
		securityType = 0x00400004 // RTW_SECURITY_WPA2_AES_PSK
	}

	result := r.rpc_wifi_connect(r.params.Ssid, r.params.Passphrase, securityType, -1, 0)
	if result != 0 {
		if debugging(debugBasic) {
			fmt.Printf("FAILED\r\n")
		}
		return netlink.ErrConnectFailed
	}

	if debugging(debugBasic) {
		fmt.Printf("CONNECTED\r\n")
	}

	if r.notifyCb != nil {
		r.notifyCb(netlink.EventNetUp)
	}

	return r.startDhcpc()
}

func (r *rtl8720dn) startDhcps() error {
	if result := r.rpc_tcpip_adapter_dhcps_start(0); result == -1 {
		return netdev.ErrStartingDHCPServer
	}
	return nil
}

func (r *rtl8720dn) startAP() error {
	if len(r.params.Ssid) == 0 {
		return netlink.ErrMissingSSID
	}

	if len(r.params.Passphrase) != 0 && len(r.params.Passphrase) < 8 {
		return netlink.ErrShortPassphrase
	}

	if debugging(debugBasic) {
		fmt.Printf("Starting Wifi AP as SSID '%s'...", r.params.Ssid)
	}

	// Start the connection process
	securityType := uint32(0) // RTW_SECURITY_OPEN
	if len(r.params.Passphrase) != 0 {
		securityType = 0x00400004 // RTW_SECURITY_WPA2_AES_PSK
	}

	result := r.rpc_wifi_start_ap(r.params.Ssid, r.params.Passphrase, securityType, defaultChannel)
	if result != 0 {
		if debugging(debugBasic) {
			fmt.Printf("FAILED\r\n")
		}
		return netlink.ErrConnectFailed
	}

	if debugging(debugBasic) {
		fmt.Printf("LISTENING\r\n")
	}

	if r.notifyCb != nil {
		r.notifyCb(netlink.EventNetUp)
	}

	return r.startDhcps()
}

func (r *rtl8720dn) showDriver() {
	if r.driverShown {
		return
	}
	if debugging(debugBasic) {
		fmt.Printf("\r\n")
		fmt.Printf("%s\r\n\r\n", driverName)
		fmt.Printf("Driver version           : %s\r\n", drivers.Version)
	}
	r.driverShown = true
}

func (r *rtl8720dn) initWifi() error {
	if result := r.rpc_tcpip_adapter_init(); result == -1 {
		return fmt.Errorf("TCP/IP adapter init failed")
	}
	if result := r.rpc_wifi_off(); result == -1 {
		return errors.New("Error turning off WiFi")
	}
	if result := r.rpc_wifi_on(RTW_MODE_STA); result == -1 {
		return errors.New("Error turning on WiFi")
	}
	if result := r.rpc_wifi_disconnect(); result == -1 {
		return errors.New("Error disconnecting WiFi")
	}
	return nil
}

func (r *rtl8720dn) setupUART() {
	r.uart = r.cfg.Uart
	r.uart.Configure(machine.UARTConfig{TX: r.cfg.Tx,
		RX: r.cfg.Rx, BaudRate: r.cfg.Baudrate})
}

func (r *rtl8720dn) start() error {
	en := r.cfg.En
	if en == 0 {
		return fmt.Errorf("Must set Config.En")
	}
	en.Configure(machine.PinConfig{Mode: machine.PinOutput})
	en.Low()
	time.Sleep(100 * time.Millisecond)
	en.High()
	time.Sleep(1000 * time.Millisecond)
	r.setupUART()
	return r.initWifi()
}

func (r *rtl8720dn) stop() {
	r.rpc_tcpip_adapter_stop(0)
	r.cfg.En.Low()
}

func (r *rtl8720dn) showDevice() {
	if r.deviceShown {
		return
	}
	if debugging(debugBasic) {
		fmt.Printf("RTL8720 firmware version : %s\r\n", r.getFwVersion())
		fmt.Printf("MAC address              : %s\r\n", r.getMACAddr())
		fmt.Printf("\r\n")
	}
	r.deviceShown = true
}

func (r *rtl8720dn) showIP() {
	if debugging(debugBasic) {
		ip, subnet, gateway, _ := r.getIP()
		fmt.Printf("\r\n")
		fmt.Printf("DHCP-assigned IP         : %s\r\n", ip)
		fmt.Printf("DHCP-assigned subnet     : %s\r\n", subnet)
		fmt.Printf("DHCP-assigned gateway    : %s\r\n", gateway)
		fmt.Printf("\r\n")
	}
}

func (r *rtl8720dn) networkDown() bool {
	result := r.rpc_wifi_is_connected_to_ap()
	return result != 0
}

func (r *rtl8720dn) watchdog() {
	ticker := time.NewTicker(r.params.WatchdogTimeout)
	for {
		select {
		case <-r.killWatchdog:
			return
		case <-ticker.C:
			r.mu.Lock()
			if r.networkDown() {
				if debugging(debugBasic) {
					fmt.Printf("Watchdog: Wifi NOT CONNECTED, trying again...\r\n")
				}
				if r.notifyCb != nil {
					r.notifyCb(netlink.EventNetDown)
				}
				r.netConnect(false)
			}
			r.mu.Unlock()
		}
	}
}

func (r *rtl8720dn) netConnect(reset bool) error {
	if reset {
		if err := r.start(); err != nil {
			return err
		}
	}
	r.showDevice()

retry:
	for i := 0; r.params.Retries == 0 || i < r.params.Retries; i++ {
		switch r.params.ConnectMode {
		case netlink.ConnectModeAP:
			if err := r.startAP(); err != nil {
				if err == netlink.ErrConnectFailed {
					continue
				}
				return err
			}
			break retry

		default:
			if err := r.connectToAP(); err != nil {
				if err == netlink.ErrConnectFailed {
					continue
				}
				return err
			}
			break retry
		}
	}

	if r.networkDown() {
		return netlink.ErrConnectFailed
	}

	r.showIP()
	return nil
}

func (r *rtl8720dn) NetConnect(params *netlink.ConnectParams) error {

	r.mu.Lock()
	defer r.mu.Unlock()

	if r.netConnected {
		return netlink.ErrConnected
	}

	r.params = params

	r.showDriver()

	if err := r.netConnect(true); err != nil {
		return err
	}

	r.netConnected = true

	if r.params.WatchdogTimeout != 0 {
		go r.watchdog()
	}

	return nil
}

func (r *rtl8720dn) netDisconnect() {
	r.disconnect()
}

func (r *rtl8720dn) NetDisconnect() {

	r.mu.Lock()
	defer r.mu.Unlock()

	if !r.netConnected {
		return
	}

	if r.params.WatchdogTimeout != 0 {
		r.killWatchdog <- true
	}
	r.netDisconnect()
	r.stop()

	r.netConnected = false

	if debugging(debugBasic) {
		fmt.Printf("\r\nDisconnected from Wifi SSID '%s'\r\n\r\n", r.params.Ssid)
	}

	if r.notifyCb != nil {
		r.notifyCb(netlink.EventNetDown)
	}
}

func (r *rtl8720dn) NetNotify(cb func(netlink.Event)) {
	r.notifyCb = cb
}

func (r *rtl8720dn) GetHostByName(name string) (netip.Addr, error) {

	if debugging(debugNetdev) {
		fmt.Printf("[GetHostByName] name: %s\r\n", name)
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	var ip [4]byte
	result := r.rpc_netconn_gethostbyname(name, ip[:])
	if result == -1 {
		return netip.Addr{}, netdev.ErrHostUnknown
	}

	addr, ok := netip.AddrFromSlice(ip[:])
	if !ok {
		return netip.Addr{}, netdev.ErrMalAddr
	}

	return addr, nil
}

func (r *rtl8720dn) GetHardwareAddr() (net.HardwareAddr, error) {

	if debugging(debugNetdev) {
		fmt.Printf("[GetHardwareAddr]\r\n")
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	mac := strings.ReplaceAll(r.getMACAddr(), ":", "")
	addr, err := hex.DecodeString(mac)

	return net.HardwareAddr(addr), err
}

func (r *rtl8720dn) Addr() (netip.Addr, error) {

	if debugging(debugNetdev) {
		fmt.Printf("[GetIPAddr]\r\n")
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	ip, _, _, err := r.getIP()

	return ip, err
}

func (r *rtl8720dn) clientTLS() uint32 {
	client := r.rpc_wifi_ssl_client_create()
	r.rpc_wifi_ssl_init(client)
	r.rpc_wifi_ssl_set_timeout(client, 120*1000 /* usec? */)
	return client
}

// See man socket(2) for standard Berkely sockets for Socket, Bind, etc.
// The driver strives to meet the function and semantics of socket(2).

func (r *rtl8720dn) Socket(domain int, stype int, protocol int) (int, error) {

	if debugging(debugNetdev) {
		fmt.Printf("[Socket] domain: %d, type: %d, protocol: %d\r\n",
			domain, stype, protocol)
	}

	switch domain {
	case netdev.AF_INET:
	default:
		return -1, netdev.ErrFamilyNotSupported
	}

	var newSock int32

	r.mu.Lock()
	defer r.mu.Unlock()

	switch {
	case protocol == netdev.IPPROTO_TCP && stype == netdev.SOCK_STREAM:
		newSock = r.rpc_lwip_socket(netdev.AF_INET, netdev.SOCK_STREAM,
			netdev.IPPROTO_TCP)
	case protocol == netdev.IPPROTO_TLS && stype == netdev.SOCK_STREAM:
		// TODO Investigate: using client number as socket number;
		// TODO this may cause a problem if mixing TLS and non-TLS sockets?
		newSock = int32(r.clientTLS())
	case protocol == netdev.IPPROTO_UDP && stype == netdev.SOCK_DGRAM:
		newSock = r.rpc_lwip_socket(netdev.AF_INET, netdev.SOCK_DGRAM,
			netdev.IPPROTO_UDP)
	default:
		return -1, netdev.ErrProtocolNotSupported
	}

	if newSock == -1 {
		return -1, netdev.ErrNoMoreSockets
	}

	socket := newSocket(protocol)
	r.sockets[sock(newSock)] = socket

	return int(newSock), nil
}

func ipToName(ip netip.AddrPort) []byte {
	name := make([]byte, 16)
	name[0] = 0x00
	name[1] = netdev.AF_INET
	name[2] = byte(ip.Port() >> 8)
	name[3] = byte(ip.Port())
	if ip.Addr().Is4() {
		addr := ip.Addr().As4()
		name[4] = byte(addr[0])
		name[5] = byte(addr[1])
		name[6] = byte(addr[2])
		name[7] = byte(addr[3])
	}
	return name
}

func nameToIp(name []byte) netip.AddrPort {
	port := uint16(name[2])<<8 | uint16(name[3])
	addr, _ := netip.AddrFromSlice(name[4:8])
	return netip.AddrPortFrom(addr, port)
}

func (r *rtl8720dn) Bind(sockfd int, ip netip.AddrPort) error {

	if debugging(debugNetdev) {
		fmt.Printf("[Bind] sockfd: %d, addr: %s\r\n", sockfd, ip)
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	var sock = sock(sockfd)
	var socket = r.sockets[sock]
	var name = ipToName(ip)

	switch socket.protocol {
	case netdev.IPPROTO_TCP, netdev.IPPROTO_UDP:
		result := r.rpc_lwip_bind(int32(sock), name, uint32(len(name)))
		if result == -1 {
			return fmt.Errorf("Bind to %s failed", ip)
		}
	default:
		return netdev.ErrProtocolNotSupported
	}

	return nil
}

func (r *rtl8720dn) Connect(sockfd int, host string, ip netip.AddrPort) error {

	port := ip.Port()

	if debugging(debugNetdev) {
		if host == "" {
			fmt.Printf("[Connect] sockfd: %d, addr: %s\r\n", sockfd, ip)
		} else {
			fmt.Printf("[Connect] sockfd: %d, host: %s:%d\r\n", sockfd, host, port)
		}
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	var sock = sock(sockfd)
	var socket = r.sockets[sock]
	var name = ipToName(ip)

	// Start the connection
	switch socket.protocol {
	case netdev.IPPROTO_TCP, netdev.IPPROTO_UDP:
		result := r.rpc_lwip_connect(int32(sock), name, uint32(len(name)))
		if result == -1 {
			return fmt.Errorf("Connect to %s failed", ip)
		}
	case netdev.IPPROTO_TLS:
		result := r.rpc_wifi_start_ssl_client(uint32(sock),
			host, uint32(port), 0)
		if result == -1 {
			return fmt.Errorf("Connect to %s:%d failed", host, port)
		}
	}

	return nil
}

func (r *rtl8720dn) Listen(sockfd int, backlog int) error {

	if debugging(debugNetdev) {
		fmt.Printf("[Listen] sockfd: %d\r\n", sockfd)
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	var sock = sock(sockfd)
	var socket = r.sockets[sock]

	switch socket.protocol {
	case netdev.IPPROTO_TCP:
		result := r.rpc_lwip_listen(int32(sock), int32(backlog))
		if result == -1 {
			return fmt.Errorf("Listen failed")
		}
		result = r.rpc_lwip_fcntl(int32(sock), netdev.F_SETFL, O_NONBLOCK)
		if result == -1 {
			return fmt.Errorf("Fcntl failed")
		}
	case netdev.IPPROTO_UDP:
		result := r.rpc_lwip_listen(int32(sock), int32(backlog))
		if result == -1 {
			return fmt.Errorf("Listen failed")
		}
	default:
		return netdev.ErrProtocolNotSupported
	}

	return nil
}

func (r *rtl8720dn) Accept(sockfd int) (int, netip.AddrPort, error) {

	if debugging(debugNetdev) {
		fmt.Printf("[Accept] sockfd: %d\r\n", sockfd)
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	var newSock int32
	var lsock = sock(sockfd)
	var socket = r.sockets[lsock]
	var name = ipToName(netip.AddrPort{})

	switch socket.protocol {
	case netdev.IPPROTO_TCP:
	default:
		return -1, netip.AddrPort{}, netdev.ErrProtocolNotSupported
	}

	for {
		// Accept() will be sleeping most of the time, checking for a
		// new clients every 1/10 sec.
		r.mu.Unlock()
		time.Sleep(100 * time.Millisecond)
		r.mu.Lock()

		// Check if a client connected.  O_NONBLOCK is set on lsock.
		namelen := uint32(len(name))
		newSock = r.rpc_lwip_accept(int32(lsock), name, &namelen)
		if newSock == -1 {
			// No new client
			time.Sleep(100 * time.Millisecond)
			continue
		}

		// Get remote peer ip:port
		namelen = uint32(len(name))
		result := r.rpc_lwip_getpeername(int32(newSock), name, &namelen)
		if result == -1 {
			return -1, netip.AddrPort{}, fmt.Errorf("Getpeername failed")
		}
		raddr := nameToIp(name)

		// If we've already seen this socket, we can re-use
		// the socket and return it.  But, only if the socket
		// is closed.  If it's not closed, we'll just come back
		// later to reuse it.

		clientSocket, ok := r.sockets[sock(newSock)]
		if ok {
			// Wait for client to Close
			if clientSocket.inuse {
				continue
			}
			// Reuse client socket
			return int(newSock), raddr, nil
		}

		// Create new socket for client and return fd
		r.sockets[sock(newSock)] = newSocket(socket.protocol)
		return int(newSock), raddr, nil
	}
}

func (r *rtl8720dn) sendChunk(sockfd int, buf []byte, deadline time.Time) (int, error) {
	var sock = sock(sockfd)
	var socket = r.sockets[sock]

	// Check if we've timed out
	if !deadline.IsZero() {
		if time.Now().After(deadline) {
			return -1, netdev.ErrTimeout
		}
	}

	switch socket.protocol {
	case netdev.IPPROTO_TCP, netdev.IPPROTO_UDP:
		result := r.rpc_lwip_send(int32(sock), buf, 0x00000008)
		if result == -1 {
			return -1, fmt.Errorf("Send error")
		}
		return int(result), nil
	case netdev.IPPROTO_TLS:
		result := r.rpc_wifi_send_ssl_data(uint32(sock), buf, uint16(len(buf)))
		if result == -1 {
			return -1, fmt.Errorf("TLS Send error")
		}
		return int(result), nil
	}

	return -1, netdev.ErrProtocolNotSupported
}

func (r *rtl8720dn) Send(sockfd int, buf []byte, flags int,
	deadline time.Time) (int, error) {

	if debugging(debugNetdev) {
		fmt.Printf("[Send] sockfd: %d, len(buf): %d, flags: %d\r\n",
			sockfd, len(buf), flags)
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	// Break large bufs into chunks

	chunkSize := 1436
	for i := 0; i < len(buf); i += chunkSize {
		end := i + chunkSize
		if end > len(buf) {
			end = len(buf)
		}
		_, err := r.sendChunk(sockfd, buf[i:end], deadline)
		if err != nil {
			return -1, err
		}
	}

	return len(buf), nil
}

func (r *rtl8720dn) Recv(sockfd int, buf []byte, flags int,
	deadline time.Time) (int, error) {

	if debugging(debugNetdev) {
		fmt.Printf("[Recv] sockfd: %d, len(buf): %d, flags: %d\r\n",
			sockfd, len(buf), flags)
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	var sock = sock(sockfd)
	var socket = r.sockets[sock]
	var length = len(buf)
	var n int32

	// Limit length read size to chunk large read requests
	if length > 1436 {
		length = 1436
	}

	for {
		// Check if we've timed out
		if !deadline.IsZero() {
			if time.Now().After(deadline) {
				return -1, netdev.ErrTimeout
			}
		}

		switch socket.protocol {
		case netdev.IPPROTO_TCP, netdev.IPPROTO_UDP:
			n = r.rpc_lwip_recv(int32(sock), buf[:length],
				uint32(length), 0x00000008, 0)
		case netdev.IPPROTO_TLS:
			n = r.rpc_wifi_get_ssl_receive(uint32(sock),
				buf[:length], int32(length))
		}

		if n < 0 {
			r.mu.Unlock()
			time.Sleep(100 * time.Millisecond)
			r.mu.Lock()
			continue
		} else if n == 0 {
			if debugging(debugNetdev) {
				fmt.Printf("[<--Recv] sockfd: %d, n: %d EOF\r\n",
					sock, n)
			}
			return -1, io.EOF
		}

		if debugging(debugNetdev) {
			fmt.Printf("[<--Recv] sockfd: %d, n: %d\r\n",
				sock, n)
		}

		return int(n), nil
	}
}

func (r *rtl8720dn) Close(sockfd int) error {

	if debugging(debugNetdev) {
		fmt.Printf("[Close] sockfd: %d\r\n", sockfd)
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	var sock = sock(sockfd)
	var socket = r.sockets[sock]
	var result int32

	if !socket.inuse {
		return nil
	}

	switch socket.protocol {
	case netdev.IPPROTO_TCP, netdev.IPPROTO_UDP:
		result = r.rpc_lwip_close(int32(sock))
	case netdev.IPPROTO_TLS:
		r.rpc_wifi_stop_ssl_socket(uint32(sock))
		r.rpc_wifi_ssl_client_destroy(uint32(sock))
	}

	if result == -1 {
		return netdev.ErrClosingSocket
	}

	socket.inuse = false

	return nil
}

func (r *rtl8720dn) SetSockOpt(sockfd int, level int, opt int, value interface{}) error {

	if debugging(debugNetdev) {
		fmt.Printf("[SetSockOpt] sockfd: %d\r\n", sockfd)
	}

	return netdev.ErrNotSupported
}

func (r *rtl8720dn) disconnect() error {
	result := r.rpc_wifi_disconnect()
	if result == -1 {
		return fmt.Errorf("Error disconnecting Wifi")
	}
	return nil
}

func (r *rtl8720dn) getFwVersion() string {
	return r.rpc_system_version()
}

func (r *rtl8720dn) getMACAddr() string {
	var mac [18]uint8
	r.rpc_wifi_get_mac_address(mac[:])
	return string(mac[:])
}

func (r *rtl8720dn) getIP() (ip, subnet, gateway netip.Addr, err error) {
	var ip_info [12]byte
	result := r.rpc_tcpip_adapter_get_ip_info(0, ip_info[:])
	if result == -1 {
		err = fmt.Errorf("Get IP info failed")
		return
	}
	ip, _ = netip.AddrFromSlice(ip_info[0:4])
	subnet, _ = netip.AddrFromSlice(ip_info[4:8])
	gateway, _ = netip.AddrFromSlice(ip_info[8:12])
	return
}
