// SPDX-License-Identifier: Unlicense OR MIT

package input

import (
	"image"
	"sort"

	"gioui.org/f32"
	"gioui.org/io/event"
	"gioui.org/io/key"
)

// EditorState represents the state of an editor needed by input handlers.
type EditorState struct {
	Selection struct {
		Transform f32.Affine2D
		key.Range
		key.Caret
	}
	Snippet key.Snippet
}

type TextInputState uint8

type keyQueue struct {
	order    []event.Tag
	dirOrder []dirFocusEntry
	hint     key.InputHint
}

// keyState is the input state related to key events.
type keyState struct {
	focus   event.Tag
	state   TextInputState
	content EditorState
}

type keyHandler struct {
	// visible will be true if the InputOp is present
	// in the current frame.
	visible bool
	// reset tracks whether the handler has seen a
	// focus reset.
	reset        bool
	hint         key.InputHint
	orderPlusOne int
	dirOrder     int
	trans        f32.Affine2D
}

type keyFilter []key.Filter

type dirFocusEntry struct {
	tag    event.Tag
	row    int
	area   int
	bounds image.Rectangle
}

const (
	TextInputKeep TextInputState = iota
	TextInputClose
	TextInputOpen
)

func (k *keyHandler) inputHint(hint key.InputHint) {
	k.hint = hint
}

// InputState returns the input state and returns a state
// reset to [TextInputKeep].
func (s keyState) InputState() (keyState, TextInputState) {
	state := s.state
	s.state = TextInputKeep
	return s, state
}

// InputHint returns the input hint from the focused handler and whether it was
// changed since the last call.
func (q *keyQueue) InputHint(handlers map[event.Tag]*handler, state keyState) (key.InputHint, bool) {
	focused, ok := handlers[state.focus]
	if !ok {
		return q.hint, false
	}
	old := q.hint
	q.hint = focused.key.hint
	return q.hint, old != q.hint
}

func (k *keyHandler) Reset() {
	k.visible = false
	k.orderPlusOne = 0
	k.hint = key.HintAny
}

func (q *keyQueue) Reset() {
	q.order = q.order[:0]
	q.dirOrder = q.dirOrder[:0]
}

func (k *keyHandler) ResetEvent() (event.Event, bool) {
	if k.reset {
		return nil, false
	}
	k.reset = true
	return key.FocusEvent{Focus: false}, true
}

func (q *keyQueue) Frame(handlers map[event.Tag]*handler, state keyState) keyState {
	if state.focus != nil {
		if h, ok := handlers[state.focus]; !ok || !h.filter.focusable || !h.key.visible {
			// Remove focus from the handler that is no longer focusable.
			state.focus = nil
			state.state = TextInputClose
		}
	}
	q.updateFocusLayout(handlers)
	return state
}

// updateFocusLayout partitions input handlers handlers into rows
// for directional focus moves.
//
// The approach is greedy: pick the topmost handler and create a row
// containing it. Then, extend the handler bounds to a horizontal beam
// and add to the row every handler whose center intersect it. Repeat
// until no handlers remain.
func (q *keyQueue) updateFocusLayout(handlers map[event.Tag]*handler) {
	order := q.dirOrder
	// Sort by ascending y position.
	sort.SliceStable(order, func(i, j int) bool {
		return order[i].bounds.Min.Y < order[j].bounds.Min.Y
	})
	row := 0
	for len(order) > 0 {
		h := &order[0]
		h.row = row
		bottom := h.bounds.Max.Y
		end := 1
		for ; end < len(order); end++ {
			h := &order[end]
			center := (h.bounds.Min.Y + h.bounds.Max.Y) / 2
			if center > bottom {
				break
			}
			h.row = row
		}
		// Sort row by ascending x position.
		sort.SliceStable(order[:end], func(i, j int) bool {
			return order[i].bounds.Min.X < order[j].bounds.Min.X
		})
		order = order[end:]
		row++
	}
	for i, o := range q.dirOrder {
		handlers[o.tag].key.dirOrder = i
	}
}

