module Job::StateMachine
  extend ActiveSupport::Concern

  module ClassMethods
    #
    # Helper methods for display of transitions
    #

    def to_dot(*args)
      new.to_dot(*args)
    end

    def to_svg(*args)
      new.to_svg(*args)
    end
  end

  def transitions
    @transitions ||= load_transitions
  end

  def state=(next_state)
    # '*' refers to any state; if next_state is '*', remain in current state.
    super unless next_state.nil? || next_state == '*'
  end

  # test whether the transition is allowed; if yes, transit to next state
  def transit_state(signal)
    permitted_transitions = transitions[signal.to_sym]
    unless permitted_transitions.nil?
      next_state = permitted_transitions[state]

      # if current state is not explicitly permitted, is any state (referred by '*') permitted?
      next_state ||= permitted_transitions['*']
      self.state = next_state
    end
    !!next_state
  end

  def signal_abort(*args)
    signal(:abort, *args)
  end

  def signal_start(*args)
    signal(:start, *args)
  end

  def signal(signal, *args)
    signal = :abort_job if signal == :abort
    if transit_state(signal)
      save
      send(signal, *args) if respond_to?(signal)
    else
      raise _("%{signal} is not permitted at state %{state}") % {:signal => signal, :state => state}
    end
  end

  def queue_signal(*args, priority: MiqQueue::NORMAL_PRIORITY, role: nil, deliver_on: nil, server_guid: nil, queue_name: nil, msg_timeout: nil)
    MiqQueue.put(
      :class_name  => self.class.name,
      :method_name => "signal",
      :instance_id => id,
      :priority    => priority,
      :role        => role,
      :zone        => zone,
      :queue_name  => queue_name,
      :task_id     => guid,
      :args        => args,
      :deliver_on  => deliver_on,
      :server_guid => server_guid,
      :msg_timeout => msg_timeout
    )
  end

  #
  # Helper methods for display of transitions
  #

  def to_dot(include_starred_states = false)
    all_states = transitions.values.map(&:to_a).flatten.uniq.reject { |t| t == "*" }

    "".tap do |s|
      s << "digraph #{self.class.name.inspect} {\n"
      transitions.each do |signal, signal_transitions|
        signal_transitions.each do |from, to|
          next if !include_starred_states && (from == "*" || to == "*")

          from = from == "*" ? all_states : [from]
          to   = to == "*"   ? all_states : [to]
          from.product(to).each { |f, t| s << "  #{f} -> #{t} [ label=\"#{signal}\" ]\n" }
        end
      end
      s << "}\n"
    end
  end

  def to_svg(include_starred_states = false)
    require "open3"
    out, err, _status = Open3.capture3("dot -Tsvg", :stdin_data => to_dot(include_starred_states))

    raise "Error from graphviz:\n#{err}" if err.present?

    out
  end
end
