// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build !quicbasicnet && (darwin || linux)

package quic

import (
	"encoding/binary"
	"net"
	"net/netip"
	"sync"
	"unsafe"

	"golang.org/x/sys/unix"
)

// Network interface for platforms using sendmsg/recvmsg with cmsgs.

type netUDPConn struct {
	c         *net.UDPConn
	localAddr netip.AddrPort
}

func newNetUDPConn(uc *net.UDPConn) (*netUDPConn, error) {
	a, _ := uc.LocalAddr().(*net.UDPAddr)
	localAddr := a.AddrPort()
	if localAddr.Addr().IsUnspecified() {
		// If the conn is not bound to a specified (non-wildcard) address,
		// then set localAddr.Addr to an invalid netip.Addr.
		// This better conveys that this is not an address we should be using,
		// and is a bit more efficient to test against.
		localAddr = netip.AddrPortFrom(netip.Addr{}, localAddr.Port())
	}

	sc, err := uc.SyscallConn()
	if err != nil {
		return nil, err
	}
	sc.Control(func(fd uintptr) {
		// Ask for ECN info and (when we aren't bound to a fixed local address)
		// destination info.
		//
		// If any of these calls fail, we won't get the requested information.
		// That's fine, we'll gracefully handle the lack.
		unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVTOS, 1)
		unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_RECVTCLASS, 1)
		if !localAddr.IsValid() {
			unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_PKTINFO, 1)
			unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_RECVPKTINFO, 1)
		}
	})

	return &netUDPConn{
		c:         uc,
		localAddr: localAddr,
	}, nil
}

func (c *netUDPConn) Close() error { return c.c.Close() }

func (c *netUDPConn) LocalAddr() netip.AddrPort {
	a, _ := c.c.LocalAddr().(*net.UDPAddr)
	return a.AddrPort()
}

func (c *netUDPConn) Read(f func(*datagram)) {
	// We shouldn't ever see all of these messages at the same time,
	// but the total is small so just allocate enough space for everything we use.
	const (
		inPktinfoSize  = 12 // int + in_addr + in_addr
		in6PktinfoSize = 20 // in6_addr + int
		ipTOSSize      = 4
		ipv6TclassSize = 4
	)
	control := make([]byte, 0+
		unix.CmsgSpace(inPktinfoSize)+
		unix.CmsgSpace(in6PktinfoSize)+
		unix.CmsgSpace(ipTOSSize)+
		unix.CmsgSpace(ipv6TclassSize))

	for {
		d := newDatagram()
		n, controlLen, _, peerAddr, err := c.c.ReadMsgUDPAddrPort(d.b, control)
		if err != nil {
			return
		}
		if n == 0 {
			continue
		}
		d.localAddr = c.localAddr
		d.peerAddr = unmapAddrPort(peerAddr)
		d.b = d.b[:n]
		parseControl(d, control[:controlLen])
		f(d)
	}
}

var cmsgPool = sync.Pool{
	New: func() any {
		return new([]byte)
	},
}

func (c *netUDPConn) Write(dgram datagram) error {
	controlp := cmsgPool.Get().(*[]byte)
	control := *controlp
	defer func() {
		*controlp = control[:0]
		cmsgPool.Put(controlp)
	}()

	localIP := dgram.localAddr.Addr()
	if localIP.IsValid() {
		if localIP.Is4() {
			control = appendCmsgIPSourceAddrV4(control, localIP)
		} else {
			control = appendCmsgIPSourceAddrV6(control, localIP)
		}
	}
	if dgram.ecn != ecnNotECT {
		if dgram.peerAddr.Addr().Is4() {
			control = appendCmsgECNv4(control, dgram.ecn)
		} else {
			control = appendCmsgECNv6(control, dgram.ecn)
		}
	}

	_, _, err := c.c.WriteMsgUDPAddrPort(dgram.b, control, dgram.peerAddr)
	return err
}

