package glfw

import (
	"context"
	"image/color"
	_ "image/png" // for the icon
	"math"
	"runtime"
	"time"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/driver/desktop"
	"fyne.io/fyne/v2/internal/app"
	"fyne.io/fyne/v2/internal/build"
	"fyne.io/fyne/v2/internal/cache"
	"fyne.io/fyne/v2/internal/driver"
	"fyne.io/fyne/v2/internal/driver/common"
	"fyne.io/fyne/v2/internal/scale"
)

const (
	dragMoveThreshold = 2 // how far can we move before it is a drag
	windowIconSize    = 256
)

func (w *window) Title() string {
	return w.title
}

func (w *window) SetTitle(title string) {
	w.title = title

	w.runOnMainWhenCreated(func() {
		w.view().SetTitle(title)
	})
}

func (w *window) FullScreen() bool {
	return w.fullScreen
}

// minSizeOnScreen gets the padded minimum size of a window content in screen pixels
func (w *window) minSizeOnScreen() (int, int) {
	// get minimum size of content inside the window
	return w.screenSize(w.canvas.MinSize())
}

// screenSize computes the actual output size of the given content size in screen pixels
func (w *window) screenSize(canvasSize fyne.Size) (int, int) {
	return scale.ToScreenCoordinate(w.canvas, canvasSize.Width), scale.ToScreenCoordinate(w.canvas, canvasSize.Height)
}

func (w *window) Resize(size fyne.Size) {
	// we cannot perform this until window is prepared as we don't know it's scale!
	bigEnough := size.Max(w.canvas.canvasSize(w.canvas.Content().MinSize()))
	w.runOnMainWhenCreated(func() {
		w.viewLock.Lock()

		width, height := scale.ToScreenCoordinate(w.canvas, bigEnough.Width), scale.ToScreenCoordinate(w.canvas, bigEnough.Height)
		if w.fixedSize || !w.visible { // fixed size ignores future `resized` and if not visible we may not get the event
			w.shouldWidth, w.shouldHeight = width, height
			w.width, w.height = width, height
		}
		w.viewLock.Unlock()
		w.requestedWidth, w.requestedHeight = width, height
		if runtime.GOOS != "js" {
			w.view().SetSize(width, height)
		}
	})
}

func (w *window) FixedSize() bool {
	return w.fixedSize
}

func (w *window) SetFixedSize(fixed bool) {
	w.fixedSize = fixed
	if w.view() != nil {
		w.runOnMainWhenCreated(w.fitContent)
	}
}

func (w *window) Padded() bool {
	return w.canvas.padded
}

func (w *window) SetPadded(padded bool) {
	w.canvas.SetPadded(padded)

	w.runOnMainWhenCreated(w.fitContent)
}

func (w *window) Icon() fyne.Resource {
	if w.icon == nil {
		return fyne.CurrentApp().Icon()
	}

	return w.icon
}

func (w *window) MainMenu() *fyne.MainMenu {
	return w.mainmenu
}

func (w *window) SetMainMenu(menu *fyne.MainMenu) {
	w.mainmenu = menu
	w.runOnMainWhenCreated(func() {
		w.canvas.buildMenu(w, menu)
	})
}

func (w *window) SetOnClosed(closed func()) {
	w.onClosed = closed
}

func (w *window) SetCloseIntercept(callback func()) {
	w.onCloseIntercepted = callback
}

func (w *window) calculatedScale() float32 {
	return calculateScale(userScale(), fyne.CurrentDevice().SystemScaleForWindow(w), w.detectScale())
}

func (w *window) detectTextureScale() float32 {
	view := w.view()
	winWidth, _ := view.GetSize()
	texWidth, _ := view.GetFramebufferSize()
	return float32(texWidth) / float32(winWidth)
}

func (w *window) Show() {
	go w.doShow()
}

