// SPDX-License-Identifier: Unlicense OR MIT

package app

import (
	"errors"
	"fmt"
	"image"
	"image/color"
	"runtime"
	"sync"
	"time"
	"unicode/utf8"

	"gioui.org/f32"
	"gioui.org/font/gofont"
	"gioui.org/gpu"
	"gioui.org/internal/debug"
	"gioui.org/internal/ops"
	"gioui.org/io/event"
	"gioui.org/io/input"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
)

// Option configures a window.
type Option func(unit.Metric, *Config)

// Window represents an operating system window.
//
// The zero-value Window is useful; the GUI window is created and shown the first
// time the [Event] method is called. On iOS or Android, the first Window represents
// the window previously created by the platform.
//
// More than one Window is not supported on iOS, Android, WebAssembly.
type Window struct {
	initialOpts    []Option
	initialActions []system.Action

	ctx context
	gpu gpu.GPU
	// timer tracks the delayed invalidate goroutine.
	timer struct {
		// quit is shuts down the goroutine.
		quit chan struct{}
		// update the invalidate time.
		update chan time.Time
	}

	animating    bool
	hasNextFrame bool
	nextFrame    time.Time
	// viewport is the latest frame size with insets applied.
	viewport image.Rectangle
	// metric is the metric from the most recent frame.
	metric      unit.Metric
	queue       input.Router
	cursor      pointer.Cursor
	decorations struct {
		op.Ops
		// enabled tracks the Decorated option as
		// given to the Option method. It may differ
		// from Config.Decorated depending on platform
		// capability.
		enabled bool
		Config
		height        unit.Dp
		currentHeight int
		*material.Theme
		*widget.Decorations
	}
	nocontext bool
	// semantic data, lazily evaluated if requested by a backend to speed up
	// the cases where semantic data is not needed.
	semantic struct {
		// uptodate tracks whether the fields below are up to date.
		uptodate bool
		root     input.SemanticID
		prevTree []input.SemanticNode
		tree     []input.SemanticNode
		ids      map[input.SemanticID]input.SemanticNode
	}
	imeState editorState
	driver   driver
	// gpuErr tracks the GPU error that is to be reported when
	// the window is closed.
	gpuErr error

	// invMu protects mayInvalidate.
	invMu         sync.Mutex
	mayInvalidate bool

	// coalesced tracks the most recent events waiting to be delivered
	// to the client.
	coalesced eventSummary
	// frame tracks the most recent frame event.
	lastFrame struct {
		sync bool
		size image.Point
		off  image.Point
		deco op.CallOp
	}
}

type eventSummary struct {
	wakeup       bool
	cfg          *ConfigEvent
	view         *ViewEvent
	frame        *frameEvent
	framePending bool
	destroy      *DestroyEvent
}

type callbacks struct {
	w *Window
}

func decoHeightOpt(h unit.Dp) Option {
	return func(m unit.Metric, c *Config) {
		c.decoHeight = h
	}
}

func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops, sigChan chan<- struct{}) error {
	signal := func() {
		if sigChan != nil {
			// We're done with frame, let the client continue.
			sigChan <- struct{}{}
			// Signal at most once.
			sigChan = nil
		}
	}
	defer signal()
	for {
		if w.gpu == nil && !w.nocontext {
			var err error
			if w.ctx == nil {
				w.ctx, err = w.driver.NewContext()
				if err != nil {
					return err
				}
				sync = true
			}
		}
		if sync && w.ctx != nil {
			if err := w.ctx.Refresh(); err != nil {
				if errors.Is(err, errOutOfDate) {
					// Surface couldn't be created for transient reasons. Skip
					// this frame and wait for the next.
					return nil
				}
				w.destroyGPU()
				if errors.Is(err, gpu.ErrDeviceLost) {
					continue
				}
				return err
			}
		}
		if w.ctx != nil {
			if err := w.ctx.Lock(); err != nil {
				w.destroyGPU()
				return err
			}
		}
		if w.gpu == nil && !w.nocontext {
			gpu, err := gpu.New(w.ctx.API())
			if err != nil {
				w.ctx.Unlock()
				w.destroyGPU()
				return err
			}
			w.gpu = gpu
		}
		if w.gpu != nil {
			if err := w.frame(frame, size); err != nil {
				w.ctx.Unlock()
				if errors.Is(err, errOutOfDate) {
					// GPU surface needs refreshing.
					sync = true
					continue
				}
				w.destroyGPU()
				if errors.Is(err, gpu.ErrDeviceLost) {
					continue
				}
				return err
			}
		}
		w.queue.Frame(frame)
		// Let the client continue as soon as possible, in particular before
		// a potentially blocking Present.
		signal()
		var err error
		if w.gpu != nil {
			err = w.ctx.Present()
			w.ctx.Unlock()
		}
		return err
	}
}

