--[[ 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. --]]

local TH = require "TH" 

class "Room"

function Room:Room(x, y, w, h, id, room_info, world, hospital, door, door2)
  self.id = id
  self.world = world
  self.hospital = hospital
  
  self.room_info = room_info
  self:initRoom(x, y, w, h, door, door2)
end

function Room:initRoom(x, y, w, h, door, door2)
  self.x = x
  self.y = y
  self.width = w
  self.height = h
  self.maximum_patients = 1 -- A good default for most rooms
  door.room = self
  self.door = door
  -- If it's a swing door we have two doors
  self.door2 = door2
  door:setDynamicInfo('text', {
    self.room_info.name, 
    _S.dynamic_info.object.queue_size:format(0), 
    _S.dynamic_info.object.queue_expected:format(0)
  })
  self.built = false
  self.crashed = false
  
  self.world.map.th:markRoom(x, y, w, h, self.room_info.floor_tile, self.id)
  
  self.humanoids = {--[[a set rather than a list]]}
  self.approaching_staff = {--[[a set rather than a list]]}
  self.objects = {--[[a set rather than a list]]}
end

function Room:getEntranceXY(inside)
  local door = self.door
  local x, y = door.tile_x, door.tile_y
  if (inside and self.world:getRoom(x, y) ~= self) or (not inside and self.world:getRoom(x, y) == self) then
    if door.direction == "north" then
      y = y - 1
    elseif door.direction == "west" then
      x = x - 1
    end
  end
  return x, y
end

function Room:createLeaveAction()
  local x, y = self:getEntranceXY(false)
  return {name = "walk", x = x, y = y, is_leaving = true}
end

function Room:createEnterAction(humanoid_entering)
  local x, y = self:getEntranceXY(true)
  if humanoid_entering and class.is(humanoid_entering, Staff) and self.approaching_staff then
    self.approaching_staff[humanoid_entering] = true
  end
  return {name = "walk", x = x, y = y, 
    is_entering = (humanoid_entering and self.approaching_staff) and self or true}
end

function Room:getPatient()
  for humanoid in pairs(self.humanoids) do
    if class.is(humanoid, Patient) then
      return humanoid
    end
  end
end

function Room:getPatientCount()
  local count = 0
  for humanoid in pairs(self.humanoids) do
    if class.is(humanoid, Patient) then
      count = count + 1
    end
  end
  return count
end

function Room:dealtWithPatient(patient)
  patient = patient or self:getPatient()
  patient:setNextAction(self:createLeaveAction())
  patient:addToTreatmentHistory(self.room_info)

  if patient.disease and not patient.diagnosed then
    -- Patient not yet diagnosed, hence just been in a diagnosis room.
    -- Increment diagnosis_progress, and send patient back to GP.

    -- Base: 0 .. 0.4 depending on difficulty of disease
    local diagnosis_base = 0.4 * (1 - patient.disease.diagnosis_difficulty)
    if diagnosis_base < 0 then
      diagnosis_base = 0
    end
    -- Bonus: 0.2 .. 0.4 (random) for perfectly skilled doctor. Less for less skilled doctors.
    local diagnosis_bonus = 0
    if self.staff_member then
      diagnosis_bonus = (0.2 + 0.2 * math.random()) * self.staff_member.profile.skill
    end
    
    patient:modifyDiagnosisProgress(diagnosis_base + diagnosis_bonus)
    patient:queueAction{name = "seek_room", room_type = "gp"}
    self.hospital:receiveMoneyForTreatment(patient)
  elseif patient.disease and patient.diagnosed then
    -- Patient just been in a cure room, so either patient now cured, or needs
    -- to move onto next cure room.
    patient.cure_rooms_visited = patient.cure_rooms_visited + 1
    local next_room = patient.disease.treatment_rooms[patient.cure_rooms_visited + 1]
    if next_room then
      -- Do not say that it is a treatment room here, since that check should already have been made.
      patient:queueAction{name = "seek_room", room_type = next_room}
    else
      -- Patient is "done" at the hospital
      patient:treated()
    end
  else
    patient:queueAction{name = "meander", count = 2}
    patient:queueAction{name = "idle"}
  end

  -- The staff member(s) might be needed somewhere else.
  self:findWorkForStaff()
