package main

import (
	"encoding/binary"
	"fmt"
	"math"
	"path/filepath"
	"sync/atomic"
	"unsafe"

	"golang.org/x/sys/unix"

	"g.fc.run/theater/cabletest/internal/drm"
)

// Drawing lands in memory and is blitted to a buffer the display is not
// reading, then swapped in whole at a vertical blank. Writing into the live
// scanout buffer instead, as fbdev invites, races the beam: the blit takes a
// few hundred microseconds and the display reads half of each frame.
//
// Two buffers would be enough to stop the panel tearing. More than two is for
// anything reading a frame back out: they are cycled in order, so a buffer is
// left alone for the three frames between going on screen and coming round
// again, and reading one out of uncached scanout memory takes a good fraction
// of a frame.
const scanoutBuffers = 4

type scanout struct {
	fbID uint32
	mem  []byte
}

// w and h are the logical canvas, which is portrait; pw and ph are the panel,
// which is landscape.
type framebuffer struct {
	fd      int
	back    []byte
	scratch []byte
	w       int
	h       int
	pw      int
	ph      int
	stride  int

	bufs   [scanoutBuffers]scanout
	front  int
	crtcID uint32
	connID uint32

	// One token per completed flip. The render loop waits on this rather than on
	// a timer, so drawing is paced by the panel instead of by a guess at its rate.
	flips chan struct{}

	// Set before the fd goes, so the event reader can tell shutdown from failure.
	closing atomic.Bool
}

func (fb *framebuffer) offset(x, y int) int {
	return drm.Offset(x, y, fb.stride, fb.pw)
}

func (fb *framebuffer) fromPanel(x, y int) (int, int) {
	return y, fb.pw - 1 - x
}

func cardResources(fd int) (crtcs, conns []uint32, err error) {
	var res drm.ModeCardRes
	if err := drm.Ioctl(fd, drm.GetResources, unsafe.Pointer(&res)); err != nil {
		return nil, nil, fmt.Errorf("get resources: %w", err)
	}
	if res.CountCRTCs == 0 || res.CountConns == 0 {
		return nil, nil, fmt.Errorf("card has no crtcs or connectors")
	}
	crtcs = make([]uint32, res.CountCRTCs)
	conns = make([]uint32, res.CountConns)
	res.CountFBs, res.CountEncs = 0, 0
	res.FBIDPtr, res.EncIDPtr = 0, 0
	res.CrtcIDPtr = uint64(uintptr(unsafe.Pointer(&crtcs[0])))
	res.ConnIDPtr = uint64(uintptr(unsafe.Pointer(&conns[0])))
	if err := drm.Ioctl(fd, drm.GetResources, unsafe.Pointer(&res)); err != nil {
		return nil, nil, fmt.Errorf("get resources: %w", err)
	}
	return crtcs, conns, nil
}

// The preferred mode is the panel's native one; anything else would be the
// driver scaling a wrong-sized image onto it.
func preferredMode(fd int, connID uint32) (drm.ModeInfo, error) {
	c := drm.ModeGetConnector{ConnectorID: connID}
	if err := drm.Ioctl(fd, drm.GetConnector, unsafe.Pointer(&c)); err != nil {
		return drm.ModeInfo{}, err
	}
	if c.CountModes == 0 {
		return drm.ModeInfo{}, fmt.Errorf("connector %d reported no modes", connID)
	}
	modes := make([]drm.ModeInfo, c.CountModes)
	q := drm.ModeGetConnector{
		ConnectorID: connID,
		CountModes:  c.CountModes,
		ModesPtr:    uint64(uintptr(unsafe.Pointer(&modes[0]))),
	}
	if err := drm.Ioctl(fd, drm.GetConnector, unsafe.Pointer(&q)); err != nil {
		return drm.ModeInfo{}, err
	}
	if q.CountModes == 0 {
		return drm.ModeInfo{}, fmt.Errorf("connector %d reported no modes", connID)
	}
	for _, m := range modes[:q.CountModes] {
		if m.Type&drm.TypePreferred != 0 {
			return m, nil
		}
	}
	return modes[0], nil
}