func (w *window) doShow() {
	if w.view() != nil {
		w.doShowAgain()
		return
	}

	<-w.driver.waitForStart

	w.createLock.Do(w.create)
	if w.view() == nil {
		return
	}

	runOnMain(func() {
		w.viewLock.Lock()
		w.visible = true
		w.viewLock.Unlock()
		view := w.view()
		view.SetTitle(w.title)

		if !build.IsWayland && w.centered {
			w.doCenterOnScreen() // lastly center if that was requested
		}
		view.Show()

		// save coordinates
		if !build.IsWayland {
			w.xpos, w.ypos = view.GetPos()
		}

		if w.fullScreen { // this does not work if called before viewport.Show()
			go func() {
				time.Sleep(time.Millisecond * 100)
				w.SetFullScreen(true)
			}()
		}
	})

	// show top canvas element
	if content := w.canvas.Content(); content != nil {
		content.Show()

		runOnDraw(w, func() {
			w.driver.repaintWindow(w)
		})
	}
}

func (w *window) Hide() {
	runOnMain(func() {
		w.viewLock.Lock()
		if w.closing || w.viewport == nil {
			w.viewLock.Unlock()
			return
		}

		w.visible = false
		v := w.viewport
		w.viewLock.Unlock()

		v.Hide()

		// hide top canvas element
		if content := w.canvas.Content(); content != nil {
			content.Hide()
		}
	})
}

func (w *window) Close() {
	if w.isClosing() {
		return
	}

	// trigger callbacks - early so window still exists
	if w.onClosed != nil {
		w.QueueEvent(w.onClosed)
	}

	// set w.closing flag inside draw thread to ensure we can free textures
	runOnDraw(w, func() {
		w.viewLock.Lock()
		w.closing = true
		w.viewLock.Unlock()
		w.viewport.SetShouldClose(true)

		cache.RangeTexturesFor(w.canvas, w.canvas.Painter().Free)
	})

	w.canvas.WalkTrees(nil, func(node *common.RenderCacheNode, _ fyne.Position) {
		if wid, ok := node.Obj().(fyne.Widget); ok {
			cache.DestroyRenderer(wid)
		}
	})
}

func (w *window) ShowAndRun() {
	w.Show()
	fyne.CurrentApp().Run()
}

// Clipboard returns the system clipboard
func (w *window) Clipboard() fyne.Clipboard {
	return &clipboard{}
}

func (w *window) Content() fyne.CanvasObject {
	return w.canvas.Content()
}

func (w *window) SetContent(content fyne.CanvasObject) {
	w.viewLock.RLock()
	visible := w.visible
	w.viewLock.RUnlock()
	// hide old canvas element
	if visible && w.canvas.Content() != nil {
		w.canvas.Content().Hide()
	}

	w.canvas.SetContent(content)

	// show new canvas element
	if content != nil {
		content.Show()
	}
	w.RescaleContext()
}

func (w *window) Canvas() fyne.Canvas {
	return w.canvas
}

func (w *window) processClosed() {
	if w.onCloseIntercepted != nil {
		w.QueueEvent(w.onCloseIntercepted)
		return
	}

	go w.Close() // unsure which thread this comes from, so don't block
}

// destroy this window and, if it's the last window quit the app
func (w *window) destroy(d *gLDriver) {
	cache.CleanCanvas(w.canvas)

	if w.master {
		d.Quit()
	} else if runtime.GOOS == "darwin" {
		go d.focusPreviousWindow()
	}
	w.DestroyEventQueue()
}

func (w *window) processMoved(x, y int) {
	if !w.fullScreen { // don't save the move to top left when changing to fullscreen
		// save coordinates
		w.xpos, w.ypos = x, y
	}

	if w.canvas.detectedScale == w.detectScale() {
		return
	}

	w.canvas.detectedScale = w.detectScale()
	go w.canvas.reloadScale()
}

func (w *window) processResized(width, height int) {
	canvasSize := w.computeCanvasSize(width, height)
	if !w.fullScreen {
		w.width = scale.ToScreenCoordinate(w.canvas, canvasSize.Width)
		w.height = scale.ToScreenCoordinate(w.canvas, canvasSize.Height)
	}

	if !w.visible { // don't redraw if hidden
		w.canvas.Resize(canvasSize)
		return
	}

	if w.fixedSize {
		w.canvas.Resize(canvasSize)
		w.fitContent()
		return
	}

	w.platformResize(canvasSize)
}

