package device

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"image"
	"image/jpeg"
	_ "image/png"
	"runtime"
	"strings"
	"sync"

	"github.com/sstallion/go-hid"
	"golang.org/x/image/draw"
)

const VendorID = 0x0fd9

// ModelInfo describes hardware-specific constants for a Stream Deck model.
type ModelInfo struct {
	KeyCount    int
	Cols        int
	Rows        int
	ImageWidth  int
	ImageHeight int
	// FlipX/FlipY: the XL renders images mirrored; we pre-flip before sending.
	FlipX bool
	FlipY bool
}

// models maps USB product IDs to their hardware specs.
var models = map[uint16]ModelInfo{
	0x00ba: {KeyCount: 32, Cols: 8, Rows: 4, ImageWidth: 96, ImageHeight: 96, FlipX: true, FlipY: true}, // XL v2
	0x006c: {KeyCount: 32, Cols: 8, Rows: 4, ImageWidth: 96, ImageHeight: 96, FlipX: true, FlipY: true}, // XL v1
	0x006d: {KeyCount: 15, Cols: 5, Rows: 3, ImageWidth: 72, ImageHeight: 72, FlipX: true, FlipY: true}, // MK.2
}

const (
	reportSize       = 1024
	imageHeaderSize  = 8
	imagePayloadSize = reportSize - imageHeaderSize
	readReportSize   = 512
)

// StreamDeck represents an open Stream Deck device.
type StreamDeck struct {
	mu    sync.Mutex
	dev   *hid.Device
	model ModelInfo
}

// Open finds and opens the first Stream Deck with the given product ID.
func Open(vendorID, productID uint16) (*StreamDeck, error) {
	if err := hid.Init(); err != nil {
		return nil, fmt.Errorf("hid init: %w", err)
	}

	m, ok := models[productID]
	if !ok {
		return nil, fmt.Errorf("unsupported product ID: 0x%04x (add it to internal/device/streamdeck.go)", productID)
	}

	dev, err := hid.OpenFirst(vendorID, productID)
	if err != nil {
		var hint string
		switch runtime.GOOS {
		case "linux":
			hint = "try: sudo chmod a+rw /dev/hidraw*"
		case "darwin":
			hint = "try: brew install hidapi; check System Settings → Privacy & Security → Input Monitoring"
		default:
			hint = "check device permissions"
		}
		return nil, fmt.Errorf("open device 0x%04x:0x%04x: %w (%s)", vendorID, productID, err, hint)
	}

	return &StreamDeck{dev: dev, model: m}, nil
}

// Close releases the device.
func (sd *StreamDeck) Close() error {
	_ = hid.Exit()
	return sd.dev.Close()
}

// KeyCount returns the number of keys on this device.
func (sd *StreamDeck) KeyCount() int { return sd.model.KeyCount }

// Reset clears all key images and returns the device to its default state.
func (sd *StreamDeck) Reset() error {
	report := make([]byte, 32)
	report[0] = 0x03
	report[1] = 0x02
	_, err := sd.dev.SendFeatureReport(report)
	return err
}

// SetBrightness sets the display brightness (0–100).
func (sd *StreamDeck) SetBrightness(pct int) error {
	if pct < 0 {
		pct = 0
	}
	if pct > 100 {
		pct = 100
	}
	report := make([]byte, 32)
	report[0] = 0x03
	report[1] = 0x08
	report[2] = byte(pct)
	_, err := sd.dev.SendFeatureReport(report)
	return err
}

// EncodeFrame scales img to key size and returns the JPEG bytes ready to send.
// Use this to pre-encode animation frames once rather than on every tick.
func (sd *StreamDeck) EncodeFrame(img image.Image) ([]byte, error) {
	dst := image.NewRGBA(image.Rect(0, 0, sd.model.ImageWidth, sd.model.ImageHeight))
	draw.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil)
	if sd.model.FlipX || sd.model.FlipY {
		dst = flipImage(dst, sd.model.FlipX, sd.model.FlipY)
	}
	var buf bytes.Buffer
	if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: 95}); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

// SetKeyFrame sends pre-encoded JPEG bytes (from EncodeFrame) to a key.
func (sd *StreamDeck) SetKeyFrame(keyIndex int, jpegData []byte) error {
	return sd.sendKeyImageData(keyIndex, jpegData)
}

