package cache

import (
	"os"
	"sync/atomic"
	"time"

	"fyne.io/fyne/v2"
)

var (
	cacheDuration     = 1 * time.Minute
	cleanTaskInterval = cacheDuration / 2

	expiredObjects                = make([]fyne.CanvasObject, 0, 50)
	lastClean                     time.Time
	skippedCleanWithCanvasRefresh = false

	// testing purpose only
	timeNow func() time.Time = time.Now
)

func init() {
	if t, err := time.ParseDuration(os.Getenv("FYNE_CACHE")); err == nil {
		cacheDuration = t
		cleanTaskInterval = cacheDuration / 2
	}
}

// Clean run cache clean task, it should be called on paint events.
func Clean(canvasRefreshed bool) {
	now := timeNow()
	// do not run clean task too fast
	if now.Sub(lastClean) < 10*time.Second {
		if canvasRefreshed {
			skippedCleanWithCanvasRefresh = true
		}
		return
	}
	if skippedCleanWithCanvasRefresh {
		skippedCleanWithCanvasRefresh = false
		canvasRefreshed = true
	}
	if !canvasRefreshed && now.Sub(lastClean) < cleanTaskInterval {
		return
	}
	destroyExpiredSvgs(now)
	if canvasRefreshed {
		// Destroy renderers on canvas refresh to avoid flickering screen.
		destroyExpiredRenderers(now)
		// canvases cache should be invalidated only on canvas refresh, otherwise there wouldn't
		// be a way to recover them later
		destroyExpiredCanvases(now)
	}
	lastClean = timeNow()
}

// CleanCanvas performs a complete remove of all the objects that belong to the specified
// canvas. Usually used to free all objects from a closing windows.
func CleanCanvas(canvas fyne.Canvas) {
	deletingObjs := make([]fyne.CanvasObject, 0, 50)

	canvasesLock.RLock()
	for obj, cinfo := range canvases {
		if cinfo.canvas == canvas {
			deletingObjs = append(deletingObjs, obj)
		}
	}
	canvasesLock.RUnlock()
	if len(deletingObjs) == 0 {
		return
	}

	canvasesLock.Lock()
	for _, dobj := range deletingObjs {
		delete(canvases, dobj)
	}
	canvasesLock.Unlock()

	renderersLock.Lock()
	for _, dobj := range deletingObjs {
		wid, ok := dobj.(fyne.Widget)
		if !ok {
			continue
		}
		winfo, ok := renderers[wid]
		if !ok {
			continue
		}
		winfo.renderer.Destroy()
		delete(renderers, wid)
	}
	renderersLock.Unlock()
}

// CleanCanvases runs cache clean tasks for canvases that are being refreshed. This is called on paint events.
func CleanCanvases(refreshingCanvases []fyne.Canvas) {
	now := timeNow()

	// do not run clean task too fast
	if now.Sub(lastClean) < 10*time.Second {
		return
	}

	if now.Sub(lastClean) < cleanTaskInterval {
		return
	}

	destroyExpiredSvgs(now)

	deletingObjs := make([]fyne.CanvasObject, 0, 50)

	canvasesLock.RLock()
	for obj, cinfo := range canvases {
		if cinfo.isExpired(now) && matchesACanvas(cinfo, refreshingCanvases) {
			deletingObjs = append(deletingObjs, obj)
		}
	}
	canvasesLock.RUnlock()
	if len(deletingObjs) == 0 {
		return
	}

	canvasesLock.Lock()
	for _, dobj := range deletingObjs {
		delete(canvases, dobj)
	}
	canvasesLock.Unlock()

	renderersLock.Lock()
	for _, dobj := range deletingObjs {
		wid, ok := dobj.(fyne.Widget)
		if !ok {
			continue
		}
		rinfo, ok := renderers[wid]
		if !ok {
			continue
		}
		if rinfo.isExpired(now) {
			rinfo.renderer.Destroy()
			delete(renderers, wid)
		}
	}
	renderersLock.Unlock()
	lastClean = timeNow()
}

// ResetThemeCaches clears all the svg and text size cache maps
func ResetThemeCaches() {
	svgs.Range(func(key, value interface{}) bool {
		svgs.Delete(key)
		return true
	})

	fontSizeLock.Lock()
	fontSizeCache = map[fontSizeEntry]fontMetric{}
	fontSizeLock.Unlock()
}

// destroyExpiredCanvases deletes objects from the canvases cache.
func destroyExpiredCanvases(now time.Time) {
	expiredObjects = expiredObjects[:0]
	canvasesLock.RLock()
	for obj, cinfo := range canvases {
		if cinfo.isExpired(now) {
			expiredObjects = append(expiredObjects, obj)
		}
	}
	canvasesLock.RUnlock()
	if len(expiredObjects) > 0 {
		canvasesLock.Lock()
		for i, exp := range expiredObjects {
			delete(canvases, exp)
			expiredObjects[i] = nil
		}
		canvasesLock.Unlock()
	}
}

// destroyExpiredRenderers deletes the renderer from the cache and calls
// renderer.Destroy()
func destroyExpiredRenderers(now time.Time) {
	expiredObjects = expiredObjects[:0]
	renderersLock.RLock()
	for wid, rinfo := range renderers {
		if rinfo.isExpired(now) {
			rinfo.renderer.Destroy()
			expiredObjects = append(expiredObjects, wid)
		}
	}
	renderersLock.RUnlock()
	if len(expiredObjects) > 0 {
		renderersLock.Lock()
		for i, exp := range expiredObjects {
			delete(renderers, exp.(fyne.Widget))
			expiredObjects[i] = nil
		}
		renderersLock.Unlock()
	}
}

// matchesACanvas returns true if the canvas represented by the canvasInfo object matches one of
// the canvases passed in in 'canvases', otherwise false is returned.
func matchesACanvas(cinfo *canvasInfo, canvases []fyne.Canvas) bool {
	canvas := cinfo.canvas

	for _, obj := range canvases {
		if obj == canvas {
			return true
		}
	}
	return false
}

type expiringCache struct {
	expires atomic.Value // time.Time
}

// isExpired check if the cache data is expired.
func (c *expiringCache) isExpired(now time.Time) bool {
	t := c.expires.Load()
	if t == nil {
		return (time.Time{}).Before(now)
	}
	return t.(time.Time).Before(now)
}

// setAlive updates expiration time.
func (c *expiringCache) setAlive() {
	c.expires.Store(timeNow().Add(cacheDuration))
}

type expiringCacheNoLock struct {
	expires time.Time
}

// isExpired check if the cache data is expired.
func (c *expiringCacheNoLock) isExpired(now time.Time) bool {
	return c.expires.Before(now)
}

// setAlive updates expiration time.
func (c *expiringCacheNoLock) setAlive() {
	t := timeNow().Add(cacheDuration)
	c.expires = t
}