func (w *window) processFrameSized(width, height int) {
	if width == 0 || height == 0 || runtime.GOOS != "darwin" {
		return
	}

	winWidth, _ := w.view().GetSize()
	newTexScale := float32(width) / float32(winWidth) // This will be > 1.0 on a HiDPI screen
	w.canvas.RLock()
	texScale := w.canvas.texScale
	w.canvas.RUnlock()
	if texScale != newTexScale {
		w.canvas.Lock()
		w.canvas.texScale = newTexScale
		w.canvas.Unlock()
		w.canvas.Refresh(w.canvas.Content()) // reset graphics to apply texture scale
	}
}

func (w *window) processRefresh() {
	refreshWindow(w)
}

func (w *window) findObjectAtPositionMatching(canvas *glCanvas, mouse fyne.Position, matches func(object fyne.CanvasObject) bool) (fyne.CanvasObject, fyne.Position, int) {
	return driver.FindObjectAtPositionMatching(mouse, matches, canvas.Overlays().Top(), canvas.menu, canvas.Content())
}

func (w *window) processMouseMoved(xpos float64, ypos float64) {
	w.mouseLock.Lock()
	previousPos := w.mousePos
	w.mousePos = fyne.NewPos(scale.ToFyneCoordinate(w.canvas, int(xpos)), scale.ToFyneCoordinate(w.canvas, int(ypos)))
	mousePos := w.mousePos
	mouseButton := w.mouseButton
	mouseDragPos := w.mouseDragPos
	mouseOver := w.mouseOver
	w.mouseLock.Unlock()

	cursor := desktop.Cursor(desktop.DefaultCursor)

	obj, pos, _ := w.findObjectAtPositionMatching(w.canvas, mousePos, func(object fyne.CanvasObject) bool {
		if cursorable, ok := object.(desktop.Cursorable); ok {
			cursor = cursorable.Cursor()
		}

		_, hover := object.(desktop.Hoverable)
		return hover
	})

	if w.cursor != cursor {
		// cursor has changed, store new cursor and apply change via glfw
		rawCursor, isCustomCursor := fyneToNativeCursor(cursor)
		w.cursor = cursor

		if rawCursor == nil {
			w.view().SetInputMode(CursorMode, CursorHidden)
		} else {
			w.view().SetInputMode(CursorMode, CursorNormal)
			w.SetCursor(rawCursor)
		}
		w.setCustomCursor(rawCursor, isCustomCursor)
	}

	if w.mouseButton != 0 && w.mouseButton != desktop.MouseButtonSecondary && !w.mouseDragStarted {
		obj, pos, _ := w.findObjectAtPositionMatching(w.canvas, previousPos, func(object fyne.CanvasObject) bool {
			_, ok := object.(fyne.Draggable)
			return ok
		})

		deltaX := mousePos.X - mouseDragPos.X
		deltaY := mousePos.Y - mouseDragPos.Y
		overThreshold := math.Abs(float64(deltaX)) >= dragMoveThreshold || math.Abs(float64(deltaY)) >= dragMoveThreshold

		if wid, ok := obj.(fyne.Draggable); ok && overThreshold {
			w.mouseLock.Lock()
			w.mouseDragged = wid
			w.mouseDraggedOffset = previousPos.Subtract(pos)
			w.mouseDraggedObjStart = obj.Position()
			w.mouseDragStarted = true
			w.mouseLock.Unlock()
		}
	}

	w.mouseLock.RLock()
	isObjDragged := w.objIsDragged(obj)
	isMouseOverDragged := w.objIsDragged(mouseOver)
	w.mouseLock.RUnlock()
	if obj != nil && !isObjDragged {
		ev := &desktop.MouseEvent{Button: mouseButton}
		ev.AbsolutePosition = mousePos
		ev.Position = pos

		if hovered, ok := obj.(desktop.Hoverable); ok {
			if hovered == mouseOver {
				w.QueueEvent(func() { hovered.MouseMoved(ev) })
			} else {
				w.mouseOut()
				w.mouseIn(hovered, ev)
			}
		} else if mouseOver != nil {
			isChild := false
			driver.WalkCompleteObjectTree(mouseOver.(fyne.CanvasObject),
				func(co fyne.CanvasObject, p1, p2 fyne.Position, s fyne.Size) bool {
					if co == obj {
						isChild = true
						return true
					}
					return false
				}, nil)
			if !isChild {
				w.mouseOut()
			}
		}
	} else if mouseOver != nil && !isMouseOverDragged {
		w.mouseOut()
	}

	w.mouseLock.RLock()
	mouseButton = w.mouseButton
	mouseDragged := w.mouseDragged
	mouseDraggedObjStart := w.mouseDraggedObjStart
	mouseDraggedOffset := w.mouseDraggedOffset
	mouseDragPos = w.mouseDragPos
	w.mouseLock.RUnlock()
	if mouseDragged != nil && mouseButton != desktop.MouseButtonSecondary {
		if w.mouseButton > 0 {
			draggedObjDelta := mouseDraggedObjStart.Subtract(mouseDragged.(fyne.CanvasObject).Position())
			ev := &fyne.DragEvent{}
			ev.AbsolutePosition = mousePos
			ev.Position = mousePos.Subtract(mouseDraggedOffset).Add(draggedObjDelta)
			ev.Dragged = fyne.NewDelta(mousePos.X-mouseDragPos.X, mousePos.Y-mouseDragPos.Y)
			wd := mouseDragged
			w.QueueEvent(func() { wd.Dragged(ev) })
		}

		w.mouseLock.Lock()
		w.mouseDragStarted = true
		w.mouseDragPos = mousePos
		w.mouseLock.Unlock()
	}
}

