// Copyright 2018 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package atlas

import (
	"fmt"
	"image"
	"math"
	"math/bits"
	"runtime"
	"slices"
	"sync"
	"sync/atomic"

	"github.com/hajimehoshi/ebiten/v2/internal/debug"
	"github.com/hajimehoshi/ebiten/v2/internal/graphics"
	"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
	"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
	"github.com/hajimehoshi/ebiten/v2/internal/packing"
	"github.com/hajimehoshi/ebiten/v2/internal/shaderir"
)

var (
	minSourceSize      = 0
	minDestinationSize = 0
	maxSize            = 0
)

func appendDeferred(f func()) {
	deferredM.Lock()
	defer deferredM.Unlock()
	deferred = append(deferred, f)
}

func flushDeferred() {
	deferredM.Lock()
	fs := deferred
	deferred = nil
	deferredM.Unlock()

	for _, f := range fs {
		f()
	}
}

// baseCountToPutOnSourceBackend represents the base time duration when the image can be put onto an atlas.
// Actual time duration is increased in an exponential way for each usage as a rendering target.
const baseCountToPutOnSourceBackend = 10

func putImagesOnSourceBackend() {
	// The counter usedAsDestinationCount is updated at most once per frame (#2676).
	imagesUsedAsDestination.forEach(func(i *Image) {
		// i.backend can be nil after deallocate is called.
		if i.backend == nil {
			i.usedAsDestinationCount = 0
			return
		}
		// This counter is not updated when the backend is created in this frame.
		if !i.backendCreatedInThisFrame && i.usedAsDestinationCount < math.MaxInt {
			i.usedAsDestinationCount++
		}
		i.backendCreatedInThisFrame = false
	})
	imagesUsedAsDestination.clear()

	imagesToPutOnSourceBackend.forEach(func(i *Image) {
		// i.backend can be nil after deallocate is called.
		if i.backend == nil {
			i.usedAsSourceCount = 0
			return
		}
		if i.usedAsSourceCount < math.MaxInt {
			i.usedAsSourceCount++
		}
		if i.usedAsSourceCount >= baseCountToPutOnSourceBackend*(1<<uint(min(i.usedAsDestinationCount, 31))) {
			i.putOnSourceBackend()
			i.usedAsSourceCount = 0
		}
	})
	imagesToPutOnSourceBackend.clear()
}

// backend is a big texture atlas that can have multiple images.
// backend is a texture in GPU.
type backend struct {
	width  int
	height int

	// backendImage is an implementation backendImage stored in the GPU.
	backendImage *graphicscommand.Image

	// page is an atlas map. Each part is called a node.
	// If page is nil, the backend's image is isolated and not on an atlas.
	page *packing.Page

	// screen indicates whether this backend is used as a screen image or not.
	screen bool

	// source reports whether this backend is mainly used a rendering source, but this is not 100%.
	//
	// If a non-source (destination) image is used as a source many times,
	// the image's backend might be turned into a source backend to optimize draw calls.
	//
	// source is always false when the image type is not ImageTypeRegular.
	source bool

	// sourceInThisFrame reports whether this backend is used as a source in this frame.
	// sourceInThisFrame is reset every frame.
	sourceInThisFrame bool

	// restoreInfo is information for restoration from a context loss.
	restoreInfo restoreInfo
}

type restoreInfo struct {
	valid  bool
	screen bool
	pixels *graphics.ManagedBytes
	region image.Rectangle
}