func parseControl(d *datagram, control []byte) {
	for len(control) > 0 {
		hdr, data, remainder, err := unix.ParseOneSocketControlMessage(control)
		if err != nil {
			return
		}
		control = remainder
		switch hdr.Level {
		case unix.IPPROTO_IP:
			switch hdr.Type {
			case unix.IP_TOS, unix.IP_RECVTOS:
				// (Linux sets the type to IP_TOS, Darwin to IP_RECVTOS,
				// just check for both.)
				if ecn, ok := parseIPTOS(data); ok {
					d.ecn = ecn
				}
			case unix.IP_PKTINFO:
				if a, ok := parseInPktinfo(data); ok {
					d.localAddr = netip.AddrPortFrom(a, d.localAddr.Port())
				}
			}
		case unix.IPPROTO_IPV6:
			switch hdr.Type {
			case unix.IPV6_TCLASS:
				// 32-bit integer containing the traffic class field.
				// The low two bits are the ECN field.
				if ecn, ok := parseIPv6TCLASS(data); ok {
					d.ecn = ecn
				}
			case unix.IPV6_PKTINFO:
				if a, ok := parseIn6Pktinfo(data); ok {
					d.localAddr = netip.AddrPortFrom(a, d.localAddr.Port())
				}
			}
		}
	}
}

// IPV6_TCLASS is specified by RFC 3542 as an int.

func parseIPv6TCLASS(b []byte) (ecnBits, bool) {
	if len(b) != 4 {
		return 0, false
	}
	return ecnBits(binary.NativeEndian.Uint32(b) & ecnMask), true
}

func appendCmsgECNv6(b []byte, ecn ecnBits) []byte {
	b, data := appendCmsg(b, unix.IPPROTO_IPV6, unix.IPV6_TCLASS, 4)
	binary.NativeEndian.PutUint32(data, uint32(ecn))
	return b
}

// struct in_pktinfo {
//   unsigned int   ipi_ifindex;  /* send/recv interface index */
//   struct in_addr ipi_spec_dst; /* Local address */
//   struct in_addr ipi_addr;     /* IP Header dst address */
// };

// parseInPktinfo returns the destination address from an IP_PKTINFO.
func parseInPktinfo(b []byte) (dst netip.Addr, ok bool) {
	if len(b) != 12 {
		return netip.Addr{}, false
	}
	return netip.AddrFrom4([4]byte(b[8:][:4])), true
}

// appendCmsgIPSourceAddrV4 appends an IP_PKTINFO setting the source address
// for an outbound datagram.
func appendCmsgIPSourceAddrV4(b []byte, src netip.Addr) []byte {
	// struct in_pktinfo {
	//   unsigned int   ipi_ifindex;  /* send/recv interface index */
	//   struct in_addr ipi_spec_dst; /* Local address */
	//   struct in_addr ipi_addr;     /* IP Header dst address */
	// };
	b, data := appendCmsg(b, unix.IPPROTO_IP, unix.IP_PKTINFO, 12)
	ip := src.As4()
	copy(data[4:], ip[:])
	return b
}

// struct in6_pktinfo {
//   struct in6_addr  ipi6_addr;    /* src/dst IPv6 address */
//   unsigned int     ipi6_ifindex; /* send/recv interface index */
// };

// parseIn6Pktinfo returns the destination address from an IPV6_PKTINFO.
func parseIn6Pktinfo(b []byte) (netip.Addr, bool) {
	if len(b) != 20 {
		return netip.Addr{}, false
	}
	return netip.AddrFrom16([16]byte(b[:16])).Unmap(), true
}

// appendCmsgIPSourceAddrV6 appends an IPV6_PKTINFO setting the source address
// for an outbound datagram.
func appendCmsgIPSourceAddrV6(b []byte, src netip.Addr) []byte {
	b, data := appendCmsg(b, unix.IPPROTO_IPV6, unix.IPV6_PKTINFO, 20)
	ip := src.As16()
	copy(data[0:], ip[:])
	return b
}

// appendCmsg appends a cmsg with the given level, type, and size to b.
// It returns the new buffer, and the data section of the cmsg.
func appendCmsg(b []byte, level, typ int32, size int) (_, data []byte) {
	off := len(b)
	b = append(b, make([]byte, unix.CmsgSpace(size))...)
	h := (*unix.Cmsghdr)(unsafe.Pointer(&b[off]))
	h.Level = level
	h.Type = typ
	h.SetLen(unix.CmsgLen(size))
	return b, b[off+unix.CmsgSpace(0):][:size]
}
