package main

import (
	"hash/crc32"
	"sync"
	"sync/atomic"

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

type rxStats struct {
	frames   atomic.Uint64
	bytes    atomic.Uint64
	badMagic atomic.Uint64
	badHdr   atomic.Uint64
	badLen   atomic.Uint64
	crcErr   atomic.Uint64
	rxErrs   atomic.Uint64

	// Frames counted into the interval their mac receive stamp falls in, rather
	// than the interval a worker got round to draining them in.
	newest  atomic.Int64
	buckets [rateBuckets]rxBucket
}

// One sample interval of arrivals, keyed by the mac's clock, with enough of them
// kept that a bucket is read long before its slot comes round again.
const (
	rateBucketNs   = int64(sampleInterval)
	rateBuckets    = 64
	rateBucketSecs = float64(rateBucketNs) / 1e9
)

type rxBucket struct {
	epoch  atomic.Int64
	frames atomic.Uint64
	bytes  atomic.Uint64
}

// Only the owning worker writes its own buckets, so a slot coming round again is
// simply zeroed before it is claimed for the new epoch.
func (s *rxStats) commit(e int64, frames, bytes uint64) {
	b := &s.buckets[e&(rateBuckets-1)]
	if b.epoch.Load() != e {
		b.frames.Store(0)
		b.bytes.Store(0)
		b.epoch.Store(e)
	}
	b.frames.Add(frames)
	b.bytes.Add(bytes)
	if e > s.newest.Load() {
		s.newest.Store(e)
	}
}

// Frames drained together that fell in the same epoch, so the buckets take one
// pair of adds per epoch a batch spans rather than one per frame.
type rateRun struct {
	stats  *rxStats
	epoch  int64
	frames uint64
	bytes  uint64
}

func (r *rateRun) add(stamp int64, n uint64) {
	if e := stamp / rateBucketNs; e != r.epoch {
		r.flush()
		r.epoch = e
	}
	r.frames++
	r.bytes += n
}

func (r *rateRun) flush() {
	if r.frames == 0 {
		return
	}
	r.stats.commit(r.epoch, r.frames, r.bytes)
	r.frames, r.bytes = 0, 0
}

// What this worker counted into one epoch, or nothing if that epoch has already
// fallen out of the ring.
func (s *rxStats) bucket(e int64) (frames, bytes uint64) {
	b := &s.buckets[e&(rateBuckets-1)]
	if b.epoch.Load() != e {
		return 0, 0
	}
	return b.frames.Load(), b.bytes.Load()
}

type rxWorker struct {
	fd     int
	batch  int
	stream uint16
	spec   *frameSpec
	stats  *rxStats
	loss   *lossWindow
	ready  *sync.WaitGroup
}

func (w *rxWorker) run(done *atomic.Bool) {
	bufs := make([][]byte, w.batch)
	for i := range bufs {
		bufs[i] = make([]byte, maxFrame)
		for j := 0; j < maxFrame; j += 4096 {
			bufs[i][j] = 0
		}
	}
	hdrs, oob := newRxMmsghdrs(bufs)
	run := rateRun{stats: w.stats}

	w.ready.Done()

	for !done.Load() {
		// The kernel overwrites each Controllen with what it wrote, so they are
		// reset before every call.
		for i := range hdrs {
			hdrs[i].hdr.Controllen = cmsgLen
		}
		n, err := recvmmsg(w.fd, hdrs, unix.MSG_WAITFORONE)
		if n <= 0 {
			if err != nil && err != unix.EAGAIN && err != unix.EINTR {
				w.stats.rxErrs.Add(1)
			}
			continue
		}
		var frames, bytes uint64
		for i := 0; i < n; i++ {
			buf := bufs[i][:int(hdrs[i].len)]
			p, st := parseHeader(buf)
			if st != hdrOK {
				if st == hdrForeign {
					w.stats.badMagic.Add(1)
				} else {
					w.stats.badHdr.Add(1)
				}
				continue
			}
			frames++
			bytes += uint64(len(buf))
			if ts, ok := hwTimestamp(oob[i][:hdrs[i].hdr.Controllen]); ok {
				run.add(ts, uint64(len(buf)))
			}

			// The ethertype this socket is bound to already says which stream the
			// frame belongs to, so a header naming another one is damaged, as is a
			// sequence number the sender never reached.
			if p.stream != w.stream || !w.loss.observe(p.seq) {
				w.stats.badHdr.Add(1)
				continue
			}

			want, ok := w.spec.expectedCRC(p.patIdx, p.payLen)
			if !ok {
				w.stats.badLen.Add(1)
				continue
			}
			pay := buf[minFrame : minFrame+p.payLen]
			if crc32.Checksum(pay, crcTable) != want {
				w.stats.crcErr.Add(1)
			}
		}
		// Nothing reads these between frames, and they share a cache line, so a
		// batch commits once rather than locking the line for every frame.
		w.stats.frames.Add(frames)
		w.stats.bytes.Add(bytes)
		run.flush()
	}
}