func (w *window) objIsDragged(obj any) bool {
	if w.mouseDragged != nil && obj != nil {
		draggedObj, _ := obj.(fyne.Draggable)
		return draggedObj == w.mouseDragged
	}
	return false
}

func (w *window) mouseIn(obj desktop.Hoverable, ev *desktop.MouseEvent) {
	w.QueueEvent(func() {
		if obj != nil {
			obj.MouseIn(ev)
		}
		w.mouseLock.Lock()
		w.mouseOver = obj
		w.mouseLock.Unlock()
	})
}

func (w *window) mouseOut() {
	w.QueueEvent(func() {
		w.mouseLock.RLock()
		mouseOver := w.mouseOver
		w.mouseLock.RUnlock()
		if mouseOver != nil {
			mouseOver.MouseOut()
			w.mouseLock.Lock()
			w.mouseOver = nil
			w.mouseLock.Unlock()
		}
	})
}

func (w *window) processMouseClicked(button desktop.MouseButton, action action, modifiers fyne.KeyModifier) {
	w.mouseLock.RLock()
	w.mouseDragPos = w.mousePos
	mousePos := w.mousePos
	mouseDragStarted := w.mouseDragStarted
	w.mouseLock.RUnlock()
	if mousePos.IsZero() { // window may not be focused (darwin mostly) and so position callbacks not happening
		xpos, ypos := w.view().GetCursorPos()
		w.mouseLock.Lock()
		w.mousePos = fyne.NewPos(scale.ToFyneCoordinate(w.canvas, int(xpos)), scale.ToFyneCoordinate(w.canvas, int(ypos)))
		mousePos = w.mousePos
		w.mouseLock.Unlock()
	}

	co, pos, _ := w.findObjectAtPositionMatching(w.canvas, mousePos, func(object fyne.CanvasObject) bool {
		switch object.(type) {
		case fyne.Tappable, fyne.SecondaryTappable, fyne.DoubleTappable, fyne.Focusable, desktop.Mouseable:
			return true
		case fyne.Draggable:
			if mouseDragStarted {
				return true
			}
		}

		return false
	})
	ev := &fyne.PointEvent{
		Position:         pos,
		AbsolutePosition: mousePos,
	}

	coMouse := co
	if wid, ok := co.(desktop.Mouseable); ok {
		mev := &desktop.MouseEvent{
			Button:   button,
			Modifier: modifiers,
		}
		mev.Position = ev.Position
		mev.AbsolutePosition = mousePos
		w.mouseClickedHandleMouseable(mev, action, wid)
	}

	if wid, ok := co.(fyne.Focusable); !ok || wid != w.canvas.Focused() {
		ignore := false
		_, _, _ = w.findObjectAtPositionMatching(w.canvas, mousePos, func(object fyne.CanvasObject) bool {
			switch object.(type) {
			case fyne.Focusable:
				ignore = true
				return true
			}

			return false
		})

		if !ignore { // if a parent item under the mouse has focus then ignore this tap unfocus
			w.canvas.Unfocus()
		}
	}

	w.mouseLock.Lock()
	if action == press {
		w.mouseButton |= button
	} else if action == release {
		w.mouseButton &= ^button
	}

	mouseDragged := w.mouseDragged
	mouseDragStarted = w.mouseDragStarted
	mouseOver := w.mouseOver
	shouldMouseOut := w.objIsDragged(mouseOver) && !w.objIsDragged(coMouse)
	mousePressed := w.mousePressed
	w.mouseLock.Unlock()

	if action == release && mouseDragged != nil {
		if mouseDragStarted {
			w.QueueEvent(mouseDragged.DragEnd)
			w.mouseLock.Lock()
			w.mouseDragStarted = false
			w.mouseLock.Unlock()
		}
		if shouldMouseOut {
			w.mouseOut()
		}
		w.mouseLock.Lock()
		w.mouseDragged = nil
		w.mouseLock.Unlock()
	}

	_, tap := co.(fyne.Tappable)
	secondary, altTap := co.(fyne.SecondaryTappable)
	if tap || altTap {
		if action == press {
			w.mouseLock.Lock()
			w.mousePressed = co
			w.mouseLock.Unlock()
		} else if action == release {
			if co == mousePressed {
				if button == desktop.MouseButtonSecondary && altTap {
					w.QueueEvent(func() { secondary.TappedSecondary(ev) })
				}
			}
		}
	}

	// Check for double click/tap on left mouse button
	if action == release && button == desktop.MouseButtonPrimary && !mouseDragStarted {
		w.mouseClickedHandleTapDoubleTap(co, ev)
	}
}

