// package net is intended to provide compatible interfaces with the
// Go standard library's net package.
package net

import (
	"errors"
	"strconv"
	"strings"
	"time"
)

// DialUDP makes a UDP network connection. raadr is the port that the messages will
// be sent to, and laddr is the port that will be listened to in order to
// receive incoming messages.
func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPSerialConn, error) {
	addr := raddr.IP.String()
	sendport := strconv.Itoa(raddr.Port)
	listenport := strconv.Itoa(laddr.Port)

	// disconnect any old socket
	ActiveDevice.DisconnectSocket()

	// connect new socket
	err := ActiveDevice.ConnectUDPSocket(addr, sendport, listenport)
	if err != nil {
		return nil, err
	}

	return &UDPSerialConn{SerialConn: SerialConn{Adaptor: ActiveDevice}, laddr: laddr, raddr: raddr}, nil
}

// ListenUDP listens for UDP connections on the port listed in laddr.
func ListenUDP(network string, laddr *UDPAddr) (*UDPSerialConn, error) {
	addr := "0"
	sendport := "0"
	listenport := strconv.Itoa(laddr.Port)

	// disconnect any old socket
	ActiveDevice.DisconnectSocket()

	// connect new socket
	err := ActiveDevice.ConnectUDPSocket(addr, sendport, listenport)
	if err != nil {
		return nil, err
	}

	return &UDPSerialConn{SerialConn: SerialConn{Adaptor: ActiveDevice}, laddr: laddr}, nil
}

// DialTCP makes a TCP network connection. raadr is the port that the messages will
// be sent to, and laddr is the port that will be listened to in order to
// receive incoming messages.
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPSerialConn, error) {
	addr := raddr.IP.String()
	sendport := strconv.Itoa(raddr.Port)

	// disconnect any old socket?
	//ActiveDevice.DisconnectSocket()

	// connect new socket
	err := ActiveDevice.ConnectTCPSocket(addr, sendport)
	if err != nil {
		return nil, err
	}

	return &TCPSerialConn{SerialConn: SerialConn{Adaptor: ActiveDevice}, laddr: laddr, raddr: raddr}, nil
}

// Dial connects to the address on the named network.
// It tries to provide a mostly compatible interface
// to net.Dial().
func Dial(network, address string) (Conn, error) {
	switch network {
	case "tcp":
		raddr, err := ResolveTCPAddr(network, address)
		if err != nil {
			return nil, err
		}

		c, e := DialTCP(network, &TCPAddr{}, raddr)
		return c.opConn(), e
	case "udp":
		raddr, err := ResolveUDPAddr(network, address)
		if err != nil {
			return nil, err
		}

		c, e := DialUDP(network, &UDPAddr{}, raddr)
		return c.opConn(), e
	default:
		return nil, errors.New("invalid network for dial")
	}
}

// SerialConn is a loosely net.Conn compatible implementation
type SerialConn struct {
	Adaptor Adapter
}

// UDPSerialConn is a loosely net.Conn compatible intended to support
// UDP over serial.
type UDPSerialConn struct {
	SerialConn
	laddr *UDPAddr
	raddr *UDPAddr
}

// NewUDPSerialConn returns a new UDPSerialConn/
func NewUDPSerialConn(c SerialConn, laddr, raddr *UDPAddr) *UDPSerialConn {
	return &UDPSerialConn{SerialConn: c, raddr: raddr}
}

// TCPSerialConn is a loosely net.Conn compatible intended to support
// TCP over serial.
type TCPSerialConn struct {
	SerialConn
	laddr *TCPAddr
	raddr *TCPAddr
}

// NewTCPSerialConn returns a new TCPSerialConn/
func NewTCPSerialConn(c SerialConn, laddr, raddr *TCPAddr) *TCPSerialConn {
	return &TCPSerialConn{SerialConn: c, raddr: raddr}
}

// Read reads data from the connection.
// TODO: implement the full method functionality:
// Read can be made to time out and return an Error with Timeout() == true
// after a fixed time limit; see SetDeadline and SetReadDeadline.
func (c *SerialConn) Read(b []byte) (n int, err error) {
	// read only the data that has been received via "+IPD" socket
	return c.Adaptor.ReadSocket(b)
}

