// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s

package model

import (
	"fmt"
	"log/slog"
	"sync"

	"github.com/derailed/k9s/internal/slogs"
)

const (
	// StackPush denotes an add on the stack.
	StackPush StackAction = 1 << iota

	// StackPop denotes a delete on the stack.
	StackPop
)

// StackAction represents an action on the stack.
type StackAction int

// StackEvent represents an operation on a view stack.
type StackEvent struct {
	// Kind represents the event condition.
	Action StackAction

	// Item represents the targeted item.
	Component Component
}

// StackListener represents a stack listener.
type StackListener interface {
	// StackPushed indicates a new item was added.
	StackPushed(Component)

	// StackPopped indicates an item was deleted
	StackPopped(old, new Component)

	// StackTop indicates the top of the stack
	StackTop(Component)
}

// Stack represents a stacks of components.
type Stack struct {
	components []Component
	listeners  []StackListener
	mx         sync.RWMutex
}

// NewStack returns a new initialized stack.
func NewStack() *Stack {
	return &Stack{}
}

// Flatten returns a string representation of the component stack.
func (s *Stack) Flatten() []string {
	s.mx.RLock()
	defer s.mx.RUnlock()

	ss := make([]string, len(s.components))
	for i, c := range s.components {
		ss[i] = c.Name()
	}
	return ss
}

// RemoveListener removes a listener.
func (s *Stack) RemoveListener(l StackListener) {
	victim := -1
	for i, lis := range s.listeners {
		if lis == l {
			victim = i
			break
		}
	}
	if victim == -1 {
		return
	}
	s.listeners = append(s.listeners[:victim], s.listeners[victim+1:]...)
}

// AddListener registers a stack listener.
func (s *Stack) AddListener(l StackListener) {
	s.listeners = append(s.listeners, l)
	if !s.Empty() {
		l.StackTop(s.Top())
	}
}

// Push adds a new item.
func (s *Stack) Push(c Component) {
	if top := s.Top(); top != nil {
		top.Stop()
	}

	s.mx.Lock()
	s.components = append(s.components, c)
	s.mx.Unlock()
	s.notify(StackPush, c)
}

// Pop removed the top item and returns it.
func (s *Stack) Pop() (Component, bool) {
	if s.Empty() {
		return nil, false
	}

	var c Component
	s.mx.Lock()
	c = s.components[len(s.components)-1]
	c.Stop()
	s.components = s.components[:len(s.components)-1]
	s.mx.Unlock()

	s.notify(StackPop, c)

	return c, true
}

// Peek returns stack state.
func (s *Stack) Peek() []Component {
	s.mx.RLock()
	defer s.mx.RUnlock()

	return s.components
}

// Clear clear out the stack using pops.
func (s *Stack) Clear() {
	for range s.components {
		s.Pop()
	}
}

// Empty returns true if the stack is empty.
func (s *Stack) Empty() bool {
	s.mx.RLock()
	defer s.mx.RUnlock()

	return len(s.components) == 0
}

// IsLast indicates if stack only has one item left.
func (s *Stack) IsLast() bool {
	return len(s.components) == 1
}

// Previous returns the previous component if any.
func (s *Stack) Previous() Component {
	if s.Empty() {
		return nil
	}
	if s.IsLast() {
		return s.Top()
	}

	return s.components[len(s.components)-2]
}

// Top returns the top most item or nil if the stack is empty.
func (s *Stack) Top() Component {
	if s.Empty() {
		return nil
	}

	s.mx.RLock()
	defer s.mx.RUnlock()
	return s.components[len(s.components)-1]
}

func (s *Stack) notify(a StackAction, c Component) {
	for _, l := range s.listeners {
		switch a {
		case StackPush:
			l.StackPushed(c)
		case StackPop:
			l.StackPopped(c, s.Top())
		}
	}
}

// ----------------------------------------------------------------------------
// Helpers...

// Dump prints out the stack.
func (s *Stack) Dump() {
	slog.Debug("Stack Dump", slogs.Stack, fmt.Sprintf("%p", s))
	for i, c := range s.components {
		slog.Debug(fmt.Sprintf("%d -- %s -- %#v", i, c.Name(), c))
	}
	slog.Debug("------------------")
}