end

--! Checks if the room still needs the staff in it and otherwise 
-- sends them away if they're needed somewhere else.
function Room:findWorkForStaff()
  -- It the the staff member is idle we can send him/her somewhere else
  for humanoid in pairs(self.humanoids) do
    -- Don't check handymen
    if class.is(humanoid, Staff) and humanoid.humanoid_class ~= "Handyman" and humanoid:isIdle() then
      local room = self.world:getNearestRoomNeedingStaff(humanoid)
      if room then
        humanoid:setNextAction(self:createLeaveAction())
        humanoid:queueAction(room:createEnterAction())
      end
    end
  end
end

local profile_attributes = {
  Psychiatrist = "is_psychiatrist",
  Surgeon = "is_surgeon",
  Researcher = "is_researcher",
}

-- Given any type of staff criteria (required/maximum), subtract the staff in the room and return the result
function Room:getMissingStaff(criteria, extended_search)
  local result = {}
  for attribute, count in pairs(criteria) do
    for humanoid in pairs(self.humanoids) do
      if class.is(humanoid, Staff) and humanoid:fulfillsCriterium(attribute) and not humanoid.action_queue[1].is_leaving then
        count = count - 1
      end
    end
    -- (Backwards compatibility)
    if self.approaching_staff and extended_search then
      -- Also check for staff currently heading for this room
      for humanoid in pairs(self.approaching_staff) do
        if humanoid:fulfillsCriterium(attribute) and humanoid ~= extended_search then
          count = count - 1
        end
      end
    end
    if count <= 0 then
      count = nil
    end
    result[attribute] = count
  end
  return result
end

function Room:testStaffCriteria(criteria, extra_humanoid, extended_search)
  -- criteria should be required_staff or maximum_staff table.
  -- if extra_humanoid is nil, then returns true if the humanoids in the room
  -- meet the given criteria, and false otherwise.
  -- if extra_humanoid is not nil, then returns true if the given humanoid
  -- would assist in satisfying the given criteria, and false if they would not.
  -- If extended_search is true staff heading for the room are also considered.
  -- If extended_search is a humanoid that single humanoid will not count.
  local missing = self:getMissingStaff(criteria, extended_search and extra_humanoid or nil)
  
  if extra_humanoid then
    local class = extra_humanoid.humanoid_class
    if class == "Surgeon" then
      class = "Doctor"
    end
    if missing[class] then
      return true
    end
    if class == "Doctor" then
      -- check for special proficiencies
      for attribute, profile_attribute in pairs(profile_attributes) do
        if extra_humanoid.profile and extra_humanoid.profile[profile_attribute] == 1.0 and missing[attribute] then
          return true
        end
      end
    end
    return false
  else
    for attribute, count in pairs(missing) do
      return false
    end
    return true
  end
end

local no_staff = {}
function Room:getMaximumStaffCriteria()
  -- Some rooms have dynamic criteria (i.e. dependent upon the number of items
  -- in the room), so this method is provided for such rooms to override it.
  return self.room_info.maximum_staff or self.room_info.required_staff or no_staff
end

function Room:getRequiredStaffCriteria()
  return self.room_info.required_staff or no_staff
end

