package main

import (
	"bytes"
	"errors"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"math"
	"os/exec"

	"github.com/liyue201/goqr"
	"github.com/makiuchi-d/gozxing"
	qrx "github.com/makiuchi-d/gozxing/qrcode"
	qrgen "github.com/skip2/go-qrcode"
)

// GenerateQRPNG returns PNG bytes for the given text.
func GenerateQRPNG(text string, size int) ([]byte, error) {
	if size <= 0 {
		size = 256
	}
	// Use Low EC for larger modules; improves decode reliability for long payloads
	png, err := qrgen.Encode(text, qrgen.Low, size)
	if err != nil {
		return nil, err
	}
	return png, nil
}

// DecodeQR decodes first QR code text from an image.
func DecodeQR(img image.Image) (string, error) {
	// Try basic decode first
	codes, err := goqr.Recognize(img)
	if err == nil && len(codes) > 0 {
		return string(codes[0].Payload), nil
	}
	if err != nil {
	}

	// Convert palette/indexed images to RGBA first to avoid issues with goqr
	if _, ok := img.ColorModel().(color.Palette); ok {
		bounds := img.Bounds()
		rgba := image.NewRGBA(bounds)
		draw.Draw(rgba, bounds, img, bounds.Min, draw.Src)
		img = rgba

		// Try again after conversion
		codes, err := goqr.Recognize(img)
		if err == nil && len(codes) > 0 {
			return string(codes[0].Payload), nil
		}
	}

	// Try multiple preprocessing strategies to improve recognition from clipboard screenshots
	attempts := []image.Image{}
	add := func(im image.Image) {
		if im != nil {
			attempts = append(attempts, im)
		}
	}
	add(img)

	bounds := img.Bounds()
	w, h := bounds.Dx(), bounds.Dy()

	// Helper: nearest-neighbour scale
	scale := func(src image.Image, fw, fh int) image.Image {
		if fw <= 0 || fh <= 0 {
			return src
		}
		dst := image.NewNRGBA(image.Rect(0, 0, fw, fh))
		sb := src.Bounds()
		sw, sh := float64(sb.Dx()), float64(sb.Dy())
		for y := 0; y < fh; y++ {
			sy := int(float64(y)/float64(fh)*sh + 0.5)
			if sy >= sb.Dy() {
				sy = sb.Dy() - 1
			}
			for x := 0; x < fw; x++ {
				sx := int(float64(x)/float64(fw)*sw + 0.5)
				if sx >= sb.Dx() {
					sx = sb.Dx() - 1
				}
				dst.Set(x, y, src.At(sb.Min.X+sx, sb.Min.Y+sy))
			}
		}
		return dst
	}

	// Helper: binarize with adaptive threshold (simple mean + variance fudge)
	binarize := func(src image.Image) image.Image {
		b := src.Bounds()
		gray := image.NewGray(b)
		var sum, sum2 float64
		total := float64(b.Dx() * b.Dy())
		for y := b.Min.Y; y < b.Max.Y; y++ {
			for x := b.Min.X; x < b.Max.X; x++ {
				r, g, bl, _ := src.At(x, y).RGBA()
				lum := float64((r*299 + g*587 + bl*114) / 1000 >> 8) // 0-255 approx
				sum += lum
				sum2 += lum * lum
			}
		}
		mean := sum / total
		variance := (sum2 / total) - mean*mean
		stddev := math.Sqrt(math.Max(variance, 0))
		// threshold tweaks: lower slightly for dark backgrounds
		thr := mean - stddev*0.15
		if thr < 50 {
			thr = 50
		}
		if thr > 200 {
			thr = 200
		}
		for y := b.Min.Y; y < b.Max.Y; y++ {
			for x := b.Min.X; x < b.Max.X; x++ {
				r, g, bl, _ := src.At(x, y).RGBA()
				lum := float64((r*299 + g*587 + bl*114) / 1000 >> 8)
				var c color.Gray
				if lum < thr {
					c = color.Gray{Y: 0}
				} else {
					c = color.Gray{Y: 255}
				}
				gray.Set(x, y, c)
			}
		}
		return gray
	}

	// Scale strategy: even if image is not tiny, scaled variants sometimes help goqr lock onto module grid
	// 1) If very small, aggressive scale
	if w < 280 || h < 280 {
		factor := 2
		if w < 160 {
			factor = 3
		}
		if w < 100 {
			factor = 4
		}
		add(scale(img, w*factor, h*factor))
	}
	// 2) Always try moderate upscale variants for mid-sized screenshots (helps when original was downscaled with interpolation)
	if w >= 180 && w <= 900 { // avoid exploding huge images
		add(scale(img, w*2, h*2))
		if w*3 < 2000 && h*3 < 2000 { // guard upper bound
			add(scale(img, w*3, h*3))
		}
	}

	// Add binarized versions (original + scaled)
	add(binarize(img))
	if w < 320 || h < 320 {
		factor := 2
		add(binarize(scale(img, w*factor, h*factor)))
	}
	// Also binarize any newly added large upscales (skip if too big)
	for _, cand := range attempts { // safe: attempts already contains originals
		cw := cand.Bounds().Dx()
		ch := cand.Bounds().Dy()
		if (cw <= 1400 && ch <= 1400) && (cw > w || ch > h) { // only for upscaled variants within limit
			add(binarize(cand))
		}
	}

	// Add contrast-enhanced alpha-flattened variant
	flatten := func(src image.Image) image.Image {
		b := src.Bounds()
		dst := image.NewRGBA(b)
		draw.Draw(dst, b, image.Black, image.Point{}, draw.Src)
		draw.Draw(dst, b, src, b.Min, draw.Over)
		return dst
	}
	add(flatten(img))

	// Rotations (90,180,270) in case of orientation issues
	rotate := func(src image.Image) []image.Image {
		b := src.Bounds()
		w := b.Dx()
		h := b.Dy()
		r90 := image.NewRGBA(image.Rect(0, 0, h, w))
		r180 := image.NewRGBA(image.Rect(0, 0, w, h))
		r270 := image.NewRGBA(image.Rect(0, 0, h, w))
		for y := 0; y < h; y++ {
			for x := 0; x < w; x++ {
				c := src.At(b.Min.X+x, b.Min.Y+y)
				// 90
				r90.Set(h-1-y, x, c)
				// 180
				r180.Set(w-1-x, h-1-y, c)
				// 270
				r270.Set(y, w-1-x, c)
			}
		}
		return []image.Image{r90, r180, r270}
	}
	for _, r := range rotate(img) {
		add(r)
	}

	var firstErr error
	for _, attempt := range attempts {
		codes, err := goqr.Recognize(attempt)
		if err != nil {
			if firstErr == nil {
				firstErr = err
			}
			continue
		}
		if len(codes) > 0 {
			return string(codes[0].Payload), nil
		}
	}

	// ZXing helper used across multiple branches
	decodeZX := func(src image.Image, hints map[gozxing.DecodeHintType]interface{}) (string, error) {
		binSrc := gozxing.NewLuminanceSourceFromImage(src)
		// Try hybrid first (usually better for photos)
		bmpH, _ := gozxing.NewBinaryBitmap(gozxing.NewHybridBinarizer(binSrc))
		reader := qrx.NewQRCodeReader()
		if res, err := reader.Decode(bmpH, hints); err == nil {
			return res.GetText(), nil
		}
		// Fallback to global histogram
		bmpG, _ := gozxing.NewBinaryBitmap(gozxing.NewGlobalHistgramBinarizer(binSrc))
		if res, err := reader.Decode(bmpG, hints); err == nil {
			return res.GetText(), nil
		} else {
			return "", err
		}
	}
	// Also try ZXing on all preprocessed attempts
	for _, attempt := range attempts {
		if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
			gozxing.DecodeHintType_TRY_HARDER:   true,
			gozxing.DecodeHintType_PURE_BARCODE: true,
		}); err == nil && txt != "" {
			return txt, nil
		}
		if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
			gozxing.DecodeHintType_TRY_HARDER: true,
		}); err == nil && txt != "" {
			return txt, nil
		}
	}
	// --- Heuristic fallback: auto-invert + quiet-zone pad + gozxing ---
	addQuietZone := func(src image.Image) image.Image {
		b := src.Bounds()
		pad := int(math.Max(float64(b.Dx()/32), 8))
		out := image.NewRGBA(image.Rect(0, 0, b.Dx()+pad*2, b.Dy()+pad*2))
		// fill white
		for y := 0; y < out.Bounds().Dy(); y++ {
			for x := 0; x < out.Bounds().Dx(); x++ {
				out.Set(x, y, color.White)
			}
		}
		draw.Draw(out, b.Add(image.Point{X: pad, Y: pad}), src, b.Min, draw.Src)
		return out
	}
	invert := func(src image.Image) image.Image {
		b := src.Bounds()
		out := image.NewRGBA(b)
		for y := b.Min.Y; y < b.Max.Y; y++ {
			for x := b.Min.X; x < b.Max.X; x++ {
				r, g, bb, a := src.At(x, y).RGBA()
				out.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - bb/257), uint8(a / 257)})
			}
		}
		return out
	}
	// estimate dark pixel ratio
	b := img.Bounds()
	var dark, total int
	for y := b.Min.Y; y < b.Max.Y; y++ {
		for x := b.Min.X; x < b.Max.X; x++ {
			r, g, bb, _ := img.At(x, y).RGBA()
			lum := (r*299 + g*587 + bb*114) / 1000
			if lum < 128<<8 {
				dark++
			}
			total++
		}
	}
	ratio := float64(dark) / float64(total)
	needInvert := ratio > 0.85 // almost full dark background (white modules?)
	var modified []image.Image
	base := img
	if needInvert {
		base = invert(base)
	}
	modified = append(modified, addQuietZone(base))
	modified = append(modified, addQuietZone(invert(base)))
	for _, m := range modified {
		if codes, err := goqr.Recognize(m); err == nil && len(codes) > 0 {
			return string(codes[0].Payload), nil
		}
		// Also try ZXing on these variants before moving on
		if txt, err := decodeZX(m, map[gozxing.DecodeHintType]interface{}{
			gozxing.DecodeHintType_TRY_HARDER:   true,
			gozxing.DecodeHintType_PURE_BARCODE: true,
		}); err == nil && txt != "" {
			return txt, nil
		}
		if txt, err := decodeZX(m, map[gozxing.DecodeHintType]interface{}{
			gozxing.DecodeHintType_TRY_HARDER: true,
		}); err == nil && txt != "" {
			return txt, nil
		}
	}
	// ZXing with TRY_HARDER and PURE_BARCODE hints which often fix dense PNGs
	hints := map[gozxing.DecodeHintType]interface{}{
		gozxing.DecodeHintType_TRY_HARDER: true,
		// Many of our images are perfect render PNGs from go-qrcode -> enable PURE_BARCODE
		gozxing.DecodeHintType_PURE_BARCODE: true,
	}
	if txt, err := decodeZX(base, hints); err == nil && txt != "" {
		return txt, nil
	}
	// Try again without PURE_BARCODE in case quiet zone is cropped
	hints2 := map[gozxing.DecodeHintType]interface{}{gozxing.DecodeHintType_TRY_HARDER: true}
	if txt, err := decodeZX(base, hints2); err == nil && txt != "" {
		return txt, nil
	}
	// Try ZXing on rotations as well
	for _, r := range rotate(base) {
		if txt, err := decodeZX(r, hints2); err == nil && txt != "" {
			return txt, nil
		}
		if txt, err := decodeZX(r, hints); err == nil && txt != "" {
			return txt, nil
		}
	}

	// Extra pure-QR recovery: try small insets and simple upscales with PURE_BARCODE.
	inset := func(src image.Image, px int) image.Image {
		if px <= 0 {
			return src
		}
		b := src.Bounds()
		r := image.Rect(b.Min.X+px, b.Min.Y+px, b.Max.X-px, b.Max.Y-px)
		if r.Dx() <= 10 || r.Dy() <= 10 {
			return src
		}
		out := image.NewRGBA(image.Rect(0, 0, r.Dx(), r.Dy()))
		draw.Draw(out, out.Bounds(), src, r.Min, draw.Src)
		return out
	}
	cropSides := func(src image.Image, l, t, r, b int) image.Image {
		bb := src.Bounds()
		rr := image.Rect(bb.Min.X+l, bb.Min.Y+t, bb.Max.X-r, bb.Max.Y-b)
		if rr.Dx() <= 10 || rr.Dy() <= 10 || rr.Min.X >= rr.Max.X || rr.Min.Y >= rr.Max.Y {
			return src
		}
		out := image.NewRGBA(image.Rect(0, 0, rr.Dx(), rr.Dy()))
		draw.Draw(out, out.Bounds(), src, rr.Min, draw.Src)
		return out
	}
	tryPure := func(src image.Image) (string, bool) {
		for _, px := range []int{1, 2, 3} {
			if txt, err := decodeZX(inset(src, px), hints); err == nil && txt != "" {
				return txt, true
			}
		}
		return "", false
	}
	// Try on base, binarized base and scaled variants
	if txt, ok := tryPure(base); ok {
		return txt, nil
	}
	if txt, ok := tryPure(binarize(base)); ok {
		return txt, nil
	}
	if w < 800 && h < 800 { // upscale to mitigate rounding of module size
		if txt, ok := tryPure(scale(base, w*2, h*2)); ok {
			return txt, nil
		}
		if txt, ok := tryPure(scale(base, w*3, h*3)); ok {
			return txt, nil
		}
	}

	// Brute-force small asymmetric crops to satisfy ZXing's 1 (mod 4) dimension constraint.
	for _, src := range []image.Image{base, binarize(base)} {
		for l := 0; l <= 2; l++ {
			for t := 0; t <= 2; t++ {
				for r := 0; r <= 2; r++ {
					for btm := 0; btm <= 2; btm++ {
						cand := cropSides(src, l, t, r, btm)
						if cand == src {
							continue
						}
						if txt, err := decodeZX(cand, hints); err == nil && txt != "" {
							return txt, nil
						}
						if txt, err := decodeZX(cand, hints2); err == nil && txt != "" {
							return txt, nil
						}
					}
				}
			}
		}
	}

	// Bounding box crop of dark pixels, then upscale and retry
	boundsAll := base.Bounds()
	minX, minY := boundsAll.Max.X, boundsAll.Max.Y
	maxX, maxY := boundsAll.Min.X, boundsAll.Min.Y
	for y := boundsAll.Min.Y; y < boundsAll.Max.Y; y++ {
		for x := boundsAll.Min.X; x < boundsAll.Max.X; x++ {
			r, g, bb, _ := base.At(x, y).RGBA()
			lum := (r*299 + g*587 + bb*114) / 1000
			if lum < 180<<8 { // treat as dark
				if x < minX {
					minX = x
				}
				if y < minY {
					minY = y
				}
				if x > maxX {
					maxX = x
				}
				if y > maxY {
					maxY = y
				}
			}
		}
	}
	if minX < maxX && minY < maxY {
		crop := image.NewRGBA(image.Rect(0, 0, maxX-minX+1, maxY-minY+1))
		draw.Draw(crop, crop.Bounds(), base, image.Point{X: minX, Y: minY}, draw.Src)
		for _, up := range []int{2, 3, 4} {
			cw := crop.Bounds().Dx() * up
			ch := crop.Bounds().Dy() * up
			if cw > 2400 || ch > 2400 {
				continue
			}
			scaled := image.NewRGBA(image.Rect(0, 0, cw, ch))
			for y := 0; y < ch; y++ {
				sy := y / up
				for x := 0; x < cw; x++ {
					sx := x / up
					scaled.Set(x, y, crop.At(sx, sy))
				}
			}
			if txt, err := decodeZX(scaled, hints2); err == nil && txt != "" {
				return txt, nil
			}
		}
	}
	if firstErr != nil {
		return "", firstErr
	}
	// Final external fallback: try zbarimg if available on the system
	if cmd, lookErr := exec.LookPath("zbarimg"); lookErr == nil {
		buf := &bytes.Buffer{}
		if err := png.Encode(buf, img); err == nil {
			c := exec.Command(cmd, "-q", "--raw", "-")
			c.Stdin = bytes.NewReader(buf.Bytes())
			out, err := c.Output()
			if err == nil && len(out) > 0 {
				return string(out), nil
			}
		}
	}
	return "", errors.New("no qr code found")
}