func (w *Window) frame(frame *op.Ops, viewport image.Point) error {
	if runtime.GOOS == "js" {
		// Use transparent black when Gio is embedded, to allow mixing of Gio and
		// foreign content below.
		w.gpu.Clear(color.NRGBA{A: 0x00, R: 0x00, G: 0x00, B: 0x00})
	} else {
		w.gpu.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
	}
	target, err := w.ctx.RenderTarget()
	if err != nil {
		return err
	}
	return w.gpu.Frame(frame, target, viewport)
}

func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) {
	w.coalesced.framePending = false
	wrapper := &w.decorations.Ops
	off := op.Offset(w.lastFrame.off).Push(wrapper)
	ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal))
	off.Pop()
	w.lastFrame.deco.Add(wrapper)
	if err := w.validateAndProcess(w.lastFrame.size, w.lastFrame.sync, wrapper, ack); err != nil {
		w.destroyGPU()
		w.gpuErr = err
		w.driver.Perform(system.ActionClose)
		return
	}
	w.updateState()
	w.updateCursor()
}

func (w *Window) updateState() {
	for k := range w.semantic.ids {
		delete(w.semantic.ids, k)
	}
	w.semantic.uptodate = false
	q := &w.queue
	switch q.TextInputState() {
	case input.TextInputOpen:
		w.driver.ShowTextInput(true)
	case input.TextInputClose:
		w.driver.ShowTextInput(false)
	}
	if hint, ok := q.TextInputHint(); ok {
		w.driver.SetInputHint(hint)
	}
	if mime, txt, ok := q.WriteClipboard(); ok {
		w.driver.WriteClipboard(mime, txt)
	}
	if q.ClipboardRequested() {
		w.driver.ReadClipboard()
	}
	oldState := w.imeState
	newState := oldState
	newState.EditorState = q.EditorState()
	if newState != oldState {
		w.imeState = newState
		w.driver.EditorStateChanged(oldState, newState)
	}
	if t, ok := q.WakeupTime(); ok {
		w.setNextFrame(t)
	}
	w.updateAnimation()
}

// Invalidate the window such that a [FrameEvent] will be generated immediately.
// If the window is inactive, an unspecified event is sent instead.
//
// Note that Invalidate is intended for externally triggered updates, such as a
// response from a network request. The [op.InvalidateCmd] command is more efficient
// for animation.
//
// Invalidate is safe for concurrent use.
func (w *Window) Invalidate() {
	w.invMu.Lock()
	defer w.invMu.Unlock()
	if w.mayInvalidate {
		w.mayInvalidate = false
		w.driver.Invalidate()
	}
}

// Option applies the options to the window. The options are hints; the platform is
// free to ignore or adjust them.
func (w *Window) Option(opts ...Option) {
	if len(opts) == 0 {
		return
	}
	if w.driver == nil {
		w.initialOpts = append(w.initialOpts, opts...)
		return
	}
	w.Run(func() {
		cnf := Config{Decorated: w.decorations.enabled}
		for _, opt := range opts {
			opt(w.metric, &cnf)
		}
		w.decorations.enabled = cnf.Decorated
		decoHeight := w.decorations.height
		if !w.decorations.enabled {
			decoHeight = 0
		}
		opts = append(opts, decoHeightOpt(decoHeight))
		w.driver.Configure(opts)
		w.setNextFrame(time.Time{})
		w.updateAnimation()
	})
}

