package main

import (
	"fmt"
	"unsafe"

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

const (
	hwtstampTxOn      = 1
	hwtstampFilterAll = 1
)

type hwtstampConfig struct {
	flags    int32
	txType   int32
	rxFilter int32
}

// Receive stamping is filtered by protocol and ours is not PTP, so nothing
// narrower than "all" will see our frames.
func checkTimestamping(fd int, ifname string) checkResult {
	res := checkResult{item: ifname + " hw timestamps"}
	desc := func(c hwtstampConfig) string {
		return fmt.Sprintf("tx_type=%d rx_filter=%d", c.txType, c.rxFilter)
	}

	hwtstampCall := func(req uintptr, cfg *hwtstampConfig) error {
		var ifr dataIfreq
		copy(ifr.name[:], ifname)
		ifr.data = unsafe.Pointer(cfg)
		if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req,
			uintptr(unsafe.Pointer(&ifr))); errno != 0 {
			return errno
		}
		return nil
	}

	var have hwtstampConfig
	if err := hwtstampCall(unix.SIOCGHWTSTAMP, &have); err != nil {
		res.err = err
		return res
	}
	if have.txType == hwtstampTxOn && have.rxFilter == hwtstampFilterAll {
		res.state = desc(have)
		return res
	}

	// The ioctl reports back what the driver actually applied, which can be
	// narrower than what was asked for.
	want := hwtstampConfig{txType: hwtstampTxOn, rxFilter: hwtstampFilterAll}
	if err := hwtstampCall(unix.SIOCSHWTSTAMP, &want); err != nil {
		res.err = err
		res.state = "could not set"
		return res
	}
	if want.txType != hwtstampTxOn || want.rxFilter != hwtstampFilterAll {
		res.err = fmt.Errorf("driver applied %s instead", desc(want))
		return res
	}
	res.fixed = true
	res.state = fmt.Sprintf("was %s, now %s", desc(have), desc(want))
	return res
}

func enableTxTimestamps(fd int) error {
	return unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_TIMESTAMPING,
		unix.SOF_TIMESTAMPING_TX_HARDWARE|
			unix.SOF_TIMESTAMPING_RAW_HARDWARE|
			unix.SOF_TIMESTAMPING_OPT_TSONLY)
}

func enableRxTimestamps(fd int) error {
	return unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_TIMESTAMPING,
		unix.SOF_TIMESTAMPING_RX_HARDWARE|unix.SOF_TIMESTAMPING_RAW_HARDWARE)
}

// Three timespecs, of which the third is the raw hardware clock. A zero there
// means the mac did not produce a stamp.
const scmTimestampingLen = 3 * int(unsafe.Sizeof(unix.Timespec{}))

// A control message's payload starts at its header rounded up to a pointer, and
// the next starts at its length rounded up the same way.
const (
	cmsgAlignMask = unix.SizeofPtr - 1
	cmsgDataOff   = (unix.SizeofCmsghdr + cmsgAlignMask) &^ cmsgAlignMask
)

// Walked by hand rather than through ParseSocketControlMessage, which allocates
// per call on a path that runs for every frame received.
func hwTimestamp(oob []byte) (int64, bool) {
	for off := 0; off+unix.SizeofCmsghdr <= len(oob); {
		h := (*unix.Cmsghdr)(unsafe.Pointer(&oob[off]))
		n := int(h.Len)
		if n < unix.SizeofCmsghdr || off+n > len(oob) {
			return 0, false
		}
		if h.Level == unix.SOL_SOCKET && h.Type == unix.SCM_TIMESTAMPING {
			if cmsgDataOff+scmTimestampingLen > n {
				return 0, false
			}
			ts := (*[3]unix.Timespec)(unsafe.Pointer(&oob[off+cmsgDataOff]))
			ns := ts[2].Nano()
			return ns, ns != 0
		}
		off += (n + cmsgAlignMask) &^ cmsgAlignMask
	}
	return 0, false
}