// DecodeQRCodes returns all QR payloads recognized in an image. It is used by
// the desktop chat scanner, where a selected screen region may contain more
// than one message.
func DecodeQRCodes(img image.Image) ([]string, error) {
	if img == nil {
		return nil, errors.New("prázdný obrázek")
	}
	seen := make(map[string]struct{})
	texts := make([]string, 0)
	addCodes := func(codes []*goqr.QRData) {
		for _, code := range codes {
			if code == nil {
				continue
			}
			text := string(code.Payload)
			if text == "" {
				continue
			}
			if _, ok := seen[text]; ok {
				continue
			}
			seen[text] = struct{}{}
			texts = append(texts, text)
		}
	}
	recognize := func(candidate image.Image) bool {
		codes, err := goqr.Recognize(candidate)
		if err != nil {
			return false
		}
		before := len(texts)
		addCodes(codes)
		return len(texts) > before
	}
	scale := func(src image.Image, factor int) image.Image {
		bounds := src.Bounds()
		if factor <= 1 || bounds.Dx() <= 0 || bounds.Dy() <= 0 {
			return src
		}
		dst := image.NewRGBA(image.Rect(0, 0, bounds.Dx()*factor, bounds.Dy()*factor))
		for y := 0; y < dst.Bounds().Dy(); y++ {
			for x := 0; x < dst.Bounds().Dx(); x++ {
				dst.Set(x, y, src.At(bounds.Min.X+x/factor, bounds.Min.Y+y/factor))
			}
		}
		return dst
	}
	crop := func(src image.Image, x, y, width, height int) image.Image {
		bounds := src.Bounds()
		r := image.Rect(bounds.Min.X+x, bounds.Min.Y+y, bounds.Min.X+x+width, bounds.Min.Y+y+height)
		if r.Min.X < bounds.Min.X || r.Min.Y < bounds.Min.Y || r.Max.X > bounds.Max.X || r.Max.Y > bounds.Max.Y {
			return nil
		}
		out := image.NewRGBA(image.Rect(0, 0, width, height))
		draw.Draw(out, out.Bounds(), src, r.Min, draw.Src)
		return out
	}

	bounds := img.Bounds()
	candidates := []image.Image{img}
	if max(bounds.Dx(), bounds.Dy()) <= 2400 {
		candidates = append(candidates, scale(img, 2))
	}
	for _, candidate := range candidates {
		recognize(candidate)
	}

	// A selected chat region often contains several small QR codes. Scan four
	// heavily overlapping tiles as well as the complete image so that one code
	// does not hide another from the detector's global pass.
	if bounds.Dx() >= 80 && bounds.Dy() >= 80 {
		tileWidth := bounds.Dx() * 2 / 3
		tileHeight := bounds.Dy() * 2 / 3
		for _, origin := range [][2]int{
			{0, 0},
			{bounds.Dx() - tileWidth, 0},
			{0, bounds.Dy() - tileHeight},
			{bounds.Dx() - tileWidth, bounds.Dy() - tileHeight},
		} {
			tile := crop(img, origin[0], origin[1], tileWidth, tileHeight)
			if tile == nil {
				continue
			}
			found := recognize(tile)
			if !found && max(tile.Bounds().Dx(), tile.Bounds().Dy()) <= 1600 {
				found = recognize(scale(tile, 2))
			}
			if !found {
				if text, err := DecodeQR(tile); err == nil && text != "" {
					if _, exists := seen[text]; !exists {
						seen[text] = struct{}{}
						texts = append(texts, text)
					}
				}
			}
		}
	}
	if len(texts) == 0 {
		if text, err := DecodeQR(img); err == nil && text != "" {
			texts = append(texts, text)
		}
	}
	if len(texts) == 0 {
		return nil, errors.New("QR kód nenalezen")
	}
	return texts, nil
}