// Run f in the same thread as the native window event loop, and wait for f to
// return or the window to close. If the window has not yet been created,
// Run calls f directly.
//
// Note that most programs should not call Run; configuring a Window with
// [CustomRenderer] is a notable exception.
func (w *Window) Run(f func()) {
	if w.driver == nil {
		f()
		return
	}
	done := make(chan struct{})
	w.driver.Run(func() {
		defer close(done)
		f()
	})
	<-done
}

func (w *Window) updateAnimation() {
	if w.driver == nil {
		return
	}
	animate := false
	if w.hasNextFrame {
		if dt := time.Until(w.nextFrame); dt <= 0 {
			animate = true
		} else {
			// Schedule redraw.
			w.scheduleInvalidate(w.nextFrame)
		}
	}
	if animate != w.animating {
		w.animating = animate
		w.driver.SetAnimating(animate)
	}
}

func (w *Window) scheduleInvalidate(t time.Time) {
	if w.timer.quit == nil {
		w.timer.quit = make(chan struct{})
		w.timer.update = make(chan time.Time)
		go func() {
			var timer *time.Timer
			for {
				var timeC <-chan time.Time
				if timer != nil {
					timeC = timer.C
				}
				select {
				case <-w.timer.quit:
					w.timer.quit <- struct{}{}
					return
				case t := <-w.timer.update:
					if timer != nil {
						timer.Stop()
					}
					timer = time.NewTimer(time.Until(t))
				case <-timeC:
					w.Invalidate()
				}
			}
		}()
	}
	w.timer.update <- t
}

func (w *Window) setNextFrame(at time.Time) {
	if !w.hasNextFrame || at.Before(w.nextFrame) {
		w.hasNextFrame = true
		w.nextFrame = at
	}
}

func (c *callbacks) SetDriver(d driver) {
	if d == nil {
		panic("nil driver")
	}
	c.w.invMu.Lock()
	defer c.w.invMu.Unlock()
	c.w.driver = d
}

func (c *callbacks) ProcessFrame(frame *op.Ops, ack chan<- struct{}) {
	c.w.processFrame(frame, ack)
}

func (c *callbacks) ProcessEvent(e event.Event) bool {
	return c.w.processEvent(e)
}

// SemanticRoot returns the ID of the semantic root.
func (c *callbacks) SemanticRoot() input.SemanticID {
	c.w.updateSemantics()
	return c.w.semantic.root
}

// LookupSemantic looks up a semantic node from an ID. The zero ID denotes the root.
func (c *callbacks) LookupSemantic(semID input.SemanticID) (input.SemanticNode, bool) {
	c.w.updateSemantics()
	n, found := c.w.semantic.ids[semID]
	return n, found
}

func (c *callbacks) AppendSemanticDiffs(diffs []input.SemanticID) []input.SemanticID {
	c.w.updateSemantics()
	if tree := c.w.semantic.prevTree; len(tree) > 0 {
		c.w.collectSemanticDiffs(&diffs, c.w.semantic.prevTree[0])
	}
	return diffs
}

func (c *callbacks) SemanticAt(pos f32.Point) (input.SemanticID, bool) {
	c.w.updateSemantics()
	return c.w.queue.SemanticAt(pos)
}

func (c *callbacks) EditorState() editorState {
	return c.w.imeState
}

func (c *callbacks) SetComposingRegion(r key.Range) {
	c.w.imeState.compose = r
}

func (c *callbacks) EditorInsert(text string) {
	sel := c.w.imeState.Selection.Range
	c.EditorReplace(sel, text)
	start := sel.Start
	if sel.End < start {
		start = sel.End
	}
	sel.Start = start + utf8.RuneCountInString(text)
	sel.End = sel.Start
	c.SetEditorSelection(sel)
}

func (c *callbacks) EditorReplace(r key.Range, text string) {
	c.w.imeState.Replace(r, text)
	c.w.driver.ProcessEvent(key.EditEvent{Range: r, Text: text})
	c.w.driver.ProcessEvent(key.SnippetEvent(c.w.imeState.Snippet.Range))
}

func (c *callbacks) SetEditorSelection(r key.Range) {
	c.w.imeState.Selection.Range = r
	c.w.driver.ProcessEvent(key.SelectionEvent(r))
}

