// Copyright (c) 2026 Harry Huang
package maptracker

import (
	"fmt"
	"image"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"github.com/MaaXYZ/MaaEnd/agent/go-service/pkg/minicv"
	"github.com/MaaXYZ/MaaEnd/agent/go-service/pkg/resource"
	maa "github.com/MaaXYZ/maa-framework-go/v4"
	"github.com/rs/zerolog/log"
)

var (
	Resource = &MapTrackerResource{
		PointerTemplateLoader: minicv.NewTemplateLoaderOfDynamicPath(
			func() string { return resource.FindResource("resource/image/MapTracker/pointer.png") },
		),
		ZoomInTemplate: minicv.NewTemplateLoaderOfDynamicPath(
			func() string { return resource.FindResource("resource/image/MapTracker/BigMapZoomIn.png") },
		),
		ZoomOutTemplate: minicv.NewTemplateLoaderOfDynamicPath(
			func() string { return resource.FindResource("resource/image/MapTracker/BigMapZoomOut.png") },
		),
	}
)

const (
	MAP_BBOX_DATA_PATH     = "data/MapTracker/map_bbox_data.json"
	MAP_EXTERNAL_DATA_PATH = "data/MapTracker/map_external_data.json"
	MAP_DIR                = "resource/image/MapTracker/map"
)

// MapTrackerResource stores globally shared map resources for map-tracker.
type MapTrackerResource struct {
	RawMapsOnce sync.Once
	RawMaps     []MapCache
	RawMapsErr  error

	IntegralCacheMu sync.Mutex

	PointerTemplateLoader *minicv.TemplateLoader
	ZoomInTemplate        *minicv.TemplateLoader
	ZoomOutTemplate       *minicv.TemplateLoader
}

// MapCache represents a preloaded map image.
type MapCache struct {
	Name    string
	Img     *image.RGBA
	OffsetX int
	OffsetY int

	cachedIntegralArray *minicv.IntegralArray
}

// GetIntegralArray lazily initializes integral array when first needed.
func (m *MapCache) GetIntegralArray() minicv.IntegralArray {
	Resource.IntegralCacheMu.Lock()
	defer Resource.IntegralCacheMu.Unlock()

	if m.cachedIntegralArray == nil {
		integral := minicv.GetIntegralArray(m.Img)
		m.cachedIntegralArray = &integral
	}
	return *m.cachedIntegralArray
}

// InitRawMaps initializes global raw maps cache exactly once.
func (r *MapTrackerResource) InitRawMaps(ctx *maa.Context) {
	r.RawMapsOnce.Do(func() {
		r.RawMaps, r.RawMapsErr = r.LoadMaps()
		if r.RawMapsErr != nil {
			log.Error().Err(r.RawMapsErr).Msg("Failed to load maps")
		} else {
			log.Info().Int("mapsCount", len(r.RawMaps)).Msg("Map images loaded")
		}
	})
}

// LoadMaps loads all map images from the resource directory and crops them when map bbox data exists.
func (r *MapTrackerResource) LoadMaps() ([]MapCache, error) {
	mapDir := resource.FindResource(MAP_DIR)
	if mapDir == "" {
		return nil, fmt.Errorf("map directory not found (searched in cache and standard locations)")
	}

	rectList := make(map[string][]int)
	err := resource.ReadJsonResource(MAP_BBOX_DATA_PATH, &rectList)
	if err != nil {
		log.Warn().Err(err).Msg("Failed to load map bbox data")
	}

	entries, err := os.ReadDir(mapDir)
	if err != nil {
		return nil, fmt.Errorf("failed to read map directory: %w", err)
	}

	type indexedFile struct {
		idx      int
		filename string
	}
	files := make([]indexedFile, 0, len(entries))
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}

		filename := entry.Name()
		if !strings.HasSuffix(filename, ".png") {
			continue
		}
		files = append(files, indexedFile{idx: len(files), filename: filename})
	}

	type result struct {
		idx int
		m   MapCache
		ok  bool
	}

	results := make([]MapCache, len(files))
	okFlags := make([]bool, len(files))
	resChan := make(chan result, len(files))
	sem := make(chan struct{}, 4)
	var wg sync.WaitGroup

	for _, f := range files {
		wg.Add(1)
		sem <- struct{}{}
		go func(item indexedFile) {
			defer wg.Done()
			defer func() { <-sem }()

			filename := item.filename
			imgPath := filepath.Join(mapDir, filename)
			file, err := os.Open(imgPath)
			if err != nil {
				log.Warn().Err(err).Str("path", imgPath).Msg("Failed to open map image")
				return
			}

			img, _, err := image.Decode(file)
			file.Close()
			if err != nil {
				log.Warn().Err(err).Str("path", imgPath).Msg("Failed to decode map image")
				return
			}

			name := strings.TrimSuffix(filename, ".png")
			fullRGBA := minicv.ImageConvertRGBA(img)

			imgRGBA := fullRGBA
			offsetX, offsetY := 0, 0

			if bboxRect, ok := rectList[name]; ok && len(bboxRect) == 4 {
				rect := image.Rect(bboxRect[0], bboxRect[1], bboxRect[2], bboxRect[3])
				expand := RAW_MAP_BBOX_EXPAND_PX
				rect = image.Rect(rect.Min.X-expand, rect.Min.Y-expand, rect.Max.X+expand, rect.Max.Y+expand)

				clipped := rect.Intersect(fullRGBA.Bounds())
				imgRGBA = minicv.ImageCropRect(fullRGBA, rect)
				if !clipped.Empty() {
					offsetX, offsetY = clipped.Min.X, clipped.Min.Y
				}
			}

			resChan <- result{
				idx: item.idx,
				m: MapCache{
					Name:    name,
					Img:     imgRGBA,
					OffsetX: offsetX,
					OffsetY: offsetY,
				},
				ok: true,
			}
		}(f)
	}

	go func() {
		wg.Wait()
		close(resChan)
	}()

	for res := range resChan {
		if !res.ok {
			continue
		}
		results[res.idx] = res.m
		okFlags[res.idx] = true
	}

	maps := make([]MapCache, 0, len(files))
	for idx := range results {
		if okFlags[idx] {
			maps = append(maps, results[idx])
		}
	}

	if len(maps) == 0 {
		return nil, fmt.Errorf("no valid map images found in %s", mapDir)
	}

	return maps, nil
}