// Write writes data to the connection.
// TODO: implement the full method functionality for timeouts.
// Write can be made to time out and return an Error with Timeout() == true
// after a fixed time limit; see SetDeadline and SetWriteDeadline.
func (c *SerialConn) Write(b []byte) (n int, err error) {
	// specify that is a data transfer to the
	// currently open socket, not commands to the ESP8266/ESP32.
	err = c.Adaptor.StartSocketSend(len(b))
	if err != nil {
		return
	}
	n, err = c.Adaptor.Write(b)
	if err != nil {
		return n, err
	}
	/* TODO(bcg): this is kind of specific to espat, should maybe refactor */
	_, err = c.Adaptor.Response(1000)
	if err != nil {
		return n, err
	}
	return n, err
}

// Close closes the connection.
// Currently only supports a single Read or Write operations without blocking.
func (c *SerialConn) Close() error {
	c.Adaptor.DisconnectSocket()
	return nil
}

// LocalAddr returns the local network address.
func (c *UDPSerialConn) LocalAddr() Addr {
	return c.laddr.opAddr()
}

// RemoteAddr returns the remote network address.
func (c *UDPSerialConn) RemoteAddr() Addr {
	return c.raddr.opAddr()
}

func (c *UDPSerialConn) opConn() Conn {
	if c == nil {
		return nil
	}
	return c
}

// LocalAddr returns the local network address.
func (c *TCPSerialConn) LocalAddr() Addr {
	return c.laddr.opAddr()
}

// RemoteAddr returns the remote network address.
func (c *TCPSerialConn) RemoteAddr() Addr {
	return c.raddr.opAddr()
}

func (c *TCPSerialConn) opConn() Conn {
	if c == nil {
		return nil
	}
	return c
}

// SetDeadline sets the read and write deadlines associated
// with the connection. It is equivalent to calling both
// SetReadDeadline and SetWriteDeadline.
//
// A deadline is an absolute time after which I/O operations
// fail with a timeout (see type Error) instead of
// blocking. The deadline applies to all future and pending
// I/O, not just the immediately following call to Read or
// Write. After a deadline has been exceeded, the connection
// can be refreshed by setting a deadline in the future.
//
// An idle timeout can be implemented by repeatedly extending
// the deadline after successful Read or Write calls.
//
// A zero value for t means I/O operations will not time out.
func (c *SerialConn) SetDeadline(t time.Time) error {
	return nil
}

// SetReadDeadline sets the deadline for future Read calls
// and any currently-blocked Read call.
// A zero value for t means Read will not time out.
func (c *SerialConn) SetReadDeadline(t time.Time) error {
	return nil
}

// SetWriteDeadline sets the deadline for future Write calls
// and any currently-blocked Write call.
// Even if write times out, it may return n > 0, indicating that
// some of the data was successfully written.
// A zero value for t means Write will not time out.
func (c *SerialConn) SetWriteDeadline(t time.Time) error {
	return nil
}

// ResolveTCPAddr returns an address of TCP end point.
//
// The network must be a TCP network name.
func ResolveTCPAddr(network, address string) (*TCPAddr, error) {
	// TODO: make sure network is 'tcp'
	// separate domain from port, if any
	r := strings.Split(address, ":")
	addr, err := ActiveDevice.GetDNS(r[0])
	if err != nil {
		return nil, err
	}
	ip := IP(addr)
	if len(r) > 1 {
		port, e := strconv.Atoi(r[1])
		if e != nil {
			return nil, e
		}
		return &TCPAddr{IP: ip, Port: port}, nil
	}
	return &TCPAddr{IP: ip}, nil
}

// ResolveUDPAddr returns an address of UDP end point.
//
// The network must be a UDP network name.
func ResolveUDPAddr(network, address string) (*UDPAddr, error) {
	// TODO: make sure network is 'udp'
	// separate domain from port, if any
	r := strings.Split(address, ":")
	addr, err := ActiveDevice.GetDNS(r[0])
	if err != nil {
		return nil, err
	}
	ip := IP(addr)
	if len(r) > 1 {
		port, e := strconv.Atoi(r[1])
		if e != nil {
			return nil, e
		}
		return &UDPAddr{IP: ip, Port: port}, nil
	}

	return &UDPAddr{IP: ip}, nil
}

