//go:build linux
// +build linux

package packet

import (
	"context"
	"encoding/binary"
	"errors"
	"math"
	"net"
	"os"

	"github.com/josharian/native"
	"github.com/mdlayher/socket"
	"golang.org/x/sys/unix"
)

// A conn is the net.PacketConn implementation for packet sockets. We can use
// socket.Conn directly on Linux to implement most of the necessary methods.
type conn = socket.Conn

// readFrom implements the net.PacketConn ReadFrom method using recvfrom(2).
func (c *Conn) readFrom(b []byte) (int, net.Addr, error) {
	// From net.PacketConn documentation:
	//
	// "[ReadFrom] 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."
	//
	// c.opError will return nil if no error, but either way we return all the
	// information that we have.
	n, sa, err := c.c.Recvfrom(context.Background(), b, 0)
	return n, fromSockaddr(sa), c.opError(opRead, err)
}

// writeTo implements the net.PacketConn WriteTo method.
func (c *Conn) writeTo(b []byte, addr net.Addr) (int, error) {
	sa, err := c.toSockaddr("sendto", addr)
	if err != nil {
		return 0, c.opError(opWrite, err)
	}

	// TODO(mdlayher): it's curious that unix.Sendto does not return the number
	// of bytes actually sent. Fake it for now, but investigate upstream.
	if err := c.c.Sendto(context.Background(), b, 0, sa); err != nil {
		return 0, c.opError(opWrite, err)
	}

	return len(b), nil
}

// setPromiscuous wraps setsockopt(2) for the unix.PACKET_MR_PROMISC option.
func (c *Conn) setPromiscuous(enable bool) error {
	mreq := unix.PacketMreq{
		Ifindex: int32(c.ifIndex),
		Type:    unix.PACKET_MR_PROMISC,
	}

	membership := unix.PACKET_DROP_MEMBERSHIP
	if enable {
		membership = unix.PACKET_ADD_MEMBERSHIP
	}

	return c.opError(
		opSetsockopt,
		c.c.SetsockoptPacketMreq(unix.SOL_PACKET, membership, &mreq),
	)
}

// stats wraps getsockopt(2) for tpacket_stats* types.
func (c *Conn) stats() (*Stats, error) {
	const (
		level = unix.SOL_PACKET
		name  = unix.PACKET_STATISTICS
	)

	// Try to fetch V3 statistics first, they contain more detailed information.
	if stats, err := c.c.GetSockoptTpacketStatsV3(level, name); err == nil {
		return &Stats{
			Packets:          stats.Packets,
			Drops:            stats.Drops,
			FreezeQueueCount: stats.Freeze_q_cnt,
		}, nil
	}

	// There was an error fetching v3 stats, try to fall back.
	stats, err := c.c.GetSockoptTpacketStats(level, name)
	if err != nil {
		return nil, c.opError(opGetsockopt, err)
	}

	return &Stats{
		Packets: stats.Packets,
		Drops:   stats.Drops,
		// FreezeQueueCount is not present.
	}, nil
}

// listen is the entry point for Listen on Linux.
func listen(ifi *net.Interface, socketType Type, protocol int, cfg *Config) (*Conn, error) {
	if cfg == nil {
		// Default configuration.
		cfg = &Config{}
	}

	// Convert Type to the matching SOCK_* constant.
	var typ int
	switch socketType {
	case Raw:
		typ = unix.SOCK_RAW
	case Datagram:
		typ = unix.SOCK_DGRAM
	default:
		return nil, errors.New("packet: invalid Type value")
	}

	// Protocol is intentionally zero in call to socket(2); we can set it on
	// bind(2) instead. Package raw notes: "Do not specify a protocol to avoid
	// capturing packets which to not match cfg.Filter."
	c, err := socket.Socket(unix.AF_PACKET, typ, 0, network, nil)
	if err != nil {
		return nil, err
	}

	conn, err := bind(c, ifi.Index, protocol, cfg)
	if err != nil {
		_ = c.Close()
		return nil, err
	}

	return conn, nil
}