func (b *backend) tryAlloc(width, height int) (*packing.Node, bool) {
	if b.page == nil {
		return nil, false
	}
	n := b.page.Alloc(width, height)
	if n == nil {
		// The page can't be extended anymore. Return as failure.
		return nil, false
	}

	pageW, pageH := b.page.Size()
	if w, h := b.backendImage.InternalSize(); pageW <= w && pageH <= h {
		return n, true
	}

	// Extend the image.
	newImg := graphicscommand.NewImage(pageW, pageH, b.screen, "")
	clearImage(newImg, image.Rect(0, 0, pageW, pageH))
	src := b.backendImage
	srcs := [graphics.ShaderSrcImageCount]*graphicscommand.Image{src}
	sw, sh := src.InternalSize()
	vs := make([]float32, 4*graphics.VertexFloatCount)
	graphics.QuadVerticesFromDstAndSrc(vs, 0, 0, float32(sw), float32(sh), 0, 0, float32(sw), float32(sh), 1, 1, 1, 1)
	is := graphics.QuadIndices()
	dr := image.Rect(0, 0, sw, sh)
	newImg.DrawTriangles(srcs, vs, is, graphicsdriver.BlendCopy, dr, [graphics.ShaderSrcImageCount]image.Rectangle{dr}, NearestFilterShader.ensureShader(), nil)
	src.Dispose()
	b.backendImage = newImg
	b.width = pageW
	b.height = pageH

	return n, true
}

func (b *backend) clear(region image.Rectangle) {
	if b.backendImage == nil {
		return
	}
	clearImage(b.backendImage, region)
}

func clearImage(dst *graphicscommand.Image, region image.Rectangle) {
	vs := make([]float32, 4*graphics.VertexFloatCount)
	graphics.QuadVerticesFromDstAndSrc(vs, float32(region.Min.X), float32(region.Min.Y), float32(region.Max.X), float32(region.Max.Y), 0, 0, 0, 0, 0, 0, 0, 0)
	is := graphics.QuadIndices()
	dst.DrawTriangles([graphics.ShaderSrcImageCount]*graphicscommand.Image{}, vs, is, graphicsdriver.BlendClear, region, [graphics.ShaderSrcImageCount]image.Rectangle{}, clearShader.ensureShader(), nil)
}

var (
	// backendsM is a mutex for critical sections of the backend and packing.Node objects.
	backendsM sync.Mutex

	// inFrame indicates whether the current state is in between BeginFrame and EndFrame or not.
	// If inFrame is false, function calls on an image should be deferred until the next BeginFrame.
	inFrame bool

	initOnce  sync.Once
	inited    atomic.Bool
	suspended atomic.Bool

	// theBackends is a set of atlases.
	theBackends []*backend

	imagesToPutOnSourceBackend imageSmallSet

	imagesUsedAsDestination imageSmallSet

	deferred []func()

	// deferredM is a mutex for the slice operations. This must not be used for other usages.
	deferredM sync.Mutex
)

// ImageType represents the type of an image.
type ImageType int

const (
	// ImageTypeRegular is a regular image, that can be on a big texture atlas (backend).
	ImageTypeRegular ImageType = iota

	// ImageTypeScreen is a screen image that is not on an atlas.
	// A screen image is also unmanaged.
	ImageTypeScreen

	// ImageTypeUnmanaged is an unmanaged image that is not on an atlas.
	ImageTypeUnmanaged
)

// Image is a rectangle pixel set that might be on an atlas.
type Image struct {
	*imageImpl

	cleanup runtime.Cleanup
}

type imageImpl struct {
	width     int
	height    int
	imageType ImageType

	backend                   *backend
	backendCreatedInThisFrame bool

	node *packing.Node

	// usedAsSourceCount represents how long the image is used as a rendering source and kept not modified with
	// DrawTriangles.
	// In the current implementation, if an image is being modified by DrawTriangles, the image is separated from
	// a backend image for an atlas by ensureIsolatedFromSource.
	//
	// The type is int64 instead of int to avoid overflow when comparing the limitation.
	//
	// usedAsSourceCount is increased if the image is used as a rendering source, or set to 0 if the image is
	// modified.
	//
	// WritePixels doesn't affect this value since WritePixels can be done on images on an atlas.
	usedAsSourceCount int64

	// usedAsDestinationCount represents how many times an image is used as a rendering destination at DrawTriangles.
	// usedAsDestinationCount affects the calculation when to put the image onto a texture atlas again.
	//
	// usedAsDestinationCount is never reset.
	usedAsDestinationCount int
}