func (c *callbacks) SetEditorSnippet(r key.Range) {
	if sn := c.EditorState().Snippet.Range; sn == r {
		// No need to expand.
		return
	}
	c.w.driver.ProcessEvent(key.SnippetEvent(r))
}

func (w *Window) moveFocus(dir key.FocusDirection) {
	w.queue.MoveFocus(dir)
	if _, handled := w.queue.WakeupTime(); handled {
		w.queue.RevealFocus(w.viewport)
	} else {
		var v image.Point
		switch dir {
		case key.FocusRight:
			v = image.Pt(+1, 0)
		case key.FocusLeft:
			v = image.Pt(-1, 0)
		case key.FocusDown:
			v = image.Pt(0, +1)
		case key.FocusUp:
			v = image.Pt(0, -1)
		default:
			return
		}
		const scrollABit = unit.Dp(50)
		dist := v.Mul(int(w.metric.Dp(scrollABit)))
		w.queue.ScrollFocus(dist)
	}
}

func (c *callbacks) ClickFocus() {
	c.w.queue.ClickFocus()
	c.w.setNextFrame(time.Time{})
	c.w.updateAnimation()
}

func (c *callbacks) ActionAt(p f32.Point) (system.Action, bool) {
	return c.w.queue.ActionAt(p)
}

func (w *Window) destroyGPU() {
	if w.gpu != nil {
		w.ctx.Lock()
		w.gpu.Release()
		w.ctx.Unlock()
		w.gpu = nil
	}
	if w.ctx != nil {
		w.ctx.Release()
		w.ctx = nil
	}
}

// updateSemantics refreshes the semantics tree, the id to node map and the ids of
// updated nodes.
func (w *Window) updateSemantics() {
	if w.semantic.uptodate {
		return
	}
	w.semantic.uptodate = true
	w.semantic.prevTree, w.semantic.tree = w.semantic.tree, w.semantic.prevTree
	w.semantic.tree = w.queue.AppendSemantics(w.semantic.tree[:0])
	w.semantic.root = w.semantic.tree[0].ID
	for _, n := range w.semantic.tree {
		w.semantic.ids[n.ID] = n
	}
}

// collectSemanticDiffs traverses the previous semantic tree, noting changed nodes.
func (w *Window) collectSemanticDiffs(diffs *[]input.SemanticID, n input.SemanticNode) {
	newNode, exists := w.semantic.ids[n.ID]
	// Ignore deleted nodes, as their disappearance will be reported through an
	// ancestor node.
	if !exists {
		return
	}
	diff := newNode.Desc != n.Desc || len(n.Children) != len(newNode.Children)
	for i, ch := range n.Children {
		if !diff {
			newCh := newNode.Children[i]
			diff = ch.ID != newCh.ID
		}
		w.collectSemanticDiffs(diffs, ch)
	}
	if diff {
		*diffs = append(*diffs, n.ID)
	}
}

func (c *callbacks) Invalidate() {
	c.w.setNextFrame(time.Time{})
	c.w.updateAnimation()
	// Guarantee a wakeup, even when not animating.
	c.w.processEvent(wakeupEvent{})
}

func (c *callbacks) nextEvent() (event.Event, bool) {
	return c.w.nextEvent()
}

func (w *Window) nextEvent() (event.Event, bool) {
	s := &w.coalesced
	defer func() {
		// Every event counts as a wakeup.
		s.wakeup = false
	}()
	switch {
	case s.framePending:
		// If the user didn't call FrameEvent.Event, process
		// an empty frame.
		w.processFrame(new(op.Ops), nil)
	case s.view != nil:
		e := *s.view
		s.view = nil
		return e, true
	case s.destroy != nil:
		e := *s.destroy
		// Clear pending events after DestroyEvent is delivered.
		*s = eventSummary{}
		return e, true
	case s.cfg != nil:
		e := *s.cfg
		s.cfg = nil
		return e, true
	case s.frame != nil:
		e := *s.frame
		s.frame = nil
		s.framePending = true
		return e.FrameEvent, true
	case s.wakeup:
		return wakeupEvent{}, true
	}
	w.invMu.Lock()
	defer w.invMu.Unlock()
	w.mayInvalidate = w.driver != nil
	return nil, false
}