// MoveFocus attempts to move the focus in the direction of dir.
func (q *keyQueue) MoveFocus(handlers map[event.Tag]*handler, state keyState, dir key.FocusDirection) (keyState, []taggedEvent) {
	if len(q.dirOrder) == 0 {
		return state, nil
	}
	order := 0
	if state.focus != nil {
		order = handlers[state.focus].key.dirOrder
	}
	focus := q.dirOrder[order]
	switch dir {
	case key.FocusForward, key.FocusBackward:
		if len(q.order) == 0 {
			break
		}
		order := 0
		if dir == key.FocusBackward {
			order = -1
		}
		if state.focus != nil {
			order = handlers[state.focus].key.orderPlusOne - 1
			if dir == key.FocusForward {
				order++
			} else {
				order--
			}
		}
		order = (order + len(q.order)) % len(q.order)
		return q.Focus(handlers, state, q.order[order])
	case key.FocusRight, key.FocusLeft:
		next := order
		if state.focus != nil {
			next = order + 1
			if dir == key.FocusLeft {
				next = order - 1
			}
		}
		if 0 <= next && next < len(q.dirOrder) {
			newFocus := q.dirOrder[next]
			if newFocus.row == focus.row {
				return q.Focus(handlers, state, newFocus.tag)
			}
		}
	case key.FocusUp, key.FocusDown:
		delta := +1
		if dir == key.FocusUp {
			delta = -1
		}
		nextRow := 0
		if state.focus != nil {
			nextRow = focus.row + delta
		}
		var closest event.Tag
		dist := int(1e6)
		center := (focus.bounds.Min.X + focus.bounds.Max.X) / 2
	loop:
		for 0 <= order && order < len(q.dirOrder) {
			next := q.dirOrder[order]
			switch next.row {
			case nextRow:
				nextCenter := (next.bounds.Min.X + next.bounds.Max.X) / 2
				d := center - nextCenter
				if d < 0 {
					d = -d
				}
				if d > dist {
					break loop
				}
				dist = d
				closest = next.tag
			case nextRow + delta:
				break loop
			}
			order += delta
		}
		if closest != nil {
			return q.Focus(handlers, state, closest)
		}
	}
	return state, nil
}

func (q *keyQueue) BoundsFor(k *keyHandler) image.Rectangle {
	order := k.dirOrder
	return q.dirOrder[order].bounds
}

func (q *keyQueue) AreaFor(k *keyHandler) int {
	order := k.dirOrder
	return q.dirOrder[order].area
}

func (k *keyFilter) Matches(focus event.Tag, e key.Event, system bool) bool {
	for _, f := range *k {
		if keyFilterMatch(focus, f, e, system) {
			return true
		}
	}
	return false
}

func keyFilterMatch(focus event.Tag, f key.Filter, e key.Event, system bool) bool {
	if f.Focus != nil && f.Focus != focus {
		return false
	}
	if (f.Name != "" || system) && f.Name != e.Name {
		return false
	}
	if e.Modifiers&f.Required != f.Required {
		return false
	}
	if e.Modifiers&^(f.Required|f.Optional) != 0 {
		return false
	}
	return true
}

func (q *keyQueue) Focus(handlers map[event.Tag]*handler, state keyState, focus event.Tag) (keyState, []taggedEvent) {
	if focus == state.focus {
		return state, nil
	}
	state.content = EditorState{}
	var evts []taggedEvent
	if state.focus != nil {
		evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: false}})
	}
	state.focus = focus
	if state.focus != nil {
		evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: true}})
	}
	if state.focus == nil || state.state == TextInputKeep {
		state.state = TextInputClose
	}
	return state, evts
}

func (s keyState) softKeyboard(show bool) keyState {
	if show {
		s.state = TextInputOpen
	} else {
		s.state = TextInputClose
	}
	return s
}

func (k *keyFilter) Add(f key.Filter) {
	for _, f2 := range *k {
		if f == f2 {
			return
		}
	}
	*k = append(*k, f)
}

func (k *keyFilter) Merge(k2 keyFilter) {
	*k = append(*k, k2...)
}

func (q *keyQueue) inputOp(tag event.Tag, state *keyHandler, t f32.Affine2D, area int, bounds image.Rectangle) {
	state.visible = true
	if state.orderPlusOne == 0 {
		state.orderPlusOne = len(q.order) + 1
		q.order = append(q.order, tag)
		q.dirOrder = append(q.dirOrder, dirFocusEntry{tag: tag, area: area, bounds: bounds})
	}
	state.trans = t
}

func (q *keyQueue) setSelection(state keyState, req key.SelectionCmd) keyState {
	if req.Tag != state.focus {
		return state
	}
	state.content.Selection.Range = req.Range
	state.content.Selection.Caret = req.Caret
	return state
}

func (q *keyQueue) editorState(handlers map[event.Tag]*handler, state keyState) EditorState {
	s := state.content
	if f := state.focus; f != nil {
		s.Selection.Transform = handlers[f].key.trans
	}
	return s
}

func (q *keyQueue) setSnippet(state keyState, req key.SnippetCmd) keyState {
	if req.Tag == state.focus {
		state.content.Snippet = req.Snippet
	}
	return state
}

func (t TextInputState) String() string {
	switch t {
	case TextInputKeep:
		return "Keep"
	case TextInputClose:
		return "Close"
	case TextInputOpen:
		return "Open"
	default:
		panic("unexpected value")
	}
}
