package main

import (
	"sync"
	"sync/atomic"
	"time"

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

const (
	// Above any real stream number, so a probe is never taken for payload.
	probeStream  = 0xffff
	probeSize    = 64
	probePattern = 0

	// The mac has only a handful of transmit stamp slots. Asking faster than it
	// can drain them gets slots recycled while a stamp is still outstanding, and
	// the one that comes back then belongs to a different frame.
	probeInterval = 200 * time.Millisecond
	probeTimeout  = 20 * time.Millisecond
	// A phy costs microseconds and a hundred metres of copper costs five hundred
	// nanoseconds, so anything past this is a broken stamp, not a slow frame.
	probeMaxDelay = 50 * time.Microsecond
	probePendCap  = 256
)

// Both ports hang off one PTP clock, so a transmit stamp from one and a receive
// stamp from the other subtract directly. Both are taken at the mac, so all host
// time and all queueing falls outside the stamped interval, which is why load
// does not move it.
type cableStats struct {
	mu      sync.Mutex
	min     int64
	samples uint64
	floor   int64

	txPend map[uint64]int64
	rxPend map[uint64]int64
}

type cableView struct {
	min   int64
	floor int64
	ok    bool
}

// Averaging the two directions cancels the phy asymmetry between them, which is
// about 790ns and swamps any cable, so one direction alone cannot give a length.
func cableMetres(views []view, nsPerM float64) (float64, bool) {
	if len(views) == 0 {
		return 0, false
	}
	var excess float64
	for _, v := range views {
		if !v.cable.ok {
			return 0, false
		}
		excess += float64(v.cable.min - v.cable.floor)
	}
	return excess / float64(len(views)) / nsPerM, true
}

func newCableStats() *cableStats {
	return &cableStats{
		txPend: make(map[uint64]int64, probePendCap),
		rxPend: make(map[uint64]int64, probePendCap),
	}
}

func (c *cableStats) put(seq uint64, ts int64, tx bool) {
	c.mu.Lock()
	defer c.mu.Unlock()

	mine, theirs := c.txPend, c.rxPend
	if !tx {
		mine, theirs = c.rxPend, c.txPend
	}
	other, ok := theirs[seq]
	if !ok {
		if len(mine) >= probePendCap {
			clear(mine)
		}
		mine[seq] = ts
		return
	}
	delete(theirs, seq)

	delta := ts - other
	if tx {
		delta = -delta
	}
	// The driver rebuilds a full timestamp from a truncated hardware value plus a
	// cached clock read, and a stale cache lands hundreds of milliseconds out. A
	// minimum would latch onto the first of those and never recover.
	if delta <= 0 || delta > int64(probeMaxDelay) {
		return
	}
	if c.samples == 0 || delta < c.min {
		c.min = delta
	}
	if c.floor == 0 || delta < c.floor {
		c.floor = delta
	}
	c.samples++
}

func (c *cableStats) view() cableView {
	c.mu.Lock()
	defer c.mu.Unlock()
	return cableView{c.min, c.floor, c.samples > 0}
}

func (c *cableStats) reset() {
	c.mu.Lock()
	c.min, c.samples = 0, 0
	clear(c.txPend)
	clear(c.rxPend)
	c.mu.Unlock()
}

type probeSender struct {
	fd    int
	spec  *frameSpec
	stats *cableStats
}

func (p *probeSender) run(done *atomic.Bool, startTx <-chan struct{}) {
	buf := make([]byte, probeSize)
	p.spec.prefill(buf, probePattern)
	oob := make([]byte, 512)
	scratch := make([]byte, 1)

	<-startTx

	tick := time.NewTicker(probeInterval)
	defer tick.Stop()

	var seq uint64
	for !done.Load() {
		<-tick.C

		// Stamps are matched to sends by position in the queue, so one that
		// arrived after its probe gave up would be handed to this probe.
		for {
			if _, _, _, _, err := unix.Recvmsg(p.fd, scratch, oob,
				unix.MSG_ERRQUEUE|unix.MSG_DONTWAIT); err != nil {
				break
			}
		}

		putHeader(buf, probePattern, probeStream, seq, probeSize-minFrame)
		err := unix.Send(p.fd, buf, 0)
		// The sequence advances even when a probe fails, so a stale receive half
		// can never be paired with a later probe that reused its number.
		cur := seq
		seq++
		if err != nil {
			continue
		}
		ts, ok := p.awaitTx(scratch, oob)
		if !ok {
			continue
		}
		p.stats.put(cur, ts, true)
	}
}

func (p *probeSender) awaitTx(scratch, oob []byte) (int64, bool) {
	fds := []unix.PollFd{{Fd: int32(p.fd), Events: unix.POLLERR}}
	deadline := time.Now().Add(probeTimeout)
	for {
		left := time.Until(deadline)
		if left <= 0 {
			return 0, false
		}
		// Rounded up, since truncating would give up with time still on the
		// clock and shave the last fraction of a millisecond off every wait.
		ms := int((left + time.Millisecond - 1) / time.Millisecond)
		n, err := unix.Poll(fds, ms)
		if err == unix.EINTR {
			continue
		}
		if err != nil || n == 0 {
			return 0, false
		}
		_, oobn, _, _, err := unix.Recvmsg(p.fd, scratch, oob,
			unix.MSG_ERRQUEUE|unix.MSG_DONTWAIT)
		if err == unix.EAGAIN || err == unix.EINTR {
			continue
		}
		if err != nil {
			return 0, false
		}
		return hwTimestamp(oob[:oobn])
	}
}

type probeReceiver struct {
	fd    int
	stats *cableStats
	ready *sync.WaitGroup
}

func (r *probeReceiver) run(done *atomic.Bool) {
	buf := make([]byte, maxFrame)
	oob := make([]byte, 512)

	r.ready.Done()

	for !done.Load() {
		n, oobn, _, _, err := unix.Recvmsg(r.fd, buf, oob, 0)
		if err != nil {
			continue
		}
		h, st := parseHeader(buf[:n])
		if st != hdrOK || h.stream != probeStream {
			continue
		}
		ts, ok := hwTimestamp(oob[:oobn])
		if !ok {
			continue
		}
		r.stats.put(h.seq, ts, false)
	}
}
