//go:build darwin

package darwin

/*
#cgo CFLAGS: -fobjc-arc
#cgo LDFLAGS: -framework Cocoa -framework ApplicationServices -framework Carbon
#include "darwin.h"
#include <stdlib.h>
*/
import "C"

import (
	"errors"
	"log"
	"os"
	"runtime"
	"sync"
	"time"
	"unsafe"

	"chirami/internal/platform"
)

var (
	activeMu      sync.Mutex
	activeBackend *backend
)

func init() {
	runtime.LockOSThread()
}

//export chr_onSelectionTrigger
func chr_onSelectionTrigger(kind C.int) {
	activeMu.Lock()
	b := activeBackend
	activeMu.Unlock()
	if b != nil {
		b.triggerSelection(int(kind))
	}
}

//export chr_onEvent
func chr_onEvent(kind C.int, x, y C.double) {
	activeMu.Lock()
	b := activeBackend
	activeMu.Unlock()
	if b != nil {
		b.sendEvent(platform.EventKind(kind), float64(x), float64(y))
	}
}

type backend struct {
	selections       chan string
	shots            chan []byte
	shotsOnce        sync.Once
	selectionTrigger chan int // 0 = poll/click, 1 = drag/double-click mouseUp, 2 = key
	events           chan platform.Event
	stopCh           chan struct{}
	stopOnce         sync.Once

	mu        sync.Mutex
	lastScale float64
}

func newBackend() *backend {
	return &backend{
		selections:       make(chan string, 16),
		shots:            make(chan []byte, 1),
		selectionTrigger: make(chan int, 4),
		events:           make(chan platform.Event, 32),
		stopCh:           make(chan struct{}),
		lastScale:        2.0,
	}
}

func (b *backend) Selections() <-chan string {
	return b.selections
}

// Screenshots starts watching for the system screenshot shortcuts on first
// use, so only an app that wants them asks for Input Monitoring.
func (b *backend) Screenshots() <-chan []byte {
	b.shotsOnce.Do(func() { go b.watchShortcuts() })
	return b.shots
}

// watchShortcuts installs the shortcut listener, retrying while the user has
// yet to grant Input Monitoring.
func (b *backend) watchShortcuts() {
	for i := 0; !bool(C.chr_startScreenshotWatch()); i++ {
		if i == 0 {
			log.Println("screenshots: grant Input Monitoring to chirami (System Settings > Privacy & Security > Input Monitoring) so it can tell when you take a screenshot")
		}
		select {
		case <-b.stopCh:
			return
		case <-time.After(3 * time.Second):
		}
	}
}

//export chr_onScreenshot
func chr_onScreenshot(bytes unsafe.Pointer, n C.int) {
	activeMu.Lock()
	b := activeBackend
	activeMu.Unlock()
	if b == nil {
		return
	}
	png := C.GoBytes(bytes, n)
	select {
	case b.shots <- png:
	default: // the app is still busy with the last one
	}
}

func (b *backend) Events() <-chan platform.Event {
	return b.events
}

func (b *backend) Locate() (platform.Anchor, error) {
	a := C.chr_getAnchor()
	anchor := platform.Anchor{
		X: float64(a.x),
		Y: float64(a.y),
		Screen: platform.Rect{
			X: float64(a.screenX),
			Y: float64(a.screenY),
			W: float64(a.screenW),
			H: float64(a.screenH),
		},
		Scale: float64(a.scale),
	}
	b.mu.Lock()
	b.lastScale = anchor.Scale
	b.mu.Unlock()
	return anchor, nil
}

func (b *backend) Pointer() (x, y float64, ok bool) {
	var px, py C.double
	C.chr_getPointer(&px, &py)
	return float64(px), float64(py), true
}