function Room:onHumanoidEnter(humanoid)
  assert(not self.humanoids[humanoid], "Humanoid entering a room that they are already in")
  -- If this humanoid for some strange reason happens to enter a non-active room,
  -- just leave.
  if not self.is_active then
    if class.is(humanoid, Patient) then
        self:makePatientLeave(humanoid)
        humanoid:queueAction({name = "seek_room", room_type = self.room_info.id})
      else
        humanoid:setNextAction(self:createLeaveAction())
        humanoid:queueAction({name = "meander"})
      end
  end
  if humanoid.humanoid_class == "Handyman" then
    -- Handymen can always enter a room (to repair stuff, water plants, etc.)
    self.humanoids[humanoid] = true
    -- Check for machines which need repair or plants which need watering if
    -- the handyman didn't arrive as a part of a job
    if not humanoid.action_queue[1].is_job then
      local machine, plant
      
      for object in pairs(self.world:findAllObjectsNear(self:getEntranceXY(true))) do
        if class.is(object, Machine) and object.times_used ~= 0 then
          machine = object
          break
        elseif class.is(object, Plant) and object:needsWatering() then
         plant = object
        end
      end
      if machine then
        machine:setRepairing(humanoid)
        local x, y = machine:getRepairTile()
        humanoid:setNextAction({
          name = "walk",
          x = x,
          y = y,
          is_job = machine,
        }, 1)
        humanoid:queueAction(machine:createRepairAction(humanoid))
        local leave_action = self:createLeaveAction()
        leave_action.is_job = machine
        humanoid:queueAction(leave_action)
        humanoid:queueAction{name = "meander"}
      elseif plant then
        plant:createHandymanActions(humanoid)
      else -- Found nothing of interest in this room, so leave it.
        humanoid:setNextAction(self:createLeaveAction())
        humanoid:queueAction{name = "meander"}
      end
    end
    return
  end
  if class.is(humanoid, Staff) then
    -- If the room is already full of staff, or the staff member isn't relevant
    -- to the room, then make them leave. Otherwise, take control of them.
    if not self:staffFitsInRoom(humanoid) then
      self.humanoids[humanoid] = true
      humanoid:setNextAction(self:createLeaveAction())
      humanoid:queueAction{name = "meander"}
    else
      self.humanoids[humanoid] = true
      self:commandEnteringStaff(humanoid)
    end
    self:tryAdvanceQueue()
    return
  end
  self.humanoids[humanoid] = true
  self:tryAdvanceQueue()
  if class.is(humanoid, Patient) then
    -- Check if the staff requirements are still fulfilled (the staff might have left / been picked up meanwhile)
    if self:testStaffCriteria(self:getRequiredStaffCriteria()) then
      self:commandEnteringPatient(humanoid)
    else
      humanoid:setNextAction(self:createLeaveAction())
      humanoid:queueAction(self:createEnterAction())
    end
  end
end

-- Returns false if the room is already full of staff or if the given member of staff cannot help out.
-- Otherwise returns true.
-- If extended_search is true approaching staff is also considered.
function Room:staffFitsInRoom(staff, extended_search)
  local criteria = self:getMaximumStaffCriteria()
  if self:testStaffCriteria(criteria) or not self:testStaffCriteria(criteria, staff, extended_search) then
    return false
  end
  return true
end

-- Tests whether this room is awaiting more staff to be able to do business
function Room:isWaitingToGetStaff(staff)
  if not self.is_active or (self.door.queue:patientSize() == 0 
  and not (self.door.reserved_for and class.is(self.door.reserved_for, Patient) or false)) then
    return false
  end
  return self:staffFitsInRoom(staff, true)
end

function Room:commandEnteringStaff(humanoid)
  -- To be extended in derived classes
  self:tryToFindNearbyPatients()
  humanoid:setDynamicInfoText("")
  if self.approaching_staff then
    for staff, _ in pairs(self.approaching_staff) do
      self.approaching_staff[humanoid] = nil
      -- Another member of staff just entered, reroute others going to this room.
      if staff ~= humanoid and not self:staffFitsInRoom(staff) then
        local room = staff:getRoom()
        if room then
          -- just leave the room, then meander
          -- could do with a better scheme... however don't just meander where you are
          -- because that most likely breaks the room
          staff:setNextAction(room:createLeaveAction())
          staff:queueAction{name = "meander"}
        else
          room = staff.world:getNearestRoomNeedingStaff(staff)
          if room then
            staff:setNextAction(room:createEnterAction())
          else
            staff:setNextAction{name = "meander"}
          end
        end
      end
    end
  end
  -- This variable is used to avoid multiple calls for staff (sound played only)
  self.sound_played = nil
end

