--[[ Copyright (c) 2009 Peter "Corsix" Cawley

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. --]]

class "QueueAction" (HumanoidAction)

---@type QueueAction
local QueueAction = _G["QueueAction"]

--! Queue for something (door or reception desk).
--!param x X position of the queue.
--!param y Y position of the queue.
--!param queue (Queue) Queue to join
function QueueAction:QueueAction(x, y, queue)
  assert(type(x) == "number", "Invalid value for parameter 'x'")
  assert(type(y) == "number", "Invalid value for parameter 'y'")
  assert(class.is(queue, Queue), "Invalid value for parameter 'queue'")

  self:HumanoidAction("queue")
  self.x = x -- Position of the queue(?)
  self.y = y
  self.face_x = nil -- Tile to turn the face to.
  self.face_y = nil
  self.queue = queue
  self.reserve_when_done = nil -- Object to reserve when leaving the queue.
end

--! Set the object to reserve when queueing is done.
--!param door (object) Object to reserve when leaving the queue.
--!return (action) self, for daisy-chaining.
function QueueAction:setReserveWhenDone(door)
  assert(class.is(door, Door), "Invalid value for parameter 'door'")

  self.reserve_when_done = door
  return self
end

--! Set the tile to face.
--!param face_x (int) X coordinate of the tile to face.
--!param face_y (int) Y coordinate of the tile to face.
--!return (action) self, for daisy-chaining.
function QueueAction:setFaceDirection(face_x, face_y)
  assert(type(face_x) == "number", "Invalid value for parameter 'face_x'")
  assert(type(face_y) == "number", "Invalid value for parameter 'face_y'")

  self.face_x = face_x
  self.face_y = face_y
  return self
end

local function get_direction(x, y, facing_x, facing_y)
  if facing_y < y then
    return "north"
  elseif facing_y > y then
    return "south"
  end
  if facing_x > x then
    return "east"
  elseif facing_x < x then
    return "west"
  end
end

local function interrupt_head(humanoid, n)
  while n > 1 do
    local action = humanoid.action_queue[n]
    if action.name == "use_object" then
      -- Pull object usages out of the queue
      if action.object and action.object:isReservedFor(humanoid) then
        action.object:removeReservedUser(humanoid)
      end
      table.remove(humanoid.action_queue, n)
    else
      -- Mark other actions as needing interruption
      assert(action.must_happen)
      action.todo_interrupt = true
    end
    n = n - 1
  end

  local action = humanoid.action_queue[n]
  assert(action.must_happen)
  local on_interrupt = action.on_interrupt
  if on_interrupt then
    action.on_interrupt = nil
    on_interrupt(action, humanoid)
  end
end

local function action_queue_find_idle(action, humanoid)
  local found_any = false
  for i, current_action in ipairs(humanoid.action_queue) do
    if current_action.name == "idle" then
      found_any = true
      if humanoid.action_queue[i + 1] == action then
        return i
      end
    end
  end
  if found_any then
    print("Warning: Proper idle not in action_queue")
  end
  return -1
end

local function action_queue_find_drink_action(action, humanoid)
  local found_any = false
  for i, current_action in ipairs(humanoid.action_queue) do
    if current_action.name == "use_object" and current_action.object.object_type.id == "drinks_machine" then
      found_any = true
      if humanoid.action_queue[i + 1] == action then
        return i
      end
    end
  end
  if found_any then
    error("Proper drink action not in action_queue")
  else
    return -1
  end
end

-- Finish standing includes currently going to the drinks machine.
local function action_queue_finish_standing(action, humanoid)
  local index = action_queue_find_idle(action, humanoid)
  if index == -1 then
    -- Maybe going to the drinks machine?
    index = action_queue_find_drink_action(action, humanoid)
    if index == -1 then
      -- Attempt to recover by assuming the person is sitting down.
      print("Warning: Idle not in action_queue")
      if humanoid:getCurrentAction().name == "use_object" then
        -- It is likely that the person is sitting down.
        return action_queue_leave_bench(action, humanoid)
      else
        error("This person seems to neither be standing nor sitting?!")
      end
    end
  end
  interrupt_head(humanoid, index)
  index = index + 1
  while index >= 1 do
    local current_action = humanoid.action_queue[index]
    if current_action == action then
      return index - 1
    end
    index = index - 1
  end
  error("Queue action not in action_queue")
