// logogen converts an image to a .bin file suitable for embedding in badge firmware.
//
// Usage:
//
//	logogen -size <target|WxH> -image <path|shortname> [-output logo.bin] [-images-dir images] [-cache-dir cache]
//
// Examples:
//
//	logogen -size nicebadge -image tinygo
//	logogen -size gopherbadge -image gopherconeu
//	logogen -size 320x240 -image /path/to/logo.jpg
//	logogen -size gobadge -image ./my-logo.png -output logo.bin
//
// Short names resolve to image files inside -images-dir.
// If a predefined resized variant exists (e.g. tinygo_320x240.png), it is used directly.
// Generated .bin files are cached in -cache-dir to avoid reprocessing.
package main

import (
	"flag"
	"fmt"
	"image"
	_ "image/jpeg"
	_ "image/png"
	"log"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	draw2 "golang.org/x/image/draw"
)

type resolution struct {
	width, height int
}

func (r resolution) String() string {
	return fmt.Sprintf("%dx%d", r.width, r.height)
}

var predefinedTargets = map[string]resolution{
	"nicebadge":   {240, 135},
	"gopherbadge": {320, 240},
	"gobadge":     {160, 128},
	"pybadge":     {160, 128},
}

func main() {
	size := flag.String("size", "", "Resolution: WxH or target name (nicebadge, gopherbadge, gobadge, pybadge)")
	imageArg := flag.String("image", "", "Image: file path (relative/absolute) or short name (tinygo, gopherconeu, golab, ...)")
	output := flag.String("output", "logo.bin", "Output .bin file path")
	imagesDir := flag.String("images-dir", "images", "Directory with source images and predefined variants")
	cacheDir := flag.String("cache-dir", "cache", "Directory for cached .bin files")
	flag.Parse()

	if *size == "" {
		flag.Usage()
		log.Fatal("\nMust specify -size (e.g. -size nicebadge or -size 320x240)")
	}
	if *imageArg == "" {
		flag.Usage()
		log.Fatal("\nMust specify -image (file path or short name)")
	}

	res := parseSize(*size)

	// Determine whether -image is a file path or a short name.
	// Heuristic: contains path separator, starts with '.' or '..' , or has a file extension → path.
	isPath := strings.ContainsAny(*imageArg, "/\\") ||
		strings.HasPrefix(*imageArg, ".") ||
		filepath.Ext(*imageArg) != ""

	var baseName string // used as cache/predefined key
	var srcPath string  // resolved source image file

	if isPath {
		absPath, err := filepath.Abs(*imageArg)
		if err != nil {
			log.Fatalf("invalid image path %q: %v", *imageArg, err)
		}
		srcPath = absPath
		base := filepath.Base(absPath)
		baseName = strings.TrimSuffix(base, filepath.Ext(base))
	} else {
		baseName = *imageArg
		srcPath = findSourceImage(*imageArg, res, *imagesDir)
	}

	cacheFile := filepath.Join(*cacheDir, fmt.Sprintf("%s_%s.bin", baseName, res))

	// 1. Check cache.
	if data, err := os.ReadFile(cacheFile); err == nil {
		fmt.Printf("Using cached: %s\n", cacheFile)
		writeOutput(*output, data)
		fmt.Printf("Written: %s (%s)\n", *output, res)
		return
	}

	// 2. Load, resize if needed, convert.
	img := loadAndResize(srcPath, res)
	binData := convertToBin(img, res)

	// 3. Save cache.
	if err := os.MkdirAll(*cacheDir, 0755); err != nil {
		log.Printf("Warning: cannot create cache dir %s: %v", *cacheDir, err)
	} else if err := os.WriteFile(cacheFile, binData, 0644); err != nil {
		log.Printf("Warning: cannot save cache %s: %v", cacheFile, err)
	} else {
		fmt.Printf("Cached: %s\n", cacheFile)
	}

	writeOutput(*output, binData)
	fmt.Printf("Generated: %s (%s, %d bytes)\n", *output, res, len(binData))
}

// parseSize accepts either a target name (nicebadge, gopherbadge, ...) or WxH format.
func parseSize(s string) resolution {
	if res, ok := predefinedTargets[strings.ToLower(s)]; ok {
		return res
	}
	parts := strings.SplitN(strings.ToLower(s), "x", 2)
	if len(parts) != 2 {
		log.Fatalf("invalid size %q: must be WxH (e.g. 320x240) or a target name (nicebadge, gopherbadge, gobadge, pybadge)", s)
	}
	w, err1 := strconv.Atoi(parts[0])
	h, err2 := strconv.Atoi(parts[1])
	if err1 != nil || err2 != nil || w <= 0 || h <= 0 {
		log.Fatalf("invalid size %q: width and height must be positive integers", s)
	}
	return resolution{w, h}
}

// findSourceImage looks for the best source image for a short name in imagesDir.
// Priority:
//  1. <name>_<WxH>.png / .jpg  — predefined resized variant (no resize needed)
//  2. <name>.png / .jpg / .jpeg — base source image (will be resized)
func findSourceImage(name string, res resolution, imagesDir string) string {
	resStr := res.String()
	extensions := []string{".png", ".jpg", ".jpeg"}

	// Try predefined resized variant first.
	for _, ext := range extensions {
		p := filepath.Join(imagesDir, fmt.Sprintf("%s_%s%s", name, resStr, ext))
		if _, err := os.Stat(p); err == nil {
			fmt.Printf("Using predefined: %s\n", p)
			return p
		}
	}

	// Try base source image.
	for _, ext := range extensions {
		p := filepath.Join(imagesDir, name+ext)
		if _, err := os.Stat(p); err == nil {
			return p
		}
	}

	log.Fatalf("image not found for short name %q in directory %q\n"+
		"Expected one of: %s_%s.png, %s.png, %s.jpg, ...",
		name, imagesDir, name, resStr, name, name)
	return ""
}

// loadAndResize opens a JPEG or PNG and scales it to the target resolution.
// If the image is already the exact target size, scaling is skipped.
func loadAndResize(path string, res resolution) image.Image {
	f, err := os.Open(path)
	if err != nil {
		log.Fatalf("cannot open image %s: %v", path, err)
	}
	defer f.Close()

	img, format, err := image.Decode(f)
	if err != nil {
		log.Fatalf("cannot decode image %s: %v", path, err)
	}
	fmt.Printf("Loaded: %s (%s, %dx%d)\n", path, format, img.Bounds().Dx(), img.Bounds().Dy())

	if img.Bounds().Dx() == res.width && img.Bounds().Dy() == res.height {
		return img
	}

	fmt.Printf("Resizing to %s...\n", res)
	dst := image.NewRGBA(image.Rect(0, 0, res.width, res.height))
	draw2.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw2.Over, nil)
	return dst
}

// convertToBin encodes the image pixels as a hex string: "RRGGBB" per pixel, row-major.
// This matches the format expected by showLogoBin() in badge.go.
func convertToBin(img image.Image, res resolution) []byte {
	var sb strings.Builder
	sb.Grow(res.width * res.height * 6)
	for y := 0; y < res.height; y++ {
		for x := 0; x < res.width; x++ {
			r, g, b, _ := img.At(x, y).RGBA()
			fmt.Fprintf(&sb, "%02x%02x%02x", uint8(r>>8), uint8(g>>8), uint8(b>>8))
		}
	}
	return []byte(sb.String())
}

func writeOutput(path string, data []byte) {
	if err := os.WriteFile(path, data, 0644); err != nil {
		log.Fatalf("cannot write output %s: %v", path, err)
	}
}