function Room:commandEnteringPatient(humanoid)
  -- To be extended in derived classes
  self.door.queue.visitor_count = self.door.queue.visitor_count + 1
  humanoid:updateDynamicInfo("")
  
  for humanoid in pairs(self.humanoids) do -- Staff is no longer waiting
    if class.is(humanoid, Staff) then
      if humanoid.humanoid_class ~= "Handyman" then
        humanoid:setMood("staff_wait", "deactivate")
        humanoid:setDynamicInfoText("")
      end
    end
  end
end

function Room:tryAdvanceQueue()
  if self.door.queue:size() > 0 and not self.door.user 
  and not self.door.reserved_for then
    local front = self.door.queue:front()
    -- These two conditions differ by the waiting symbol
    
    if self:canHumanoidEnter(front) then
      self.door.queue:pop()
      self.door:updateDynamicInfo()
      if self.room_info.id ~= "staff_room" then -- Do nothing if it is the staff_room
        for humanoid in pairs(self.humanoids) do -- Staff is now waiting
          if class.is(humanoid, Staff) then
            if humanoid.humanoid_class ~= "Handyman" then
              humanoid:setMood("staff_wait", "activate")
              humanoid:setDynamicInfoText(_S.dynamic_info.staff.actions.waiting_for_patient)
            end
          end
        end
      end
    elseif self.humanoids[front] then
      self.door.queue:pop()
      self.door:updateDynamicInfo()
    end
  end
end

function Room:onHumanoidLeave(humanoid)
  assert(self.humanoids[humanoid], "Humanoid leaving a room that they are not in")
  self.humanoids[humanoid] = nil
  local staff_leaving = false
  if class.is(humanoid, Patient) then
    -- Some staff member in the room might be waiting to get to the staffroom.
    for humanoid in pairs(self.humanoids) do
      -- In a rare case a handyman that just decided he wants to go to the staffroom
      -- could be in the room at the same time as a patient leaves.
      if class.is(humanoid, Staff) and humanoid.humanoid_class ~= "Handyman" then
        if humanoid.staffroom_needed then
          humanoid:setNextAction{name = "seek_staffroom", must_happen = true}
          staff_leaving = true
        end
      end
    end
    -- There might be other similar rooms with patients queueing
    if self.door.queue and self.door.queue:reportedSize() == 0 then
      self:tryToFindNearbyPatients()
    end
  end
  if not staff_leaving then
    self:tryAdvanceQueue()
  else
    -- Staff is leaving. If there is still a need for this room (people are in the queue) then call
    -- someone new.
    if self.door.queue:patientSize() > 0 then
      self.world:callForStaff(self)
    end
  end
  if class.is(humanoid, Staff) then
    -- Make patients leave the room if there are no longer enough staff
    if not self:testStaffCriteria(self:getRequiredStaffCriteria()) then
      for humanoid in pairs(self.humanoids) do
        if class.is(humanoid, Patient) then
          local found_leaving_action = false
          for _, action in ipairs(humanoid.action_queue) do
            if action.is_leaving then
              found_leaving_action = true
              break
            end
          end
          if not found_leaving_action then
            self:makePatientLeave(humanoid)
            humanoid:queueAction(self:createEnterAction())
          end
        end
      end
    end
    -- The handyman is now finished doing something important, he might want to go
    -- to the staffroom
    if humanoid.staffroom_needed and humanoid.humanoid_class == "Handyman" then
      humanoid:setNextAction{name = "seek_staffroom", must_happen = true}
    end
    -- Remove any unwanted moods the staff member might have
    humanoid:setMood("staff_wait", "deactivate")
  end
  -- The player might be waiting to edit this room
  if not self.is_active then
    local i = 0
    for humanoid in pairs(self.humanoids) do
      i = i + 1
    end
    if i == 0 then
      local ui = self.world.ui
      ui:addWindow(UIEditRoom(ui, self))
      ui:setCursor(ui.default_cursor)
    end
  end
end