end

local function action_queue_leave_bench(action, humanoid)
  local index
  for i, current_action in ipairs(humanoid.action_queue) do
    -- Check to see that we haven't
    -- gotten to the actual queue action yet.
    -- Instead of crashing if we have, try with the assumption
    -- that we're actually standing.
    if current_action == action then
      print("Warning: A patient supposedly sitting down was not," ..
        " trying to recover assuming he/she is standing.")
      return action_queue_finish_standing(action, humanoid)
    end
    if current_action.name == "use_object" then
      if humanoid.action_queue[i + 1] == action then
        interrupt_head(humanoid, i)
        index = i
        break
      end
    end
  end
  index = index + 1
  while index >= 1 do
    local current_action = humanoid.action_queue[index]
    if current_action == action then
      return index - 1
    end
    index = index - 1
  end
  error("Queue action not in action_queue")
end

local action_queue_on_change_position = permanent"action_queue_on_change_position"( function(action, humanoid)
  -- Only proceed with this handler if the patient is still in the queue
  if not action.is_in_queue then
    return
  end

  -- Find out if we have to be standing up - considering humanoid_class covers both health inspector and VIP
  local must_stand = not class.is(humanoid, Patient) or action.is_leaving or
    (humanoid.disease and humanoid.disease.must_stand)
  local queue = action.queue
  if not must_stand then
    for i = 1, queue.bench_threshold do
      if queue:reportedHumanoid(i) == humanoid then
        must_stand = true
        break
      end
    end
  end

  -- Maybe we can sit down?
  if not must_stand then
    -- Where are we in relation to door/reception desk (goal).
    -- Specify the distance we no longer look for a bench from our goal tile,
    -- and the maximum distance we will look for one too.
    local DISTANCE_THRESHOLD, MAX_DISTANCE = 4, 10
    if action.current_bench_distance and
        action.current_bench_distance <= DISTANCE_THRESHOLD then
      return
    end
    local hum_x, hum_y = humanoid.tile_x, humanoid.tile_y
    local goal_x, goal_y = action.x, action.y

    local distance_from_goal = humanoid.world:getPathDistance(hum_x, hum_y,
                                                              goal_x, goal_y)
    if not distance_from_goal then
      error("Patient in a queue has no calculated distance from themselves to their target door/desk!")
    end

    -- Look for a free bench that is closer to the goal. If we are standing
    -- expand the search radius to increase the chance of getting a seat.
    local bench_look_distance = not action:isStanding() and
        math.max(2 * distance_from_goal / 3, DISTANCE_THRESHOLD) or MAX_DISTANCE

    local bench, bx, by, new_dist = humanoid.world:getFreeBench(goal_x, goal_y, bench_look_distance)
    if bench then
      local num_actions_prior
      if action:isStanding() then
        num_actions_prior = action_queue_finish_standing(action, humanoid)
      else
        num_actions_prior = action_queue_leave_bench(action, humanoid)
      end
      action.current_bench_distance = new_dist
      humanoid:queueAction(WalkAction(bx, by):setMustHappen(true), num_actions_prior)
      humanoid:queueAction(UseObjectAction(bench):setMustHappen(true), num_actions_prior + 1)
      bench.reserved_for = humanoid
      return
    elseif not action:isStanding() then
      -- Already sitting down, so nothing to do.
      return
    end
  end

  -- Stand up in the correct position in the queue
  local standing_index = 0
  local our_room = humanoid:getRoom()
  for _, person in ipairs(queue) do
    if person == humanoid then
      break
    end
    if queue.callbacks[person]:isStanding() and person:getRoom() == our_room then
      standing_index = standing_index + 1
    end
  end
  local ix, iy = humanoid.world:getIdleTile(action.x, action.y, standing_index)
  assert(ix and iy)
  local facing_x, facing_y
  if standing_index == 0 then
    facing_x, facing_y = action.face_x or action.x, action.face_y or action.y
  else
    facing_x, facing_y = humanoid.world:getIdleTile(action.x, action.y, standing_index - 1)
  end
  assert(facing_x and facing_y)
  local idle_direction = get_direction(ix, iy, facing_x, facing_y)
  if action:isStanding() then
    local idle_index = action_queue_find_idle(action, humanoid)
    if idle_index == -1 then
      idle_index = action_queue_find_drink_action(action, humanoid)
      if idle_index ~= -1 then
        -- Going to get a drink. Do nothing since it will be fixed after getting the drink.
        return
      else
        error("Could not find an idle or drink action when trying to stand in line.")
      end
    end
    humanoid.action_queue[idle_index].direction = idle_direction
    humanoid:queueAction(WalkAction(ix, iy):setMustHappen(true):setIsLeaving(humanoid:isLeaving()), idle_index - 1)
  else
    action.current_bench_distance = nil
    local num_actions_prior = action_queue_leave_bench(action, humanoid)
    humanoid:queueAction(WalkAction(ix, iy):setMustHappen(true), num_actions_prior)
    humanoid:queueAction(IdleAction():setDirection(idle_direction):setMustHappen(true),
        num_actions_prior + 1)
  end
end)