// moveTo moves its content to the given image dst.
// After moveTo is called, the image i is no longer available.
//
// moveTo is similar to C++'s move semantics.
func (i *Image) moveTo(dst *Image) {
	dst.deallocateImpl()
	dst.cleanup.Stop()

	impl := *i.imageImpl
	dst.imageImpl = &impl
	if dst.backend != nil {
		dst.cleanup = runtime.AddCleanup(dst, (*imageImpl).cleanup, dst.imageImpl)
	}

	// i is no longer available but the finalizer must not be called
	// since i and dst share the same backend and the same node.
	i.cleanup.Stop()
}

func (i *imageImpl) isOnAtlas() bool {
	return i.node != nil
}

func (i *Image) isOnSourceBackend() bool {
	if i.backend == nil {
		return false
	}
	return i.backend.source
}

func (i *Image) resetUsedAsSourceCount() {
	i.usedAsSourceCount = 0
	imagesToPutOnSourceBackend.remove(i)
}

func (i *imageImpl) paddingSize() int {
	if i.imageType == ImageTypeRegular {
		return 1
	}
	return 0
}

func (i *Image) ensureIsolatedFromSource(backends []*backend) {
	i.resetUsedAsSourceCount()

	// imagesUsedAsDestination affects the counter usedAsDestination.
	// The larger this counter is, the harder it is for the image to be transferred to the source backend.
	imagesUsedAsDestination.add(i)

	if i.backend == nil {
		// `sourceInThisFrame` of `backends` should be true, so `backends` should be in `bs`.
		var bs []*backend
		for _, b := range theBackends {
			if b.sourceInThisFrame {
				bs = append(bs, b)
			}
		}
		i.allocate(bs, false)
		i.backendCreatedInThisFrame = true
		return
	}

	if !i.isOnAtlas() {
		return
	}

	// Check if i has the same backend as the given backends.
	if !slices.ContainsFunc(backends, func(b *backend) bool {
		return i.backend == b
	}) {
		return
	}

	newI := NewImage(i.width, i.height, i.imageType)

	// Call allocate explicitly in order to have an isolated backend from the specified backends.
	// `sourceInThisFrame` of `backends` should be true, so `backends` should be in `bs`.
	bs := []*backend{i.backend}
	for _, b := range theBackends {
		if b.sourceInThisFrame {
			bs = append(bs, b)
		}
	}
	newI.allocate(bs, false)

	w, h := float32(i.width), float32(i.height)
	vs := make([]float32, 4*graphics.VertexFloatCount)
	graphics.QuadVerticesFromDstAndSrc(vs, 0, 0, w, h, 0, 0, w, h, 1, 1, 1, 1)
	is := graphics.QuadIndices()
	dr := image.Rect(0, 0, i.width, i.height)
	sr := image.Rect(0, 0, i.width, i.height)

	newI.drawTriangles([graphics.ShaderSrcImageCount]*Image{i}, vs, is, graphicsdriver.BlendCopy, dr, [graphics.ShaderSrcImageCount]image.Rectangle{sr}, NearestFilterShader, nil)
	newI.moveTo(i)
}

func (i *Image) putOnSourceBackend() {
	if i.backend == nil {
		i.allocate(nil, true)
		return
	}

	if i.isOnSourceBackend() {
		return
	}

	if !i.canBePutOnAtlas() {
		panic("atlas: putOnSourceBackend cannot be called on a image that cannot be on an atlas")
	}

	if i.imageType != ImageTypeRegular {
		panic(fmt.Sprintf("atlas: the image type must be ImageTypeRegular but %d", i.imageType))
	}

	newI := NewImage(i.width, i.height, ImageTypeRegular)
	newI.allocate(nil, true)

	w, h := float32(i.width), float32(i.height)
	vs := make([]float32, 4*graphics.VertexFloatCount)
	graphics.QuadVerticesFromDstAndSrc(vs, 0, 0, w, h, 0, 0, w, h, 1, 1, 1, 1)
	is := graphics.QuadIndices()
	dr := image.Rect(0, 0, i.width, i.height)
	sr := image.Rect(0, 0, i.width, i.height)
	newI.drawTriangles([graphics.ShaderSrcImageCount]*Image{i}, vs, is, graphicsdriver.BlendCopy, dr, [graphics.ShaderSrcImageCount]image.Rectangle{sr}, NearestFilterShader, nil)

	newI.moveTo(i)
	i.usedAsSourceCount = 0

	if !i.isOnSourceBackend() {
		panic("atlas: i must be on a source backend but not")
	}
}