// bind binds the *socket.Conn to finalize *Conn setup.
func bind(c *socket.Conn, ifIndex, protocol int, cfg *Config) (*Conn, error) {
	if len(cfg.Filter) > 0 {
		// The caller wants to apply a BPF filter before bind(2).
		if err := c.SetBPF(cfg.Filter); err != nil {
			return nil, err
		}
	}

	// packet(7) says we sll_protocol must be in network byte order.
	pnet, err := htons(protocol)
	if err != nil {
		return nil, err
	}

	// TODO(mdlayher): investigate the possibility of sll_ifindex = 0 because we
	// could bind to any interface.
	err = c.Bind(&unix.SockaddrLinklayer{
		Protocol: pnet,
		Ifindex:  ifIndex,
	})
	if err != nil {
		return nil, err
	}

	lsa, err := c.Getsockname()
	if err != nil {
		return nil, err
	}

	// Parse the physical layer address; sll_halen tells us how many bytes of
	// sll_addr we should treat as valid.
	lsall := lsa.(*unix.SockaddrLinklayer)
	addr := make(net.HardwareAddr, lsall.Halen)
	copy(addr, lsall.Addr[:])

	return &Conn{
		c: c,

		addr:     &Addr{HardwareAddr: addr},
		ifIndex:  ifIndex,
		protocol: pnet,
	}, nil
}

// fromSockaddr converts an opaque unix.Sockaddr to *Addr. If sa is nil, it
// returns nil. It panics if sa is not of type *unix.SockaddrLinklayer.
func fromSockaddr(sa unix.Sockaddr) *Addr {
	if sa == nil {
		return nil
	}

	sall := sa.(*unix.SockaddrLinklayer)

	return &Addr{
		// The syscall already allocated sa; just slice into it with the
		// appropriate length and type conversion rather than making a copy.
		HardwareAddr: net.HardwareAddr(sall.Addr[:sall.Halen]),
	}
}

// toSockaddr converts a net.Addr to an opaque unix.Sockaddr. It returns an
// error if the fields cannot be packed into a *unix.SockaddrLinklayer.
func (c *Conn) toSockaddr(
	op string,
	addr net.Addr,
) (unix.Sockaddr, error) {
	// The typical error convention for net.Conn types is
	// net.OpError(os.SyscallError(syscall.Errno)), so all calls here should
	// return os.SyscallError(syscall.Errno) so the caller can apply the final
	// net.OpError wrapper.

	// Ensure the correct Addr type.
	a, ok := addr.(*Addr)
	if !ok || a.HardwareAddr == nil {
		return nil, os.NewSyscallError(op, unix.EINVAL)
	}

	// Pack Addr and Conn metadata into the appropriate sockaddr fields. From
	// packet(7):
	//
	// "When you send packets it is enough to specify sll_family, sll_addr,
	// sll_halen, sll_ifindex, and sll_protocol. The other fields should be 0."
	//
	// sll_family is set on the conversion to unix.RawSockaddrLinklayer.
	sa := unix.SockaddrLinklayer{
		Ifindex:  c.ifIndex,
		Protocol: c.protocol,
	}

	// Ensure the input address does not exceed the amount of space available;
	// for example an IPoIB address is 20 bytes.
	if len(a.HardwareAddr) > len(sa.Addr) {
		return nil, os.NewSyscallError(op, unix.EINVAL)
	}

	sa.Halen = uint8(len(a.HardwareAddr))
	copy(sa.Addr[:], a.HardwareAddr)

	return &sa, nil
}

// htons converts a short (uint16) from host-to-network byte order.
func htons(i int) (uint16, error) {
	if i < 0 || i > math.MaxUint16 {
		return 0, errors.New("packet: protocol value out of range")
	}

	// Store as big endian, retrieve as native endian.
	var b [2]byte
	binary.BigEndian.PutUint16(b[:], uint16(i))

	return native.Endian.Uint16(b[:]), nil
}
