lib_statemachine
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © robbatt

//@version=5

// @description simple state machine that allows tracking a state an manipulating it with conditions
library("lib_statemachine", overlay = true)

export type StateMachine
    int state = 0
	int neutral = 0
	bool enabled = true
    int reset_counter = 0
    

// @function will step the state of the state machine from one to the next in case of condition
// @param this (StateMachine) the state machine to use
// @param before (int) from state
// @param after (int) to state
// @param condition (bool) if condition is true
// @returns true (bool) if the state of the statemachine changed
export method step(StateMachine this, int before, int after, bool condition = true) =>
    if condition and this.enabled and this.state == before
        this.state := after
        this.reset_counter := 0
        true
    else
        false

// @function will change the state of the state machine to the next in case of condition (not depending on previous state)
// @param this (StateMachine) the state machine to use
// @param after (int) to state
// @param condition (bool) if condition is true
// @returns true (bool) if the state of the statemachine changed
export method step(StateMachine this, int after, bool condition = true) =>
    this.step(this.state, after, condition)

// @function will return true if the state of the state machine was changed in this iteration
// @param this (StateMachine) the state machine to use
// @returns true (bool) if the state of the statemachine changed
export method changed(StateMachine this, int within_bars = 1) =>
    bars = ta.barssince(this.state[1] != this.state)
	na(bars) ? this.state != this.neutral : bars < within_bars


// @function will reset the state machine if a certain 'condition' appears 'min_occurrences' times
// @param this (StateMachine) the state machine to use
// @param condition (bool) reset condition 
// @param min_occurrences (int) min times 'condition' must appear for the reset to happen
// @returns true (bool) if the state of the statemachine changed
export method reset(StateMachine this, bool condition = true, int min_occurrences = 1) =>
    if this.reset_counter + 1 < min_occurrences and this.state != this.neutral and condition
        this.reset_counter := this.reset_counter + 1
        false
    else if this.reset_counter + 1 == min_occurrences
        this.reset_counter := 0
        this.state := this.neutral
        true
    else
        false

//////////////////////////////////////
// 			    TEST
//////////////////////////////////////

// import robbatt/lib_log/2 as LOG
// import robbatt/lib_unit/3 as UNIT

// var UNIT.Test assert = UNIT.Test.new(strict = true, verbose = true)
// assert.init()

// var s = StateMachine.new()
// assert.equals(s.state, 0, 'state initially 0')
// assert.is_true(s.enabled, 'statemachine initially enabled')

// // positive
// s.step(0, 1)
// assert.equals(s.state, 1, 'state 0->1')
// assert.is_true(s.changed(), 'state should be marked as changed after step 0->1')
// s.step(1, 5)
// assert.equals(s.state, 5, 'state 1->5')
// s.step(5, 0)
// assert.equals(s.state, 0, 'state 5->0')

// // negative
// s.step(0, -1)
// assert.equals(s.state, -1, 'state 0->-1')
// s.step(-1, -5)
// assert.equals(s.state, -5, 'state -1->-5')

// // reset
// s.reset()
// assert.equals(s.state, 0, 'reset state -5->0')
// s.step(0, 1)
// s.reset(min_occurrences = 2)
// assert.equals(s.state, 1, 'this reset requires 2 calls to reset')
// s.reset(min_occurrences = 2)
// assert.equals(s.state, 0, 'this reset requires 2 calls to reset, should be reset after second call')

// plot(s.state, 'State')
