// Package fsm defines a finite state machine
package fsm

import "fmt"

// State represents a single node in the graph.
type State struct {
	// Transitions indicates which out arrows this node has
	Transitions []Transition
}

type (
	// StateID represents the Identifier of the state.
	StateID int8
	// StateIDSlice represents the list of state identifiers.
	StateIDSlice []StateID
	// States is the map from state identifier to the state itself
	States map[StateID]State
)

// Len is defined here such that we can sort the slice
func (v StateIDSlice) Len() int {
	return len(v)
}

// Less is defined here such that we can sort the slice
func (v StateIDSlice) Less(i, j int) bool {
	return v[i] < v[j]
}

// Swap is defined here such that we can sort the slice
func (v StateIDSlice) Swap(i, j int) {
	v[i], v[j] = v[j], v[i]
}

// Transition indicates an arrow in the state graph.
type Transition struct {
	// To represents the to-be-new state
	To StateID
	// Description is what type of message the arrow gets in the graph
	Description string
}

// FSM represents the total graph.
type FSM struct {
	// States is the map from state ID to states
	States States

	// Current is the current state represented by the identifier
	Current StateID

	// Name represents the descriptive name of this state machine
	Name string

	// StateCallback is the function ran when a transition occurs
	// It takes the old state, the new state and the data and returns if this is handled by the client
	StateCallback func(StateID, StateID, any) bool

	// GetStateName gets the name of a state as a string
	GetStateName func(StateID) string

	// initial is the initial state that we can always go back to
	initial StateID
}

// NewFSM creates a new finite state machine
func NewFSM(current StateID, states States, callback func(StateID, StateID, any) bool, nameGen func(StateID) string) FSM {
	return FSM{
		States:        states,
		Current:       current,
		StateCallback: callback,
		GetStateName:  nameGen,
		initial:       current,
	}
}

// InState returns whether or not the state machine is in the given 'check' state.
func (fsm *FSM) InState(check StateID) bool {
	return check == fsm.Current
}

// CheckTransition returns an error whether or not a transition to
// state `desired` is possible
func (fsm *FSM) CheckTransition(desired StateID) error {
	// initial or begin state is fine
	// 0 = deregistered
	if desired == fsm.initial || desired == 0 {
		return nil
	}
	for _, ts := range fsm.States[fsm.Current].Transitions {
		if ts.To == desired {
			return nil
		}
	}
	return fmt.Errorf("fsm invalid transition attempt from '%s' to '%s'", fsm.GetStateName(fsm.Current), fsm.GetStateName(desired))
}

// GoTransitionRequired transitions the state machine to a new state with associated state data 'data'
// If this transition is not handled by the client, it returns an error.
func (fsm *FSM) GoTransitionRequired(newState StateID, data any) error {
	oldState := fsm.Current

	handled, err := fsm.GoTransitionWithData(newState, data)
	// transition ios not possible
	if err != nil {
		return err
	}
	// transition is not handled
	if !handled {
		return fmt.Errorf("fsm failed transition from '%s' to '%s', is this required transition handled?", fsm.GetStateName(oldState), fsm.GetStateName(newState))
	}
	return nil
}

// GoTransitionWithData is a helper that transitions the state machine toward the 'newState' with associated state data 'data'
// It returns whether or not the transition is handled by the client.
func (fsm *FSM) GoTransitionWithData(newState StateID, data any) (bool, error) {
	if err := fsm.CheckTransition(newState); err != nil {
		return false, err
	}

	prev := fsm.Current
	fsm.Current = newState
	return fsm.StateCallback(prev, newState, data), nil
}

// GoTransition is an alias to call GoTransitionWithData but have an empty string as data.
func (fsm *FSM) GoTransition(newState StateID) (bool, error) {
	// No data means the callback is never required
	return fsm.GoTransitionWithData(newState, "")
}
