package common

import (
	"image/color"
	"reflect"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/internal"
	"fyne.io/fyne/v2/internal/app"
	"fyne.io/fyne/v2/internal/async"
	"fyne.io/fyne/v2/internal/cache"
	"fyne.io/fyne/v2/internal/driver"
	"fyne.io/fyne/v2/internal/painter/gl"
	"fyne.io/fyne/v2/internal/theme"
)

// SizeableCanvas defines a canvas with size related functions.
type SizeableCanvas interface {
	fyne.Canvas
	Resize(fyne.Size)
	MinSize() fyne.Size
}

// Canvas defines common canvas implementation.
type Canvas struct {
	OnFocus   func(obj fyne.Focusable)
	OnUnfocus func()

	impl SizeableCanvas

	contentFocusMgr *app.FocusManager
	menuFocusMgr    *app.FocusManager
	overlays        *overlayStack

	shortcut fyne.ShortcutHandler

	painter gl.Painter

	// Any object that requests to enter to the refresh queue should
	// not be omitted as it is always a rendering task's decision
	// for skipping frames or drawing calls.
	//
	// If an object failed to ender the refresh queue, the object may
	// disappear or blink from the view at any frames. As of this reason,
	// the refreshQueue is an unbounded queue which is able to cache
	// arbitrary number of fyne.CanvasObject for the rendering.
	refreshQueue *async.CanvasObjectQueue
	dirty        bool

	mWindowHeadTree, contentTree, menuTree *renderCacheTree
}

// AddShortcut adds a shortcut to the canvas.
func (c *Canvas) AddShortcut(shortcut fyne.Shortcut, handler func(shortcut fyne.Shortcut)) {
	c.shortcut.AddShortcut(shortcut, handler)
}

func (c *Canvas) DrawDebugOverlay(obj fyne.CanvasObject, pos fyne.Position, size fyne.Size) {
	switch obj.(type) {
	case fyne.Widget:
		r := canvas.NewRectangle(color.Transparent)
		r.StrokeColor = color.NRGBA{R: 0xcc, G: 0x33, B: 0x33, A: 0xff}
		r.StrokeWidth = 1
		r.Resize(obj.Size())
		c.Painter().Paint(r, pos, size)

		t := canvas.NewText(reflect.ValueOf(obj).Elem().Type().Name(), r.StrokeColor)
		t.TextSize = 10
		c.Painter().Paint(t, pos.AddXY(2, 2), size)
	case *fyne.Container:
		r := canvas.NewRectangle(color.Transparent)
		r.StrokeColor = color.NRGBA{R: 0x33, G: 0x33, B: 0xcc, A: 0xff}
		r.StrokeWidth = 1
		r.Resize(obj.Size())
		c.Painter().Paint(r, pos, size)
	}
}

// EnsureMinSize ensure canvas min size.
//
// This function uses lock.
func (c *Canvas) EnsureMinSize() bool {
	if c.impl.Content() == nil {
		return false
	}
	windowNeedsMinSizeUpdate := false
	csize := c.impl.Size()
	min := c.impl.MinSize()

	var parentNeedingUpdate *RenderCacheNode

	setup := func(node *RenderCacheNode, pos fyne.Position) {
		if !node.obj.Visible() {
			return
		}
		if th, ok := node.Obj().(*container.ThemeOverride); ok {
			theme.PushRenderingTheme(th.Theme)
		}
	}
	ensureMinSize := func(node *RenderCacheNode, pos fyne.Position) {
		obj := node.obj
		cache.SetCanvasForObject(obj, c.impl, func() {
			if img, ok := obj.(*canvas.Image); ok {
				img.Refresh() // this may now have a different texScale
			}
		})

		if parentNeedingUpdate == node {
			c.updateLayout(obj)
			parentNeedingUpdate = nil
		}

		if !obj.Visible() {
			return
		}
		minSize := obj.MinSize()

		minSizeChanged := node.minSize != minSize
		if minSizeChanged {
			node.minSize = minSize
			if node.parent != nil {
				parentNeedingUpdate = node.parent
			} else {
				windowNeedsMinSizeUpdate = true
				size := obj.Size()
				expectedSize := minSize.Max(size)
				if expectedSize != size && size != csize {
					obj.Resize(expectedSize)
				} else {
					c.updateLayout(obj)
				}
			}
		}

		if _, ok := node.Obj().(*container.ThemeOverride); ok {
			theme.PopRenderingTheme()
		}
	}
	c.WalkTrees(setup, ensureMinSize)

	shouldResize := windowNeedsMinSizeUpdate && (csize.Width < min.Width || csize.Height < min.Height)
	if shouldResize {
		c.impl.Resize(csize.Max(min))
	}
	return windowNeedsMinSizeUpdate
}