func (w *window) mouseClickedHandleMouseable(mev *desktop.MouseEvent, action action, wid desktop.Mouseable) {
	mousePos := mev.AbsolutePosition
	if action == press {
		w.QueueEvent(func() { wid.MouseDown(mev) })
	} else if action == release {
		w.mouseLock.RLock()
		mouseDragged := w.mouseDragged
		mouseDraggedOffset := w.mouseDraggedOffset
		w.mouseLock.RUnlock()
		if mouseDragged == nil {
			w.QueueEvent(func() { wid.MouseUp(mev) })
		} else {
			if dragged, ok := mouseDragged.(desktop.Mouseable); ok {
				mev.Position = mousePos.Subtract(mouseDraggedOffset)
				w.QueueEvent(func() { dragged.MouseUp(mev) })
			} else {
				w.QueueEvent(func() { wid.MouseUp(mev) })
			}
		}
	}
}

func (w *window) mouseClickedHandleTapDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent) {
	_, doubleTap := co.(fyne.DoubleTappable)
	if doubleTap {
		w.mouseLock.Lock()
		w.mouseClickCount++
		w.mouseLastClick = co
		mouseCancelFunc := w.mouseCancelFunc
		w.mouseLock.Unlock()
		if mouseCancelFunc != nil {
			mouseCancelFunc()
			return
		}
		go w.waitForDoubleTap(co, ev)
	} else {
		w.mouseLock.Lock()
		if wid, ok := co.(fyne.Tappable); ok && co == w.mousePressed {
			w.QueueEvent(func() { wid.Tapped(ev) })
		}
		w.mousePressed = nil
		w.mouseLock.Unlock()
	}
}

func (w *window) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent) {
	var ctx context.Context
	w.mouseLock.Lock()
	ctx, w.mouseCancelFunc = context.WithDeadline(context.TODO(), time.Now().Add(w.driver.DoubleTapDelay()))
	defer w.mouseCancelFunc()
	w.mouseLock.Unlock()

	<-ctx.Done()

	w.mouseLock.Lock()
	defer w.mouseLock.Unlock()

	if w.mouseClickCount == 2 && w.mouseLastClick == co {
		if wid, ok := co.(fyne.DoubleTappable); ok {
			w.QueueEvent(func() { wid.DoubleTapped(ev) })
		}
	} else if co == w.mousePressed {
		if wid, ok := co.(fyne.Tappable); ok {
			w.QueueEvent(func() { wid.Tapped(ev) })
		}
	}

	w.mouseClickCount = 0
	w.mousePressed = nil
	w.mouseCancelFunc = nil
	w.mouseLastClick = nil
}