func (w *Window) processEvent(e event.Event) bool {
	switch e2 := e.(type) {
	case wakeupEvent:
		w.coalesced.wakeup = true
	case frameEvent:
		if e2.Size == (image.Point{}) {
			panic(errors.New("internal error: zero-sized Draw"))
		}
		w.metric = e2.Metric
		w.hasNextFrame = false
		e2.Frame = w.driver.Frame
		e2.Source = w.queue.Source()
		// Prepare the decorations and update the frame insets.
		viewport := image.Rectangle{
			Min: image.Point{
				X: e2.Metric.Dp(e2.Insets.Left),
				Y: e2.Metric.Dp(e2.Insets.Top),
			},
			Max: image.Point{
				X: e2.Size.X - e2.Metric.Dp(e2.Insets.Right),
				Y: e2.Size.Y - e2.Metric.Dp(e2.Insets.Bottom),
			},
		}
		// Scroll to focus if viewport is shrinking in any dimension.
		if old, new := w.viewport.Size(), viewport.Size(); new.X < old.X || new.Y < old.Y {
			w.queue.RevealFocus(viewport)
		}
		w.viewport = viewport
		wrapper := &w.decorations.Ops
		wrapper.Reset()
		m := op.Record(wrapper)
		offset := w.decorate(e2.FrameEvent, wrapper)
		w.lastFrame.deco = m.Stop()
		w.lastFrame.size = e2.Size
		w.lastFrame.sync = e2.Sync
		w.lastFrame.off = offset
		e2.Size = e2.Size.Sub(offset)
		w.coalesced.frame = &e2
	case DestroyEvent:
		if w.gpuErr != nil {
			e2.Err = w.gpuErr
		}
		w.destroyGPU()
		w.invMu.Lock()
		w.mayInvalidate = false
		w.driver = nil
		w.invMu.Unlock()
		if q := w.timer.quit; q != nil {
			q <- struct{}{}
			<-q
		}
		w.coalesced.destroy = &e2
	case ViewEvent:
		if !e2.Valid() && w.gpu != nil {
			w.ctx.Lock()
			w.gpu.Release()
			w.gpu = nil
			w.ctx.Unlock()
		}
		w.coalesced.view = &e2
	case ConfigEvent:
		w.decorations.Decorations.Maximized = e2.Config.Mode == Maximized
		wasFocused := w.decorations.Config.Focused
		w.decorations.Config = e2.Config
		e2.Config = w.effectiveConfig()
		w.coalesced.cfg = &e2
		if f := w.decorations.Config.Focused; f != wasFocused {
			w.queue.Queue(key.FocusEvent{Focus: f})
		}
		t, handled := w.queue.WakeupTime()
		if handled {
			w.setNextFrame(t)
			w.updateAnimation()
		}
		return handled
	case event.Event:
		focusDir := key.FocusDirection(-1)
		if e, ok := e2.(key.Event); ok && e.State == key.Press {
			isMobile := runtime.GOOS == "ios" || runtime.GOOS == "android"
			switch {
			case e.Name == key.NameTab && e.Modifiers == 0:
				focusDir = key.FocusForward
			case e.Name == key.NameTab && e.Modifiers == key.ModShift:
				focusDir = key.FocusBackward
			case e.Name == key.NameUpArrow && e.Modifiers == 0 && isMobile:
				focusDir = key.FocusUp
			case e.Name == key.NameDownArrow && e.Modifiers == 0 && isMobile:
				focusDir = key.FocusDown
			case e.Name == key.NameLeftArrow && e.Modifiers == 0 && isMobile:
				focusDir = key.FocusLeft
			case e.Name == key.NameRightArrow && e.Modifiers == 0 && isMobile:
				focusDir = key.FocusRight
			}
		}
		e := e2
		if focusDir != -1 {
			e = input.SystemEvent{Event: e}
		}
		w.queue.Queue(e)
		t, handled := w.queue.WakeupTime()
		if focusDir != -1 && !handled {
			w.moveFocus(focusDir)
			t, handled = w.queue.WakeupTime()
		}
		w.updateCursor()
		if handled {
			w.setNextFrame(t)
			w.updateAnimation()
		}
		return handled
	}
	return true
}

