//go:build windows

package main

// Kucuk onizleme: Windows Shell'in IShellItemImageFactory::GetImage cagrisiyla dosyanin Explorer'daki
// onizlemesi alinir (ilgili thumbnail saglayicisi kayitliysa). Saglayici yoksa hata doner ve cagiran
// o dosya icin onizleme eklemez. Thumbs.db ayristirilmaz, dosya formatlari cozulmez.

import (
	"bytes"
	"crypto/sha1"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"image"
	"image/png"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"syscall"
	"time"
	"unsafe"
)

const (
	kucukOnizlemeBoyut       = 96
	kucukOnizlemeToplamLimit = 200 * 1024
	siiGbfThumbnailOnly      = 0x8
	siiGbfIconOnly           = 0x4
	simgePx                  = 32
	simgeToplamLimit         = 120 * 1024
	coinitApartmentThreaded  = 0x2
	rpcEChangedMode          = 0x80010106
	vtQueryInterface         = 0
	vtRelease                = 2
	vtGetImage               = 3
)

var (
	modShell32 = syscall.NewLazyDLL("shell32.dll")
	modOle32   = syscall.NewLazyDLL("ole32.dll")
	modGdi32   = syscall.NewLazyDLL("gdi32.dll")
	modUser32  = syscall.NewLazyDLL("user32.dll")

	procSHCreateItemFromParsingName = modShell32.NewProc("SHCreateItemFromParsingName")
	procCoInitializeEx              = modOle32.NewProc("CoInitializeEx")
	procCoUninitialize              = modOle32.NewProc("CoUninitialize")
	procGetObjectW                  = modGdi32.NewProc("GetObjectW")
	procGetDIBits                   = modGdi32.NewProc("GetDIBits")
	procDeleteObject                = modGdi32.NewProc("DeleteObject")
	procGetDC                       = modUser32.NewProc("GetDC")
	procReleaseDC                   = modUser32.NewProc("ReleaseDC")
)

var (
	iidIShellItem             = syscall.GUID{Data1: 0x43826d1e, Data2: 0xe718, Data3: 0x42ee, Data4: [8]byte{0xbc, 0x55, 0xa1, 0xe2, 0x61, 0xc3, 0x7b, 0xfe}}
	iidIShellItemImageFactory = syscall.GUID{Data1: 0xbcc18b79, Data2: 0xba16, Data3: 0x442f, Data4: [8]byte{0x80, 0xc4, 0x8a, 0x59, 0xc3, 0x0c, 0x46, 0x3b}}
)

// Onizlemesi olabilecek uzantilar: resimler, PDF ve CATIA dosyalari (saglayici yoksa atlanir).
var onizlemeliUzantilar = map[string]bool{
	".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".bmp": true,
	".tif": true, ".tiff": true, ".webp": true, ".pdf": true,
	".catpart": true, ".catproduct": true, ".catdrawing": true, ".catprocess": true,
	// Videolar: Shell kare onizlemesi verir (Explorer'daki gibi).
	".mp4": true, ".mkv": true, ".avi": true, ".mov": true, ".wmv": true,
	".webm": true, ".m4v": true, ".mpg": true, ".mpeg": true, ".3gp": true,
}

type winBitmap struct {
	Type       int32
	Width      int32
	Height     int32
	WidthBytes int32
	Planes     uint16
	BitsPixel  uint16
	Bits       uintptr
}

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

// comMetod: COM nesnesinin vtable'indaki n. metodun adresi.
func comMetod(obj unsafe.Pointer, n int) uintptr {
	vtbl := *(*unsafe.Pointer)(obj)
	return *(*uintptr)(unsafe.Add(vtbl, n*int(unsafe.Sizeof(uintptr(0)))))
}

func comBirak(obj unsafe.Pointer) {
	if obj == nil {
		return
	}
	_, _, _ = syscall.SyscallN(comMetod(obj, vtRelease), uintptr(obj))
}

// kabukiKucukOnizleme: dosyanin Shell kucuk resmini (thumbnail) PNG data URL olarak dondurur.
// Cagiran, COM'un ayni iplikte baslatildigini (comBaslat) ve iplik kilidini onceden kurmalidir.
func kabukiKucukOnizleme(yol string) (string, error) {
	return kabukiGoruntu(yol, kucukOnizlemeBoyut, siiGbfThumbnailOnly)
}

