package cview

import "sync"

// Focusable provides a method which determines if a primitive has focus.
// Composed primitives may be focused based on the focused state of their
// contained primitives.
type Focusable interface {
	HasFocus() bool
}

type focusElement struct {
	primitive Primitive
	disabled  bool
}

// FocusManager manages application focus.
type FocusManager struct {
	elements []*focusElement

	focused    int
	wrapAround bool

	setFocus func(p Primitive)

	sync.RWMutex
}

// NewFocusManager returns a new FocusManager object.
func NewFocusManager(setFocus func(p Primitive)) *FocusManager {
	return &FocusManager{setFocus: setFocus}
}

// SetWrapAround sets the flag that determines whether navigation will wrap
// around. That is, navigating forwards on the last field will move the
// selection to the first field (similarly in the other direction). If set to
// false, the focus won't change when navigating forwards on the last element
// or navigating backwards on the first element.
func (f *FocusManager) SetWrapAround(wrapAround bool) {
	f.Lock()
	defer f.Unlock()

	f.wrapAround = wrapAround
}

// Add adds an element to the focus handler.
func (f *FocusManager) Add(p ...Primitive) {
	f.Lock()
	defer f.Unlock()

	for _, primitive := range p {
		f.elements = append(f.elements, &focusElement{primitive: primitive})
	}
}

// AddAt adds an element to the focus handler at the specified index.
func (f *FocusManager) AddAt(index int, p Primitive) {
	f.Lock()
	defer f.Unlock()

	if index < 0 || index > len(f.elements) {
		panic("index out of range")
	}

	element := &focusElement{primitive: p}

	if index == len(f.elements) {
		f.elements = append(f.elements, element)
		return
	}
	f.elements = append(f.elements[:index+1], f.elements[index:]...)
	f.elements[index] = element
}

// Focus focuses the provided element.
func (f *FocusManager) Focus(p Primitive) {
	f.Lock()
	defer f.Unlock()

	for i, element := range f.elements {
		if p == element.primitive && !element.disabled {
			f.focused = i
			break
		}
	}
	f.setFocus(f.elements[f.focused].primitive)
}

// FocusPrevious focuses the previous element.
func (f *FocusManager) FocusPrevious() {
	f.Lock()
	defer f.Unlock()

	f.focused--
	f.updateFocusIndex(true)
	f.setFocus(f.elements[f.focused].primitive)
}

// FocusNext focuses the next element.
func (f *FocusManager) FocusNext() {
	f.Lock()
	defer f.Unlock()

	f.focused++
	f.updateFocusIndex(false)
	f.setFocus(f.elements[f.focused].primitive)
}

// FocusAt focuses the element at the provided index.
func (f *FocusManager) FocusAt(index int) {
	f.Lock()
	defer f.Unlock()

	f.focused = index
	f.setFocus(f.elements[f.focused].primitive)
}

// GetFocusIndex returns the index of the currently focused element.
func (f *FocusManager) GetFocusIndex() int {
	f.Lock()
	defer f.Unlock()

	return f.focused
}

// GetFocusedPrimitive returns the currently focused primitive.
func (f *FocusManager) GetFocusedPrimitive() Primitive {
	f.Lock()
	defer f.Unlock()

	return f.elements[f.focused].primitive
}

func (f *FocusManager) updateFocusIndex(decreasing bool) {
	for i := 0; i < len(f.elements); i++ {
		if f.focused < 0 {
			if f.wrapAround {
				f.focused = len(f.elements) - 1
			} else {
				f.focused = 0
			}
		} else if f.focused >= len(f.elements) {
			if f.wrapAround {
				f.focused = 0
			} else {
				f.focused = len(f.elements) - 1
			}
		}

		item := f.elements[f.focused]
		if !item.disabled {
			break
		}

		if decreasing {
			f.focused--
		} else {
			f.focused++
		}
	}
}

// Transform modifies the current focus.
func (f *FocusManager) Transform(tr Transformation) {
	var decreasing bool
	switch tr {
	case TransformFirstItem:
		f.focused = 0
		decreasing = true
	case TransformLastItem:
		f.focused = len(f.elements) - 1
	case TransformPreviousItem:
		f.focused--
		decreasing = true
	case TransformNextItem:
		f.focused++
	}
	f.updateFocusIndex(decreasing)
}
