//go:build darwin

// clipprobe 是一个只读诊断工具：监听本机剪贴板变化，打印每次变化时
// NSPasteboard 上挂的全部类型（UTI）。它不读取也不上传任何剪贴板内容，
// 只看「类型」，用于在真机上经验性地确定各应用（尤其密码管理器）复制时
// 打哪些 UTI，从而让 D4 的敏感内容过滤基于实测数据而非猜测。
//
// 用法：go run ./cmd/clipprobe（或 go build 后运行二进制），然后依次从
// 普通文本编辑器 / 1Password / Bitwarden / 浏览器密码自动填充 等复制，观察
// 各自打印的 UTI。含 org.nspasteboard.ConcealedType / TransientType /
// AutoGeneratedType 或厂商私有 UTI（如 com.agilebits.onepassword）者，即为
// D4 应跳过上传的敏感内容。
//
// 实现用 ebitengine/purego + objc 运行时直调 AppKit，无 cgo、可随 Wails
// 交叉编译；调用模式照搬 golang.design/x/clipboard 的 darwin 实现。
package main

import (
	"fmt"
	"runtime"
	"time"
	"unsafe"

	"github.com/ebitengine/purego"
	"github.com/ebitengine/purego/objc"
)

func main() {
	// objc 调用必须钉在同一 OS 线程上（autorelease pool 的语义要求）。
	runtime.LockOSThread()

	// 加载 AppKit / Foundation，使 NSPasteboard / NSAutoreleasePool 等类可解析。
	for _, fw := range []string{
		"/System/Library/Frameworks/AppKit.framework/AppKit",
		"/System/Library/Frameworks/Foundation.framework/Foundation",
	} {
		if _, err := purego.Dlopen(fw, purego.RTLD_NOW|purego.RTLD_GLOBAL); err != nil {
			panic(fmt.Sprintf("clipprobe: 无法加载 %s: %v", fw, err))
		}
	}

	pbClass := objc.GetClass("NSPasteboard")
	poolClass := objc.GetClass("NSAutoreleasePool")
	var (
		selGeneral     = objc.RegisterName("generalPasteboard")
		selChangeCount = objc.RegisterName("changeCount")
		selTypes       = objc.RegisterName("types")
		selCount       = objc.RegisterName("count")
		selObjectAt    = objc.RegisterName("objectAtIndex:")
		selUTF8        = objc.RegisterName("UTF8String")
		selAlloc       = objc.RegisterName("alloc")
		selInit        = objc.RegisterName("init")
		selRelease     = objc.RegisterName("release")
	)

	fmt.Println("clipprobe：监听剪贴板，打印每次变化的 pasteboard 类型（UTI）。Ctrl+C 退出。")
	fmt.Println("依次从 普通文本 / 1Password / Bitwarden / 浏览器密码自动填充 复制，观察各自的 UTI。")
	fmt.Println("含 org.nspasteboard.ConcealedType / TransientType / AutoGeneratedType")
	fmt.Println("或厂商私有 UTI（com.agilebits.onepassword 等）者 = 应跳过上传的敏感内容。")
	fmt.Println("（本工具只读类型、不读内容、不上传。）")
	fmt.Println("--------")

	last := -1
	for {
		pool := objc.ID(poolClass).Send(selAlloc).Send(selInit)

		general := objc.ID(pbClass).Send(selGeneral)
		cc := int(general.Send(selChangeCount))
		if cc != last {
			last = cc
			types := general.Send(selTypes)
			n := int(types.Send(selCount))
			fmt.Printf("[changeCount=%d] %d 个类型：\n", cc, n)
			for i := 0; i < n; i++ {
				item := types.Send(selObjectAt, i)
				cstr := item.Send(selUTF8)
				fmt.Printf("  - %s\n", cString(uintptr(cstr)))
			}
		}

		objc.ID(pool).Send(selRelease)
		time.Sleep(500 * time.Millisecond)
	}
}

// cString reads a NUL-terminated C string (from -[NSString UTF8String]) into a
// Go string.
func cString(p uintptr) string {
	if p == 0 {
		return "(nil)"
	}
	var b []byte
	for i := 0; ; i++ {
		c := *(*byte)(unsafe.Pointer(p + uintptr(i)))
		if c == 0 {
			break
		}
		b = append(b, c)
	}
	return string(b)
}