// kabukiGoruntu: Shell'den verilen boyut ve bayrakla (kucuk resim ya da yalniz simge) PNG data URL dondurur.
func kabukiGoruntu(yol string, boyutPx int, bayrak uintptr) (string, error) {
	yolPtr, err := syscall.UTF16PtrFromString(yol)
	if err != nil {
		return "", err
	}
	var item unsafe.Pointer
	r, _, _ := procSHCreateItemFromParsingName.Call(
		uintptr(unsafe.Pointer(yolPtr)), 0,
		uintptr(unsafe.Pointer(&iidIShellItem)),
		uintptr(unsafe.Pointer(&item)),
	)
	if r != 0 || item == nil {
		return "", fmt.Errorf("shell ogesi yok: 0x%x", r)
	}
	defer comBirak(item)

	var fabrika unsafe.Pointer
	r, _, _ = syscall.SyscallN(comMetod(item, vtQueryInterface),
		uintptr(item), uintptr(unsafe.Pointer(&iidIShellItemImageFactory)), uintptr(unsafe.Pointer(&fabrika)))
	if r != 0 || fabrika == nil {
		return "", errors.New("onizleme saglayicisi yok")
	}
	defer comBirak(fabrika)

	// SIZE (cx, cy) 8 bayt; x64'te tek register ile gecer.
	boyut := uintptr(boyutPx) | uintptr(boyutPx)<<32
	var hbm uintptr
	r, _, _ = syscall.SyscallN(comMetod(fabrika, vtGetImage),
		uintptr(fabrika), boyut, bayrak, uintptr(unsafe.Pointer(&hbm)))
	if r != 0 || hbm == 0 {
		return "", errors.New("onizleme yok")
	}
	defer procDeleteObject.Call(hbm)

	var bm winBitmap
	if n, _, _ := procGetObjectW.Call(hbm, unsafe.Sizeof(bm), uintptr(unsafe.Pointer(&bm))); n == 0 {
		return "", errors.New("bitmap bilgisi alinamadi")
	}
	w, h := int(bm.Width), int(bm.Height)
	if w <= 0 || h <= 0 || w*h > 512*512 {
		return "", errors.New("gecersiz onizleme boyutu")
	}

	hdc, _, _ := procGetDC.Call(0)
	if hdc == 0 {
		return "", errors.New("ekran dc alinamadi")
	}
	defer procReleaseDC.Call(0, hdc)

	bi := winBitmapInfoHeader{
		Size:     uint32(unsafe.Sizeof(winBitmapInfoHeader{})),
		Width:    int32(w),
		Height:   -int32(h), // ust-asagi satir sirasi
		Planes:   1,
		BitCount: 32,
	}
	pix := make([]byte, w*h*4)
	if n, _, _ := procGetDIBits.Call(hdc, hbm, 0, uintptr(h),
		uintptr(unsafe.Pointer(&pix[0])), uintptr(unsafe.Pointer(&bi)), 0); n == 0 {
		return "", errors.New("piksel verisi alinamadi")
	}

	// Alfa kanali: simgeler (ikonlar) seffaf pikseller icerir. Hic alfa yoksa (fotograflar) tum piksel opak kalir.
	alfaVar := false
	for i := 3; i < len(pix); i += 4 {
		if pix[i] != 0 {
			alfaVar = true
			break
		}
	}
	img := image.NewRGBA(image.Rect(0, 0, w, h))
	for i := 0; i+3 < len(pix); i += 4 {
		r, g, b := pix[i+2], pix[i+1], pix[i]
		a := byte(255)
		if alfaVar {
			a = pix[i+3]
			if a == 0 {
				r, g, b = 0, 0, 0
			}
		}
		img.Pix[i] = r
		img.Pix[i+1] = g
		img.Pix[i+2] = b
		img.Pix[i+3] = a
	}
	var buf bytes.Buffer
	if err := png.Encode(&buf, img); err != nil {
		return "", err
	}
	return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()), nil
}

// comBaslat: COM'u bu iplikte baslatir. Donen fonksiyon CoUninitialize'i yapar (gerekirse).
func comBaslat() (func(), error) {
	hr, _, _ := procCoInitializeEx.Call(0, coinitApartmentThreaded)
	switch {
	case hr == 0 || hr == 1: // S_OK veya S_FALSE: CoUninitialize gerekir.
		return func() { procCoUninitialize.Call() }, nil
	case hr == rpcEChangedMode:
		return func() {}, nil
	}
	return nil, fmt.Errorf("com baslatilamadi: 0x%x", hr)
}

// kucukAyarlari: kullanicinin Everything ayarindan gelen onizleme secenekleri (sinirlar kenetlenmis).
type kucukAyarlari struct {
	Acik       bool
	MaksDeneme int // en fazla denenecek dosya sayisi
	SureSn     int // toplam sure (saniye)
	ToplamKb   int // sonuca eklenecek toplam onizleme boyutu (KB)
}

// kucukAyarOku: ayar JSON'undan onizleme secenekleri. Eksik ya da gecersiz alanlar varsayilana doner.
func kucukAyarOku(ayar json.RawMessage) kucukAyarlari {
	a := kucukAyarlari{MaksDeneme: 40, SureSn: 3, ToplamKb: 200}
	var s struct {
		KucukResim bool `json:"kucukResim"`
		MaksDeneme int  `json:"kucukMaksDeneme"`
		SureSn     int  `json:"kucukSureSn"`
		ToplamKb   int  `json:"kucukToplamKb"`
	}
	if len(ayar) == 0 || json.Unmarshal(ayar, &s) != nil {
		return a
	}
	a.Acik = s.KucukResim
	if s.MaksDeneme >= 1 && s.MaksDeneme <= 200 {
		a.MaksDeneme = s.MaksDeneme
	}
	if s.SureSn >= 1 && s.SureSn <= 30 {
		a.SureSn = s.SureSn
	}
	if s.ToplamKb >= 50 && s.ToplamKb <= 500 {
		a.ToplamKb = s.ToplamKb
	}
	return a
}

