// Package espat implements TCP/UDP wireless communication over serial
// with a separate ESP8266 or ESP32 board using the Espressif AT command set
// across a UART interface.
//
// In order to use this driver, the ESP8266/ESP32 must be flashed with firmware
// supporting the AT command set. Many ESP8266/ESP32 chips already have this firmware
// installed by default. You will need to install this firmware if you have an
// ESP8266 that has been flashed with NodeMCU (Lua) or Arduino firmware.
//
// AT Command Core repository:
// https://github.com/espressif/esp32-at
//
// Datasheet:
// https://www.espressif.com/sites/default/files/documentation/0a-esp8266ex_datasheet_en.pdf
//
// AT command set:
// https://www.espressif.com/sites/default/files/documentation/4a-esp8266_at_instruction_set_en.pdf
//
// 02/2023    sfeldma@gmail.com    Heavily modified to use netdev interface

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

import (
	"errors"
	"fmt"
	"machine"
	"net"
	"net/netip"
	"strconv"
	"strings"
	"sync"
	"time"

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

type Config struct {
	// UART config
	Uart *machine.UART
	Tx   machine.Pin
	Rx   machine.Pin
}

type socket struct {
	inUse    bool
	protocol int
	laddr    netip.AddrPort
}

type Device struct {
	cfg  *Config
	uart *machine.UART
	// command responses that come back from the ESP8266/ESP32
	response []byte
	// data received from a TCP/UDP connection forwarded by the ESP8266/ESP32
	data   []byte
	socket socket
	mu     sync.Mutex
}

func NewDevice(cfg *Config) *Device {
	return &Device{
		cfg:      cfg,
		response: make([]byte, 1500),
		data:     make([]byte, 0, 1500),
	}
}

func (d *Device) NetConnect(params *netlink.ConnectParams) error {

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

	d.uart = d.cfg.Uart
	d.uart.Configure(machine.UARTConfig{TX: d.cfg.Tx, RX: d.cfg.Rx})

	// Connect to ESP8266/ESP32
	fmt.Printf("Connecting to device...")

	for i := 0; i < 5; i++ {
		if d.Connected() {
			break
		}
		time.Sleep(1 * time.Second)
	}

	if !d.Connected() {
		fmt.Printf("FAILED\r\n")
		return netlink.ErrConnectFailed
	}

	fmt.Printf("CONNECTED\r\n")

	// Connect to Wifi AP
	fmt.Printf("Connecting to Wifi SSID '%s'...", params.Ssid)

	d.SetWifiMode(WifiModeClient)

	err := d.ConnectToAP(params.Ssid, params.Passphrase, 10 /* secs */)
	if err != nil {
		fmt.Printf("FAILED\r\n")
		return err
	}

	fmt.Printf("CONNECTED\r\n")

	ip, err := d.Addr()
	if err != nil {
		return err
	}
	fmt.Printf("DHCP-assigned IP: %s\r\n", ip)
	fmt.Printf("\r\n")

	return nil
}

func (d *Device) NetDisconnect() {
	d.DisconnectFromAP()
	fmt.Printf("\r\nDisconnected from Wifi\r\n\r\n")
}

func (d *Device) NetNotify(cb func(netlink.Event)) {
	fmt.Printf("\r\n%s\r\n", netlink.ErrNotSupported)
}

func (d *Device) GetHostByName(name string) (netip.Addr, error) {
	ip, err := d.GetDNS(name)
	if err != nil {
		return netip.Addr{}, err
	}
	return netip.ParseAddr(ip)
}

func (d *Device) GetHardwareAddr() (net.HardwareAddr, error) {
	return net.HardwareAddr{}, netlink.ErrNotSupported
}

func (d *Device) Addr() (netip.Addr, error) {
	resp, err := d.GetClientIP()
	if err != nil {
		return netip.Addr{}, err
	}
	prefix := "+CIPSTA:ip:"
	for _, line := range strings.Split(resp, "\n") {
		if ok := strings.HasPrefix(line, prefix); ok {
			ip := line[len(prefix)+1 : len(line)-2]
			return netip.ParseAddr(ip)
		}
	}
	return netip.Addr{}, fmt.Errorf("Error getting IP address")
}

func (d *Device) Socket(domain int, stype int, protocol int) (int, error) {

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

	switch {
	case protocol == netdev.IPPROTO_TCP && stype == netdev.SOCK_STREAM:
	case protocol == netdev.IPPROTO_TLS && stype == netdev.SOCK_STREAM:
	case protocol == netdev.IPPROTO_UDP && stype == netdev.SOCK_DGRAM:
	default:
		return -1, netdev.ErrProtocolNotSupported
	}

	// Only supporting single connection mode, so only one socket at a time
	if d.socket.inUse {
		return -1, netdev.ErrNoMoreSockets
	}
	d.socket.inUse = true
	d.socket.protocol = protocol

	return 0, nil
}

func (d *Device) Bind(sockfd int, ip netip.AddrPort) error {
	d.socket.laddr = ip
	return nil
}

func (d *Device) Connect(sockfd int, host string, ip netip.AddrPort) error {
	var err error
	var addr = ip.Addr().String()
	var rport = strconv.Itoa(int(ip.Port()))
	var lport = strconv.Itoa(int(d.socket.laddr.Port()))

	switch d.socket.protocol {
	case netdev.IPPROTO_TCP:
		err = d.ConnectTCPSocket(addr, rport)
	case netdev.IPPROTO_UDP:
		err = d.ConnectUDPSocket(addr, rport, lport)
	case netdev.IPPROTO_TLS:
		err = d.ConnectSSLSocket(host, rport)
	}

	if err != nil {
		if host == "" {
			return fmt.Errorf("Connect to %s timed out", ip)
		} else {
			return fmt.Errorf("Connect to %s:%d timed out", host, ip.Port())
		}
	}

	return nil
}

func (d *Device) Listen(sockfd int, backlog int) error {
	switch d.socket.protocol {
	case netdev.IPPROTO_UDP:
	default:
		return netdev.ErrProtocolNotSupported
	}
	return nil
}

func (d *Device) Accept(sockfd int) (int, netip.AddrPort, error) {
	return -1, netip.AddrPort{}, netdev.ErrNotSupported
}

func (d *Device) sendChunk(sockfd int, buf []byte, deadline time.Time) (int, error) {
	// Check if we've timed out
	if !deadline.IsZero() {
		if time.Now().After(deadline) {
			return -1, netdev.ErrTimeout
		}
	}
	err := d.StartSocketSend(len(buf))
	if err != nil {
		return -1, err
	}
	n, err := d.Write(buf)
	if err != nil {
		return -1, err
	}
	_, err = d.Response(1000)
	if err != nil {
		return -1, err
	}
	return n, err
}

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

	d.mu.Lock()
	defer d.mu.Unlock()

	// Break large bufs into chunks so we don't overrun the hw queue

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

	return len(buf), nil
}

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

	d.mu.Lock()
	defer d.mu.Unlock()

	var length = len(buf)

	// 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
			}
		}

		n, err := d.ReadSocket(buf[:length])
		if err != nil {
			return -1, err
		}
		if n == 0 {
			d.mu.Unlock()
			time.Sleep(100 * time.Millisecond)
			d.mu.Lock()
			continue
		}

		return n, nil
	}
}