local action_queue_is_standing = permanent"action_queue_is_standing"( function(action)
  return not action.current_bench_distance
end)

local action_queue_on_leave = permanent"action_queue_on_leave"( function(action, humanoid)
  action.is_in_queue = false
  if action.reserve_when_done then
    action.reserve_when_done:addReservedUser(humanoid)
  end
  for i, current_action in ipairs(humanoid.action_queue) do
    if current_action == action then
      interrupt_head(humanoid, i)
      return
    end
  end
  error("Queue action not in action_queue")
end)

-- While queueing one could get thirsty.
local action_queue_get_soda = permanent"action_queue_get_soda"(
function(action, humanoid, machine, mx, my, fun_after_use)
  local num_actions_prior
  if action:isStanding() then
    num_actions_prior = action_queue_finish_standing(action, humanoid)
  else
    num_actions_prior = action_queue_leave_bench(action, humanoid)
  end

  -- Callback function used after the drinks machine has been used.
  local --[[persistable:action_queue_get_soda_after_use]] function after_use()
    fun_after_use() -- Defined in patient:tickDay
    -- If the patient is still in the queue, insert an idle action so that
    -- change_position can do its work.
    -- Note that it is inserted after the currently executing use_object action.
    if action.is_in_queue and not humanoid.going_home then
      humanoid:queueAction(IdleAction():setMustHappen(true), 1)
      action_queue_on_change_position(action, humanoid)
    end
  end

  -- Walk to the machine and then use it.
  humanoid:queueAction(WalkAction(mx, my):setMustHappen(true), num_actions_prior)
  humanoid:queueAction(UseObjectAction(machine):setAfterUse(after_use)
      :setMustHappen(true), num_actions_prior + 1)
  machine:addReservedUser(humanoid)

  -- Make sure no one thinks we're sitting down anymore.
  action.current_bench_distance = nil
end)

local action_queue_interrupt = permanent"action_queue_interrupt"( function(action, humanoid)
  if action.is_in_queue then
    action.queue:removeValue(humanoid)
    if action.reserve_when_done then
      action.reserve_when_done:updateDynamicInfo()
    end
  end
  if action.reserve_when_done then
    if action.reserve_when_done:isReservedFor(humanoid) then
      action.reserve_when_done:removeReservedUser(humanoid)
    end
  end
  humanoid:finishAction()
end)

local function action_queue_start(action, humanoid)
  local queue = action.queue

  if action.done_init then
    return
  end
  action.done_init = true
  action.must_happen = true
  action.on_interrupt = action_queue_interrupt
  action.onChangeQueuePosition = action_queue_on_change_position
  action.onLeaveQueue = action_queue_on_leave
  action.onGetSoda = action_queue_get_soda
  action.isStanding = action_queue_is_standing

  action.is_in_queue = true
  queue:unexpect(humanoid)
  queue:push(humanoid, action)

  local door = action.reserve_when_done
  if door then
    door:updateDynamicInfo()
    if class.is(humanoid, Patient) then
      humanoid:updateDynamicInfo(_S.dynamic_info.patient.actions.queueing_for:format(door.room.room_info.name))
    end
  end
  humanoid:queueAction(IdleAction():setMustHappen(true):setIsLeaving(humanoid:isLeaving()), 0)
  action:onChangeQueuePosition(humanoid)

  if queue.same_room_priority then
    queue.same_room_priority:getRoom():tryAdvanceQueue()
  end
end

return action_queue_start