// onbellekKlasoru: onizlemelerin diske yazildigi klasor (kullanici onbellek klasoru altinda).
func onbellekKlasoru() string {
	taban, err := os.UserCacheDir()
	if err != nil {
		taban = os.TempDir()
	}
	return filepath.Join(taban, "fikoyt-eklenti-everything", "kucuk")
}

// onizlemeAl: once diskteki onbellege bakar (yol, boyut ve degisim zamani anahtar); yoksa Shell'den alir,
// CATIA dosyalari icin gomulu onizlemeye duser ve sonucu onbellege yazar.
// Donus: onizleme ve basari bilgisi.
func onizlemeAl(tam, klasor string) (string, bool) {
	anahtar := ""
	if st, err := os.Stat(tam); err == nil {
		ham := fmt.Sprintf("%s|%d|%d", tam, st.Size(), st.ModTime().UnixNano())
		h := sha1.Sum([]byte(ham))
		anahtar = hex.EncodeToString(h[:]) + ".dat"
		if veri, err := os.ReadFile(filepath.Join(klasor, anahtar)); err == nil && len(veri) > 0 {
			return string(veri), true
		}
	}
	kucuk, err := kabukiKucukOnizleme(tam)
	if err != nil && catUzantiMi(filepath.Base(tam)) {
		kucuk, err = catGomuluOnizleme(tam, kucukOnizlemeBoyut)
	}
	if err != nil {
		return "", false
	}
	if anahtar != "" {
		_ = os.MkdirAll(klasor, 0o700)
		_ = os.WriteFile(filepath.Join(klasor, anahtar), []byte(kucuk), 0o600)
	}
	return kucuk, true
}

// onizIsi: paralel isleme gonderilen tek dosya.
type onizIsi struct {
	idx int
	tam string
	uz  string
}

// onizlemeSonucu: bir isin sonucu.
type onizlemeSonucu struct {
	idx    int
	kucuk  string
	basari bool
}

// kucukOnizlemeleriEkle: arama sonucundaki dosyalara "kucuk" alani ekler.
// Isler paralel calisir (her is parcasi kendi COM baslatmasini yapar); sonuc sirasi korunur.
// Deneme sayisi ve sure sinirlidir; uzanti basina ust uste 3 basarisizlik ve hic basari yoksa uzanti atlanir.
// Toplam boyut sinirina sigan onizlemeler sirayla eklenir.
func kucukOnizlemeleriEkle(veri json.RawMessage, a kucukAyarlari) json.RawMessage {
	var s struct {
		TotalResults int              `json:"totalResults"`
		Results      []map[string]any `json:"results"`
	}
	if err := json.Unmarshal(veri, &s); err != nil {
		return veri
	}
	bitis := time.Now().Add(time.Duration(a.SureSn) * time.Second)
	klasor := onbellekKlasoru()

	const isciSayisi = 4
	kanal := make(chan onizIsi)
	cikis := make(chan onizlemeSonucu, len(s.Results))
	var mu sync.Mutex
	basari := map[string]int{}
	basarisiz := map[string]int{}
	var wg sync.WaitGroup
	for w := 0; w < isciSayisi; w++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			runtime.LockOSThread()
			defer runtime.UnlockOSThread()
			kapat, err := comBaslat()
			if err != nil {
				for range kanal {
				}
				return
			}
			defer kapat()
			for j := range kanal {
				kucuk, ok := onizlemeAl(j.tam, klasor)
				mu.Lock()
				if ok {
					basari[j.uz]++
				} else {
					basarisiz[j.uz]++
				}
				mu.Unlock()
				cikis <- onizlemeSonucu{idx: j.idx, kucuk: kucuk, basari: ok}
			}
		}()
	}

	deneme := 0
	for idx, r := range s.Results {
		if deneme >= a.MaksDeneme || time.Now().After(bitis) {
			break
		}
		ad, _ := r["name"].(string)
		yol, _ := r["path"].(string)
		if ad == "" || r["type"] != "file" {
			continue
		}
		uz := strings.ToLower(filepath.Ext(ad))
		mu.Lock()
		atla := basarisiz[uz] >= 3 && basari[uz] == 0
		mu.Unlock()
		if atla {
			continue
		}
		tam := ad
		if yol != "" {
			tam = yol + `\` + ad
		}
		deneme++
		kanal <- onizIsi{idx: idx, tam: tam, uz: uz}
	}
	close(kanal)
	wg.Wait()
	close(cikis)

	kucukler := map[int]string{}
	for c := range cikis {
		if c.basari {
			kucukler[c.idx] = c.kucuk
		}
	}
	toplam := 0
	for idx, r := range s.Results {
		k, ok := kucukler[idx]
		if !ok {
			continue
		}
		if toplam+len(k) > a.ToplamKb*1024 {
			continue
		}
		toplam += len(k)
		r["kucuk"] = k
	}
	out, err := json.Marshal(s)
	if err != nil {
		return veri
	}
	return out
}