// The following definitions are here to support a Golang standard package
// net-compatible interface for IP until TinyGo can compile the net package.

// IP is an IP address. Unlike the standard implementation, it is only
// a buffer of bytes that contains the string form of the IP address, not the
// full byte format used by the Go standard .
type IP []byte

// UDPAddr here to serve as compatible type. until TinyGo can compile the net package.
type UDPAddr struct {
	IP   IP
	Port int
	Zone string // IPv6 scoped addressing zone; added in Go 1.1
}

// Network returns the address's network name, "udp".
func (a *UDPAddr) Network() string { return "udp" }

func (a *UDPAddr) String() string {
	if a == nil {
		return "<nil>"
	}
	if a.Port != 0 {
		return a.IP.String() + ":" + strconv.Itoa(a.Port)
	}
	return a.IP.String()
}

func (a *UDPAddr) opAddr() Addr {
	if a == nil {
		return nil
	}
	return a
}

// TCPAddr here to serve as compatible type. until TinyGo can compile the net package.
type TCPAddr struct {
	IP   IP
	Port int
	Zone string // IPv6 scoped addressing zone
}

// Network returns the address's network name, "tcp".
func (a *TCPAddr) Network() string { return "tcp" }

func (a *TCPAddr) String() string {
	if a == nil {
		return "<nil>"
	}
	if a.Port != 0 {
		return a.IP.String() + ":" + strconv.Itoa(a.Port)
	}
	return a.IP.String()
}

func (a *TCPAddr) opAddr() Addr {
	if a == nil {
		return nil
	}
	return a
}

// ParseIP parses s as an IP address, returning the result.
func ParseIP(s string) IP {
	b := strings.Split(s, ".")
	if len(b) == 4 {
		ip := make([]byte, 4)

		for i := range ip {
			x, _ := strconv.ParseUint(b[i], 10, 8)
			ip[i] = byte(x)
		}

		return IP(ip)
	}

	return IP([]byte(s))
}

// String returns the string form of the IP address ip.
func (ip IP) String() string {
	return string(ip)
}

// Conn is a generic stream-oriented network connection.
// This interface is from the Go standard library.
type Conn interface {
	// Read reads data from the connection.
	// Read can be made to time out and return an Error with Timeout() == true
	// after a fixed time limit; see SetDeadline and SetReadDeadline.
	Read(b []byte) (n int, err error)

	// Write writes data to the connection.
	// Write can be made to time out and return an Error with Timeout() == true
	// after a fixed time limit; see SetDeadline and SetWriteDeadline.
	Write(b []byte) (n int, err error)

	// Close closes the connection.
	// Any blocked Read or Write operations will be unblocked and return errors.
	Close() error

	// LocalAddr returns the local network address.
	LocalAddr() Addr

	// RemoteAddr returns the remote network address.
	RemoteAddr() Addr

	// SetDeadline sets the read and write deadlines associated
	// with the connection. It is equivalent to calling both
	// SetReadDeadline and SetWriteDeadline.
	//
	// A deadline is an absolute time after which I/O operations
	// fail with a timeout (see type Error) instead of
	// blocking. The deadline applies to all future and pending
	// I/O, not just the immediately following call to Read or
	// Write. After a deadline has been exceeded, the connection
	// can be refreshed by setting a deadline in the future.
	//
	// An idle timeout can be implemented by repeatedly extending
	// the deadline after successful Read or Write calls.
	//
	// A zero value for t means I/O operations will not time out.
	SetDeadline(t time.Time) error

	// SetReadDeadline sets the deadline for future Read calls
	// and any currently-blocked Read call.
	// A zero value for t means Read will not time out.
	SetReadDeadline(t time.Time) error

	// SetWriteDeadline sets the deadline for future Write calls
	// and any currently-blocked Write call.
	// Even if write times out, it may return n > 0, indicating that
	// some of the data was successfully written.
	// A zero value for t means Write will not time out.
	SetWriteDeadline(t time.Time) error
}

// Addr represents a network end point address.
type Addr interface {
	Network() string // name of the network (for example, "tcp", "udp")
	String() string  // string form of address (for example, "192.0.2.1:25", "[2001:db8::1]:80")
}