// Event blocks until an event is received from the window, such as
// [FrameEvent], or until [Invalidate] is called. The window is created
// and shown the first time Event is called.
func (w *Window) Event() event.Event {
	if w.driver == nil {
		w.init()
	}
	if w.driver == nil {
		e, ok := w.nextEvent()
		if !ok {
			panic("window initialization failed without a DestroyEvent")
		}
		return e
	}
	return w.driver.Event()
}

func (w *Window) init() {
	debug.Parse()
	// Measure decoration height.
	deco := new(widget.Decorations)
	theme := material.NewTheme()
	theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))
	decoStyle := material.Decorations(theme, deco, 0, "")
	gtx := layout.Context{
		Ops: new(op.Ops),
		// Measure in Dp.
		Metric: unit.Metric{},
	}
	// Allow plenty of space.
	gtx.Constraints.Max.Y = 200
	dims := decoStyle.Layout(gtx)
	decoHeight := unit.Dp(dims.Size.Y)
	defaultOptions := []Option{
		Size(800, 600),
		Title("Gio"),
		Decorated(true),
		decoHeightOpt(decoHeight),
	}
	options := append(defaultOptions, w.initialOpts...)
	w.initialOpts = nil
	var cnf Config
	cnf.apply(unit.Metric{}, options)

	w.nocontext = cnf.CustomRenderer
	w.decorations.Theme = theme
	w.decorations.Decorations = deco
	w.decorations.enabled = cnf.Decorated
	w.decorations.height = decoHeight
	w.imeState.compose = key.Range{Start: -1, End: -1}
	w.semantic.ids = make(map[input.SemanticID]input.SemanticNode)
	newWindow(&callbacks{w}, options)
	for _, acts := range w.initialActions {
		w.Perform(acts)
	}
	w.initialActions = nil
}

func (w *Window) updateCursor() {
	if c := w.queue.Cursor(); c != w.cursor {
		w.cursor = c
		w.driver.SetCursor(c)
	}
}

func (w *Window) fallbackDecorate() bool {
	cnf := w.decorations.Config
	return w.decorations.enabled && !cnf.Decorated && cnf.Mode != Fullscreen && !w.nocontext
}

// decorate the window if enabled and returns the corresponding Insets.
func (w *Window) decorate(e FrameEvent, o *op.Ops) image.Point {
	if !w.fallbackDecorate() {
		return image.Pt(0, 0)
	}
	deco := w.decorations.Decorations
	allActions := system.ActionMinimize | system.ActionMaximize | system.ActionUnmaximize |
		system.ActionClose | system.ActionMove
	style := material.Decorations(w.decorations.Theme, deco, allActions, w.decorations.Config.Title)
	// Update the decorations based on the current window mode.
	var actions system.Action
	switch m := w.decorations.Config.Mode; m {
	case Windowed:
		actions |= system.ActionUnmaximize
	case Minimized:
		actions |= system.ActionMinimize
	case Maximized:
		actions |= system.ActionMaximize
	case Fullscreen:
		actions |= system.ActionFullscreen
	default:
		panic(fmt.Errorf("unknown WindowMode %v", m))
	}
	gtx := layout.Context{
		Ops:         o,
		Now:         e.Now,
		Source:      e.Source,
		Metric:      e.Metric,
		Constraints: layout.Exact(e.Size),
	}
	// Update the window based on the actions on the decorations.
	opts, acts := splitActions(deco.Update(gtx))
	if len(opts) > 0 {
		w.driver.Configure(opts)
	}
	if acts != 0 {
		w.driver.Perform(acts)
	}
	style.Layout(gtx)
	// Offset to place the frame content below the decorations.
	decoHeight := gtx.Dp(w.decorations.Config.decoHeight)
	if w.decorations.currentHeight != decoHeight {
		w.decorations.currentHeight = decoHeight
		w.coalesced.cfg = &ConfigEvent{Config: w.effectiveConfig()}
	}
	return image.Pt(0, decoHeight)
}

