// Copyright (c) 2022-2024 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

package main

import (
	"fmt"
	"image"
	"image/color"
	"sync/atomic"
	"time"
)

// The default background color.
// We use a non transparent black.
var colorBackground = color.RGBA{0, 0, 0, 255}

// StitchedImageCacheGridSize defines the worker chunk size when the cache image is regenerated.
var StitchedImageCacheGridSize = 256

// StitchedImageBlendMethod defines how tiles are blended together.
type StitchedImageBlendMethod interface {
	Draw(tiles []*ImageTile, destImage *image.RGBA) // Draw is called when a new cache image is generated.
}

// StitchedImageOverlay defines an interface for arbitrary overlays that can be drawn over the stitched image.
type StitchedImageOverlay interface {
	Draw(*image.RGBA)
}

// StitchedImage combines several ImageTile objects into a single RGBA image.
// The way the images are combined/blended is defined by the blendFunc.
type StitchedImage struct {
	tiles       ImageTiles
	bounds      image.Rectangle
	blendMethod StitchedImageBlendMethod
	overlays    []StitchedImageOverlay

	cacheRowHeight  int
	cacheRows       []StitchedImageCache
	cacheRowYOffset int // Defines the pixel offset of the first cache row.

	oldCacheRowIndex int
	queryCounter     atomic.Int64
}

// NewStitchedImage creates a new image from several single image tiles.
func NewStitchedImage(tiles ImageTiles, bounds image.Rectangle, blendMethod StitchedImageBlendMethod, cacheRowHeight int, overlays []StitchedImageOverlay) (*StitchedImage, error) {
	if bounds.Empty() {
		return nil, fmt.Errorf("given boundaries are empty")
	}
	if blendMethod == nil {
		return nil, fmt.Errorf("no blending method given")
	}
	if cacheRowHeight <= 0 {
		return nil, fmt.Errorf("invalid cache row height of %d pixels", cacheRowHeight)
	}

	stitchedImage := &StitchedImage{
		tiles:       tiles,
		bounds:      bounds,
		blendMethod: blendMethod,
		overlays:    overlays,
	}

	// Generate cache image rows.
	maxRow := (bounds.Dy() - 1) / cacheRowHeight
	var cacheRows []StitchedImageCache
	for i := 0; i <= maxRow; i++ {
		rect := image.Rect(bounds.Min.X, bounds.Min.Y+i*cacheRowHeight, bounds.Max.X, bounds.Min.Y+(i+1)*cacheRowHeight)
		cacheRows = append(cacheRows, NewStitchedImageCache(stitchedImage, rect.Intersect(bounds)))
	}
	stitchedImage.cacheRowHeight = cacheRowHeight
	stitchedImage.cacheRowYOffset = -bounds.Min.Y
	stitchedImage.cacheRows = cacheRows

	// Start ticker to automatically invalidate caches.
	// Due to this, the stitchedImage object is not composable, as this goroutine will always have a reference.
	go func() {
		ticker := time.NewTicker(1 * time.Second)
		for range ticker.C {
			for rowIndex := range stitchedImage.cacheRows {
				stitchedImage.cacheRows[rowIndex].InvalidateAuto(3) // Invalidate cache row after 3 seconds of being idle.
			}
		}
	}()

	return stitchedImage, nil
}

// ColorModel returns the Image's color model.
func (si *StitchedImage) ColorModel() color.Model {
	return color.RGBAModel
}

// Bounds returns the domain for which At can return non-zero color.
// The bounds do not necessarily contain the point (0, 0).
func (si *StitchedImage) Bounds() image.Rectangle {
	return si.bounds
}

func (si *StitchedImage) At(x, y int) color.Color {
	return si.RGBAAt(x, y)
}

// At returns the color of the pixel at (x, y).
//
// This is optimized to be read line by line (scanning), it will be much slower with random access.
//
// For the `Progress()` method to work correctly, every pixel should be queried exactly once.
//
//	At(Bounds().Min.X, Bounds().Min.Y) // returns the top-left pixel of the image.
//	At(Bounds().Max.X-1, Bounds().Max.Y-1) // returns the bottom-right pixel.
func (si *StitchedImage) RGBAAt(x, y int) color.RGBA {
	// Assume that every pixel is only queried once.
	si.queryCounter.Add(1)

	// Determine the cache rowIndex index.
	rowIndex := (y + si.cacheRowYOffset) / si.cacheRowHeight
	if rowIndex < 0 || rowIndex >= len(si.cacheRows) {
		return colorBackground
	}

	// Check if we advanced/changed the row index.
	// This doesn't happen a lot, so stuff inside this can be a bit more expensive.
	if si.oldCacheRowIndex != rowIndex {
		// Pre generate the new row asynchronously.
		newRowIndex := rowIndex + 1
		if newRowIndex >= 0 && newRowIndex < len(si.cacheRows) {
			go si.cacheRows[newRowIndex].Regenerate()
		}

		// Invalidate all tiles that are above the next row.
		si.tiles.InvalidateAboveY((rowIndex+1)*si.cacheRowHeight - si.cacheRowYOffset)

		si.oldCacheRowIndex = rowIndex
	}

	return si.cacheRows[rowIndex].RGBAAt(x, y)
}

// Opaque returns whether the image is fully opaque.
//
// For more speed and smaller file size, StitchedImage will be marked as non-transparent.
// This will speed up image saving by 2x, as there is no need to iterate over the whole image just to find a single non opaque pixel.
func (si *StitchedImage) Opaque() bool {
	return true
}

// Progress returns the approximate progress of any process that scans the image from top to bottom.
func (si *StitchedImage) Progress() (value, max int) {
	size := si.Bounds().Size()

	return int(si.queryCounter.Load()), size.X * size.Y
}

// SubStitchedImage returns an image representing the portion of the image p visible through r.
// The returned image references to the original stitched image, and therefore reuses its cache.
func (si *StitchedImage) SubStitchedImage(r image.Rectangle) SubStitchedImage {
	return SubStitchedImage{
		StitchedImage: si,
		bounds:        si.Bounds().Intersect(r),
	}
}
