// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

// Package transport implements various networking related
// functions used throughout the Pion modules.
package transport

import (
	"context"
	"errors"
	"io"
	"net"
	"time"
)

var (
	// ErrNoAddressAssigned ...
	ErrNoAddressAssigned = errors.New("no address assigned")
	// ErrNotSupported ...
	ErrNotSupported = errors.New("not supported yey")
	// ErrInterfaceNotFound ...
	ErrInterfaceNotFound = errors.New("interface not found")
	// ErrNotUDPAddress ...
	ErrNotUDPAddress = errors.New("not a UDP address")
)

// Net is an interface providing common networking functions which are
// similar to the functions provided by standard net package.
type Net interface {
	// ListenPacket announces on the local network address.
	//
	// The network must be "udp", "udp4", "udp6", "unixgram", or an IP
	// transport. The IP transports are "ip", "ip4", or "ip6" followed by
	// a colon and a literal protocol number or a protocol name, as in
	// "ip:1" or "ip:icmp".
	//
	// For UDP and IP networks, if the host in the address parameter is
	// empty or a literal unspecified IP address, ListenPacket listens on
	// all available IP addresses of the local system except multicast IP
	// addresses.
	// To only use IPv4, use network "udp4" or "ip4:proto".
	// The address can use a host name, but this is not recommended,
	// because it will create a listener for at most one of the host's IP
	// addresses.
	// If the port in the address parameter is empty or "0", as in
	// "127.0.0.1:" or "[::1]:0", a port number is automatically chosen.
	// The LocalAddr method of PacketConn can be used to discover the
	// chosen port.
	//
	// See func Dial for a description of the network and address
	// parameters.
	//
	// ListenPacket uses context.Background internally; to specify the context, use
	// ListenConfig.ListenPacket.
	ListenPacket(network string, address string) (net.PacketConn, error)

	// ListenUDP acts like ListenPacket for UDP networks.
	//
	// The network must be a UDP network name; see func Dial for details.
	//
	// If the IP field of laddr is nil or an unspecified IP address,
	// ListenUDP listens on all available IP addresses of the local system
	// except multicast IP addresses.
	// If the Port field of laddr is 0, a port number is automatically
	// chosen.
	ListenUDP(network string, locAddr *net.UDPAddr) (UDPConn, error)

	// ListenTCP acts like Listen for TCP networks.
	//
	// The network must be a TCP network name; see func Dial for details.
	//
	// If the IP field of laddr is nil or an unspecified IP address,
	// ListenTCP listens on all available unicast and anycast IP addresses
	// of the local system.
	// If the Port field of laddr is 0, a port number is automatically
	// chosen.
	ListenTCP(network string, laddr *net.TCPAddr) (TCPListener, error)

	// Dial connects to the address on the named network.
	//
	// Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only),
	// "udp", "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4"
	// (IPv4-only), "ip6" (IPv6-only), "unix", "unixgram" and
	// "unixpacket".
	//
	// For TCP and UDP networks, the address has the form "host:port".
	// The host must be a literal IP address, or a host name that can be
	// resolved to IP addresses.
	// The port must be a literal port number or a service name.
	// If the host is a literal IPv6 address it must be enclosed in square
	// brackets, as in "[2001:db8::1]:80" or "[fe80::1%zone]:80".
	// The zone specifies the scope of the literal IPv6 address as defined
	// in RFC 4007.
	// The functions JoinHostPort and SplitHostPort manipulate a pair of
	// host and port in this form.
	// When using TCP, and the host resolves to multiple IP addresses,
	// Dial will try each IP address in order until one succeeds.
	//
	// Examples:
	//
	//	Dial("tcp", "golang.org:http")
	//	Dial("tcp", "192.0.2.1:http")
	//	Dial("tcp", "198.51.100.1:80")
	//	Dial("udp", "[2001:db8::1]:domain")
	//	Dial("udp", "[fe80::1%lo0]:53")
	//	Dial("tcp", ":80")
	//
	// For IP networks, the network must be "ip", "ip4" or "ip6" followed
	// by a colon and a literal protocol number or a protocol name, and
	// the address has the form "host". The host must be a literal IP
	// address or a literal IPv6 address with zone.
	// It depends on each operating system how the operating system
	// behaves with a non-well known protocol number such as "0" or "255".
	//
	// Examples:
	//
	//	Dial("ip4:1", "192.0.2.1")
	//	Dial("ip6:ipv6-icmp", "2001:db8::1")
	//	Dial("ip6:58", "fe80::1%lo0")
	//
	// For TCP, UDP and IP networks, if the host is empty or a literal
	// unspecified IP address, as in ":80", "0.0.0.0:80" or "[::]:80" for
	// TCP and UDP, "", "0.0.0.0" or "::" for IP, the local system is
	// assumed.
	//
	// For Unix networks, the address must be a file system path.
	Dial(network, address string) (net.Conn, error)

	// DialUDP acts like Dial for UDP networks.
	//
	// The network must be a UDP network name; see func Dial for details.
	//
	// If laddr is nil, a local address is automatically chosen.
	// If the IP field of raddr is nil or an unspecified IP address, the
	// local system is assumed.
	DialUDP(network string, laddr, raddr *net.UDPAddr) (UDPConn, error)

	// DialTCP acts like Dial for TCP networks.
	//
	// The network must be a TCP network name; see func Dial for details.
	//
	// If laddr is nil, a local address is automatically chosen.
	// If the IP field of raddr is nil or an unspecified IP address, the
	// local system is assumed.
	DialTCP(network string, laddr, raddr *net.TCPAddr) (TCPConn, error)

	// ResolveIPAddr returns an address of IP end point.
	//
	// The network must be an IP network name.
	//
	// If the host in the address parameter is not a literal IP address,
	// ResolveIPAddr resolves the address to an address of IP end point.
	// Otherwise, it parses the address as a literal IP address.
	// The address parameter can use a host name, but this is not
	// recommended, because it will return at most one of the host name's
	// IP addresses.
	//
	// See func Dial for a description of the network and address
	// parameters.
	ResolveIPAddr(network, address string) (*net.IPAddr, error)

	// ResolveUDPAddr returns an address of UDP end point.
	//
	// The network must be a UDP network name.
	//
	// If the host in the address parameter is not a literal IP address or
	// the port is not a literal port number, ResolveUDPAddr resolves the
	// address to an address of UDP end point.
	// Otherwise, it parses the address as a pair of literal IP address
	// and port number.
	// The address parameter can use a host name, but this is not
	// recommended, because it will return at most one of the host name's
	// IP addresses.
	//
	// See func Dial for a description of the network and address
	// parameters.
	ResolveUDPAddr(network, address string) (*net.UDPAddr, error)

	// ResolveTCPAddr returns an address of TCP end point.
	//
	// The network must be a TCP network name.
	//
	// If the host in the address parameter is not a literal IP address or
	// the port is not a literal port number, ResolveTCPAddr resolves the
	// address to an address of TCP end point.
	// Otherwise, it parses the address as a pair of literal IP address
	// and port number.
	// The address parameter can use a host name, but this is not
	// recommended, because it will return at most one of the host name's
	// IP addresses.
	//
	// See func Dial for a description of the network and address
	// parameters.
	ResolveTCPAddr(network, address string) (*net.TCPAddr, error)

	// Interfaces returns a list of the system's network interfaces.
	Interfaces() ([]*Interface, error)

	// InterfaceByIndex returns the interface specified by index.
	//
	// On Solaris, it returns one of the logical network interfaces
	// sharing the logical data link; for more precision use
	// InterfaceByName.
	InterfaceByIndex(index int) (*Interface, error)

	// InterfaceByName returns the interface specified by name.
	InterfaceByName(name string) (*Interface, error)

	// The following functions are extensions to Go's standard net package

	CreateDialer(dialer *net.Dialer) Dialer
	CreateListenConfig(listenerConfig *net.ListenConfig) ListenConfig
}