// DecodeQRCodesFast keeps the live scanner responsive. It avoids the expensive
// tiled DecodeQR fallback and only retries a reasonably sized frame once after
// nearest-neighbour upscaling.
func DecodeQRCodesFast(img image.Image) ([]string, error) {
	if img == nil {
		return nil, errors.New("prázdný obrázek")
	}
	decode := func(candidate image.Image) []string {
		codes, err := goqr.Recognize(candidate)
		if err != nil {
			return nil
		}
		seen := make(map[string]struct{}, len(codes))
		texts := make([]string, 0, len(codes))
		for _, code := range codes {
			if code == nil {
				continue
			}
			text := string(code.Payload)
			if text == "" {
				continue
			}
			if _, exists := seen[text]; exists {
				continue
			}
			seen[text] = struct{}{}
			texts = append(texts, text)
		}
		return texts
	}

	texts := decode(img)
	if len(texts) > 0 {
		return texts, nil
	}
	bounds := img.Bounds()
	if max(bounds.Dx(), bounds.Dy()) <= 1600 {
		scaled := image.NewRGBA(image.Rect(0, 0, bounds.Dx()*2, bounds.Dy()*2))
		for y := 0; y < scaled.Bounds().Dy(); y++ {
			for x := 0; x < scaled.Bounds().Dx(); x++ {
				scaled.Set(x, y, img.At(bounds.Min.X+x/2, bounds.Min.Y+y/2))
			}
		}
		texts = decode(scaled)
	}
	if len(texts) == 0 {
		return nil, errors.New("QR kód nenalezen")
	}
	return texts, nil
}

// LoadPNG decodes raw PNG bytes to image.Image.
func LoadPNG(b []byte) (image.Image, error) {
	im, err := png.Decode(bytes.NewReader(b))
	if err != nil {
		return nil, err
	}
	return im, nil
}