func (d *Device) Close(sockfd int) error {
	d.mu.Lock()
	defer d.mu.Unlock()

	d.socket.inUse = false
	return d.DisconnectSocket()
}

func (d *Device) SetSockOpt(sockfd int, level int, opt int, value interface{}) error {
	return netdev.ErrNotSupported
}

// Connected checks if there is communication with the ESP8266/ESP32.
func (d *Device) Connected() bool {
	d.Execute(Test)

	// handle response here, should include "OK"
	_, err := d.Response(1000)
	if err != nil {
		return false
	}
	return true
}

// Write raw bytes to the UART.
func (d *Device) Write(b []byte) (n int, err error) {
	return d.uart.Write(b)
}

// Read raw bytes from the UART.
func (d *Device) Read(b []byte) (n int, err error) {
	return d.uart.Read(b)
}

// how long in milliseconds to pause after sending AT commands
const pause = 300

// Execute sends an AT command to the ESP8266/ESP32.
func (d Device) Execute(cmd string) error {
	_, err := d.Write([]byte("AT" + cmd + "\r\n"))
	return err
}

// Query sends an AT command to the ESP8266/ESP32 that returns the
// current value for some configuration parameter.
func (d Device) Query(cmd string) (string, error) {
	_, err := d.Write([]byte("AT" + cmd + "?\r\n"))
	return "", err
}

