1/* Copyright 2016 Software Freedom Conservancy Inc.
2 *
3 * This software is licensed under the GNU Lesser General Public License
4 * (version 2.1 or later).  See the COPYING file in this distribution.
5 */
6
7public class Geary.State.Machine : BaseObject {
8
9    /** The state machine's current state. */
10    public uint state { get; private set; }
11
12    /** Determines if the state machine crashes your app when mis-configured. */
13    public bool abort_on_no_transition { get; set; default = true; }
14
15    /** Determines if transition logging is enabled. */
16    public bool logging { get; private set; default = false; }
17
18    private Geary.State.MachineDescriptor descriptor;
19    private Mapping[,] transitions;
20    private unowned Transition? default_transition;
21    private bool locked = false;
22    private unowned PostTransition? post_transition = null;
23    private void *post_user = null;
24    private Object? post_object = null;
25    private Error? post_err = null;
26
27    public Machine(MachineDescriptor descriptor, Mapping[] mappings, Transition? default_transition) {
28        this.descriptor = descriptor;
29        this.default_transition = default_transition;
30
31        // verify that each state and event in the mappings are valid
32        foreach (Mapping mapping in mappings) {
33            assert(mapping.state < descriptor.state_count);
34            assert(mapping.event < descriptor.event_count);
35        }
36
37        state = descriptor.start_state;
38
39        // build a transition map with state/event IDs (i.e. offsets) pointing directly into the
40        // map
41        transitions = new Mapping[descriptor.state_count, descriptor.event_count];
42        for (int ctr = 0; ctr < mappings.length; ctr++) {
43            Mapping mapping = mappings[ctr];
44            assert(transitions[mapping.state, mapping.event] == null);
45            transitions[mapping.state, mapping.event] = mapping;
46        }
47    }
48
49    public uint issue(uint event, void *user = null, Object? object = null, Error? err = null) {
50        assert(event < descriptor.event_count);
51        assert(state < descriptor.state_count);
52
53        unowned Mapping? mapping = transitions[state, event];
54
55        unowned Transition? transition = (mapping != null) ? mapping.transition : default_transition;
56        if (transition == null) {
57            string msg = "%s: No transition defined for %s@%s".printf(to_string(),
58                descriptor.get_event_string(event), descriptor.get_state_string(state));
59
60            if (this.abort_on_no_transition)
61                error(msg);
62            else
63                critical(msg);
64
65            return state;
66        }
67
68        // guard against reentrancy ... don't want to use a non-reentrant lock because then
69        // the machine will simply hang; assertion is better to ferret out design flaws
70        if (locked) {
71            error("Fatal reentrancy on locked state machine %s: %s", descriptor.name,
72                get_event_issued_string(state, event));
73        }
74        locked = true;
75
76        uint old_state = state;
77        state = transition(state, event, user, object, err);
78        assert(state < descriptor.state_count);
79
80        if (!locked) {
81            error("Exited transition to unlocked state machine %s: %s", descriptor.name,
82                get_transition_string(old_state, event, state));
83        }
84        locked = false;
85
86        if (this.logging)
87            message("%s: %s", to_string(), get_transition_string(old_state, event, state));
88
89        // Perform post-transition if registered
90        if (post_transition != null) {
91            // clear post-transition before calling, in case this method is re-entered
92            unowned PostTransition? perform = post_transition;
93            void* perform_user = post_user;
94            Object? perform_object = post_object;
95            Error? perform_err = post_err;
96
97            post_transition = null;
98            post_user = null;
99            post_object = null;
100            post_err = null;
101
102            perform(perform_user, perform_object, perform_err);
103        }
104
105        return state;
106    }
107
108    // Must be used carefully: this allows for a delegate (callback) to be called after the
109    // *current* transition has occurred; thus, this method can *only* be called from within
110    // a TransitionHandler.
111    //
112    // Note that if a second call to this method is made inside the same transition, the earlier
113    // PostTransition is silently dropped.  Only one PostTransition call may be registered.
114    //
115    // Returns false if unable to register the PostTransition delegate for the reasons specified
116    // above.
117    public bool do_post_transition(PostTransition post_transition, void *user = null,
118        Object? object = null, Error? err = null) {
119        if (!locked) {
120            warning("%s: Attempt to register post-transition while machine is unlocked", to_string());
121
122            return false;
123        }
124
125        this.post_transition = post_transition;
126        post_user = user;
127        post_object = object;
128        post_err = err;
129
130        return true;
131    }
132
133    public string get_state_string(uint state) {
134        return descriptor.get_state_string(state);
135    }
136
137    public string get_event_string(uint event) {
138        return descriptor.get_event_string(event);
139    }
140
141    public string get_event_issued_string(uint state, uint event) {
142        return "%s@%s".printf(descriptor.get_state_string(state), descriptor.get_event_string(event));
143    }
144
145    public string get_transition_string(uint old_state, uint event, uint new_state) {
146        return "%s@%s -> %s".printf(descriptor.get_state_string(old_state), descriptor.get_event_string(event),
147            descriptor.get_state_string(new_state));
148    }
149
150    public string to_string() {
151        return "Machine %s [%s]".printf(descriptor.name, descriptor.get_state_string(state));
152    }
153}
154
155