package main

import (
	"bytes"
	"fmt"
	"image"
	"image/color"
	"path/filepath"
	"unsafe"

	"golang.org/x/sys/unix"

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

// cabletest drives the panel through drm, so /dev/fb0 holds the kernel console
// and is not worth reading. The displayed buffer is reachable from out here
// instead: GETCRTC names it, and root is allowed a handle to it without being
// drm master, so it can be read without the owning process cooperating.
const drmCardGlob = "/dev/dri/card*"

type grabber struct {
	fd    int
	crtc  uint32
	index int
}

func openGrabber() (*grabber, error) {
	paths, err := filepath.Glob(drmCardGlob)
	if err != nil {
		return nil, err
	}
	for _, p := range paths {
		fd, err := unix.Open(p, unix.O_RDWR|unix.O_CLOEXEC, 0)
		if err != nil {
			continue
		}
		crtc, index, err := activeCrtc(fd)
		if err == nil {
			return &grabber{fd: fd, crtc: crtc, index: index}, nil
		}
		unix.Close(fd)
	}
	return nil, fmt.Errorf("no drm crtc is scanning out a framebuffer")
}

func activeCrtc(fd int) (uint32, int, error) {
	var res drm.ModeCardRes
	if err := drm.Ioctl(fd, drm.GetResources, unsafe.Pointer(&res)); err != nil {
		return 0, 0, err
	}
	if res.CountCRTCs == 0 {
		return 0, 0, fmt.Errorf("card has no crtcs")
	}
	crtcs := make([]uint32, res.CountCRTCs)
	res.CountFBs, res.CountConns, res.CountEncs = 0, 0, 0
	res.FBIDPtr, res.ConnIDPtr, res.EncIDPtr = 0, 0, 0
	res.CrtcIDPtr = uint64(uintptr(unsafe.Pointer(&crtcs[0])))
	if err := drm.Ioctl(fd, drm.GetResources, unsafe.Pointer(&res)); err != nil {
		return 0, 0, err
	}
	for i, id := range crtcs {
		c := drm.ModeCrtc{CrtcID: id}
		if err := drm.Ioctl(fd, drm.GetCrtc, unsafe.Pointer(&c)); err != nil {
			continue
		}
		if c.FBID != 0 && c.ModeValid != 0 {
			return id, i, nil
		}
	}
	return 0, 0, fmt.Errorf("no crtc is scanning out a framebuffer")
}

func (g *grabber) close() { unix.Close(g.fd) }

// Reading a buffer takes about as long as a frame, so where the read starts in
// the flip cycle decides whether it stays ahead of the writer. Woken at a
// blank, the buffer GETCRTC then names has just gone on screen.
func (g *grabber) waitVblank() error {
	v := drm.WaitVblank{
		Type:     drm.VblankRelative | uint32(g.index<<drm.VblankHighCrtcShift)&drm.VblankHighCrtcMask,
		Sequence: 1,
	}
	for {
		err := drm.Ioctl(g.fd, drm.WaitVblankIO, unsafe.Pointer(&v))
		if err == unix.EINTR {
			continue
		}
		return err
	}
}

func (g *grabber) mapFront() (mem []byte, pitch, pw, ph int, err error) {
	c := drm.ModeCrtc{CrtcID: g.crtc}
	if err := drm.Ioctl(g.fd, drm.GetCrtc, unsafe.Pointer(&c)); err != nil {
		return nil, 0, 0, 0, fmt.Errorf("get crtc: %w", err)
	}
	if c.FBID == 0 {
		return nil, 0, 0, 0, fmt.Errorf("crtc %d is not scanning out anything", g.crtc)
	}

	// Only a drm master or root is given a handle to someone else's framebuffer.
	fb := drm.ModeFBCmd{FBID: c.FBID}
	if err := drm.Ioctl(g.fd, drm.GetFB, unsafe.Pointer(&fb)); err != nil {
		return nil, 0, 0, 0, fmt.Errorf("get fb %d: %w", c.FBID, err)
	}
	if fb.Handle == 0 {
		return nil, 0, 0, 0, fmt.Errorf("kernel gave no handle for fb %d", c.FBID)
	}
	if fb.Bpp != 32 {
		return nil, 0, 0, 0, fmt.Errorf("expected 32bpp, got %d", fb.Bpp)
	}

	m := drm.ModeMapDumb{Handle: fb.Handle}
	if err := drm.Ioctl(g.fd, drm.MapDumb, unsafe.Pointer(&m)); err != nil {
		return nil, 0, 0, 0, fmt.Errorf("map fb %d: %w", c.FBID, err)
	}
	pitch, pw, ph = int(fb.Pitch), int(fb.Width), int(fb.Height)
	mem, err = unix.Mmap(g.fd, int64(m.Offset), pitch*ph,
		unix.PROT_READ, unix.MAP_SHARED)
	if err != nil {
		return nil, 0, 0, 0, fmt.Errorf("mmap fb %d: %w", c.FBID, err)
	}
	return mem, pitch, pw, ph, nil
}

// Scanout memory is uncached, so a read costs a good fraction of a frame and
// could be overtaken by the next redraw. Read twice and accept only if the two
// agree: the writer leaves a buffer alone for several frames after displaying
// it, which is long enough for both. Disagreement means that reasoning is
// wrong, which is worth hearing about rather than retrying past.
func (g *grabber) frame() (*image.NRGBA, error) {
	if err := g.waitVblank(); err != nil {
		return nil, fmt.Errorf("wait for vblank: %w", err)
	}
	mem, pitch, pw, ph, err := g.mapFront()
	if err != nil {
		return nil, err
	}
	a := make([]byte, len(mem))
	copy(a, mem)
	b := make([]byte, len(mem))
	copy(b, mem)
	unix.Munmap(mem)

	if !bytes.Equal(a, b) {
		return nil, fmt.Errorf("scanout buffer was redrawn while being read")
	}
	return decode(a, pitch, pw, ph), nil
}

// Undoes the quarter turn drm.Offset applies, to get back what a person
// standing in front of the panel sees.
func decode(buf []byte, pitch, pw, ph int) *image.NRGBA {
	img := image.NewNRGBA(image.Rect(0, 0, ph, pw))
	for x := 0; x < ph; x++ {
		for y := 0; y < pw; y++ {
			o := drm.Offset(x, y, pitch, pw)
			v := uint32(buf[o]) | uint32(buf[o+1])<<8 |
				uint32(buf[o+2])<<16 | uint32(buf[o+3])<<24
			r, g, b := drm.Unpack(v)
			img.SetNRGBA(x, y, color.NRGBA{R: r, G: g, B: b, A: 255})
		}
	}
	return img
}