// Set sends an AT command with params to the ESP8266/ESP32 for a
// configuration value to be set.
func (d Device) Set(cmd, params string) error {
	_, err := d.Write([]byte("AT" + cmd + "=" + params + "\r\n"))
	return err
}

// Version returns the ESP8266/ESP32 firmware version info.
func (d Device) Version() []byte {
	d.Execute(Version)
	r, err := d.Response(2000)
	if err != nil {
		//return []byte("unknown")
		return []byte(err.Error())
	}
	return r
}

// Echo sets the ESP8266/ESP32 echo setting.
func (d Device) Echo(set bool) {
	if set {
		d.Execute(EchoConfigOn)
	} else {
		d.Execute(EchoConfigOff)
	}
	// TODO: check for success
	d.Response(100)
}

// Reset restarts the ESP8266/ESP32 firmware. Due to how the baud rate changes,
// this messes up communication with the ESP8266/ESP32 module. So make sure you know
// what you are doing when you call this.
func (d Device) Reset() {
	d.Execute(Restart)
	d.Response(100)
}

// ReadSocket returns the data that has already been read in from the responses.
func (d *Device) ReadSocket(b []byte) (n int, err error) {
	// make sure no data in buffer
	d.Response(300)

	count := len(b)
	if len(b) >= len(d.data) {
		// copy it all, then clear socket data
		count = len(d.data)
		copy(b, d.data[:count])
		d.data = d.data[:0]
	} else {
		// copy all we can, then keep the remaining socket data around
		copy(b, d.data[:count])
		copy(d.data, d.data[count:])
		d.data = d.data[:len(d.data)-count]
	}

	return count, nil
}

// Response gets the next response bytes from the ESP8266/ESP32.
// The call will retry for up to timeout milliseconds before returning nothing.
func (d *Device) Response(timeout int) ([]byte, error) {
	// read data
	var size int
	var start, end int
	pause := 100 // pause to wait for 100 ms
	retries := timeout / pause

	for {
		size = d.uart.Buffered()

		if size > 0 {
			end += size
			d.uart.Read(d.response[start:end])

			// if "+IPD" then read socket data
			if strings.Contains(string(d.response[:end]), "+IPD") {
				// handle socket data
				return nil, d.parseIPD(end)
			}

			// if "OK" then the command worked
			if strings.Contains(string(d.response[:end]), "OK") {
				return d.response[start:end], nil
			}

			// if "Error" then the command failed
			if strings.Contains(string(d.response[:end]), "ERROR") {
				return d.response[start:end], errors.New("response error:" + string(d.response[start:end]))
			}

			// if anything else, then keep reading data in?
			start = end
		}

		// wait longer?
		retries--
		if retries == 0 {
			return nil, errors.New("response timeout error:" + string(d.response[start:end]))
		}

		time.Sleep(time.Duration(pause) * time.Millisecond)
	}
}

func (d *Device) parseIPD(end int) error {
	// find the "+IPD," to get length
	s := strings.Index(string(d.response[:end]), "+IPD,")

	// find the ":"
	e := strings.Index(string(d.response[:end]), ":")

	// find the data length
	val := string(d.response[s+5 : e])

	// TODO: verify count
	v, err := strconv.Atoi(val)
	if err != nil {
		// not expected data here. what to do?
		return err
	}

	// load up the socket data
	d.data = append(d.data, d.response[e+1:e+1+v]...)
	return nil
}

// IsSocketDataAvailable returns of there is socket data available
func (d *Device) IsSocketDataAvailable() bool {
	return len(d.data) > 0 || d.uart.Buffered() > 0
}
