local h=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\109\098\111\115\046\067\111\109\098\111\083\116\101\112")local Y=require("\105\110\118\111\107\097\116\105\111\110\046\076\111\103\103\101\114")local q=require("\112\108\046\099\108\097\115\115")local t=require("\102\115\109")local I=require("\109\111\115\101\115")local x=q()Y.Extend(x)x.STATES={INITIAL="\115\116\097\114\116";PRE_FINISH="\112\114\101\095\102\105\110\105\115\104",FINISH="\102\105\110\105\115\104"}local m={PRE_FINISH="\112\114\101\095\102\105\110\105\115\104",FINISH="\102\105\110\105\115\104"}local b={}local function M(h,Y)return I.map(Y.next or{},function(q)local t=h[q]return{name=t.event,from=Y.state;to=t.state}end)end local function N(h)local Y={name=x.STATES.INITIAL,state=x.STATES.INITIAL,next={h[1].id}}local q={name=m.PRE_FINISH;from=h[#h].state,to=x.STATES.PRE_FINISH}local t={name=m.FINISH;from=x.STATES.PRE_FINISH;to=x.STATES.FINISH}local b=I.append({Y},h)local N=((I.chain(b)):map(I.partial(M,h))):flatten(true)N=((N:push(q)):push(t)):value()return{initial=x.STATES.INITIAL,events=N}end function x._init(x,Y,q,t)t=t or{}x.id=Y x.logger=t.logger x.clock=t.clock x.sequence=I.map(q,h)x.nextIds={x.sequence[1].id}x.next={x.sequence[1]}x.enterTimes={}x.leaveTimes={}x:createFSM()end function x.createFSM(Y)if b[Y.id]==nil then b[Y.id]=N(Y.sequence)end Y.fsm=t.create(b[Y.id])Y.fsm.onstatechange=function(q,t,x,m)local b=Y.clock()Y.leaveTimes[x]=b Y.enterTimes[m]=b Y.currentId=h.StepId(m)Y.current=Y.sequence[Y.currentId]Y.nextIds=Y.current and Y.current.next or{}Y.next=I.at(Y.sequence,unpack(Y.nextIds))Y:debugState("\115\116\097\116\101\095\099\104\097\110\103\101")end Y:debugState("\115\116\097\114\116")end function x.todot(h)return h.fsm:todot()end function x.debugState(Y,h)local q={state=Y.fsm.current;current=Y.currentId or"\060\110\105\108\062",next=Y.nextIds}Y:d(h,q)end function x.EnterTime(Y,h)return Y.enterTimes[h]end function x.LeaveTime(Y,h)return Y.leaveTimes[h]end function x.Progress(Y,h)local q=Y.fsm[h]if q==nil then return false end return q(Y.fsm)end function x.PreFinish(h)return h.fsm[m.PRE_FINISH](h.fsm)end function x.Finish(h)local Y=h.fsm[m.FINISH](h.fsm)if Y then h.duration=h:EnterTime(x.STATES.FINISH)-h:LeaveTime(x.STATES.INITIAL)end return Y end return x
--- ComboSequence class.
-- @classmod invokation.combos.ComboSequence
local ComboStep = require("invokation.combos.ComboStep")
local Logger = require("invokation.Logger")
local class = require("pl.class")
local fsm = require("fsm")
local m = require("moses")

local M = class()

Logger.Extend(M)

--- Sequence states
-- @table STATES
-- @tfield string INITIAL Initial
-- @tfield string PRE_FINISH Pre finish
-- @tfield string FINISH Finish
M.STATES = { INITIAL = "start", PRE_FINISH = "pre_finish", FINISH = "finish" }

local EVENTS = { PRE_FINISH = "pre_finish", FINISH = "finish" }

-- @todo Use an LRU cache
local CACHE = {}

local function stepEvents(sequence, step)
  return m.map(step.next or {}, function(nextId)
    local nextStep = sequence[nextId]

    return { name = nextStep.event, from = step.state, to = nextStep.state }
  end)
end

local function fsmDef(sequence)
  local firstStep = { name = M.STATES.INITIAL, state = M.STATES.INITIAL, next = { sequence[1].id } }
  local preFinishStep = {
    name = EVENTS.PRE_FINISH,
    from = sequence[#sequence].state,
    to = M.STATES.PRE_FINISH,
  }

  local finishStep = { name = EVENTS.FINISH, from = M.STATES.PRE_FINISH, to = M.STATES.FINISH }
  local steps = m.append({ firstStep }, sequence)
  local events = m.chain(steps):map(m.partial(stepEvents, sequence)):flatten(true)

  events = events:push(preFinishStep):push(finishStep):value()

  return { initial = M.STATES.INITIAL, events = events }
end

--- Constructor.
-- @tparam int id Combo id
-- @tparam {ComboStep.Spec,...} sequence Sequence of steps
-- @tparam[opt] table options Options
-- @tparam Logger options.logger Logger instance
-- @tparam function options.clock Clock function that returns the current time
function M:_init(id, sequence, options)
  options = options or {}

  self.id = id
  self.logger = options.logger
  self.clock = options.clock
  self.sequence = m.map(sequence, ComboStep)
  self.nextIds = { self.sequence[1].id }
  self.next = { self.sequence[1] }
  self.enterTimes = {}
  self.leaveTimes = {}

  self:createFSM()
end

function M:createFSM()
  if CACHE[self.id] == nil then
    CACHE[self.id] = fsmDef(self.sequence)
  end

  self.fsm = fsm.create(CACHE[self.id])

  self.fsm.onstatechange = function(_, _, from, to)
    local now = self.clock()

    self.leaveTimes[from] = now
    self.enterTimes[to] = now
    self.currentId = ComboStep.StepId(to)
    self.current = self.sequence[self.currentId]
    self.nextIds = (self.current and self.current.next) or {}
    self.next = m.at(self.sequence, unpack(self.nextIds))

    self:debugState("state_change")
  end

  self:debugState("start")
end

--- Generates a DOT formatted string from the sequence's FSM.
-- @treturn string DOT string
function M:todot()
  return self.fsm:todot()
end

function M:debugState(message)
  local state =
    { state = self.fsm.current, current = self.currentId or "<nil>", next = self.nextIds }
  self:d(message, state)
end

--- Returns the time when the sequence entered the given state.
-- @tparam string state State name
-- @treturn ?int Time or `nil` if state is invalid
function M:EnterTime(state)
  return self.enterTimes[state]
end

--- Returns the time when the sequence left the given state.
-- @tparam string state State name
-- @treturn ?int Time or `nil` if state is invalid
function M:LeaveTime(state)
  return self.leaveTimes[state]
end

--- Progresses the sequence with the given event if possible.
-- @tparam string event Event
-- @treturn bool `true` if succeeded, `false` otherwise
function M:Progress(event)
  local eventFn = self.fsm[event]

  if eventFn == nil then
    return false
  end

  return eventFn(self.fsm)
end

--- Progresses the sequence to pre finish if possible.
-- @treturn bool `true` if succeeded, `false` otherwise
function M:PreFinish()
  return self.fsm[EVENTS.PRE_FINISH](self.fsm)
end

--- Finishes the sequence if possible.
-- @treturn bool `true` if succeeded, `false` otherwise
function M:Finish()
  local finished = self.fsm[EVENTS.FINISH](self.fsm)

  if finished then
    self.duration = self:EnterTime(M.STATES.FINISH) - self:LeaveTime(M.STATES.INITIAL)
  end

  return finished
end

return M