// Focus makes the provided item focused.
func (c *Canvas) Focus(obj fyne.Focusable) {
	focusMgr := c.focusManager()
	if focusMgr != nil && focusMgr.Focus(obj) { // fast path – probably >99.9% of all cases
		if c.OnFocus != nil {
			c.OnFocus(obj)
		}
		return
	}

	focusMgrs := append([]*app.FocusManager{c.contentFocusMgr, c.menuFocusMgr}, c.overlays.ListFocusManagers()...)

	for _, mgr := range focusMgrs {
		if mgr == nil {
			continue
		}
		if focusMgr != mgr {
			if mgr.Focus(obj) {
				if c.OnFocus != nil {
					c.OnFocus(obj)
				}
				return
			}
		}
	}

	fyne.LogError("Failed to focus object which is not part of the canvas’ content, menu or overlays.", nil)
}

// Focused returns the current focused object.
func (c *Canvas) Focused() fyne.Focusable {
	mgr := c.focusManager()
	if mgr == nil {
		return nil
	}
	return mgr.Focused()
}

// FocusGained signals to the manager that its content got focus.
// Valid only on Desktop.
func (c *Canvas) FocusGained() {
	mgr := c.focusManager()
	if mgr == nil {
		return
	}
	mgr.FocusGained()
}

// FocusLost signals to the manager that its content lost focus.
// Valid only on Desktop.
func (c *Canvas) FocusLost() {
	mgr := c.focusManager()
	if mgr == nil {
		return
	}
	mgr.FocusLost()
}

// FocusNext focuses the next focusable item.
func (c *Canvas) FocusNext() {
	mgr := c.focusManager()
	if mgr == nil {
		return
	}
	mgr.FocusNext()
}

// FocusPrevious focuses the previous focusable item.
func (c *Canvas) FocusPrevious() {
	mgr := c.focusManager()
	if mgr == nil {
		return
	}
	mgr.FocusPrevious()
}

// FreeDirtyTextures frees dirty textures and returns the number of freed textures.
func (c *Canvas) FreeDirtyTextures() (freed uint64) {
	freeObject := func(object fyne.CanvasObject) {
		freeWalked := func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {
			// No image refresh while recursing to avoid double texture upload.
			if _, ok := obj.(*canvas.Image); ok {
				return false
			}
			if c.painter != nil {
				c.painter.Free(obj)
			}
			return false
		}

		// Image.Refresh will trigger a refresh specific to the object, while recursing on parent widget would just lead to
		// a double texture upload.
		if img, ok := object.(*canvas.Image); ok {
			if c.painter != nil {
				c.painter.Free(img)
			}
		} else {
			driver.WalkCompleteObjectTree(object, freeWalked, nil)
		}
	}

	// Within a frame, refresh tasks are requested from the Refresh method,
	// and we desire to clear out all requested operations within a frame.
	// See https://github.com/fyne-io/fyne/issues/2548.
	tasksToDo := c.refreshQueue.Len()

	shouldFilterDuplicates := (tasksToDo > 200) // filtering has overhead, not worth enabling for few tasks
	var refreshSet map[fyne.CanvasObject]struct{}
	if shouldFilterDuplicates {
		refreshSet = make(map[fyne.CanvasObject]struct{})
	}

	for c.refreshQueue.Len() > 0 {
		object := c.refreshQueue.Out()
		if !shouldFilterDuplicates {
			freed++
			freeObject(object)
		} else {
			refreshSet[object] = struct{}{}
			tasksToDo--
			if tasksToDo == 0 {
				shouldFilterDuplicates = false // stop collecting messages to avoid starvation
				for object := range refreshSet {
					freed++
					freeObject(object)
				}
			}
		}
	}

	if c.painter != nil {
		cache.RangeExpiredTexturesFor(c.impl, c.painter.Free)
	}
	return
}

