package main

import (
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"unsafe"

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

const (
	evSyn     = 0x00
	evKey     = 0x01
	evAbs     = 0x03
	synReport = 0x00
	btnTouch  = 0x14a
	absX      = 0x00
	absY      = 0x01

	inputPropDirect  = 0x01
	sizeofInputEvent = 24
)

func evIoc(nr, size uintptr) uintptr {
	const iocRead = 2
	return iocRead<<30 | size<<16 | 'E'<<8 | nr
}

// Current finger position and whether it is down. Held as state rather than
// delivered as events, so a dropped release can never leave a hold armed.
type touchState struct {
	mu   sync.Mutex
	x, y int
	down bool
}

func (t *touchState) set(x, y int, down bool) {
	t.mu.Lock()
	t.x, t.y, t.down = x, y, down
	t.mu.Unlock()
}

func (t *touchState) get() (x, y int, down bool) {
	t.mu.Lock()
	defer t.mu.Unlock()
	return t.x, t.y, t.down
}

// Picks the direct-input device that reports absolute X and Y, which is the
// touchscreen; a mouse is relative and a keyboard has no absolute axes.
func findTouchscreen() (*os.File, error) {
	paths, err := filepath.Glob("/dev/input/event*")
	if err != nil {
		return nil, err
	}
	for _, p := range paths {
		f, err := os.OpenFile(p, os.O_RDONLY, 0)
		if err != nil {
			continue
		}
		var props, absBits uint64
		if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
			evIoc(0x09, 8), uintptr(unsafe.Pointer(&props))); errno != 0 {
			f.Close()
			continue
		}
		if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
			evIoc(0x20+evAbs, 8), uintptr(unsafe.Pointer(&absBits))); errno != 0 {
			f.Close()
			continue
		}
		if props&(1<<inputPropDirect) != 0 &&
			absBits&(1<<absX) != 0 && absBits&(1<<absY) != 0 {
			return f, nil
		}
		f.Close()
	}
	return nil, fmt.Errorf("no touchscreen found among /dev/input/event*")
}

func axisRange(f *os.File, axis uintptr) (min, max int32, err error) {
	var info [6]int32
	if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
		evIoc(0x40+axis, 24), uintptr(unsafe.Pointer(&info[0]))); errno != 0 {
		return 0, 0, errno
	}
	return info[1], info[2], nil
}

// Tracks the finger position, scaled to the given screen size.
func watchTouch(w, h int) (*touchState, error) {
	f, err := findTouchscreen()
	if err != nil {
		return nil, err
	}
	minX, maxX, err := axisRange(f, absX)
	if err != nil {
		f.Close()
		return nil, fmt.Errorf("x range: %w", err)
	}
	minY, maxY, err := axisRange(f, absY)
	if err != nil {
		f.Close()
		return nil, fmt.Errorf("y range: %w", err)
	}
	if maxX <= minX || maxY <= minY {
		f.Close()
		return nil, fmt.Errorf("touchscreen axes are degenerate: x %d..%d y %d..%d",
			minX, maxX, minY, maxY)
	}

	state := &touchState{}
	go func() {
		defer holdPanic()
		defer f.Close()
		buf := make([]byte, sizeofInputEvent*32)
		var rawX, rawY int32
		var down bool
		for {
			n, err := f.Read(buf)
			// Nothing else feeds this state, so returning here would freeze the
			// last touch in place and leave the reset button dead while the panel
			// goes on looking alive.
			if err != nil {
				panic(fmt.Sprintf("reading touchscreen events: %v", err))
			}
			for o := 0; o+sizeofInputEvent <= n; o += sizeofInputEvent {
				typ := *(*uint16)(unsafe.Pointer(&buf[o+16]))
				code := *(*uint16)(unsafe.Pointer(&buf[o+18]))
				val := *(*int32)(unsafe.Pointer(&buf[o+20]))
				switch typ {
				case evAbs:
					switch code {
					case absX:
						rawX = val
					case absY:
						rawY = val
					}
				case evKey:
					if code == btnTouch {
						down = val != 0
					}
				case evSyn:
					if code != synReport {
						break
					}
					// Scaled onto [0, w-1], so full deflection is the last pixel
					// rather than one past it.
					state.set(
						int(int64(rawX-minX)*int64(w-1)/int64(maxX-minX)),
						int(int64(rawY-minY)*int64(h-1)/int64(maxY-minY)),
						down)
				}
			}
		}
	}()
	return state, nil
}

// Delivers a value every time space is pressed. Puts the terminal in
// non-canonical mode so the keypress arrives without waiting for a newline,
// and returns a function that restores the original settings.
func watchSpace() (<-chan struct{}, func()) {
	ch := make(chan struct{}, 1)
	restore := func() {}
	fd := int(os.Stdin.Fd())

	if orig, err := unix.IoctlGetTermios(fd, unix.TCGETS); err == nil {
		saved := *orig
		raw := *orig
		raw.Lflag &^= unix.ICANON | unix.ECHO
		raw.Cc[unix.VMIN] = 1
		raw.Cc[unix.VTIME] = 0
		if unix.IoctlSetTermios(fd, unix.TCSETS, &raw) == nil {
			restore = func() { unix.IoctlSetTermios(fd, unix.TCSETS, &saved) }
		}
	}

	go func() {
		buf := make([]byte, 64)
		for {
			n, err := os.Stdin.Read(buf)
			if err != nil {
				return
			}
			for _, b := range buf[:n] {
				if b != ' ' {
					continue
				}
				select {
				case ch <- struct{}{}:
				default:
				}
			}
		}
	}()

	return ch, restore
}