func (b *backend) Show(p platform.Popup) error {
	if p.Image == nil || p.Image.Rect.Dx() == 0 || p.Image.Rect.Dy() == 0 {
		return errors.New("darwin: cannot show empty popup image")
	}

	b.mu.Lock()
	scale := b.lastScale
	b.mu.Unlock()
	if scale <= 0 {
		scale = 2.0
	}

	minX := p.Image.Rect.Min.X
	minY := p.Image.Rect.Min.Y
	w := p.Image.Rect.Dx()
	h := p.Image.Rect.Dy()
	stride := p.Image.Stride

	offset := p.Image.PixOffset(minX, minY)
	rowLen := w * 4
	tightBytes := make([]byte, rowLen*h)
	for row := 0; row < h; row++ {
		srcStart := offset + row*stride
		srcEnd := srcStart + rowLen
		dstStart := row * rowLen
		copy(tightBytes[dstStart:dstStart+rowLen], p.Image.Pix[srcStart:srcEnd])
	}

	C.chr_showPopup(
		unsafe.Pointer(&tightBytes[0]),
		C.int(rowLen),
		C.int(w),
		C.int(h),
		C.double(p.X),
		C.double(p.Y),
		C.double(p.Hit.X),
		C.double(p.Hit.Y),
		C.double(p.Hit.W),
		C.double(p.Hit.H),
		C.double(scale),
	)
	return nil
}

func (b *backend) Hide() {
	C.chr_hidePopup()
}

func (b *backend) triggerSelection(kind int) {
	select {
	case b.selectionTrigger <- kind:
	default:
	}
}

func (b *backend) sendEvent(kind platform.EventKind, x, y float64) {
	ev := platform.Event{Kind: kind, X: x, Y: y}
	select {
	case b.events <- ev:
	default:
	}
}

func (b *backend) stop() {
	b.stopOnce.Do(func() {
		close(b.stopCh)
	})
}

func (b *backend) selectionLoop() {
	trusted := bool(C.chr_checkAccessibility(C.bool(true)))
	if !trusted {
		log.Println("Accessibility permission required: please grant Accessibility to this binary (System Settings > Privacy & Security > Accessibility)")
		go func() {
			retryTicker := time.NewTicker(3 * time.Second)
			defer retryTicker.Stop()
			for {
				select {
				case <-b.stopCh:
					return
				case <-retryTicker.C:
					if bool(C.chr_checkAccessibility(C.bool(false))) {
						log.Println("Accessibility permission granted")
						b.triggerSelection(0)
						return
					}
				}
			}
		}()
	}

	pollTicker := time.NewTicker(400 * time.Millisecond)
	defer pollTicker.Stop()

	allowFallbackEnv := os.Getenv("CHIRAMI_MAC_COPY_FALLBACK") == "1"
	lastText := ""

	for {
		select {
		case <-b.stopCh:
			return
		case <-pollTicker.C:
			b.triggerSelection(0) // poll trigger
		case kind := <-b.selectionTrigger:
			// Copy fallback is only permitted on real mouse-up following a drag or double click (kind == 1)
			allowFallback := allowFallbackEnv && (kind == 1)
			cstr := C.chr_getSelectedText(C.bool(allowFallback))
			var text string
			if cstr != nil {
				text = C.GoString(cstr)
				C.free(unsafe.Pointer(cstr))
			}

			if text != lastText {
				if text != "" || lastText != "" {
					lastText = text
					select {
					case b.selections <- text:
					default:
					}
				}
			}
		}
	}
}

// Run executes the application event loop on the main thread and runs app in a goroutine.
func Run(app func(platform.Backend) error) error {
	runtime.LockOSThread()

	C.chr_init()
	C.chr_installMonitors()

	b := newBackend()
	activeMu.Lock()
	activeBackend = b
	activeMu.Unlock()

	go b.selectionLoop()

	var (
		appErr  error
		appDone = make(chan struct{})
	)

	go func() {
		defer close(appDone)
		appErr = app(b)
		b.stop()
		C.chr_stop()
	}()

	C.chr_run()

	// Prevent hang if app goroutine is stuck
	select {
	case <-appDone:
	case <-time.After(2 * time.Second):
		log.Println("darwin: app did not finish within 2s after NSApp stopped")
	}

	C.chr_uninstallMonitors()

	activeMu.Lock()
	activeBackend = nil
	activeMu.Unlock()

	return appErr
}

// SaveSnapshot saves a PNG snapshot of the currently displayed popup.
func SaveSnapshot(path string) bool {
	cpath := C.CString(path)
	defer C.free(unsafe.Pointer(cpath))
	return bool(C.chr_savePopupSnapshot(cpath))
}