// Dialer is identical to net.Dialer excepts that its methods
// (Dial, DialContext) are overridden to use the Net interface.
// Use vnet.CreateDialer() to create an instance of this Dialer.
type Dialer interface {
	Dial(network, address string) (net.Conn, error)
}

// ListenConfig is identical to net.ListenConfig except that its methods
// (Listen, ListenPacket) are overridden to use the Net interface.
// Use vnet.Create:ListenConfig() to create an instance of this ListenConfig.
type ListenConfig interface {
	Listen(ctx context.Context, network, address string) (net.Listener, error)
	ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error)
}

// UDPConn is packet-oriented connection for UDP.
type UDPConn interface {
	// Close closes the connection.
	// Any blocked Read or Write operations will be unblocked and return errors.
	Close() error

	// LocalAddr returns the local network address, if known.
	LocalAddr() net.Addr

	// RemoteAddr returns the remote network address, if known.
	RemoteAddr() net.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 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.
	//
	// If the deadline is exceeded a call to Read or Write or to other
	// I/O methods will return an error that wraps os.ErrDeadlineExceeded.
	// This can be tested using errors.Is(err, os.ErrDeadlineExceeded).
	// The error's Timeout method will return true, but note that there
	// are other possible errors for which the Timeout method will
	// return true even if the deadline has not been exceeded.
	//
	// 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

	// SetReadBuffer sets the size of the operating system's
	// receive buffer associated with the connection.
	SetReadBuffer(bytes int) error

	// SetWriteBuffer sets the size of the operating system's
	// transmit buffer associated with the connection.
	SetWriteBuffer(bytes int) error

	// Read reads data from the connection.
	// Read can be made to time out and return an error after a fixed
	// time limit; see SetDeadline and SetReadDeadline.
	Read(b []byte) (n int, err error)

	// ReadFrom reads a packet from the connection,
	// copying the payload into p. It returns the number of
	// bytes copied into p and the return address that
	// was on the packet.
	// It returns the number of bytes read (0 <= n <= len(p))
	// and any error encountered. Callers should always process
	// the n > 0 bytes returned before considering the error err.
	// ReadFrom can be made to time out and return an error after a
	// fixed time limit; see SetDeadline and SetReadDeadline.
	ReadFrom(p []byte) (n int, addr net.Addr, err error)

	// ReadFromUDP acts like ReadFrom but returns a UDPAddr.
	ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error)

	// ReadMsgUDP reads a message from c, copying the payload into b and
	// the associated out-of-band data into oob. It returns the number of
	// bytes copied into b, the number of bytes copied into oob, the flags
	// that were set on the message and the source address of the message.
	//
	// The packages golang.org/x/net/ipv4 and golang.org/x/net/ipv6 can be
	// used to manipulate IP-level socket options in oob.
	ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error)

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

	// WriteTo writes a packet with payload p to addr.
	// WriteTo can be made to time out and return an Error after a
	// fixed time limit; see SetDeadline and SetWriteDeadline.
	// On packet-oriented connections, write timeouts are rare.
	WriteTo(p []byte, addr net.Addr) (n int, err error)

	// WriteToUDP acts like WriteTo but takes a UDPAddr.
	WriteToUDP(b []byte, addr *net.UDPAddr) (int, error)

	// WriteMsgUDP writes a message to addr via c if c isn't connected, or
	// to c's remote address if c is connected (in which case addr must be
	// nil). The payload is copied from b and the associated out-of-band
	// data is copied from oob. It returns the number of payload and
	// out-of-band bytes written.
	//
	// The packages golang.org/x/net/ipv4 and golang.org/x/net/ipv6 can be
	// used to manipulate IP-level socket options in oob.
	WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error)
}

