--[[ 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")

--! An `Entity` which occupies at least a single map tile and does not move.
class "Object" (Entity)

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

local orient_mirror = {
  north = "west",
  west = "north",
  east = "south",
  south = "east",
}

function Object:Object(hospital, object_type, x, y, direction, etc)
  assert(class.is(hospital, Hospital), "First argument is not a Hospital instance.")

  local th = TH.animation()
  self:Entity(th)

  if etc == "map object" then
    if direction % 2 == 0 then
      direction = "north"
    else
      direction = "west"
    end
  end

  self.ticks = object_type.ticks
  self.object_type = object_type
  self.hospital = hospital
  self.world = hospital.world
  self.user = false
  self.times_used = 0
  self:updateDynamicInfo()
  self:initOrientation(direction)
  self:setTile(x, y)
end

--! Initializes the footprint, finds out what to draw and checks for
--  split animations.
--!param direction The orientation in which the object is facing.
function Object:initOrientation(direction)
  self.direction = direction
  local object_type = self.object_type

  -- Decide the animation and the draw flags for it.
  local flags = 0
  local anim = object_type.idle_animations[direction]
  if not anim then
    anim = object_type.idle_animations[orient_mirror[direction]]
    flags = DrawFlags.FlipHorizontal
  end
  local footprint = object_type.orientations
  footprint = footprint and footprint[direction]
  if footprint and footprint.early_list then
    flags = flags + DrawFlags.EarlyList
  end

  local rap = footprint and footprint.render_attach_position
  if rap and rap[1] and type(rap[1]) == "table" then
    self.split_anims = {self.th}
    self.split_anim_positions = rap
    self.th:setCrop(rap[1].column)
    for i = 2, #rap do
      local point = rap[i]
      local th = TH.animation()
      th:setCrop(point.column)
      th:setHitTestResult(self)
      th:setPosition(Map:WorldToScreen(1-point[1], 1-point[2]))
      self.split_anims[i] = th
    end
  else
    -- Make sure these variables aren't left behind. The object
    -- might just have been moved and rotated.
    self.split_anims = nil
    self.split_anim_positions = nil
  end
  if footprint and footprint.animation_offset then
    self:setPosition(unpack(footprint.animation_offset))
  end
  footprint = footprint and footprint.footprint
  if footprint then
    self.footprint = footprint
  else
    self.footprint = nil
  end
  self:setAnimation(anim, flags)
end

--! Add methods to a class for creating and controlling a slave object
function Object.slaveMixinClass(class_method_table)
  local name = class.name(class_method_table)
  local super = class.superclass(class_method_table)
  local super_constructor = super[class.name(super)]

  -- Constructor
  class_method_table[name] = function(self, hospital, object_type, x, y, direction, ...)
    super_constructor(self, hospital, object_type, x, y, direction, ...)
    if object_type.slave_id then
      local orientation = object_type.orientations
      orientation = orientation and orientation[direction]
      if orientation.slave_position then
        x = x + orientation.slave_position[1]
        y = y + orientation.slave_position[2]
      end
      self.slave = hospital.world:newObject(object_type.slave_id, x, y, direction, ...)
      self.slave.master = self
    end
  end

  -- Slave -> Master redirects
  local function slave_to_master(method)
    local super_method = super[method]
    class_method_table[method] = function(self, ...)
      local master = self.master
      if master then
        return master[method](master, ...)
      else
        return super_method(self, ...)
      end
    end
  end
  slave_to_master("onClick")
  slave_to_master("updateDynamicInfo")
  slave_to_master("getDynamicInfo")

  -- Master -> Slave notifications
  local function master_to_slave(method)
    local super_method = super[method]
    class_method_table[method] = function(self, ...)
      local slave = self.slave
      if slave then
        slave[method](slave, ...)
      end
      return super_method(self, ...)
    end
  end
  master_to_slave("initOrientation")
  class_method_table.onDestroy = function(self, ...)
    local slave = self.slave
    if slave then
      self.world:destroyEntity(slave)
    end
    return super.onDestroy(self, ...)
  end
  class_method_table.setTile = function(self, x, y)
    if self.slave then
      local dx, dy = 0, 0
      local orientation = self.object_type.orientations
      orientation = orientation and orientation[self.direction]
      if orientation.slave_position then
        dx = orientation.slave_position[1]
        dy = orientation.slave_position[2]
      end
      if x then
        self.slave:setTile(x + dx, y + dy)
      else
        self.slave:setTile(x, y)
      end
    end
    return super.setTile(self, x, y)
  end

  return slave_to_master, master_to_slave
end

function Object:tick()
  if self.split_anims then
    if self.num_animation_ticks then
      for i = 2, #self.split_anims do
        local th = self.split_anims[i]
        for _ = 1, self.num_animation_ticks do
          th:tick()
        end
      end
    else
      for i = 2, #self.split_anims do
        self.split_anims[i]:tick()
      end
    end
  end
  return Entity.tick(self)
end

function Object:setPosition(x, y)
  if self.split_anims then
    -- The given position is for the primary tile, so position non-primary
    -- animations relative to the primary one.
    local bx, by = unpack(self.split_anim_positions[1])
    for i, th in ipairs(self.split_anims) do
      local point = self.split_anim_positions[i]
      local dx, dy = Map:WorldToScreen(1-point[1]+bx, 1-point[2]+by)
      th:setPosition(x + dx, y + dy)
    end
  else
    self.th:setPosition(x, y)
  end
  return self
end

function Object:setAnimation(animation, flags)
  if self.split_anims then
    flags = (flags or 0) + DrawFlags.Crop
    if self.permanent_flags then
      flags = flags + self.permanent_flags
    end
    if animation ~= self.animation_idx or flags ~= self.animation_flags then
      self.animation_idx = animation
      self.animation_flags = flags
      local anims = self.world.anims
      for _, th in ipairs(self.split_anims) do
        th:setAnimation(anims, animation, flags)
      end
    end
    return self
  else
    return Entity.setAnimation(self, animation, flags)
  end
end

--! Get the primary tile which the object is attached to for rendering
--[[! For objects which attach to a single tile for rendering, this method will
return the X and Y Lua world coordinates of that tile. For objects which split
their rendering over multiple tiles, one of them is arbitrarily designated as
the primary tile, and its coordinates are returned.
]]
function Object:getRenderAttachTile()
  local x, y = self.tile_x, self.tile_y
  local offset = self.object_type.orientations
  if x and offset then
    offset = offset[self.direction].render_attach_position
    if self.split_anims then
      offset = offset[1]
    end
    x = x + offset[1]
    y = y + offset[2]
  end
  return x, y
end

--! Call on object used. After object used increment use values accordingly.
function Object:incrementUsedCount()
  self.times_used = self.times_used + 1
end

--! Updates the object's dynamic info
function Object:updateDynamicInfo()
  local object = self.object_type
  if object.dynamic_info then
    self:setDynamicInfo("text", {object.name, "", _S.dynamic_info.object.times_used:format(self.times_used)})
  end
end

--! This function returns a tiles offset coordinates for
--! object use position
--! param usage_position_name (string) usage position name
--! return (table) table with {x, y, usage_position_name} table
function Object:_getUsageTile(usage_position_name)
  local object_x, object_y = self.tile_x, self.tile_y
  local orientations = self.object_type.orientations
  if orientations then
    local object_layout = orientations[self.direction]
    return Object:getXYforUsePosition(object_x, object_y, object_layout, usage_position_name)
  else
    return nil
  end
end

--! This function returns an absolute world coordinates for use position.
--! param object_x (integer) world tile x coordinate.
--! param object_y (integer) world tile y coordinate.
--! param object_layout (table) object orientation description with footprint.
--! param usage_position_name (string) usage position name.
--! return (array) array with '{x, y, usage_position_name}' tables inside.
function Object:getXYforUsePosition(object_x, object_y, object_layout, usage_position_name)
  local usage_tile_offset = object_layout[usage_position_name]
  if usage_tile_offset then
    local first = usage_tile_offset[1]
    local second = usage_tile_offset[2]
    if type(first) == "table" then -- for 'handyman_position' in 'cast_remover' case as it store table
      local result = {}
      for _, xy in ipairs(usage_tile_offset) do
        local x = object_x + xy[1]
        local y = object_y + xy[2]
        table.insert(result, {x, y, usage_position_name})
      end
      return result
    else -- for other regular cases
      local x = object_x + first
      local y = object_y + second
      return {{x, y, usage_position_name}}
    end
  else
    return nil
  end
end

--! This function returns an array with use positions names
--! return (array) use positions names
function Object:usePositionNames()
  return {
    "use_position",
    "use_position_secondary",
    "slave_position",
    "finish_use_position",
    "finish_use_position_secondary",
    "handyman_position",
  }
end

--! This function returns a table with tiles offset coordinates for
--! object use positions
--! return (table) use positions offsets
function Object:getAllUsageTiles()
  local result_table = {}
  local object_use_positions_names = Object:usePositionNames()
  for key, use_position_name in pairs(object_use_positions_names) do
    local usage_tile = self:_getUsageTile(use_position_name)
    result_table = table_merge(result_table, usage_tile)
  end
  return result_table
end

--! This function returns tile offset coordinate for "use_position_secondary"
--! return (int, int) x, y tile offset
function Object:getSecondaryUsageTile()
  local secondary_usage_tile = self:_getUsageTile("use_position_secondary")
  if secondary_usage_tile then
    local xy = secondary_usage_tile[1]
    return xy[1], xy[2]
  else
    return nil, nil
  end
end

-- This function returns a list of all "only_passable" tiles belonging to an object.
-- It must be overridden by objects which do not have a footprint, but walkable tiles (e.g. doors of any kind)
function Object:getWalkableTiles()
  local tiles = {}
  for _, xy in ipairs(self.footprint) do
    if xy.only_passable then
      tiles[#tiles + 1] = { self.tile_x + xy[1], self.tile_y + xy[2] }
    end
  end
  return tiles
end

local opposite_flags = {
  travelNorth = "travelSouth",
  travelSouth = "travelNorth",
  travelEast = "travelWest",
  travelWest = "travelEast"
}

--! The function returns the direction opposite to the one passed as a parameter.
--! For North, it returns South, and so on.
--! param passable_flag (string) 'passable_flag' that we want to invert.
--! return (string) inverted 'passable_flag'.
function Object.getComplementaryPassableFlag(passable_flag)
  return opposite_flags[passable_flag]
end

function Object.directionParameters()
  return
    {
      north = { x = 0, y = -1, buildable_flag = "buildableNorth", passable_flag = "travelNorth", needed_side = "need_north_side"},
      east = { x = 1, y = 0, buildable_flag =  "buildableEast", passable_flag = "travelEast", needed_side = "need_east_side"},
      south = { x = 0, y = 1, buildable_flag = "buildableSouth", passable_flag = "travelSouth", needed_side = "need_south_side"},
      west = { x = -1, y = 0, buildable_flag = "buildableWest", passable_flag = "travelWest", needed_side = "need_west_side"}
    }
end

function Object:setTile(x, y)
  -- Are there any tiles previously occupied by this object?
  if self.tile_x ~= nil then
    -- Deoccupy object tiles
    self:deoccupyTilesByObjectFootprintAt(x, y)
  end

  local entity_map = self.world.entity_map
  -- Remove the reference to the object at it's previous coordinates
  if entity_map then
    entity_map:removeEntity(self.tile_x,self.tile_y, self)
  end

  -- Update the objects coordinates
  self.tile_x = x
  self.tile_y = y

  -- Update the entity map for the new position
  if entity_map then
    entity_map:addEntity(x,y,self)
  end

  if x then
    -- Occupy tiles by the object
    self:occupyTilesByObjectFootprintAt(x, y)

    if self.split_anims then
      -- Draw corresponding object animations
      local map = self.world.map.th
      local pos = self.split_anim_positions
      for i = 2, #self.split_anims do
        self.split_anims[i]:setTile(map, x + pos[i][1], y + pos[i][2],
            self:getDrawingLayer())
      end
    end
  else
    self.th:setTile(nil)

    if self.split_anims then
      -- Clear corresponding object animations
      for i = 2, #self.split_anims do
        self.split_anims[i]:setTile(nil)
      end
    end
  end
  self.world:clearCaches()
  return self
end

local function coordinatesAreInFootprint(object_footprint, xpos, ypos)
  for _, xy in ipairs(object_footprint) do
    if xy[1] == xpos and xy[2] == ypos then
      return true
    end
  end
  return false
end

local function setPassableFlags(map, passable_flag, xpos, ypos, next_x, next_y, value)
  local flags1 = {}
  flags1[passable_flag] = value
  map.th:setCellFlags(xpos, ypos, flags1)
  local flags2 = {}
  flags2[Object.getComplementaryPassableFlag(passable_flag)] = value
  map.th:setCellFlags(next_x, next_y, flags2)
end

--! Set cell flags on the tiles that fall under this object's footprint as occupied.
--! param x (integer) target tile x coordinate.
--! param y (integer) target tile y coordinate.
function Object:occupyTilesByObjectFootprintAt(x, y)
  local direction_parameters = Object.directionParameters()
  local direction = self.direction
  -- 50 is a trash bin thob
  if self.object_type.thob == 50 and direction == "east" then
    direction = "west"
  end

  local ra_x, ra_y = self:getRenderAttachTile()
  self.th:setTile(self.world.map.th, ra_x, ra_y, self:getDrawingLayer())
  self.world:addObjectToTile(self, x, y)
  if self.footprint then
    local map = self.world.map.th
    local optional_found = false
    local room = self.world:getRoom(x, y)
    local roomId = room and room.id
    local next_tile_x, next_tile_y = x,y
    local passable_flag

    for _, xy in ipairs(self.footprint) do
      local change_flags = true
      local flags_to_set = {}
      local lx = x + xy[1]
      local ly = y + xy[2]
      local flags

      if xy.optional then
        if optional_found then
          -- An optional tile has been accepted, we don't need anymore such tiles.
          change_flags = false
        else
          flags = map:getCellFlags(lx, ly)
          local is_object_allowed = true
          if roomId and flags.roomId ~= roomId then
            is_object_allowed = false
          elseif xy.only_passable and not self.world.pathfinder:isReachableFromHospital(lx, ly) then
            is_object_allowed = false
          end

          if is_object_allowed then
            change_flags = true
            optional_found = true
          else
            change_flags = false
          end
        end
      end

      flags = map:getCellFlags(lx, ly)
      if xy.only_side then
        local par = direction_parameters[direction]
        flags_to_set[par["buildable_flag"]] = false
        passable_flag, next_tile_x, next_tile_y = par["passable_flag"], x + par["x"], y + par["y"]
      else
        for _, value in pairs(direction_parameters) do
          if coordinatesAreInFootprint(self.footprint, xy[1] + value["x"], xy[2] + value["y"]) or
          xy.complete_cell or xy[value["needed_side"]] then
            if map:getCellFlags(x, y, flags)[value["buildable_flag"]] == 0 then
              change_flags = false
            end
            flags_to_set[value["buildable_flag"]] = false
          end
        end
      end

      if change_flags then
        if not xy.only_side then
          map:setCellFlags(lx, ly, {
            buildable = false,
            passable = not not xy.only_passable,
          })
        end
        if not isTableEmpty(flags_to_set) then
          map:setCellFlags(lx, ly, flags_to_set)
        end
        if xy.only_side then
          if map:getCellFlags(lx, ly)[passable_flag] == true then
            self.set_passable_flags = true
            setPassableFlags(self.world.map, passable_flag, lx, ly, next_tile_x, next_tile_y, false)
          end
        end
      end
    end
  end
end

--! Set cell flags on the tiles that fall under this object's footprint as unoccupied.
--! param x (integer) target tile x coordinate.
--! param y (integer) target tile y coordinate.
function Object:deoccupyTilesByObjectFootprintAt(x, y)
  local direction_parameters = Object.directionParameters()
  local direction = self.direction
  -- 50 is a trash bin thob
  if self.object_type.thob == 50 and direction == "east" then
    direction = "west"
  end

  self.world:removeObjectFromTile(self, self.tile_x, self.tile_y)
  if self.footprint then
    local map = self.world.map.th
    for _, xy in ipairs(self.footprint) do
      local xpos, ypos = self.tile_x + xy[1], self.tile_y + xy[2]

      if xy.only_side then
        if self.set_passable_flags then
          self.set_passable_flags = nil
          local par = direction_parameters[direction]
          local passableFlag, next_tile_x, next_tile_y = par.passable_flag, xpos + par.x, ypos + par.y
          setPassableFlags(self.world.map, passableFlag, xpos, ypos, next_tile_x, next_tile_y, true)
        end
        local flags_to_set= {}
        flags_to_set[direction_parameters[direction].buildable_flag] = true
        map:setCellFlags(xpos, ypos, flags_to_set)
      else
        local flags_to_set = {}
        for _, value in pairs(direction_parameters) do
          if coordinatesAreInFootprint(self.footprint, xy[1] + value.x, xy[2] + value.y) or
              xy.complete_cell or xy[value.needed_side] then
            flags_to_set[value.buildable_flag] = true
          end
        end

        if not isTableEmpty(flags_to_set) then
          map:setCellFlags(xpos, ypos, flags_to_set)
        end
        if not map:getCellFlags(xpos, ypos).passable then
          map:setCellFlags(xpos, ypos, {
            buildable = true,
            passable = true,
          })
        else
          -- passable tiles can "belong" to multiple objects, so we have to check that
          if not self.world:isTilePartOfNearbyObject(xpos, ypos, 10) then
            -- assumption: no object defines a passable tile further than 10 tiles away from its origin
            map:setCellFlags(xpos, ypos, {
              buildable = true,
            })
          end
        end
      end
    end
  end
end

--! Set object invisible state.
--! param invisible (bool) 'true' to make the object invisible,
-- 'false' to make the object visible.
function Object:setInvisible(invisible)
  if self.split_anims then
    for _, th_anim in ipairs(self.split_anims) do
      if invisible then
        th_anim:makeInvisible()
      else
        th_anim:makeVisible()
      end
    end
  else
    if invisible then
      self.th:makeInvisible()
    else
      self.th:makeVisible()
    end
  end
end

-- Sets the user of this object to the given user. Note that if multiple_users_allowed
-- is set to true for this object "user" will always be the last added user to this object.
-- Please do not modify object.user directly.
--!param user The user that is about to use this object.
function Object:setUser(user)
  assert(user, "setUser: Expected a user, got nil") -- It makes no sense to add a nil value
  self.user = user
  if self.object_type.multiple_users_allowed then
    if not self.user_list then
      self.user_list = {}
    end
    table.insert(self.user_list, user)
  end
  self.th:makeInvisible()
  self:removeReservedUser(user)
end

-- Removes a user from the set of users if multiple_users_allowed is set to true for this
-- object. Otherwise it sets "user" to nil. Please don't directly change the variable "user".
function Object:removeUser(user)
  if self.object_type.multiple_users_allowed then
    if not user or not self.user_list then
      -- No user specified, empty the list; or the list didn't exist
      self.user_list = {}
    end
    local found = false
    for i, users in ipairs(self.user_list) do
      if users == user then
        table.remove(self.user_list, i)
        found = true
        break
      end
    end
    if not found then
      self.world:gameLog("Warning: Could not find a humanoid to remove from the user list")
    end
    if #self.user_list == 0 then
      -- No users left, make the object visible again.
      self.user = nil
      self.th:makeVisible()
    end
  else
    self.user = nil
    self.th:makeVisible()
  end
end

-- Adds a reserved user for this object. If multiple_users_allowed is set there can be
-- many users reserving the object. reserved_for will then be the user who most recently
-- reserved this object.
function Object:addReservedUser(user)
  assert(user, "Expected a user, got nil") -- It makes no sense to add a nil value
  if self.object_type.multiple_users_allowed then
    if not self.reserved_for_list then
      self.reserved_for_list = {}
    end
    table.insert(self.reserved_for_list, user)
  else
    assert(not self.reserved_for or user ~= self.reserved_for, "Object already reserved for another user")
  end
  self.reserved_for = user
end

-- If multiple_users_allowed is true: Removes the user specified from this object's list of reserved users.
-- If the argument is nil it is assumed that the list should be emptied.
-- Note that if there are many humanoids reserving this object reserved_for might still be set after a
-- call to this function.
-- Otherwise: sets reserved_for to nil.
function Object:removeReservedUser(user)
  if self.object_type.multiple_users_allowed then
    -- No user specified, delete the whole list; or no list found, make it.
    if not user or not self.reserved_for_list then
      self.reserved_for_list = {}
    end
    local found = false
    for i, users in ipairs(self.reserved_for_list) do
      if users == user then
        table.remove(self.reserved_for_list, i)
        found = true
        break
      end
    end
    if not found then
      self.world:gameLog("Warning: Could not find a humanoid to remove from the reserved user list")
    end
    if #self.reserved_for_list == 0 then
      -- No users left, set reserved_for to nil.
      self.reserved_for = nil
    end
  else
    self.reserved_for = nil
  end
end

-- Checks whether the object is reserved for the specified user.
-- If the argument is nil a check for any reserved user is done.
function Object:isReservedFor(user)
  if not user then
    if self.reserved_for_list then
      return #self.reserved_for_list > 0
    else
      return not not self.reserved_for
    end
  end
  if self.reserved_for == user then -- "Normal" use
    return true
  end
  if self.object_type.multiple_users_allowed then
    if not self.reserved_for_list then
      self.reserved_for_list = {}
    end
    for _, users in ipairs(self.reserved_for_list) do
      if users == user then
        return true
      end
    end
  end
  return false
end

--! This function is for overriding it in the inheriting Machine class.
function Object:isMachine()
  return false
end

--[[ Called when the object is clicked
!param ui (UI) The active ui.
!param button (string) Which button was clicked.
!param data (table) If some data should be retained after moving an object it is in this table.
]]
function Object:onClick(ui, button, data)
  local window = ui:getWindow(UIEditRoom)
  -- Flag 'in_pickup_mode' can be used if for example some things
  -- should only happen as long as the object is not picked up.
  if button == "right" or (button == "left" and window and window.in_pickup_mode) then
    local room = self:getRoom()
    window = window and window.visible and window

    if (not room and window) then
      return
    end

    if (not room and not self.object_type.corridor_object) then
      -- Trying to pickup an object in corridor not intended for pickup (sliding doors of the hospital).
      return
    end

    if (room and not (window and window.room == room) and not self.object_type.corridor_object) then
      -- Trying to pickup an object key to a room (sofa, machine), but not the room we're editing.
      return
    end

    local fullscreen = ui:getWindow(UIFullscreen)
    if fullscreen then
      fullscreen:close()
    end

    if self.object_type.class == "Plant" or self.object_type.class == "Machine" then
      -- remove the task associated with this object
      local taskType = "watering"
      if self.object_type.class == "Machine" then
        taskType = "repairing"
      end
      local task_index = self.hospital:getIndexOfTask(self.tile_x, self.tile_y, taskType)
      local task_exist = task_index ~= -1
      if task_exist then
        self.hospital:removeHandymanTask(task_index, taskType)
      end
    end

    local room_editing_mode = window ~= nil
    local initial_orientation = self.direction
    local object_list = {{object = self.object_type, qty = 1, existing_object = self}}

    self.picked_up = true
    if not room_editing_mode then
      self:onPickUp()
      self:setInvisible(true)
      window = UIPlaceObjects(ui, object_list, false) -- don't pay for
      ui:addWindow(window)
    else
      -- NB: the object has to be destroyed before updating/creating the window,
      -- or the blueprint will be wrong
      self.world:destroyEntity(self)
      window:stopPickupItems()
      window:addObjects(object_list, false) -- don't pay for
      window:selectObjectType(self.object_type)
      window:checkEnableConfirm() -- since we removed an object from the room, the requirements may not be met anymore
    end

    -- blueprint orientation should be as orientation was on object pickup
    window:setOrientation(initial_orientation)
    self.orientation_before = initial_orientation
    ui:playSound("pickup.wav")
  end
end

function Object:eraseObject()
  self.world.map.th:eraseObjectTypes(self.tile_x, self.tile_y)
end

function Object:resetAnimation()
  self.world.map.th:setCellFlags(self.tile_x, self.tile_y, {thob = self.object_type.thob})
  local ra_x, ra_y = self:getRenderAttachTile()
  self.th:setTile(self.world.map.th, ra_x, ra_y, self:getDrawingLayer())
end

function Object:onDestroy()
  -- Remove from room
  local room = self:getRoom()
  if room then
    room.objects[self] = nil
  end

  self:resetUsageAndReservaton()
  Entity.onDestroy(self)

  self:rebuildPassableCellFlags()
end

function Object:rebuildPassableCellFlags()
  -- Issue 1105 - rebuild wall travel<dir> and pathfinding on side object removal
  if self.object_type.class == "SideObject" then
    self.world.map.th:updatePathfinding()
    self.world:resetSideObjects()
  end
end

function Object:onPickUp()
  self:resetUsageAndReservaton()
  Entity.onPickUp(self)
end

function Object:resetUsageAndReservaton()
  self:cancelUsage()
  self:denyReservation()
end

function Object:cancelUsage()
  if self.user_list then
    for _, user in ipairs(self.user_list) do
      user:handleRemovedObject(self)
    end
    self.user_list = {}
  elseif self.user then
    self.user:handleRemovedObject(self)
  end
  self.user = nil
end

function Object:denyReservation()
  if self.reserved_for_list then
    for _, reserver in ipairs(self.reserved_for_list) do
      reserver:handleRemovedObject(self)
    end
    self.reserved_for_list = {}
  elseif self.reserved_for then
    self.reserved_for:handleRemovedObject(self)
  end
  self.reserved_for = nil
end

function Object:afterLoad(old, new)
  if old < 52 then
    self.hospital = self.world:getLocalPlayerHospital()
  end
  if old < 57 then
    if self.footprint and self.direction then
      local object_type = self.object_type
      local footprint = object_type.orientations
      footprint = footprint and footprint[self.direction]
      if footprint and footprint.animation_offset then
        self:setPosition(unpack(footprint.animation_offset))
      end
      footprint = footprint and footprint.footprint
      self.footprint = footprint
      if object_type.class == "SideObject" then
        local flags = {buildable = true}
        self.world.map.th:setCellFlags(self.tile_x, self.tile_y, flags)
      end
      self:setTile(self.tile_x, self.tile_y)
    end
  end
  if old < 173 then
    -- Fix bug with couch not being fully passable
    if self.object_type.id == "couch" then
      self:initOrientation(self.direction)
      self:setTile(self.tile_x, self.tile_y)
    end
  end
  self:updateDynamicInfo()
  return Entity.afterLoad(self, old, new)
end

local all_pathfind_dirs = {[0] = true, [1] = true, [2] = true, [3] = true}

function Object.processTypeDefinition(object_type)
  if object_type.id == "extinguisher" or object_type.id == "radiator" or
      object_type.id == "plant" or object_type.id == "reception_desk" or
      object_type.id == "bench" then
    object_type.count_category = object_type.id
  elseif object_type.id ~= "bin" and not object_type.corridor_object and
      not object_type.id:find("door") then
    object_type.count_category = "general"
  end
  if object_type.orientations then
    for _, details in pairs(object_type.orientations) do
      -- Set default values
      if not details.animation_offset then
        details.animation_offset = {0, 0}
      end
      if not details.render_attach_position then
        details.render_attach_position = {0, 0}
      end
      -- Set the usage position
      if details.use_position == "passable" then
        -- "passable" => the *first* passable tile in the footprint list
        for _, point in pairs(details.footprint) do
          if point.only_passable then
            details.use_position = {point[1], point[2]}
            break
          end
        end
      elseif not details.use_position then
        details.use_position = {0, 0}
      end
      -- Set handyman repair tile
      if object_type.default_strength and not details.handyman_position then
        details.handyman_position = details.use_position
      end
      -- Find the nearest solid tile in the footprint to the usage position
      local use_position = details.use_position
      local solid_near_use_position
      local solid_near_use_position_d = 10000
      for _, point in pairs(details.footprint) do repeat
        if point.only_passable then
          break -- continue
        end
        local d = (point[1] - use_position[1])^2 + (point[2] - use_position[2])^2
        if d >= solid_near_use_position_d then
          break -- continue
        end
        solid_near_use_position = point
        solid_near_use_position_d = d
      until true end
      if solid_near_use_position_d ~= 1 then
        details.pathfind_allowed_dirs = all_pathfind_dirs
      else
        if use_position[1] < solid_near_use_position[1] then
          details.pathfind_allowed_dirs = {[1] = true}
        elseif use_position[1] > solid_near_use_position[1] then
          details.pathfind_allowed_dirs = {[3] = true}
        elseif use_position[2] < solid_near_use_position[2] then
          details.pathfind_allowed_dirs = {[2] = true}
        else
          details.pathfind_allowed_dirs = {[0] = true}
        end
      end
      -- Adjust the footprint to make this tile the origin
      local solid_points = {}
      if solid_near_use_position then
        local x, y = unpack(solid_near_use_position)
        for _, point in pairs(details.footprint) do
          point[1] = point[1] - x
          point[2] = point[2] - y
          if not point.only_passable then
            solid_points[point[1] * 100 + point[2]] = point
          end
        end
        for _, key in ipairs({"use_position_secondary",
                              "finish_use_position",
                              "finish_use_position_secondary",
                              "smoke_position"}) do
          if details[key] then
            details[key][1] = details[key][1] - x
            details[key][2] = details[key][2] - y
          end
        end
        use_position[1] = use_position[1] - x
        use_position[2] = use_position[2] - y
        if details.slave_position then
          details.slave_position[1] = details.slave_position[1] - x
          details.slave_position[2] = details.slave_position[2] - y
        end
        local rx, ry = unpack(details.render_attach_position)
        if type(rx) == "table" then
          rx, ry = unpack(details.render_attach_position[1])
          for _, point in ipairs(details.render_attach_position) do
            point.column = point[1] - point[2]
            point[1] = point[1] - x
            point[2] = point[2] - y
          end
        else
          details.render_attach_position[1] = rx - x
          details.render_attach_position[2] = ry - y
        end
        x, y = Map:WorldToScreen(rx + 1, ry + 1)
        details.animation_offset[1] = details.animation_offset[1] - x
        details.animation_offset[2] = details.animation_offset[2] - y
      end
      -- Find the region around the solid part of the footprint
      local adjacent_set = {}
      local adjacent_list = {}
      details.adjacent_to_solid_footprint = adjacent_list
      for _, point in pairs(solid_points) do
        for _, delta in ipairs({{-1, 0}, {0, -1}, {0, 1}, {1, 0}}) do
          local x = point[1] + delta[1]
          local y = point[2] + delta[2]
          local k2 = x * 100 + y
          if not solid_points[k2] and not adjacent_set[k2] then
            adjacent_set[k2] = {x, y}
            adjacent_list[#adjacent_list + 1] = adjacent_set[k2]
          end
        end
      end
    end
  end
end

--[[ Gets the state of an object

! The state can be later used to set the state of this object. This is
useful when we would destroy and create a new object that should represent
the same object.

!return (table) state
]]
function Object:getState()
  return {times_used = self.times_used}
end

--[[ Sets the state of an object

! This is a complement to a pair function. IT will use the generated state
table to update it's state.
!param state (table) table holding the state
!return (void)
]]
function Object:setState(state)
  if state then
    self.times_used = state.times_used
  end
end

--! An object that lives on the edge of a tile.
class "SideObject" (Object)

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

function SideObject:SideObject(...)
  self:Object(...)
end

function SideObject:getDrawingLayer()
  if self.direction == "north" then
    return DrawingLayers.NorthSideObject
  elseif self.direction == "west" then
    return DrawingLayers.WestSideObject
  else
    if self.direction == "east" then
      -- 50 is a trash bin thob
      if self.object_type.thob == 50 then
        --[[ The bin has orientations north and east but they are displayed in
        the north and west part of the tile respectively. This could lead to a
        graphical glitch where a side object in the west part of the tile is
        displayed over a doctor in the middle of the tile. ]]
        return DrawingLayers.WestSideObject
      else
        return DrawingLayers.EastSideObject
      end
    else --south
      return DrawingLayers.SouthSideObject
    end
  end
end