func (w *Window) effectiveConfig() Config {
	cnf := w.decorations.Config
	cnf.Size.Y -= w.decorations.currentHeight
	cnf.Decorated = w.decorations.enabled || cnf.Decorated
	return cnf
}

// splitActions splits options from actions and return them and the remaining
// actions.
func splitActions(actions system.Action) ([]Option, system.Action) {
	var opts []Option
	walkActions(actions, func(action system.Action) {
		switch action {
		case system.ActionMinimize:
			opts = append(opts, Minimized.Option())
		case system.ActionMaximize:
			opts = append(opts, Maximized.Option())
		case system.ActionUnmaximize:
			opts = append(opts, Windowed.Option())
		case system.ActionFullscreen:
			opts = append(opts, Fullscreen.Option())
		default:
			return
		}
		actions &^= action
	})
	return opts, actions
}

// Perform the actions on the window.
func (w *Window) Perform(actions system.Action) {
	opts, acts := splitActions(actions)
	w.Option(opts...)
	if acts == 0 {
		return
	}
	if w.driver == nil {
		w.initialActions = append(w.initialActions, acts)
		return
	}
	w.Run(func() {
		w.driver.Perform(actions)
	})
}

// Title sets the title of the window.
func Title(t string) Option {
	return func(_ unit.Metric, cnf *Config) {
		cnf.Title = t
	}
}

// Size sets the size of the window. The mode will be changed to Windowed.
func Size(w, h unit.Dp) Option {
	if w <= 0 {
		panic("width must be larger than or equal to 0")
	}
	if h <= 0 {
		panic("height must be larger than or equal to 0")
	}
	return func(m unit.Metric, cnf *Config) {
		cnf.Mode = Windowed
		cnf.Size = image.Point{
			X: m.Dp(w),
			Y: m.Dp(h),
		}
	}
}

// MaxSize sets the maximum size of the window.
func MaxSize(w, h unit.Dp) Option {
	if w <= 0 {
		panic("width must be larger than or equal to 0")
	}
	if h <= 0 {
		panic("height must be larger than or equal to 0")
	}
	return func(m unit.Metric, cnf *Config) {
		cnf.MaxSize = image.Point{
			X: m.Dp(w),
			Y: m.Dp(h),
		}
	}
}

// MinSize sets the minimum size of the window.
func MinSize(w, h unit.Dp) Option {
	if w <= 0 {
		panic("width must be larger than or equal to 0")
	}
	if h <= 0 {
		panic("height must be larger than or equal to 0")
	}
	return func(m unit.Metric, cnf *Config) {
		cnf.MinSize = image.Point{
			X: m.Dp(w),
			Y: m.Dp(h),
		}
	}
}

// StatusColor sets the color of the Android status bar.
func StatusColor(color color.NRGBA) Option {
	return func(_ unit.Metric, cnf *Config) {
		cnf.StatusColor = color
	}
}

// NavigationColor sets the color of the navigation bar on Android, or the address bar in browsers.
func NavigationColor(color color.NRGBA) Option {
	return func(_ unit.Metric, cnf *Config) {
		cnf.NavigationColor = color
	}
}

// CustomRenderer controls whether the window contents is
// rendered by the client. If true, no GPU context is created.
//
// Caller must assume responsibility for rendering which includes
// initializing the render backend, swapping the framebuffer and
// handling frame pacing.
func CustomRenderer(custom bool) Option {
	return func(_ unit.Metric, cnf *Config) {
		cnf.CustomRenderer = custom
	}
}

// Decorated controls whether Gio and/or the platform are responsible
// for drawing window decorations. Providing false indicates that
// the application will either be undecorated or will draw its own decorations.
func Decorated(enabled bool) Option {
	return func(_ unit.Metric, cnf *Config) {
		cnf.Decorated = enabled
	}
}

// flushEvent is sent to detect when the user program
// has completed processing of all prior events. Its an
// [io/event.Event] but only for internal use.
type flushEvent struct{}

func (t flushEvent) ImplementsEvent() {}

// theFlushEvent avoids allocating garbage when sending
// flushEvents.
var theFlushEvent flushEvent