// TCPConn is an interface for TCP network connections.
type TCPConn interface {
	net.Conn

	// CloseRead shuts down the reading side of the TCP connection.
	// Most callers should just use Close.
	CloseRead() error

	// CloseWrite shuts down the writing side of the TCP connection.
	// Most callers should just use Close.
	CloseWrite() error

	// ReadFrom implements the io.ReaderFrom ReadFrom method.
	ReadFrom(r io.Reader) (int64, error)

	// SetLinger sets the behavior of Close on a connection which still
	// has data waiting to be sent or to be acknowledged.
	//
	// If sec < 0 (the default), the operating system finishes sending the
	// data in the background.
	//
	// If sec == 0, the operating system discards any unsent or
	// unacknowledged data.
	//
	// If sec > 0, the data is sent in the background as with sec < 0. On
	// some operating systems after sec seconds have elapsed any remaining
	// unsent data may be discarded.
	SetLinger(sec int) error

	// SetKeepAlive sets whether the operating system should send
	// keep-alive messages on the connection.
	SetKeepAlive(keepalive bool) error

	// SetKeepAlivePeriod sets period between keep-alives.
	SetKeepAlivePeriod(d time.Duration) error

	// SetNoDelay controls whether the operating system should delay
	// packet transmission in hopes of sending fewer packets (Nagle's
	// algorithm).  The default is true (no delay), meaning that data is
	// sent as soon as possible after a Write.
	SetNoDelay(noDelay bool) error

	// SetWriteBuffer sets the size of the operating system's
	// transmit buffer associated with the connection.
	SetWriteBuffer(bytes int) error

	// SetReadBuffer sets the size of the operating system's
	// receive buffer associated with the connection.
	SetReadBuffer(bytes int) error
}

// TCPListener is a TCP network listener. Clients should typically
// use variables of type Listener instead of assuming TCP.
type TCPListener interface {
	net.Listener

	// AcceptTCP accepts the next incoming call and returns the new
	// connection.
	AcceptTCP() (TCPConn, error)

	// SetDeadline sets the deadline associated with the listener.
	// A zero time value disables the deadline.
	SetDeadline(t time.Time) error
}

// Interface wraps a standard net.Interfaces and its assigned addresses.
type Interface struct {
	net.Interface
	addrs []net.Addr
}

// NewInterface creates a new interface based of a standard net.Interface.
func NewInterface(ifc net.Interface) *Interface {
	return &Interface{
		Interface: ifc,
		addrs:     nil,
	}
}

// AddAddress adds a new address to the interface.
func (ifc *Interface) AddAddress(addr net.Addr) {
	ifc.addrs = append(ifc.addrs, addr)
}

// RemoveAddress removes an address from the interface.
func (ifc *Interface) RemoveAddress(ip net.IP) bool {
	for i, addr := range ifc.addrs {
		var addrIP net.IP
		switch a := addr.(type) {
		case *net.IPNet:
			addrIP = a.IP
		case *net.IPAddr:
			addrIP = a.IP
		default:
			continue
		}
		if addrIP.Equal(ip) {
			ifc.addrs = append(ifc.addrs[:i], ifc.addrs[i+1:]...)

			return true
		}
	}

	return false
}

// Addrs returns a slice of configured addresses on the interface.
func (ifc *Interface) Addrs() ([]net.Addr, error) {
	if len(ifc.addrs) == 0 {
		return nil, ErrNoAddressAssigned
	}

	return ifc.addrs, nil
}
