import assert from "./assert.js";
import { log } from "./logging.js";

export class FSM {

  constructor(data, events) {
    // data is really only for logging/debugging
    this.data = data;
    this.events = events;
    this.state = "START";
    this.on_state = undefined;
  }

  onTransition(cb) {
    this.transition_cb = cb;
  }

  transition(next_state) {
    this.state = next_state;

    if(this.transition_cb) {
      this.transition_cb(this);
    }
  }

  event_names() {
    return Object.getOwnPropertyNames(Object.getPrototypeOf(this.events)).filter(k => k !== "constructor");
  }

  async do(event, ...args) {
    const evhandler = this.events[event];
    assert(evhandler !== undefined, `Invalid event ${event}. Available ones are '${this.event_names()}'.`);

    // NOTE: you have to use .call to pass in the this.events object or else this === undefined in the call
    const next_state = await evhandler.call(this.events, this.state, ...args);
    assert(next_state, `The event "${event}" returned "${next_state}" but must be a truthy true state.`);

    if(Array.isArray(next_state)) {
      assert(next_state.length == 2, `Returning an array only allows 2 elements (state, func) but you returned ${next_state}`);
      let [state, func] = next_state;

      log.debug(`FSM ${this.events.constructor.name}: (${event}) = ${this.state} -> ${state} (${args})`, "DATA:", this.data, func ? `THEN ${func.name}()` : undefined);

      this.transition(state);
      await func();
    } else {
      log.debug(`FSM ${this.events.constructor.name}: (${event}) = ${this.state} -> ${next_state} (${args})`, "DATA:", this.data);
      this.transition(next_state);
    }

    return this.state;
  }
}

export default FSM;