func (i *imageImpl) regionWithPadding() image.Rectangle {
	if i.backend == nil {
		panic("atlas: backend must not be nil: not allocated yet?")
	}
	if !i.isOnAtlas() {
		return image.Rect(0, 0, i.width+i.paddingSize(), i.height+i.paddingSize())
	}
	return i.node.Region()
}

// DrawTriangles draws triangles with the given image.
//
// The vertex floats are:
//
//	0: Destination X in pixels
//	1: Destination Y in pixels
//	2: Source X in pixels (the upper-left is (0, 0))
//	3: Source Y in pixels
//	4: Color R [0.0-1.0]
//	5: Color G
//	6: Color B
//	7: Color Y
func (i *Image) DrawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertices []float32, indices []uint32, blend graphicsdriver.Blend, dstRegion image.Rectangle, srcRegions [graphics.ShaderSrcImageCount]image.Rectangle, shader *Shader, uniforms []uint32) {
	backendsM.Lock()
	defer backendsM.Unlock()

	if !inFrame {
		vs := make([]float32, len(vertices))
		copy(vs, vertices)
		is := make([]uint32, len(indices))
		copy(is, indices)
		us := make([]uint32, len(uniforms))
		copy(us, uniforms)

		appendDeferred(func() {
			i.drawTriangles(srcs, vs, is, blend, dstRegion, srcRegions, shader, us)
		})
		return
	}

	i.drawTriangles(srcs, vertices, indices, blend, dstRegion, srcRegions, shader, uniforms)
}

func (i *Image) drawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertices []float32, indices []uint32, blend graphicsdriver.Blend, dstRegion image.Rectangle, srcRegions [graphics.ShaderSrcImageCount]image.Rectangle, shader *Shader, uniforms []uint32) {
	backends := make([]*backend, 0, len(srcs))
	for _, src := range srcs {
		if src == nil {
			continue
		}
		if src.backend == nil {
			// It is possible to spcify i.backend as a forbidden backend, but this might prevent a good allocation for a source image.
			// If the backend becomes the same as i's, i's backend will be changed at ensureIsolatedFromSource.
			src.allocate(nil, i.imageType == ImageTypeRegular)
		}
		backends = append(backends, src.backend)
		src.backend.sourceInThisFrame = true
	}

	i.ensureIsolatedFromSource(backends)

	for _, src := range srcs {
		// Compare i and source images after ensuring i is not on an atlas, or
		// i and a source image might share the same atlas even though i != src.
		if src != nil && i.backend.backendImage == src.backend.backendImage {
			panic("atlas: Image.DrawTriangles: source must be different from the receiver")
		}
	}

	r := i.regionWithPadding()
	// TODO: Check if dstRegion does not to violate the region.
	dstRegion = dstRegion.Add(r.Min)

	dx, dy := float32(r.Min.X), float32(r.Min.Y)

	var oxf, oyf float32
	if srcs[0] != nil {
		r := srcs[0].regionWithPadding()
		oxf, oyf = float32(r.Min.X), float32(r.Min.Y)
		n := len(vertices)
		for i := 0; i < n; i += graphics.VertexFloatCount {
			vertices[i] += dx
			vertices[i+1] += dy
			vertices[i+2] += oxf
			vertices[i+3] += oyf
		}
		if shader.unit == shaderir.Texels {
			sw, sh := srcs[0].backend.backendImage.InternalSize()
			swf, shf := float32(sw), float32(sh)
			for i := 0; i < n; i += graphics.VertexFloatCount {
				vertices[i+2] /= swf
				vertices[i+3] /= shf
			}
		}
	} else {
		n := len(vertices)
		for i := 0; i < n; i += graphics.VertexFloatCount {
			vertices[i] += dx
			vertices[i+1] += dy
		}
	}

	var imgs [graphics.ShaderSrcImageCount]*graphicscommand.Image
	for i, src := range srcs {
		if src == nil {
			continue
		}

		// A source region can be deliberately empty when this is not needed in order to avoid unexpected
		// performance issue (#1293).
		// TODO: This should no longer be needed but is kept just in case. Remove this later.
		if !srcRegions[i].Empty() {
			r := src.regionWithPadding()
			srcRegions[i] = srcRegions[i].Add(r.Min)
		}
		imgs[i] = src.backend.backendImage
		if !src.isOnSourceBackend() && src.canBePutOnAtlas() {
			// src might already registered, but assigning it again is not harmful.
			imagesToPutOnSourceBackend.add(src)
		}
	}

	i.backend.backendImage.DrawTriangles(imgs, vertices, indices, blend, dstRegion, srcRegions, shader.ensureShader(), uniforms)
}

