// Package Navstack manages a stack of NavigationItems which can be pushed or popped from the stack.
// The top most stack navigation item is used by [BubbleTea] to Update and renders it's View.
// When pushing and popping items from the stack, the new view to be presented is sent a tea.WindowSizeMsg
// to ensure it's view can be presented correctly. When the last item is popped from the stack the application will quit.
// NavigationItem models which implement the Closable interface will have their Close method called when they are popped from the stack.
// This is useful for cleaning up resources that may not be garbage collected when a view a no longer needed.
// [BubbleTea]: https://github.com/charmbracelet/bubbletea
package navstack

import (
	"errors"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/kevm/bubbleo/window"
)

// Closable is an interface for models that have resources that need to be cleaned up when
// they are no longer needed. The navigation stack checks for this interface when popping items.
type Closable interface {
	Close() error
}

type Model struct {
	stack  []NavigationItem
	window *window.Model
}

// New creates a new navigation stack model. The window is used to
// constrain the view within the container of the navigation stack.
func New(w *window.Model) Model {
	model := Model{
		stack:  []NavigationItem{},
		window: w,
	}

	return model
}

func (m Model) Init() tea.Cmd {
	top := m.Top()
	if top == nil {
		return nil
	}

	return top.Init()
}

// Push pushes a new navigation item onto the stack.
// The new navigation item is given a tea.WindowSizeMsg to ensure it's view can be presented correctly.
// The item's Init method is called and resulting command is processed by [BubbleTea].
// If top item's model implements the Closable interface the Close method is called.
// This new item will be the top most item on the stack and thus will be rendered.
func (m *Model) Push(item NavigationItem) tea.Cmd {

	top := m.Top()
	if top != nil {
		if c, ok := top.Model.(Closable); ok {
			c.Close()
		}
	}

	initCmd := item.Init()

	wmsg := m.window.GetWindowSizeMsg()
	nim, winCmd := item.Model.Update(wmsg)
	item.Model = nim

	m.stack = append(m.stack, item)
	return tea.Sequence(initCmd, winCmd)
}

// Pop removes the top most navigation item from the stack.
// If the item implements the Closable interface the Close method is called.
// The new top most item on the stack is given a tea.WindowSizeMsg to ensure it's view can be presented correctly.
// If there are no more items on the stack the application will quit.
func (m *Model) Pop() tea.Cmd {
	top := m.Top()
	if top == nil {
		return tea.Quit // should not happen
	}

	if c, ok := top.Model.(Closable); ok {
		c.Close()
	}

	m.stack = m.stack[:len(m.stack)-1]
	top = m.Top()
	if top == nil {
		return tea.Quit
	}

	initCmd := top.Init()
	nim, winCmd := top.Model.Update(m.window.GetWindowSizeMsg())
	top.Model = nim

	return tea.Sequence(winCmd, initCmd)
}

// Clear pops all the items from the stack.
func (m *Model) Clear() error {
	var errs []error
	for _, item := range m.stack {
		if c, ok := item.Model.(Closable); ok {
			err := c.Close()
			if err != nil {
				errs = append(errs, err)
			}
		}
	}

	m.stack = []NavigationItem{}
	return errors.Join(errs...)
}

// Top returns the top most navigation item on the stack.
func (m Model) Top() *NavigationItem {
	if len(m.stack) == 0 {
		return nil
	}

	top := m.stack[len(m.stack)-1]
	return &top
}

// StackSummary returns a list of titles for each item on the stack.
// This is currently used by the breadcrumb component to render the breadcrumb trail.
func (m Model) StackSummary() []string {
	summary := []string{}
	for _, item := range m.stack {
		summary = append(summary, item.Title)
	}

	return summary
}

// Update processes messages for the top most navigation item on the stack.
func (m *Model) Update(msg tea.Msg) tea.Cmd {
	top := m.Top()
	switch msg := msg.(type) {
	case tea.WindowSizeMsg: // update the window size based on offsets
		if top == nil {
			return nil
		}
		m.window.Height = msg.Height
		m.window.Width = msg.Width
		msg.Width = m.window.Width - m.window.SideOffset
		msg.Height = m.window.Height - m.window.TopOffset
		um, cmd := top.Update(msg)
		m.stack[len(m.stack)-1] = um.(NavigationItem)
		return cmd
	case ReloadCurrent:
		if top == nil {
			return nil
		}
		return top.Init()
	case PopNavigation:
		cmd := m.Pop()
		return cmd
	case PushNavigation:
		cmd := m.Push(msg.Item)
		return cmd
	default:
		if top == nil {
			return nil
		}
		um, cmd := top.Update(msg)
		m.stack[len(m.stack)-1] = um.(NavigationItem)
		return cmd
	}
}

// View renders the top most navigation item on the stack.
func (m Model) View() string {

	top := m.Top()
	if top == nil {
		return ""
	}

	return top.View()
}