package brightness

import (
	"fmt"
	"math"
	"os"
	"path/filepath"
	"strings"
	"unsafe"

	"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
	"github.com/AvengeMedia/dankgo/syncmap"
	"golang.org/x/sys/unix"
)

// sys/sys/backlight.h: brightness is a 0-100 percent;
// BACKLIGHTGETSTATUS/BACKLIGHTUPDATESTATUS = _IOWR('G', 0/1, struct
// backlight_props{uint32 brightness; uint32 nlevels; uint32 levels[100]}).
const (
	backlightDevDir       = "/dev/backlight"
	backlightGetStatus    = 0xc1984700
	backlightUpdateStatus = 0xc1984701
)

type backlightProps struct {
	brightness uint32
	nlevels    uint32
	levels     [100]uint32
}

type BacklightBackend struct {
	devices syncmap.Map[string, string]
}

func NewBacklightBackend() (*BacklightBackend, error) {
	b := &BacklightBackend{}
	if err := b.scanDevices(); err != nil {
		return nil, err
	}
	return b, nil
}

func isGenericBacklightName(name string) bool {
	rest, ok := strings.CutPrefix(name, "backlight")
	if !ok || rest == "" {
		return false
	}
	for _, r := range rest {
		if r < '0' || r > '9' {
			return false
		}
	}
	return true
}

func (b *BacklightBackend) scanDevices() error {
	entries, err := os.ReadDir(backlightDevDir)
	if err != nil {
		return fmt.Errorf("read %s: %w", backlightDevDir, err)
	}

	// backlight_register (sys/dev/backlight/backlight.c) publishes each unit
	// as backlight/backlightN plus a driver-named alias for the same cdev;
	// dedupe on the device number and keep the descriptive alias.
	names := make(map[uint64]string)
	for _, entry := range entries {
		var st unix.Stat_t
		if err := unix.Stat(filepath.Join(backlightDevDir, entry.Name()), &st); err != nil {
			continue
		}
		rdev := uint64(st.Rdev)
		current, exists := names[rdev]
		if exists && !isGenericBacklightName(current) {
			continue
		}
		names[rdev] = entry.Name()
	}

	for _, name := range names {
		b.devices.Store("backlight:"+name, filepath.Join(backlightDevDir, name))
	}
	return nil
}

func backlightIoctl(fd int, req uint, props *backlightProps) error {
	_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(unsafe.Pointer(props)))
	if errno != 0 {
		return errno
	}
	return nil
}

func readBrightness(path string) (int, error) {
	fd, err := unix.Open(path, unix.O_RDWR, 0)
	if err != nil {
		return 0, err
	}
	defer unix.Close(fd)

	var props backlightProps
	if err := backlightIoctl(fd, backlightGetStatus, &props); err != nil {
		return 0, err
	}
	return int(props.brightness), nil
}

func (b *BacklightBackend) Rescan() error {
	return b.scanDevices()
}

func (b *BacklightBackend) GetDevices() ([]Device, error) {
	devices := make([]Device, 0)

	b.devices.Range(func(id, path string) bool {
		brightness, err := readBrightness(path)
		if err != nil {
			log.Debugf("failed to read brightness for %s: %v", id, err)
			return true
		}

		devices = append(devices, Device{
			Class:          ClassBacklight,
			ID:             id,
			Name:           strings.TrimPrefix(id, "backlight:"),
			Current:        brightness,
			Max:            100,
			CurrentPercent: brightness,
			Backend:        "backlight",
		})
		return true
	})

	return devices, nil
}

func (b *BacklightBackend) SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error {
	if percent < 0 || percent > 100 {
		return fmt.Errorf("percent out of range: %d", percent)
	}

	path, ok := b.devices.Load(id)
	if !ok {
		return fmt.Errorf("device not found: %s", id)
	}

	value := percent
	switch {
	case percent == 0:
		value = 1
	case exponential:
		value = 1 + int(math.Round(math.Pow(float64(percent-1)/99.0, exponent)*99.0))
	}

	fd, err := unix.Open(path, unix.O_RDWR, 0)
	if err != nil {
		return fmt.Errorf("open %s: %w", path, err)
	}
	defer unix.Close(fd)

	props := backlightProps{brightness: uint32(value)}
	if err := backlightIoctl(fd, backlightUpdateStatus, &props); err != nil {
		return fmt.Errorf("set brightness: %w", err)
	}

	log.Debugf("set %s to %d%% (hw %d) via backlight(4)", id, percent, value)
	return nil
}