// WritePixels replaces the pixels on the image.
func (i *Image) WritePixels(pix []byte, region image.Rectangle) {
	backendsM.Lock()
	defer backendsM.Unlock()

	if !inFrame {
		copied := make([]byte, len(pix))
		copy(copied, pix)

		appendDeferred(func() {
			i.writePixels(copied, region)
		})
		return
	}

	i.writePixels(pix, region)
}

func (i *Image) writePixels(pix []byte, region image.Rectangle) {
	if l := 4 * region.Dx() * region.Dy(); len(pix) != l {
		panic(fmt.Sprintf("atlas: len(p) must be %d but %d", l, len(pix)))
	}

	i.resetUsedAsSourceCount()

	if i.backend == nil {
		if pix == nil {
			return
		}
		// Allocate as a source as this image will likely be used as a source.
		i.allocate(nil, i.imageType == ImageTypeRegular)
	}

	r := i.regionWithPadding()

	if !region.Eq(image.Rect(0, 0, i.width, i.height)) || i.paddingSize() == 0 {
		region = region.Add(r.Min)

		if pix == nil {
			i.backend.clear(region)
			return
		}

		// Copy pixels in the case when pix is modified before the graphics command is executed.
		pix2 := graphics.NewManagedBytes(len(pix), func(bs []byte) {
			copy(bs, pix)
		})
		i.backend.backendImage.WritePixels(pix2, region)
		return
	}

	// TODO: These loops assume that paddingSize is 1.
	// TODO: Is clearing edges explicitly really needed?
	const paddingSize = 1
	if paddingSize != i.paddingSize() {
		panic(fmt.Sprintf("atlas: writePixels assumes the padding is always 1 but the actual padding was %d", i.paddingSize()))
	}

	pixb := graphics.NewManagedBytes(4*r.Dx()*r.Dy(), func(bs []byte) {
		// Clear the edges. bs might not be zero-cleared.
		rowPixels := 4 * r.Dx()
		for i := range rowPixels {
			bs[rowPixels*(r.Dy()-1)+i] = 0
		}
		for j := 1; j < r.Dy(); j++ {
			bs[rowPixels*j-4] = 0
			bs[rowPixels*j-3] = 0
			bs[rowPixels*j-2] = 0
			bs[rowPixels*j-1] = 0
		}

		// Copy the content.
		for j := 0; j < region.Dy(); j++ {
			copy(bs[4*j*r.Dx():], pix[4*j*region.Dx():4*(j+1)*region.Dx()])
		}
	})
	i.backend.backendImage.WritePixels(pixb, r)
}

func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte, region image.Rectangle) (ok bool, err error) {
	backendsM.Lock()
	defer backendsM.Unlock()

	if !inFrame {
		// Not ready to read pixels. Try this later.
		return false, nil
	}

	// In the tests, BeginFrame might not be called often and then images might not be disposed (#2292).
	// To prevent memory leaks, flush the deferred functions here.
	flushDeferred()

	if i.backend == nil || i.backend.backendImage == nil {
		for i := range pixels {
			pixels[i] = 0
		}
		return true, nil
	}

	args := []graphicsdriver.PixelsArgs{
		{
			Pixels: pixels,
			Region: region.Add(i.regionWithPadding().Min),
		},
	}
	if err := i.backend.backendImage.ReadPixels(graphicsDriver, args); err != nil {
		return false, err
	}
	return true, nil
}