func (w *window) processMouseScrolled(xoff float64, yoff float64) {
	w.mouseLock.RLock()
	mousePos := w.mousePos
	w.mouseLock.RUnlock()
	co, pos, _ := w.findObjectAtPositionMatching(w.canvas, mousePos, func(object fyne.CanvasObject) bool {
		_, ok := object.(fyne.Scrollable)
		return ok
	})
	switch wid := co.(type) {
	case fyne.Scrollable:
		if math.Abs(xoff) >= scrollAccelerateCutoff {
			xoff *= scrollAccelerateRate
		}
		if math.Abs(yoff) >= scrollAccelerateCutoff {
			yoff *= scrollAccelerateRate
		}

		ev := &fyne.ScrollEvent{}
		ev.Scrolled = fyne.NewDelta(float32(xoff)*scrollSpeed, float32(yoff)*scrollSpeed)
		ev.Position = pos
		ev.AbsolutePosition = mousePos
		wid.Scrolled(ev)
	}
}

func (w *window) capturesTab(modifier fyne.KeyModifier) bool {
	captures := false

	if ent, ok := w.canvas.Focused().(fyne.Tabbable); ok {
		captures = ent.AcceptsTab()
	}
	if !captures {
		switch modifier {
		case 0:
			w.QueueEvent(w.canvas.FocusNext)
			return false
		case fyne.KeyModifierShift:
			w.QueueEvent(w.canvas.FocusPrevious)
			return false
		}
	}

	return captures
}

func (w *window) processKeyPressed(keyName fyne.KeyName, keyASCII fyne.KeyName, scancode int, action action, keyDesktopModifier fyne.KeyModifier) {
	keyEvent := &fyne.KeyEvent{Name: keyName, Physical: fyne.HardwareKey{ScanCode: scancode}}

	pendingMenuToggle := w.menuTogglePending
	pendingMenuDeactivation := w.menuDeactivationPending
	w.menuTogglePending = desktop.KeyNone
	w.menuDeactivationPending = desktop.KeyNone
	switch action {
	case release:
		if action == release && keyName != "" {
			switch keyName {
			case pendingMenuToggle:
				w.canvas.ToggleMenu()
			case pendingMenuDeactivation:
				if w.canvas.DismissMenu() {
					return
				}
			}
		}

		if w.canvas.Focused() != nil {
			if focused, ok := w.canvas.Focused().(desktop.Keyable); ok {
				w.QueueEvent(func() { focused.KeyUp(keyEvent) })
			}
		} else if w.canvas.onKeyUp != nil {
			w.QueueEvent(func() { w.canvas.onKeyUp(keyEvent) })
		}
		return // ignore key up in other core events
	case press:
		switch keyName {
		case desktop.KeyAltLeft, desktop.KeyAltRight:
			// compensate for GLFW modifiers bug https://github.com/glfw/glfw/issues/1630
			if (runtime.GOOS == "linux" && keyDesktopModifier == 0) || (runtime.GOOS != "linux" && keyDesktopModifier == fyne.KeyModifierAlt) {
				w.menuTogglePending = keyName
			}
		case fyne.KeyEscape:
			w.menuDeactivationPending = keyName
		}
		if w.canvas.Focused() != nil {
			if focused, ok := w.canvas.Focused().(desktop.Keyable); ok {
				w.QueueEvent(func() { focused.KeyDown(keyEvent) })
			}
		} else if w.canvas.onKeyDown != nil {
			w.QueueEvent(func() { w.canvas.onKeyDown(keyEvent) })
		}
	default:
		// key repeat will fall through to TypedKey and TypedShortcut
	}

	modifierOtherThanShift := (keyDesktopModifier & fyne.KeyModifierControl) |
		(keyDesktopModifier & fyne.KeyModifierAlt) |
		(keyDesktopModifier & fyne.KeyModifierSuper)
	if (keyName == fyne.KeyTab && modifierOtherThanShift == 0 && !w.capturesTab(keyDesktopModifier)) ||
		w.triggersShortcut(keyName, keyASCII, keyDesktopModifier) {
		return
	}

	// No shortcut detected, pass down to TypedKey
	focused := w.canvas.Focused()
	if focused != nil {
		w.QueueEvent(func() { focused.TypedKey(keyEvent) })
	} else if w.canvas.onTypedKey != nil {
		w.QueueEvent(func() { w.canvas.onTypedKey(keyEvent) })
	}
}