func crtcFor(fd int, c drm.ModeGetConnector, crtcs []uint32) (uint32, error) {
	encoders := []uint32{c.EncoderID}
	if c.CountEncoders > 0 {
		list := make([]uint32, c.CountEncoders)
		q := drm.ModeGetConnector{
			ConnectorID:   c.ConnectorID,
			CountEncoders: c.CountEncoders,
			EncodersPtr:   uint64(uintptr(unsafe.Pointer(&list[0]))),
		}
		if err := drm.Ioctl(fd, drm.GetConnector, unsafe.Pointer(&q)); err == nil {
			encoders = append(encoders, list[:q.CountEncoders]...)
		}
	}
	for _, id := range encoders {
		if id == 0 {
			continue
		}
		e := drm.ModeGetEncoder{EncoderID: id}
		if err := drm.Ioctl(fd, drm.GetEncoder, unsafe.Pointer(&e)); err != nil {
			continue
		}
		// Already driving this connector, otherwise anything it can be wired to.
		if e.CrtcID != 0 {
			return e.CrtcID, nil
		}
		for i, crtc := range crtcs {
			if e.PossibleCRTCs&(1<<uint(i)) != 0 {
				return crtc, nil
			}
		}
	}
	return 0, fmt.Errorf("connector %d has no usable crtc", c.ConnectorID)
}

func findDisplay(fd int) (connID, crtcID uint32, err error) {
	crtcs, conns, err := cardResources(fd)
	if err != nil {
		return 0, 0, err
	}
	for _, id := range conns {
		c := drm.ModeGetConnector{ConnectorID: id}
		if err := drm.Ioctl(fd, drm.GetConnector, unsafe.Pointer(&c)); err != nil {
			continue
		}
		if c.Connection != drm.Connected || c.CountModes == 0 {
			continue
		}
		crtc, err := crtcFor(fd, c, crtcs)
		if err != nil {
			continue
		}
		return id, crtc, nil
	}
	return 0, 0, fmt.Errorf("no connected connector with a mode")
}

// The card number is not stable across machines, so the card driving a
// connected display is the one we want, and the display it found comes back
// with it.
func openCard() (fd int, connID, crtcID uint32, err error) {
	paths, err := filepath.Glob("/dev/dri/card*")
	if err != nil {
		return -1, 0, 0, err
	}
	for _, p := range paths {
		fd, err := unix.Open(p, unix.O_RDWR|unix.O_CLOEXEC, 0)
		if err != nil {
			continue
		}
		if connID, crtcID, err := findDisplay(fd); err == nil {
			return fd, connID, crtcID, nil
		}
		unix.Close(fd)
	}
	return -1, 0, 0, fmt.Errorf("no drm device with a connected display")
}