// Deallocate deallocates the internal state.
// Even after this call, the image is still available as a new cleared image.
func (i *Image) Deallocate() {
	i.cleanup.Stop()

	backendsM.Lock()
	defer backendsM.Unlock()

	if !inFrame {
		appendDeferred(func() {
			i.deallocateImpl()
		})
		return
	}

	i.deallocateImpl()
}

func (i *imageImpl) deallocateImpl() {
	defer func() {
		i.backend = nil
		i.node = nil
	}()

	i.usedAsSourceCount = 0
	i.usedAsDestinationCount = 0

	if i.backend == nil {
		// Not allocated yet.
		return
	}

	if i.isOnAtlas() {
		i.backend.page.Free(i.node)
		if !i.backend.page.IsEmpty() {
			// As this part can be reused, this should be cleared explicitly.
			r := i.regionWithPadding()
			i.backend.clear(r)
			return
		}
	}

	i.backend.backendImage.Dispose()
	i.backend.backendImage = nil
	i.backend.restoreInfo = restoreInfo{}

	for idx, sh := range theBackends {
		if sh == i.backend {
			copy(theBackends[idx:], theBackends[idx+1:])
			theBackends[len(theBackends)-1] = nil
			theBackends = theBackends[:len(theBackends)-1]
			return
		}
	}

	panic("atlas: backend not found at an image being deallocated")
}

func NewImage(width, height int, imageType ImageType) *Image {
	// Actual allocation is done lazily, and the lock is not needed.
	return &Image{
		imageImpl: &imageImpl{
			width:     width,
			height:    height,
			imageType: imageType,
		},
	}
}

func (i *Image) canBePutOnAtlas() bool {
	if minSourceSize == 0 || minDestinationSize == 0 || maxSize == 0 {
		panic("atlas: min*Size or maxSize must be initialized")
	}
	if i.imageType != ImageTypeRegular {
		return false
	}
	return i.width+i.paddingSize() <= maxSize && i.height+i.paddingSize() <= maxSize
}

func (i *imageImpl) cleanup() {
	// A function from finalizer must not be blocked, but disposing operation can be blocked.
	// Defer this operation until it becomes safe. (#913)
	appendDeferred(func() {
		i.deallocateImpl()
	})
}

func (i *Image) allocate(forbiddenBackends []*backend, asSource bool) {
	if i.backend != nil {
		panic("atlas: the image is already allocated")
	}

	i.cleanup = runtime.AddCleanup(i, (*imageImpl).cleanup, i.imageImpl)

	if i.imageType == ImageTypeScreen {
		if asSource {
			panic("atlas: a screen image cannot be created as a source")
		}
		// A screen image doesn't have a padding.
		i.backend = &backend{
			width:        i.width,
			height:       i.height,
			backendImage: graphicscommand.NewImage(i.width, i.height, true, ""),
			screen:       true,
		}
		i.backend.clear(image.Rect(0, 0, i.width, i.height))
		theBackends = append(theBackends, i.backend)
		return
	}

	wp := i.width + i.paddingSize()
	hp := i.height + i.paddingSize()

	if !i.canBePutOnAtlas() {
		if wp > maxSize || hp > maxSize {
			panic(fmt.Sprintf("atlas: the image being put on an atlas is too big: width: %d, height: %d", i.width, i.height))
		}

		i.backend = &backend{
			width:        wp,
			height:       hp,
			backendImage: graphicscommand.NewImage(wp, hp, false, ""),
			source:       asSource,
		}
		i.backend.clear(image.Rect(0, 0, wp, hp))
		theBackends = append(theBackends, i.backend)
		return
	}

	// Check if an existing backend is available.
loop:
	for _, b := range theBackends {
		if b.source != asSource {
			continue
		}
		for _, bb := range forbiddenBackends {
			if b == bb {
				continue loop
			}
		}

		if n, ok := b.tryAlloc(wp, hp); ok {
			i.backend = b
			i.node = n
			return
		}
	}

	var width, height int
	if asSource {
		width, height = minSourceSize, minSourceSize
	} else {
		width, height = minDestinationSize, minDestinationSize
	}
	for wp > width {
		if width == maxSize {
			panic(fmt.Sprintf("atlas: the image being put on an atlas is too big: width: %d, height: %d", i.width, i.height))
		}
		width *= 2
	}
	for hp > height {
		if height == maxSize {
			panic(fmt.Sprintf("atlas: the image being put on an atlas is too big: width: %d, height: %d", i.width, i.height))
		}
		height *= 2
	}

	b := &backend{
		width:        width,
		height:       height,
		backendImage: graphicscommand.NewImage(width, height, false, ""),
		page:         packing.NewPage(width, height, maxSize),
		source:       asSource,
	}
	b.clear(image.Rect(0, 0, width, height))
	theBackends = append(theBackends, b)

	n := b.page.Alloc(wp, hp)
	if n == nil {
		panic("atlas: Alloc result must not be nil at allocate")
	}
	i.backend = b
	i.node = n
}