local tile_factor = 10     -- how many tiles further are we willing to walk for 1 person fewer in the queue
local readiness_bonus = 50 -- how many tiles further are we willing to walk if the room has all the required staff
function Room:getUsageScore()
  local queue = self.door.queue
  local score = queue:patientSize() + self:getPatientCount() - self.maximum_patients
  score = score * tile_factor
  if self:testStaffCriteria(self:getRequiredStaffCriteria()) then
    score = score - readiness_bonus
  end
  return score
end

function Room:canHumanoidEnter(humanoid)
  -- If the room is not active nobody can enter
  if not self.is_active then
    return false
  end
  -- By default, staff can always enter
  if class.is(humanoid, Staff) then
    return true
  end
  -- By default, patients can only enter if there are sufficient staff and not
  -- too many patients.
  if class.is(humanoid, Patient) and not self.needs_repair then
    return self:testStaffCriteria(self:getRequiredStaffCriteria()) and self:getPatientCount() < self.maximum_patients
  end
  -- By default, other classes of humanoids cannot enter
  return false
end

-- Function stub for rooms to implement. Called when the final confirm
-- button has been pressed when building/editing a room.
function Room:roomFinished()
  -- True once the room has been finished after initial construction, and then
  -- as long as the user doesn't edit it and go back beyond the first phase (place objects)
  self.built = true
  -- Only true when not editing the room at all.
  self.is_active = true
  -- Show information about the room if not already shown.
  if not self.world.room_built[self.room_info.id] then
    self.world.ui:addWindow(UIInformation(self.world.ui, _S.room_descriptions[self.room_info.id]))
    self.world.room_built[self.room_info.id] = true
  end
  self:tryToFindNearbyPatients()
  -- It may also happen that there are patients queueing for this room already (e.g after editing)
  self:tryAdvanceQueue()
end

function Room:tryToFindNearbyPatients()
  if not self.door.queue then
    return
  end
  local world = self.world
  local our_score = self:getUsageScore()
  local our_x, our_y = self:getEntranceXY(true)
  for _, room in pairs(self.world.rooms) do
    if room.hospital == self.hospital and room.room_info == self.room_info
    and room.door.queue and room.door.queue:reportedSize() >= 2 then
      local other_score = room:getUsageScore()
      local other_x, other_y = room:getEntranceXY(true)
      local queue = room.door.queue
      while queue:reportedSize() > 1 do
        local patient = queue:back()
        local px, py = patient.tile_x, patient.tile_y
        if world:getPathDistance(px, py, our_x, our_y) + our_score <
        world:getPathDistance(px, py, other_x, other_y) + other_score then
          -- Update the queues
          queue:removeValue(patient)
          patient.next_room_to_visit = self
          self.door.queue:expect(patient)
          self.door:updateDynamicInfo()
          
          -- Update our cached values
          our_score = self:getUsageScore()
          other_score = room:getUsageScore()
          
          -- Rewrite the action queue
          for i, action in ipairs(patient.action_queue) do
            if i ~= 1 then
              action.todo_interrupt = true
            end
            if action.name == "walk" and action.x == other_x and action.y == other_y then
              local action = self:createEnterAction()
              patient:queueAction(action, i)
              break
            end
          end
          local interrupted = patient.action_queue[1]
          local on_interrupt = interrupted.on_interrupt
          if on_interrupt then
            interrupted.on_interrupt = nil
            on_interrupt(interrupted, patient, false)
          end
        else
          break
        end
      end
    end
  end
end