// SetKeyImage scales img to the key size and sends it to the given key (0-indexed).
func (sd *StreamDeck) SetKeyImage(keyIndex int, img image.Image) error {
	// Scale to key pixel dimensions.
	dst := image.NewRGBA(image.Rect(0, 0, sd.model.ImageWidth, sd.model.ImageHeight))
	draw.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil)

	// The XL renders images flipped; pre-flip so they appear correct.
	if sd.model.FlipX || sd.model.FlipY {
		dst = flipImage(dst, sd.model.FlipX, sd.model.FlipY)
	}

	var buf bytes.Buffer
	if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: 95}); err != nil {
		return fmt.Errorf("jpeg encode: %w", err)
	}

	return sd.sendKeyImageData(keyIndex, buf.Bytes())
}

// ClearKey fills the given key with solid black.
func (sd *StreamDeck) ClearKey(keyIndex int) error {
	blank := image.NewRGBA(image.Rect(0, 0, sd.model.ImageWidth, sd.model.ImageHeight))
	return sd.SetKeyImage(keyIndex, blank)
}

// ReadButtons waits up to 250 ms for a button state report.
// Returns (nil, nil) on timeout — callers should check context and retry.
func (sd *StreamDeck) ReadButtons() ([]bool, error) {
	data := make([]byte, readReportSize)
	n, err := sd.dev.ReadWithTimeout(data, 250)
	if err != nil {
		// Linux hidraw returns errors (not (0,nil)) for non-fatal conditions:
		// timeout waiting for data, or EINTR (signal interrupted).
		// macOS IOHIDManager usually returns (0, nil) on timeout, but may also
		// return an error with a different message — catch both spellings.
		msg := strings.ToLower(err.Error())
		if strings.Contains(msg, "timeout") ||
			strings.Contains(msg, "timed out") ||
			strings.Contains(msg, "interrupted") {
			return nil, nil
		}
		return nil, err
	}
	if n == 0 {
		return nil, nil // timeout, no data
	}

	// Input report layout (XL v2): [report_id, 0x00, 0x00, 0x00, key0, key1, ...]
	const offset = 4
	if n < offset+sd.model.KeyCount {
		return nil, fmt.Errorf("short read: got %d bytes, expected at least %d", n, offset+sd.model.KeyCount)
	}

	buttons := make([]bool, sd.model.KeyCount)
	for i := 0; i < sd.model.KeyCount; i++ {
		buttons[i] = data[offset+i] == 0x01
	}
	return buttons, nil
}

// sendKeyImageData sends raw JPEG bytes to a key, split across 1024-byte HID reports.
func (sd *StreamDeck) sendKeyImageData(keyIndex int, data []byte) error {
	if keyIndex < 0 || keyIndex >= sd.model.KeyCount {
		return fmt.Errorf("key index %d out of range (device has %d keys, 0–%d)",
			keyIndex, sd.model.KeyCount, sd.model.KeyCount-1)
	}
	sd.mu.Lock()
	defer sd.mu.Unlock()
	pageIndex := 0
	for len(data) > 0 {
		chunk := data
		if len(chunk) > imagePayloadSize {
			chunk = chunk[:imagePayloadSize]
		}

		isLast := byte(0)
		if len(data) == len(chunk) {
			isLast = 1
		}

		report := make([]byte, reportSize)
		report[0] = 0x02
		report[1] = 0x07
		report[2] = byte(keyIndex)
		report[3] = isLast
		binary.LittleEndian.PutUint16(report[4:6], uint16(len(chunk)))
		binary.LittleEndian.PutUint16(report[6:8], uint16(pageIndex))
		copy(report[8:], chunk)

		if _, err := sd.dev.Write(report); err != nil {
			return fmt.Errorf("write report page %d: %w", pageIndex, err)
		}

		data = data[len(chunk):]
		pageIndex++
	}
	return nil
}

func flipImage(src *image.RGBA, flipX, flipY bool) *image.RGBA {
	b := src.Bounds()
	w, h := b.Max.X, b.Max.Y
	dst := image.NewRGBA(b)
	for y := 0; y < h; y++ {
		for x := 0; x < w; x++ {
			dx, dy := x, y
			if flipX {
				dx = w - 1 - x
			}
			if flipY {
				dy = h - 1 - y
			}
			dst.SetRGBA(dx, dy, src.RGBAAt(x, y))
		}
	}
	return dst
}