// charInput defines the character with modifiers callback which is called when a
// Unicode character is input.
//
// Characters do not map 1:1 to physical keys, as a key may produce zero, one or more characters.
func (w *window) processCharInput(char rune) {
	if focused := w.canvas.Focused(); focused != nil {
		w.QueueEvent(func() { focused.TypedRune(char) })
	} else if w.canvas.onTypedRune != nil {
		w.QueueEvent(func() { w.canvas.onTypedRune(char) })
	}
}

func (w *window) processFocused(focus bool) {
	if focus {
		if curWindow == nil {
			if f := fyne.CurrentApp().Lifecycle().(*app.Lifecycle).OnEnteredForeground(); f != nil {
				w.QueueEvent(f)
			}
		}
		curWindow = w
		w.QueueEvent(w.canvas.FocusGained)
	} else {
		w.QueueEvent(w.canvas.FocusLost)
		w.mouseLock.Lock()
		w.mousePos = fyne.Position{}
		w.mouseLock.Unlock()

		go func() { // check whether another window was focused or not
			time.Sleep(time.Millisecond * 100)
			if curWindow != w {
				return
			}

			curWindow = nil
			if f := fyne.CurrentApp().Lifecycle().(*app.Lifecycle).OnExitedForeground(); f != nil {
				w.QueueEvent(f)
			}
		}()
	}
}

func (w *window) triggersShortcut(localizedKeyName fyne.KeyName, key fyne.KeyName, modifier fyne.KeyModifier) bool {
	var shortcut fyne.Shortcut
	ctrlMod := fyne.KeyModifierControl
	if isMacOSRuntime() {
		ctrlMod = fyne.KeyModifierSuper
	}
	// User pressing physical keys Ctrl+V while using a Russian (or any non-ASCII) keyboard layout
	// is reported as a fyne.KeyUnknown key with Control modifier. We should still consider this
	// as a "Paste" shortcut.
	// See https://github.com/fyne-io/fyne/pull/2587 for discussion.
	keyName := localizedKeyName
	resemblesShortcut := (modifier&(fyne.KeyModifierControl|fyne.KeyModifierSuper) != 0)
	if (localizedKeyName == fyne.KeyUnknown) && resemblesShortcut {
		if key != fyne.KeyUnknown {
			keyName = key
		}
	}
	if modifier == ctrlMod {
		switch keyName {
		case fyne.KeyZ:
			// detect undo shortcut
			shortcut = &fyne.ShortcutUndo{}
		case fyne.KeyY:
			// detect redo shortcut
			shortcut = &fyne.ShortcutRedo{}
		case fyne.KeyV:
			// detect paste shortcut
			shortcut = &fyne.ShortcutPaste{
				Clipboard: w.Clipboard(),
			}
		case fyne.KeyC, fyne.KeyInsert:
			// detect copy shortcut
			shortcut = &fyne.ShortcutCopy{
				Clipboard: w.Clipboard(),
			}
		case fyne.KeyX:
			// detect cut shortcut
			shortcut = &fyne.ShortcutCut{
				Clipboard: w.Clipboard(),
			}
		case fyne.KeyA:
			// detect selectAll shortcut
			shortcut = &fyne.ShortcutSelectAll{}
		}
	}

	if modifier == fyne.KeyModifierShift {
		switch keyName {
		case fyne.KeyInsert:
			// detect paste shortcut
			shortcut = &fyne.ShortcutPaste{
				Clipboard: w.Clipboard(),
			}
		case fyne.KeyDelete:
			// detect cut shortcut
			shortcut = &fyne.ShortcutCut{
				Clipboard: w.Clipboard(),
			}
		}
	}

	if shortcut == nil && modifier != 0 && !isKeyModifier(keyName) && modifier != fyne.KeyModifierShift {
		shortcut = &desktop.CustomShortcut{
			KeyName:  keyName,
			Modifier: modifier,
		}
	}

	if shortcut != nil {
		if focused, ok := w.canvas.Focused().(fyne.Shortcutable); ok {
			shouldRunShortcut := true
			type selectableText interface {
				fyne.Disableable
				SelectedText() string
			}
			if selectableTextWid, ok := focused.(selectableText); ok && selectableTextWid.Disabled() {
				shouldRunShortcut = shortcut.ShortcutName() == "Copy"
			}
			if shouldRunShortcut {
				w.QueueEvent(func() { focused.TypedShortcut(shortcut) })
			}
			return shouldRunShortcut
		}
		w.QueueEvent(func() { w.canvas.TypedShortcut(shortcut) })
		return true
	}

	return false
}