function Room:crashRoom()
  self.door:closeDoor()
  
  -- Remove all humanoids in the room
  for humanoid, _ in pairs(self.humanoids) do
    humanoid:queueAction({name = "idle"}, 1)
    humanoid.user_of = nil
    -- Make sure any emergency list is not messed up.
    -- Note that these humanoids might just have been kicked. (No hospital set)
    if humanoid.is_emergency then
      table.remove(self.world:getLocalPlayerHospital().emergency_patients, humanoid.is_emergency)
    end
    -- Staff members need to be removed from the staff list.
    -- In this case also staff affects the num_deaths variable of the hospital.
    if class.is(humanoid, Staff) then
      self.world:getLocalPlayerHospital():removeStaff(humanoid)
      -- Update the staff management screen (if present) accordingly
      local window = self.world.ui:getWindow(UIStaffManagement)
      if window then
        window:updateStaffList(humanoid)
      end
      self.hospital.num_deaths = self.hospital.num_deaths + 1
    else
      -- Not staff, then it must be a patient
      humanoid:die()
    end
    self.world:destroyEntity(humanoid)
  end
  -- There might also be someone using the door, even if that person is just about to exit
  -- he/she is killed too.
  local walker = self.door.user
  if walker then
    walker.user_of = nil
    self.door:setUser(nil)
    if class.is(walker, Staff) then
      self.world:getLocalPlayerHospital():removeStaff(walker)
      -- Update the staff management screen (if present) accordingly
      local window = self.world.ui:getWindow(UIStaffManagement)
      if window then
        window:updateStaffList(walker)
      end
      self.hospital.num_deaths = self.hospital.num_deaths + 1
    else
      walker:die()
    end
    self.world:destroyEntity(walker)
  end
  -- Update percentages
  self.hospital:updatePercentages()
  
  -- Remove all objects in the room
  local fx, fy = self:getEntranceXY(true)
  for object, _ in pairs(self.world:findAllObjectsNear(fx, fy)) do
    -- Machines (i.e. objects with strength) are already done.
    if object.object_type.id ~= "door" and not object.strength then
      object.user = nil
      object.user_list = nil
      object.reserved_for = nil
      object.reserved_for_list = nil
      self.world:destroyEntity(object)
    end
  end
  
  local map = self.world.map.th
  -- TODO: Explosion, animations: 4612, 3280
  
  -- Make every floor tile have soot on them
  for x = self.x, self.x + self.width - 1 do
    for y = self.y, self.y + self.height - 1 do
      local soot = self.world:newObject("litter", x, y)
      soot:setLitterType("soot_floor")
    end
  end
  -- Make walls have soot on them too
  local ty = self.y
  local soot_type, soot, block
  for x = self.x, self.x + self.width - 1 do
    block = map:getCell(x, ty, 2)
    soot_type = "soot_wall"
    if self.world:getWallSetFromBlockId(block) == "window_tiles" then
      soot_type = "soot_window"
    end
    soot = self.world:newObject("litter", x, ty)
    soot:setLitterType(soot_type, true)
  end
  local x = self.x
  for y = self.y, self.y + self.height - 1 do
    block = map:getCell(x, y, 3)
    soot_type = "soot_wall"
    if self.world:getWallSetFromBlockId(block) == "window_tiles" then
      soot_type = "soot_window"
    end
    soot = self.world:newObject("litter", x, y)
    soot:setLitterType(soot_type)
  end
  
  self.is_active = false
  self.crashed = true
end

-- Tells the patient to leave the room. This can be overridden for special
-- handling, e.g. if the patient needs to change before leaving the room.
function Room:makePatientLeave(patient)
  local leave = self:createLeaveAction()
  leave.must_happen = true
  patient:setNextAction(leave)
end

function Room:tryToEdit()
  local i = 0
  -- Tell all humanoids that they should leave
  -- If someone is entering the room right now they are also counted.
  if self.door.user and self.door.user.action_queue[1].is_entering then
    i = 1
  end
  for humanoid, _ in pairs(self.humanoids) do
    if not humanoid.action_queue[1].is_leaving then
      if class.is(humanoid, Patient) then
        self:makePatientLeave(humanoid)
        humanoid:queueAction({name = "seek_room", room_type = self.room_info.id})
      else
        humanoid:setNextAction(self:createLeaveAction())
        humanoid:queueAction({name = "meander"})
      end
    end
    i = i + 1
  end
  -- If there were no people inside we're ready to edit the room
  if i == 0 then
  local ui = self.world.ui
    ui:addWindow(UIEditRoom(ui, self))
    ui:setCursor(ui.default_cursor)
  end
end

--! Stub to be extended in subclasses, if needed.
function Room:afterLoad(old, new)
end