// Initialize initializes the canvas.
func (c *Canvas) Initialize(impl SizeableCanvas, onOverlayChanged func()) {
	c.impl = impl
	c.refreshQueue = async.NewCanvasObjectQueue()
	c.overlays = &overlayStack{
		OverlayStack: internal.OverlayStack{
			OnChange: onOverlayChanged,
			Canvas:   impl,
		},
	}
}

// ObjectTrees return canvas object trees.
//
// This function uses lock.
func (c *Canvas) ObjectTrees() []fyne.CanvasObject {
	var content, menu fyne.CanvasObject
	if c.contentTree != nil && c.contentTree.root != nil {
		content = c.contentTree.root.obj
	}
	if c.menuTree != nil && c.menuTree.root != nil {
		menu = c.menuTree.root.obj
	}
	trees := make([]fyne.CanvasObject, 0, len(c.Overlays().List())+2)
	trees = append(trees, content)
	if menu != nil {
		trees = append(trees, menu)
	}
	trees = append(trees, c.Overlays().List()...)
	return trees
}

// Overlays returns the overlay stack.
func (c *Canvas) Overlays() fyne.OverlayStack {
	// we don't need to lock here, because overlays never changes
	return c.overlays
}

// Painter returns the canvas painter.
func (c *Canvas) Painter() gl.Painter {
	return c.painter
}

// Refresh refreshes a canvas object.
func (c *Canvas) Refresh(obj fyne.CanvasObject) {
	c.refreshQueue.In(obj)
	async.EnsureMain(c.SetDirty)
}

// RemoveShortcut removes a shortcut from the canvas.
func (c *Canvas) RemoveShortcut(shortcut fyne.Shortcut) {
	c.shortcut.RemoveShortcut(shortcut)
}

// SetContentTreeAndFocusMgr sets content tree and focus manager.
//
// This function does not use the canvas lock.
func (c *Canvas) SetContentTreeAndFocusMgr(content fyne.CanvasObject) {
	c.contentTree = &renderCacheTree{root: &RenderCacheNode{obj: content}}
	var focused fyne.Focusable
	if c.contentFocusMgr != nil {
		focused = c.contentFocusMgr.Focused() // keep old focus if possible
	}
	c.contentFocusMgr = app.NewFocusManager(content)
	if focused != nil {
		c.contentFocusMgr.Focus(focused)
	}
}

// CheckDirtyAndClear returns true if the canvas is dirty and
// clears the dirty state atomically.
func (c *Canvas) CheckDirtyAndClear() bool {
	wasDirty := c.dirty
	c.dirty = false
	return wasDirty
}

// SetDirty sets canvas dirty flag atomically.
func (c *Canvas) SetDirty() {
	c.dirty = true
}

// SetMenuTreeAndFocusMgr sets menu tree and focus manager.
//
// This function does not use the canvas lock.
func (c *Canvas) SetMenuTreeAndFocusMgr(menu fyne.CanvasObject) {
	c.menuTree = &renderCacheTree{root: &RenderCacheNode{obj: menu}}
	if menu != nil {
		c.menuFocusMgr = app.NewFocusManager(menu)
	} else {
		c.menuFocusMgr = nil
	}
}

// SetMobileWindowHeadTree sets window head tree.
//
// This function does not use the canvas lock.
func (c *Canvas) SetMobileWindowHeadTree(head fyne.CanvasObject) {
	c.mWindowHeadTree = &renderCacheTree{root: &RenderCacheNode{obj: head}}
}

// SetPainter sets the canvas painter.
func (c *Canvas) SetPainter(p gl.Painter) {
	c.painter = p
}

// TypedShortcut handle the registered shortcut.
func (c *Canvas) TypedShortcut(shortcut fyne.Shortcut) {
	c.shortcut.TypedShortcut(shortcut)
}

// Unfocus unfocuses all the objects in the canvas.
func (c *Canvas) Unfocus() {
	mgr := c.focusManager()
	if mgr == nil {
		return
	}
	if mgr.Focus(nil) && c.OnUnfocus != nil {
		c.OnUnfocus()
	}
}

// WalkTrees walks over the trees.
func (c *Canvas) WalkTrees(
	beforeChildren func(*RenderCacheNode, fyne.Position),
	afterChildren func(*RenderCacheNode, fyne.Position),
) {
	c.walkTree(c.contentTree, beforeChildren, afterChildren)
	if c.mWindowHeadTree != nil && c.mWindowHeadTree.root.obj != nil {
		c.walkTree(c.mWindowHeadTree, beforeChildren, afterChildren)
	}
	if c.menuTree != nil && c.menuTree.root.obj != nil {
		c.walkTree(c.menuTree, beforeChildren, afterChildren)
	}
	for _, tree := range c.overlays.renderCaches {
		if tree != nil {
			c.walkTree(tree, beforeChildren, afterChildren)
		}
	}
}