func (i *Image) DumpScreenshot(graphicsDriver graphicsdriver.Graphics, path string, blackbg bool) (string, error) {
	backendsM.Lock()
	defer backendsM.Unlock()

	if !inFrame {
		panic("atlas: DumpScreenshots must be called in between BeginFrame and EndFrame")
	}

	return i.backend.backendImage.Dump(graphicsDriver, path, blackbg, image.Rect(0, 0, i.width, i.height))
}

func EndFrame(graphicsDriver graphicsdriver.Graphics) error {
	backendsM.Lock()
	defer backendsM.Unlock()
	defer func() {
		inFrame = false
	}()

	if !inFrame {
		panic("atlas: inFrame must be true in EndFrame")
	}

	for _, b := range theBackends {
		b.sourceInThisFrame = false
	}

	if theGPUResourcesState.isSavingGPUResourcesRequested() {
		defer theGPUResourcesState.finishSavingGPUResources()

		flushDeferred()
		for _, b := range theBackends {
			if b.backendImage == nil {
				continue
			}
			var pixels *graphics.ManagedBytes
			var region image.Rectangle
			if !b.screen {
				if b.page != nil {
					region = b.page.AllocatedRegion()
				} else {
					// If a page doesn't exist, the image is not on an atlas.
					region = image.Rect(0, 0, b.width, b.height)
				}
				if !region.Empty() {
					var err error
					pixels = graphics.NewManagedBytes(4*region.Dx()*region.Dy(), func(bytes []byte) {
						args := []graphicsdriver.PixelsArgs{
							{
								Pixels: bytes,
								Region: region,
							},
						}
						err = b.backendImage.ReadPixels(graphicsDriver, args)
					})
					if err != nil {
						return err
					}
				}
			}
			b.restoreInfo = restoreInfo{
				valid:  true,
				screen: b.screen,
				pixels: pixels,
				region: region,
			}
		}
	}

	return nil
}

func SwapBuffers(graphicsDriver graphicsdriver.Graphics) error {
	backendsM.Lock()
	defer backendsM.Unlock()

	if inFrame {
		panic("atlas: inFrame must be false in SwapBuffer")
	}

	if debug.IsDebug {
		debug.FrameLogf("Internal image sizes:\n")
		imgs := make([]*graphicscommand.Image, 0, len(theBackends))
		for _, img := range theBackends {
			if img.backendImage == nil {
				continue
			}
			imgs = append(imgs, img.backendImage)
		}
		graphicscommand.LogImagesInfo(imgs)
	}

	if err := graphicscommand.FlushCommands(graphicsDriver, true); err != nil {
		return err
	}

	return nil
}

func floorPowerOf2(x int) int {
	if x <= 0 {
		return 0
	}
	return 1 << (bits.Len(uint(x)) - 1)
}