func (fb *framebuffer) addScanout(i int) error {
	create := drm.ModeCreateDumb{Width: uint32(fb.pw), Height: uint32(fb.ph), Bpp: 32}
	if err := drm.Ioctl(fb.fd, drm.CreateDumb, unsafe.Pointer(&create)); err != nil {
		return fmt.Errorf("create dumb buffer: %w", err)
	}
	fb.stride = int(create.Pitch)

	add := drm.ModeFBCmd{
		Width:  uint32(fb.pw),
		Height: uint32(fb.ph),
		Pitch:  create.Pitch,
		Bpp:    32,
		Depth:  24,
		Handle: create.Handle,
	}
	if err := drm.Ioctl(fb.fd, drm.AddFB, unsafe.Pointer(&add)); err != nil {
		return fmt.Errorf("add fb: %w", err)
	}
	fb.bufs[i].fbID = add.FBID

	m := drm.ModeMapDumb{Handle: create.Handle}
	if err := drm.Ioctl(fb.fd, drm.MapDumb, unsafe.Pointer(&m)); err != nil {
		return fmt.Errorf("map dumb buffer: %w", err)
	}
	mem, err := unix.Mmap(fb.fd, int64(m.Offset), int(create.Size),
		unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
	if err != nil {
		return fmt.Errorf("mmap scanout: %w", err)
	}
	fb.bufs[i].mem = mem
	return nil
}

func openFramebuffer() (*framebuffer, error) {
	fd, connID, crtcID, err := openCard()
	if err != nil {
		return nil, err
	}
	fb := &framebuffer{fd: fd, flips: make(chan struct{}, 1)}

	// Without master the modeset below is refused, and taking it is also what
	// stops the kernel console drawing into the display behind us.
	if err := drm.Ioctl(fd, drm.SetMaster, nil); err != nil {
		unix.Close(fd)
		return nil, fmt.Errorf("take drm master: %w", err)
	}

	mode, err := preferredMode(fd, connID)
	if err != nil {
		fb.close()
		return nil, err
	}
	fb.connID, fb.crtcID = connID, crtcID
	fb.pw, fb.ph = int(mode.Hdisplay), int(mode.Vdisplay)
	fb.w, fb.h = fb.ph, fb.pw

	for i := range fb.bufs {
		if err := fb.addScanout(i); err != nil {
			fb.close()
			return nil, err
		}
	}
	fb.back = make([]byte, fb.stride*fb.ph)
	fb.scratch = make([]byte, fb.stride)

	set := drm.ModeCrtc{
		SetConnectorsPtr: uint64(uintptr(unsafe.Pointer(&fb.connID))),
		CountConnectors:  1,
		CrtcID:           crtcID,
		FBID:             fb.bufs[0].fbID,
		ModeValid:        1,
		Mode:             mode,
	}
	if err := drm.Ioctl(fd, drm.SetCrtc, unsafe.Pointer(&set)); err != nil {
		fb.close()
		return nil, fmt.Errorf("set crtc: %w", err)
	}

	go func() {
		defer holdPanic()
		fb.readEvents()
	}()
	// Nothing has been flipped yet, so the first frame is owed its turn.
	fb.flips <- struct{}{}
	return fb, nil
}

// Flip completions arrive on the drm fd as a stream of length-prefixed events.
// Nothing else feeds fb.flips, so returning early here freezes the panel on its
// last frame while everything else goes on running.
func (fb *framebuffer) readEvents() {
	buf := make([]byte, 4096)
	for {
		n, err := unix.Read(fb.fd, buf)
		if fb.closing.Load() {
			return
		}
		if err == unix.EINTR || err == unix.EAGAIN {
			continue
		}
		if err != nil {
			panic(fmt.Sprintf("reading drm events: %v", err))
		}
		if n == 0 {
			panic("drm fd reported end of file")
		}
		for off := 0; off+8 <= n; {
			typ := binary.LittleEndian.Uint32(buf[off:])
			length := int(binary.LittleEndian.Uint32(buf[off+4:]))
			if length < 8 || off+length > n {
				panic(fmt.Sprintf("drm event at offset %d claims %d bytes of %d read",
					off, length, n))
			}
			if typ == drm.EventFlipComplete {
				select {
				case fb.flips <- struct{}{}:
				default:
				}
			}
			off += length
		}
	}
}

func (fb *framebuffer) close() {
	fb.closing.Store(true)
	for i := range fb.bufs {
		if fb.bufs[i].mem != nil {
			unix.Munmap(fb.bufs[i].mem)
		}
	}
	// Dropping master hands the display back to the kernel console, which
	// restores its own mode. The framebuffers and dumb buffers are reclaimed
	// when the last reference to the fd goes.
	drm.Ioctl(fb.fd, drm.DropMaster, nil)
	unix.Close(fb.fd)
}

type rgb struct {
	r, g, b uint8
}

func pixel(c rgb) uint32 { return drm.Pack(c.r, c.g, c.b) }

// A run of one repeated pixel, reused between calls: a whole panel row is the
// longest anything here needs, and every draw is on the one render goroutine.
func (fb *framebuffer) pixelRun(n int, c rgb) []byte {
	v := pixel(c)
	s := fb.scratch[:n]
	for i := 0; i+4 <= n; i += 4 {
		s[i+0] = byte(v)
		s[i+1] = byte(v >> 8)
		s[i+2] = byte(v >> 16)
		s[i+3] = byte(v >> 24)
	}
	return s
}

func (fb *framebuffer) fill(c rgb) {
	row := fb.pixelRun(fb.stride, c)
	for y := 0; y < fb.ph; y++ {
		copy(fb.back[y*fb.stride:], row)
	}
}

// One logical column is contiguous after the turn, so it fills a span at a time.
func (fb *framebuffer) rect(x0, y0, w, h int, c rgb) {
	x1, y1 := min(x0+w, fb.w), min(y0+h, fb.h)
	x0, y0 = max(x0, 0), max(y0, 0)
	if x0 >= x1 || y0 >= y1 {
		return
	}
	span := fb.pixelRun((y1-y0)*4, c)
	for x := x0; x < x1; x++ {
		copy(fb.back[fb.offset(x, y1-1):], span)
	}
}

// The rectangle the corner radius sweeps around. Distance to it is zero across
// the whole flat middle and grows only near a corner. Taking coverage from that
// rather than from a plain inside test keeps the curves smooth instead of
// stepped.
type sweep struct {
	ix0, iy0, ix1, iy1 float64
	r                  int
}

func (s sweep) pixel(fb *framebuffer, x, y int, c rgb) {
	fx, fy := float64(x), float64(y)
	dx := math.Max(math.Max(s.ix0-fx, fx-s.ix1), 0)
	dy := math.Max(math.Max(s.iy0-fy, fy-s.iy1), 0)
	cov := float64(s.r) - math.Sqrt(dx*dx+dy*dy) + 0.5
	if cov <= 0 {
		return
	}
	fb.blend(x, y, c, uint8(math.Min(cov, 1)*255))
}

// Partial coverage reaches no further than the corner blocks and the one line
// of pixels the curve runs tangent to along each flat edge. The sweep contains
// everything else outright, which fills as spans instead of a pixel at a time.
func (fb *framebuffer) roundRect(x0, y0, w, h, r int, c rgb) {
	// A radius past half the shorter side has no meaning and would put the
	// swept rectangle inside out, which matters while something is growing from
	// nothing.
	r = min(r, min(w, h)/2)
	s := sweep{float64(x0 + r), float64(y0 + r),
		float64(x0 + w - 1 - r), float64(y0 + h - 1 - r), r}

	// With no radius the sweep never reaches a whole pixel, so the sliver it
	// leaves is partly covered throughout rather than solid anywhere.
	if r == 0 {
		for y := y0; y < y0+h; y++ {
			for x := x0; x < x0+w; x++ {
				s.pixel(fb, x, y, c)
			}
		}
		return
	}

	for j := 0; j < r; j++ {
		for i := 0; i < r; i++ {
			s.pixel(fb, x0+i, y0+j, c)
			s.pixel(fb, x0+w-1-i, y0+j, c)
			s.pixel(fb, x0+i, y0+h-1-j, c)
			s.pixel(fb, x0+w-1-i, y0+h-1-j, c)
		}
	}
	for x := x0 + r; x < x0+w-r; x++ {
		s.pixel(fb, x, y0, c)
		s.pixel(fb, x, y0+h-1, c)
	}
	for y := y0 + r; y < y0+h-r; y++ {
		s.pixel(fb, x0, y, c)
		s.pixel(fb, x0+w-1, y, c)
	}

	fb.rect(x0+r, y0+1, w-2*r, h-2, c)
	fb.rect(x0+1, y0+r, r-1, h-2*r, c)
	fb.rect(x0+w-r, y0+r, r-1, h-2*r, c)
}

// Blends src over the existing pixel, with cov as 0-255 coverage.
func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) {
	if x < 0 || y < 0 || x >= fb.w || y >= fb.h || cov == 0 {
		return
	}
	o := fb.offset(x, y)
	if cov == 255 {
		v := pixel(c)
		fb.back[o+0] = byte(v)
		fb.back[o+1] = byte(v >> 8)
		fb.back[o+2] = byte(v >> 16)
		fb.back[o+3] = byte(v >> 24)
		return
	}
	a := uint32(cov)
	old := uint32(fb.back[o+0]) | uint32(fb.back[o+1])<<8 |
		uint32(fb.back[o+2])<<16 | uint32(fb.back[o+3])<<24
	orr, og, ob := drm.Unpack(old)
	mix := rgb{
		r: uint8((uint32(c.r)*a + uint32(orr)*(255-a)) / 255),
		g: uint8((uint32(c.g)*a + uint32(og)*(255-a)) / 255),
		b: uint8((uint32(c.b)*a + uint32(ob)*(255-a)) / 255),
	}
	v := pixel(mix)
	fb.back[o+0] = byte(v)
	fb.back[o+1] = byte(v >> 8)
	fb.back[o+2] = byte(v >> 16)
	fb.back[o+3] = byte(v >> 24)
}

// The copy cannot tear because nothing is displaying that buffer, and the swap
// cannot tear because the hardware does it between frames.
func (fb *framebuffer) flush() error {
	next := (fb.front + 1) % scanoutBuffers
	copy(fb.bufs[next].mem, fb.back)

	flip := drm.ModeCrtcPageFlip{
		CrtcID: fb.crtcID,
		FBID:   fb.bufs[next].fbID,
		Flags:  drm.PageFlipEvent,
	}
	if err := drm.Ioctl(fb.fd, drm.PageFlip, unsafe.Pointer(&flip)); err != nil {
		return fmt.Errorf("page flip: %w", err)
	}
	fb.front = next
	return nil
}