func (c *Canvas) focusManager() *app.FocusManager {
	if focusMgr := c.overlays.TopFocusManager(); focusMgr != nil {
		return focusMgr
	}
	if c.isMenuActive() {
		return c.menuFocusMgr
	}
	return c.contentFocusMgr
}

func (c *Canvas) isMenuActive() bool {
	if c.menuTree == nil || c.menuTree.root == nil || c.menuTree.root.obj == nil {
		return false
	}
	menu := c.menuTree.root.obj
	if am, ok := menu.(activatableMenu); ok {
		return am.IsActive()
	}
	return true
}

func (c *Canvas) walkTree(
	tree *renderCacheTree,
	beforeChildren func(*RenderCacheNode, fyne.Position),
	afterChildren func(*RenderCacheNode, fyne.Position),
) {
	var node, parent, prev *RenderCacheNode
	node = tree.root

	bc := func(obj fyne.CanvasObject, pos fyne.Position, _ fyne.Position, _ fyne.Size) bool {
		if node != nil && node.obj != obj {
			if parent.firstChild == node {
				parent.firstChild = nil
			}
			node = nil
		}
		if node == nil {
			node = &RenderCacheNode{parent: parent, obj: obj}
			if parent.firstChild == nil {
				parent.firstChild = node
			} else {
				prev.nextSibling = node
			}
		}
		if prev != nil && prev.parent != parent {
			prev = nil
		}

		if beforeChildren != nil {
			beforeChildren(node, pos)
		}

		parent = node
		node = parent.firstChild
		return false
	}
	ac := func(obj fyne.CanvasObject, pos fyne.Position, _ fyne.CanvasObject) {
		node = parent
		parent = node.parent
		if prev != nil && prev.parent != parent {
			prev.nextSibling = nil
		}

		if afterChildren != nil {
			afterChildren(node, pos)
		}

		prev = node
		node = node.nextSibling
	}
	driver.WalkVisibleObjectTree(tree.root.obj, bc, ac)
}

// RenderCacheNode represents a node in a render cache tree.
type RenderCacheNode struct {
	// structural data
	firstChild  *RenderCacheNode
	nextSibling *RenderCacheNode
	obj         fyne.CanvasObject
	parent      *RenderCacheNode
	// cache data
	minSize fyne.Size
}

// Obj returns the node object.
func (r *RenderCacheNode) Obj() fyne.CanvasObject {
	return r.obj
}

type activatableMenu interface {
	IsActive() bool
}

type overlayStack struct {
	internal.OverlayStack

	renderCaches []*renderCacheTree
}

func (o *overlayStack) Add(overlay fyne.CanvasObject) {
	if overlay == nil {
		return
	}
	o.add(overlay)
}

func (o *overlayStack) Remove(overlay fyne.CanvasObject) {
	if overlay == nil || len(o.List()) == 0 {
		return
	}
	o.remove(overlay)
}

func (o *overlayStack) add(overlay fyne.CanvasObject) {
	o.renderCaches = append(o.renderCaches, &renderCacheTree{root: &RenderCacheNode{obj: overlay}})
	o.OverlayStack.Add(overlay)
}

func (o *overlayStack) remove(overlay fyne.CanvasObject) {
	o.OverlayStack.Remove(overlay)
	overlayCount := len(o.List())

	// it is possible that overlays are removed implicitly and render caches already cleared out
	if overlayCount >= len(o.renderCaches) {
		return
	}

	o.renderCaches[overlayCount] = nil // release memory reference to removed element
	o.renderCaches = o.renderCaches[:overlayCount]
}

type renderCacheTree struct {
	root *RenderCacheNode
}

func (c *Canvas) updateLayout(objToLayout fyne.CanvasObject) {
	switch cont := objToLayout.(type) {
	case *fyne.Container:
		if cont.Layout != nil {
			layout := cont.Layout
			objects := cont.Objects
			layout.Layout(objects, cont.Size())
		}
	case fyne.Widget:
		renderer := cache.Renderer(cont)
		renderer.Layout(cont.Size())
	}
}