func BeginFrame(graphicsDriver graphicsdriver.Graphics) error {
	backendsM.Lock()
	defer backendsM.Unlock()

	if inFrame {
		panic("atlas: inFrame must be false in BeginFrame")
	}

	inFrame = true

	var err error
	initOnce.Do(func() {
		err = graphicscommand.InitializeGraphicsDriverState(graphicsDriver)
		if err != nil {
			return
		}
		if len(theBackends) != 0 {
			panic("atlas: all the images must be not on an atlas before the game starts")
		}

		// min*Size and maxSize can already be set for testings.
		if minSourceSize == 0 {
			minSourceSize = 1024
		}
		if minDestinationSize == 0 {
			minDestinationSize = 16
		}
		if maxSize == 0 {
			maxSize = floorPowerOf2(graphicscommand.MaxImageSize(graphicsDriver))
		}
		inited.Store(true)
	})
	if err != nil {
		return err
	}

	if theGPUResourcesState.startRestoringGPUResourcesIfNeeded() {
		if err := graphicscommand.ResetGraphicsDriverState(graphicsDriver); err != nil {
			return err
		}

		// Dispose all the internal shaders.
		// The current internal shader objects might have references to old GPU resources.
		theShadersWithInternalShader.deallocateInternalShaders()

		// Dispose all the images.
		// The current internal image objects might have references to old GPU resources.
		for _, b := range theBackends {
			b.backendImage.Dispose()
			b.backendImage = nil
		}

		// Restore all the images.
		for _, b := range theBackends {
			if !b.restoreInfo.valid {
				continue
			}
			b.backendImage = graphicscommand.NewImage(b.width, b.height, b.restoreInfo.screen, "")
			if b.restoreInfo.region != image.Rect(0, 0, b.width, b.height) {
				b.clear(image.Rect(0, 0, b.width, b.height))
			}
			if b.restoreInfo.pixels != nil {
				b.backendImage.WritePixels(b.restoreInfo.pixels, b.restoreInfo.region)
			}
		}
	}

	// Discard restore info whichever restoration is done or not.
	for _, b := range theBackends {
		// Do not release pixels here, as the command buffer might not be flushed yet.
		b.restoreInfo = restoreInfo{}
	}

	flushDeferred()
	putImagesOnSourceBackend()

	return nil
}

func DumpImages(graphicsDriver graphicsdriver.Graphics, dir string) (string, error) {
	backendsM.Lock()
	defer backendsM.Unlock()

	if !inFrame {
		panic("atlas: DumpImages must be called in between BeginFrame and EndFrame")
	}

	images := make([]*graphicscommand.Image, 0, len(theBackends))
	for _, backend := range theBackends {
		if backend.backendImage == nil {
			continue
		}
		images = append(images, backend.backendImage)
	}
	return graphicscommand.DumpImages(images, graphicsDriver, dir)
}

func TotalGPUImageMemoryUsageInBytes() int64 {
	backendsM.Lock()
	defer backendsM.Unlock()

	var sum int64
	for _, b := range theBackends {
		if b.backendImage == nil {
			continue
		}
		w, h := b.backendImage.InternalSize()
		sum += 4 * int64(w) * int64(h)
	}
	return sum
}

// SaveGPUResources saves GPU resources (textures and shaders) in CPU memory.
//
// Until GPU resources are saved, the app becomes suspended
// until [RestoreGPUResources] or [ResumeApp] is called.
//
// If [RestoreGPUResources] is called, the saved resources are restored in the next frame.
// If [ResumeApp] is called but not [RestoreGPUResources], the saved resources are discarded.
func SaveGPUResources() {
	if !inited.Load() {
		return
	}
	// Suspend the app. [IsSuspended] will return true after the GPU resources are actually saved.
	suspended.Store(true)
	theGPUResourcesState.requestToSaveGPUResources()
}

// IsSuspended reports whether the app is currently suspended.
func IsSuspended() bool {
	return suspended.Load() && theGPUResourcesState.areGPUResourcesSaved()
}

// AreGPUResourcesSaved reports whether GPU resources are currently saved.
func AreGPUResourcesSaved() bool {
	return theGPUResourcesState.areGPUResourcesSaved()
}

// ResumeApp unfreezes the app previously frozen by [SaveGPUResources].
func ResumeApp() {
	suspended.Store(false)
}

// RestoreGPUResources restores GPU resources in the next frame if needed.
// RestoreGPUResources reports whether there are GPU resources to be restored or not.
func RestoreGPUResources() bool {
	suspended.Store(false)
	return theGPUResourcesState.requestToRestoreGPUResources()
}