func (w *window) RunWithContext(f func()) {
	if w.isClosing() {
		return
	}
	w.view().MakeContextCurrent()

	f()

	w.DetachCurrentContext()
}

func (w *window) RescaleContext() {
	runOnMain(w.rescaleOnMain)
}

func (w *window) Context() any {
	return nil
}

func (w *window) runOnMainWhenCreated(fn func()) {
	if w.view() != nil {
		runOnMain(fn)
		return
	}

	w.pending = append(w.pending, fn)
}

func (d *gLDriver) CreateWindow(title string) fyne.Window {
	if runtime.GOOS != "js" {
		return d.createWindow(title, true)
	}

	// handling multiple windows by overlaying on the root for web
	var root fyne.Window
	d.windowLock.RLock()
	hasVisible := false
	for _, w := range d.windows {
		if w.(*window).visible {
			hasVisible = true
			root = w
			break
		}
	}
	d.windowLock.RUnlock()

	if !hasVisible {
		return d.createWindow(title, true)
	}

	c := root.Canvas().(*glCanvas)
	multi := c.webExtraWindows
	if multi == nil {
		multi = container.NewMultipleWindows()
		multi.Resize(c.Size())
		c.webExtraWindows = multi
	}
	inner := container.NewInnerWindow(title, canvas.NewRectangle(color.Transparent))
	multi.Add(inner)

	return wrapInnerWindow(inner, root, d)
}

func (d *gLDriver) createWindow(title string, decorate bool) fyne.Window {
	var ret *window
	if title == "" {
		title = defaultTitle
	}
	runOnMain(func() {
		d.initGLFW()

		ret = &window{title: title, decorate: decorate, driver: d}
		// This queue is destroyed when the window is closed.
		ret.InitEventQueue()
		go ret.RunEventQueue()

		ret.canvas = newCanvas()
		ret.canvas.context = ret
		ret.SetIcon(ret.icon)
		d.addWindow(ret)
	})
	return ret
}

func (w *window) doShowAgain() {
	if w.isClosing() {
		return
	}

	runOnMain(func() {
		// show top canvas element
		if content := w.canvas.Content(); content != nil {
			content.Show()
		}

		view := w.view()
		if !build.IsWayland {
			view.SetPos(w.xpos, w.ypos)
		}
		view.Show()
		w.viewLock.Lock()
		w.visible = true
		w.viewLock.Unlock()
	})
}

func (w *window) isClosing() bool {
	w.viewLock.RLock()
	closing := w.closing || w.viewport == nil
	w.viewLock.RUnlock()
	return closing
}

func (d *gLDriver) CreateSplashWindow() fyne.Window {
	win := d.createWindow("", false)
	win.SetPadded(false)
	win.CenterOnScreen()
	return win
}

func (d *gLDriver) AllWindows() []fyne.Window {
	return d.windows
}

func isKeyModifier(keyName fyne.KeyName) bool {
	return keyName == desktop.KeyShiftLeft || keyName == desktop.KeyShiftRight ||
		keyName == desktop.KeyControlLeft || keyName == desktop.KeyControlRight ||
		keyName == desktop.KeyAltLeft || keyName == desktop.KeyAltRight ||
		keyName == desktop.KeySuperLeft || keyName == desktop.KeySuperRight
}
