1package fsmtest
2
3import (
4	"fmt"
5	"sort"
6	"strings"
7
8	"github.com/jawher/mow.cli/internal/fsm"
9	"github.com/jawher/mow.cli/internal/matcher"
10)
11
12// NopeMatcher is a matcher that always fails
13type NopeMatcher struct{}
14
15// Match always returns false without consuming any args
16func (NopeMatcher) Match(args []string, c *matcher.ParseContext) (bool, []string) {
17	return false, args
18}
19
20// Priority returns the magic value 666
21func (NopeMatcher) Priority() int {
22	return 666
23}
24
25func (NopeMatcher) String() string {
26	return "<nope>"
27}
28
29// YepMatcher is a matcher that always succeeds without consuming any args
30type YepMatcher struct{}
31
32// Match always returns true without consuming any args
33func (YepMatcher) Match(args []string, c *matcher.ParseContext) (bool, []string) {
34	return true, args
35}
36
37// Priority returns the magic value 666
38func (YepMatcher) Priority() int {
39	return 666
40}
41
42func (YepMatcher) String() string {
43	return "<yep>"
44}
45
46// TestMatcher is a matcher with a configurable match function and priority
47type TestMatcher struct {
48	MatchFunc    func(args []string, c *matcher.ParseContext) (bool, []string)
49	TestPriority int
50}
51
52// Match executes the provided match func
53func (t TestMatcher) Match(args []string, c *matcher.ParseContext) (bool, []string) {
54	return t.MatchFunc(args, c)
55}
56
57// Priority returns the provided priority
58func (t TestMatcher) Priority() int {
59	return t.TestPriority
60}
61
62/*
63NewFsm constructs an FSM from the provided string spec and a list of defined matchers
64The spec syntax is:
65
66S1 t1 S2
67S2 t2 (S3)
68<source state name> <transition name> <target state name>
69
70states between parenthesis are final states
71*/
72func NewFsm(spec string, matchers map[string]matcher.Matcher) *fsm.State {
73	states := map[string]*fsm.State{}
74	lines := strings.FieldsFunc(spec, func(r rune) bool { return r == '\n' })
75
76	var res *fsm.State
77
78	for _, line := range lines {
79		if strings.TrimSpace(line) == "" {
80			continue
81		}
82		parts := strings.Fields(line)
83		if len(parts) != 3 {
84			panic(fmt.Sprintf("Invalid line %q: syntax: START TR END", line))
85		}
86		sn, tn, en := parts[0], parts[1], parts[2]
87		sn, sterm := stateNameTerm(sn)
88		en, eterm := stateNameTerm(en)
89
90		s, ok := states[sn]
91		if !ok {
92			s = fsm.NewState()
93			states[sn] = s
94		}
95		s.Terminal = s.Terminal || sterm
96
97		if res == nil {
98			res = s
99		}
100
101		e, ok := states[en]
102		if !ok {
103			e = fsm.NewState()
104			states[en] = e
105		}
106		e.Terminal = e.Terminal || eterm
107
108		t, ok := matchers[tn]
109		if !ok {
110			panic(fmt.Sprintf("Unknown matcher %q in line %q", tn, line))
111		}
112
113		s.T(t, e)
114	}
115	return res
116}
117
118// TransitionStrs returns a string slice with the transitions names
119func TransitionStrs(trs fsm.StateTransitions) []string {
120	var res []string
121	for _, tr := range trs {
122		res = append(res, fmt.Sprintf("%v", tr.Matcher))
123	}
124	return res
125}
126
127func stateNameTerm(name string) (string, bool) {
128	if strings.HasPrefix(name, "(") {
129		if strings.HasSuffix(name, ")") {
130			name = name[1 : len(name)-1]
131			return name, true
132		}
133		panic(fmt.Sprintf("Invalid state name %q", name))
134	}
135	return name, false
136}
137
138func mkStateNames() *stateNames {
139	return &stateNames{
140		counter: 1,
141		ids:     map[*fsm.State]int{},
142	}
143}
144
145type stateNames struct {
146	counter int
147	ids     map[*fsm.State]int
148}
149
150func (sn *stateNames) id(s *fsm.State) int {
151	res := sn.ids[s]
152	if res != 0 {
153		return res
154	}
155	res = sn.counter
156	sn.ids[s] = res
157	sn.counter++
158	return res
159}
160
161func stateName(s *fsm.State, sn *stateNames) string {
162	id := sn.id(s)
163
164	if !s.Terminal {
165		return fmt.Sprintf("S%d", id)
166	}
167	return fmt.Sprintf("(S%d)", id)
168}
169
170// FsmStr generates a string representation of the provided FSM
171func FsmStr(s *fsm.State) string {
172	lines := fsmStrVis(s, mkStateNames(), map[*fsm.State]struct{}{})
173	sort.Sort(lines)
174	return strings.Join(lines, "\n")
175}
176
177func fsmStrVis(s *fsm.State, sn *stateNames, visited map[*fsm.State]struct{}) fsmStrings {
178	if _, ok := visited[s]; ok {
179		return nil
180	}
181	visited[s] = struct{}{}
182
183	res := fsmStrings{}
184	for _, tr := range s.Transitions {
185		res = append(res, fmt.Sprintf("%s %v %s", stateName(s, sn), tr.Matcher, stateName(tr.Next, sn)))
186		res = append(res, fsmStrVis(tr.Next, sn, visited)...)
187	}
188
189	return res
190}
191
192type fsmStrings []string
193
194func (t fsmStrings) Len() int      { return len(t) }
195func (t fsmStrings) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
196func (t fsmStrings) Less(i, j int) bool {
197	a := strings.TrimFunc(t[i], isParen)
198	b := strings.TrimFunc(t[j], isParen)
199	return strings.Compare(a, b) < 0
200}
201
202func isParen(r rune) bool {
203	return r == '(' || r == ')'
204}
205