--[[ 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")
local ipairs, math_floor
    = ipairs, math.floor

-- Visually, it looks better to have the object being placed not attached to a
-- tile (so that it is always on top of walls, etc.), but for debugging it can
-- be useful to attach it to a tile.
local ATTACH_BLUEPRINT_TO_TILE = false

--! The dialog shown when placing objects.
class "UIPlaceObjects" (Window)

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

--[[ Constructor for the class.
!param ui (UI) The active ui.
!param object_list (table) a list of tables with objects to place. Keys are "object", "qty" and
"existing_object". The first is the object_type of the object, the second how many, and if the key
"existing_object" is set it should be an already existing object that is about to be moved.
In particular, if that object has a variable called current_frame then that frame will be used
when drawing the object as it is being moved.
]]
function UIPlaceObjects:UIPlaceObjects(ui, object_list, pay_for)
  self:Window()

  object_list = object_list or {} -- Default argument

  local app = ui.app
  self.modal_class = "main"
  self.ui = ui
  self.map = app.map
  self.anims = app.anims
  self.world = app.world
  self.width = 186
  self.height = 167 + #object_list * 29
  self:setDefaultPosition(0.9, 0.1)
  local selected_label_color = { red = 40, green = 40, blue = 250 }
  self.panel_sprites = app.gfx:loadSpriteTable("QData", "Req05V", true)
  self.white_font = app.gfx:loadFontAndSpriteTable("QData", "Font01V", nil, nil, { apply_ui_scale = true })
  self.blue_font = app.gfx:loadFontAndSpriteTable("QData", "Font02V", nil, nil, {ttf_color = selected_label_color, apply_ui_scale = true })
  self.title_text = _S.rooms_short.corridor_objects
  self.desc_text = _S.place_objects_window.place_objects_in_corridor

  self:addPanel(112, 0, 0) -- Dialog header
  for y = 48, 83, 7 do
    self:addPanel(113, 0, y) -- Desc text box
  end
  self:addPanel(114,   0, 90) -- Dialog mid-piece
  self:addPanel(115,   0, 100):makeButton(9, 8, 41, 42, 116, self.cancel):setSound("no4.wav"):setTooltip(_S.tooltip.place_objects_window.cancel)
  self.purchase_button =
  self:addPanel(117,  50, 100):makeButton(1, 8, 41, 42, 118, self.purchaseItems):setTooltip(_S.tooltip.place_objects_window.buy_sell)
    :setDisabledSprite(127):setSound("selectx.wav"):enable(false) -- Disabled purchase items button
  self.pickup_button =
  self:addPanel(119,  92, 100):makeButton(1, 8, 41, 42, 120, self.pickupItems):setTooltip(_S.tooltip.place_objects_window.pick_up)
    :setDisabledSprite(128):setSound("selectx.wav"):enable(false):makeToggle() -- Disabled pick up items button
  self.confirm_button =
  self:addPanel(121, 134, 100):makeButton(1, 8, 43, 42, 122, self.confirm):setTooltip(_S.tooltip.place_objects_window.confirm)
    :setDisabledSprite(129):enable(false):setSound("YesX.wav") -- Disabled confirm button

  self.list_header = self:addPanel(123, 0, 146) -- Object list header
  self.list_header.visible = false

  self.objects = {}
  self.object_footprint = {}
  self.num_slots = 0

  self:addObjects(object_list, pay_for)
  ui:setWorldHitTest(false)
  UIPlaceObjects.registerKeyHandlers(self)
end

function UIPlaceObjects:registerKeyHandlers()
  self:addKeyHandler("global_cancel", self.undo)
  self:addKeyHandler("global_cancel_alt", self.cancel)
  self:addKeyHandler("ingame_sellPickedUpItem", self.sell)
  self:addKeyHandler("ingame_rotateobject", self.tryNextOrientation)
end

-- changes the window size and buttons to num_slots slots
function UIPlaceObjects:resize(num_slots)
  if self.num_slots == num_slots then
    return
  end

  if num_slots == 0 then
    self.list_header.visible = false
  else
    self.list_header.visible = true
  end

  local function idx(i)
    return --[[persistable:place_objects_idx1]] function(window)
      if i == window.active_index then
        window:nextOrientation()
      else
        window.place_objects = true
        window:setActiveIndex(i)
        -- Stop picking up items when user presses object in list
        local edit_room_window = window.ui:getWindow(UIEditRoom)
        if edit_room_window and edit_room_window.in_pickup_mode then
          edit_room_window:stopPickupItems()
        end
      end
    end
  end

  if self.num_slots < num_slots then
      -- change last panel
    if self.num_slots > 0 then
      local last_panel = self.panels[#self.panels - 1]
      last_panel.y = last_panel.y + 4
      last_panel.sprite_index = 124
    end

    -- add new panels (save last one)
    for i = self.num_slots + 1, num_slots - 1 do
      self:addPanel(124, 0, 121 + i * 29)
        :makeButton(15, 8, 130, 23, 125, idx(i)):setSound("selectx.wav")
        :preservePanel()
    end
    -- add last new panel
    self:addPanel(156, 0, 117 + num_slots * 29)
      :makeButton(15, 12, 130, 23, 125, idx(num_slots)):setSound("selectx.wav")
      :preservePanel()
  else
    -- remove buttons
    for _ = self.num_slots, num_slots + 1, -1 do
      -- NB: Two panels per item, the latter being a dummy for the button
      self.panels[#self.panels] = nil
      self.panels[#self.panels] = nil
      self.buttons[#self.buttons] = nil
      if num_slots > 0 then
        -- change appearance of last panel
        local last_panel = self.panels[#self.panels - 1]
        last_panel.y = last_panel.y - 4
        last_panel.sprite_index = 156
      end
    end
  end
  self.num_slots = num_slots
  self.height = 167 + (num_slots) * 29
end

function UIPlaceObjects:addObjects(object_list, pay_for)
  self.visible = true -- even if no objects are to be placed, make the window visible again

  if not object_list then
    object_list = {}
  end

  if #object_list == 0 and #self.objects == 0 then
    return
  end

  -- Detect objects already existing in self.objects and increment its quantity
  -- rather than adding new objects lines.
  local new_index = 1
  while true do
    local new_object = object_list[new_index]
    if not new_object then break end
    for _, object in ipairs(self.objects) do
      if new_object.qty > 0 and new_object.object.thob == object.object.thob then
        object.qty = object.qty + new_object.qty
        if pay_for then
          local build_cost = self.ui.hospital:getObjectBuildCost(new_object.object.id)
          local msg = _S.transactions.buy_object .. ": " .. object.object.name
          self.ui.hospital:spendMoney(new_object.qty * build_cost, msg, new_object.qty * build_cost)
        end
        -- If this is an object that has been created in the world already, add it to the
        -- associated list of objects to re-place.
        if new_object.existing_object then
          if not object.existing_objects then
            object.existing_objects = {}
          end
          -- Insert the new object in the beginning of the list so that that this object
          -- is the one to be placed first. (LIFO)
          table.insert(object.existing_objects, 1, new_object.existing_object)
        end
        table.remove(object_list, new_index)
        new_index = new_index - 1
        break
      end
    end
    new_index = new_index + 1
  end

  self.place_objects = true -- When adding objects guess we want to place objects

  self.object_anim = TH.animation()
  self.object_slave_anim = TH.animation()
  local total_objects = #self.objects + #object_list
  self:resize(total_objects)

  for _, object in pairs(object_list) do
    if object.existing_object then
      object.existing_objects = {object.existing_object}
    end
    self.objects[#self.objects + 1] = object

    if pay_for then
      local build_cost = self.ui.hospital:getObjectBuildCost(object.object.id)
      local msg = _S.transactions.buy_object .. ": " .. object.object.name
      self.ui.hospital:spendMoney(object.qty * build_cost, msg, object.qty * build_cost)
    end
  end

  -- sort list by size of object (number of tiles in the first existing orientation (usually north))
  table.sort(self.objects, function(o1, o2)
    local orient1 = o1.object.orientations.north or o1.object.orientations.east or
                    o1.object.orientations.south or o1.object.orientations.west
    local orient2 = o2.object.orientations.north or o2.object.orientations.east or
                    o2.object.orientations.south or o2.object.orientations.west
    return #orient1.footprint > #orient2.footprint
  end)

  self.active_index = 0 -- avoid case of index changing from 1 to 1
  self.active_hover_index = 0
  self.hover_sound = nil
  self:setActiveIndex(1)
  self:onCursorWorldPositionChange(self.ui:getCursorPosition(self))
end

-- precondition: self.active_index has to correspond to the object to be removed
function UIPlaceObjects:removeObject(object, dont_close_if_empty, placed, move_canceled)
  local existing_object = object.existing_object

  local move_cancellation = not placed and existing_object and move_canceled
  local purchase_cancellation = not placed and not existing_object
  local sale_previously_placed = not placed and existing_object and not move_canceled

  if purchase_cancellation or sale_previously_placed then
    -- Object sell.
    -- The player should be reimbursed for the cost of the item, i.e.
    -- whether they were previously purchased. So do refund.
    local build_cost = self.ui.hospital:getObjectBuildCost(object.object.id)
    local msg = _S.transactions.sell_object .. ": " .. object.object.name
    self.ui.hospital:receiveMoney(build_cost, msg, build_cost)
  end

  if move_cancellation then
    -- Move cancelled. Place object back. Restore coordinate and orientation.
    self.object_cell_x = object.existing_object.tile_x
    self.object_cell_y = object.existing_object.tile_y
    self.object_orientation = object.existing_object.direction
    self:placeObject(nil, true)
  end

  if sale_previously_placed then
    -- Previously grabbed an existing object so the object was not deleted
    -- from its previous location, but simply made invisible.
    self.world:destroyEntity(object.existing_object)
  end

  object.qty = object.qty - 1
  -- Prefer to remove objects not yet placed
  local existing_no = object.existing_objects and #object.existing_objects or 0
  if existing_no > 0 then
    if object.qty < #object.existing_objects then
      -- The object is already as good as destroyed. It is only known in this list.
      table.remove(object.existing_objects, 1)
    end
  end
  if object.qty == 0 then
    if #self.objects == 1 then
      self:clearBlueprint()
      self.object_cell_x = nil
      self.object_cell_y = nil
      if dont_close_if_empty then
        self.list_header.visible = false
        self.place_objects = false -- No object to place
      else
        self:close(false)
        return
      end
    end
    local idx = self.active_index
    table.remove(self.objects, idx)
    self:resize(#self.objects)
    self.active_index = 0 -- avoid case of index changing from 1 to 1
    self.active_hover_index = 0
    self:setActiveIndex(1)
  else
    -- Make sure the correct frame is shown for the next object
    self:setOrientation(self.object_orientation)
  end
  -- Update blueprint
  self:setBlueprintCell(self.object_cell_x, self.object_cell_y)
  if object.object.id == "reception_desk" then -- Rebuild cache of reception desks
    self.ui.hospital:buildReceptionDesksCache()
  end
end

--! Remove all items from the menu.
--!param move_canceled (bool) moving objects has been cancelled.
function UIPlaceObjects:removeAllObjects(move_canceled)
  -- There is surely a nicer way to implement this than the current hack. Rewrite it sometime later.
  self:setActiveIndex(1)
  for _ = 1, #self.objects do
    for _ = 1, self.objects[1].qty do
      self:removeObject(self.objects[1], true, false, move_canceled)
    end
  end
end

function UIPlaceObjects:removeObjects(object_list)
  -- rewrite at some point..
  if not object_list then
    object_list = {}
  end

  for _, o in ipairs(object_list) do
    for j, p in ipairs(self.objects) do
      if o.object.id == p.object.id then
        self.active_index = j
        for _ = 1, o.qty do
          self:removeObject(p, true, false)
        end
      end
    end
  end
end

--! Close menu.
--!param move_canceled (bool) moving objects has been cancelled.
function UIPlaceObjects:close(move_canceled)
  self.ui:tutorialStep(1, {4, 5}, 1)
  self:removeAllObjects(move_canceled)
  self:clearBlueprint()
  self.ui:setWorldHitTest(true)
  return Window.close(self)
end

function UIPlaceObjects:cancel()
  self:close(false)
end

--! Helper function to stop UIEditRoom going back to doors phase
function UIPlaceObjects:sell()
  if not self.ui:getWindow(UIEditRoom) then
    self:close(false)
  end
end

function UIPlaceObjects:undo()
  self:close(true)
end

function UIPlaceObjects:setActiveIndex(index)
  if index == self.active_index or #self.objects == 0 then
    return
  end
  self.active_index = index

  local object = self.objects[self.active_index].object
  if object.id == "reception_desk" then
    self.ui:tutorialStep(1, 6, 4)
  else
    self.ui:tutorialStep(1, {4, 5}, 6)
  end
  local anims = self.anims
  local grey_scale = anims.Alt32_GreyScale
  local _, ghost = self.ui.app.gfx:getPalette("MPalette.dat")
  for _, anim in pairs(object.idle_animations) do
    anims:setAnimationGhostPalette(anim, ghost, grey_scale)
  end
  if object.slave_type then
    for _, anim in pairs(object.slave_type.idle_animations) do
      anims:setAnimationGhostPalette(anim, ghost, grey_scale)
    end
  end

  if object.locked_to_wall then
    local wx, wy, wo = self:calculateBestPlacementPosition(0, 0)
    self.object_cell_x, self.object_cell_y = wx, wy
    self:setOrientation(wo)
  else
    self.object_orientation = "west"
    self:nextOrientation()
  end
end

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

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

function UIPlaceObjects:setOrientation(orient)
  self.object_orientation = orient

  local object_data = self.objects[self.active_index]
  local object = object_data.object
  local anim = object.idle_animations[orient]
  local flag = 0
  if not anim then
    anim = object.idle_animations[orient_mirror[orient]]
    flag = 1
  end
  if object.orientations[orient].early_list then
    flag = flag + 1024
  end
  self.object_anim:setAnimation(self.anims, anim, flag)
  if object.slave_type then
    local slave_flag = flag - (flag % 2)
    local slave_anim = object.slave_type.idle_animations[orient]
    if not slave_anim then
      slave_anim = object.slave_type.idle_animations[orient_mirror[orient]]
      slave_flag = slave_flag + 1
    end
    self.object_slave_anim:setAnimation(self.anims, slave_anim, slave_flag)
  end
  local present_object
  if object_data.existing_objects and #object_data.existing_objects > 0 then
    present_object = object_data.existing_objects[1]
  end
  if present_object and present_object.current_frame then
    self.object_anim:setFrame(present_object.current_frame)
  end
  local px, py = unpack(object.orientations[orient].render_attach_position)
  if type(px) == "table" then
    px, py = unpack(px)
  end
  px, py = Map:WorldToScreen(px + 1, py + 1)
  px = object.orientations[orient].animation_offset[1] + px
  py = object.orientations[orient].animation_offset[2] + py
  self.object_anim:setPosition(px, py)
  if object.slave_type then
    px, py = unpack(object.slave_type.orientations[orient].render_attach_position)
    if type(px) == "table" then
      px, py = unpack(px)
    end
    local offset = object.orientations[orient].slave_position
    if offset then
      px = px + offset[1]
      py = py + offset[2]
    end
    px, py = Map:WorldToScreen(px + 1, py + 1)
    px = object.slave_type.orientations[orient].animation_offset[1] + px
    py = object.slave_type.orientations[orient].animation_offset[2] + py
    self.object_slave_anim:setPosition(px, py)
  end
  self:setBlueprintCell(self.object_cell_x, self.object_cell_y)
end

function UIPlaceObjects:nextOrientation()
  if not self.object_anim then
    return
  end
  local object = self.objects[self.active_index].object
  if object.locked_to_wall then
    -- Orientation is dictated by the nearest wall
    return
  end
  local orient = self.object_orientation
  repeat
    orient = orient_next[orient]
  until object.orientations[orient]
  self:setOrientation(orient)
end

function UIPlaceObjects:tryNextOrientation()
  if #self.objects > 0 then
    self.ui:playSound("swoosh.wav")
    self.objects[self.active_index].orientation_before = self.object_orientation
    self:nextOrientation()
  end
end

function UIPlaceObjects:onMouseUp(button, x, y)
  local repaint = Window.onMouseUp(self, button, x, y)

  -- We don't want to place objects because we are selecting new objects for adding in a room being built/edited
  -- Or the game is paused.
  if not self.place_objects or not self.world.user_actions_allowed then
    return
  end

  if button == "right" then
    self:tryNextOrientation()
    repaint = true
  elseif button == "left" then
    if #self.objects > 0 then
      local s = TheApp.config.ui_scale
      if 0 <= x and x < self.width * s and 0 <= y and y < self.height * s then -- luacheck: ignore 542
        -- Click within window - do nothing
      elseif self.object_cell_x and self.object_cell_y then
        if self.object_blueprint_good then
          if not self.world:anyHumanoidObscuringArea(self.object_cell_x, self.object_cell_y) then
            self:placeObject()
            repaint = true
          end
        else
          self.ui:tutorialStep(3, {13, 15}, 14)
        end
      end
    end
  end

  return repaint
end

function UIPlaceObjects:placeObject(dont_close_if_empty, dont_remove)
  if not self.place_objects then
    -- We don't want to place objects because we are selecting new objects for adding in a room being built/edited
    return
  end

  local object = self.objects[self.active_index]
  if object.object.id == "reception_desk" then self.ui:tutorialStep(1, 4, "next") end

  local real_obj
  -- There might be an existing object that has been picked up.
  if object.existing_objects and #object.existing_objects > 0 then
    real_obj = object.existing_objects[1]
    table.remove(object.existing_objects, 1)
  end
  local room = self.room or self.world:getRoom(self.object_cell_x, self.object_cell_y)
  if real_obj then
    -- If there is such an object then we don't want to make a new one, but move this one instead.
    if real_obj.picked_up and not real_obj.th:isVisible() then
      -- Previously grabbed an existing object so the object was not actually
      -- deleted from its previous location, but simply made invisible.
      real_obj:setTile(nil)
      real_obj:setInvisible(false)
      real_obj:rebuildPassableCellFlags()
    end
    real_obj.picked_up = false

    if real_obj.orientation_before and real_obj.orientation_before ~= self.object_orientation then
      real_obj:initOrientation(self.object_orientation)
    end
    self.world:prepareFootprintTilesForBuild(real_obj.footprint, self.object_cell_x, self.object_cell_y)
    real_obj:setTile(self.object_cell_x, self.object_cell_y)
    self.world:objectPlaced(real_obj)
    if real_obj.slave then
      self.world:objectPlaced(real_obj.slave)
    end
    if real_obj:isMachine() then
      -- Machines may have some state like smoke and etc. Update it.
      real_obj:placed(room)
    end
    if class.is(real_obj, Machine) then
      real_obj:setHandymanRepairPosition(self.object_orientation)
    end
  else
    local object_footprint = object.object.orientations[self.object_orientation].footprint
    self.world:prepareFootprintTilesForBuild(object_footprint, self.object_cell_x, self.object_cell_y)
    real_obj = self.world:newObject(object.object.id,
        self.object_cell_x, self.object_cell_y, self.object_orientation)
    real_obj:setState(object.state)
  end

  if room then
    room.objects[real_obj] = true
  end

  self.ui:playSound("place_r.wav")

  if not dont_remove then
    self:removeObject(object, dont_close_if_empty, true, false)
  end
  object.orientation_before = nil

  return real_obj
end

function UIPlaceObjects:draw(canvas, x, y)
  if not self.visible then
    return -- Do nothing if dialog is not visible
  end
  -- Don't show the object if the game is paused
  if self.world.user_actions_allowed then
    if not ATTACH_BLUEPRINT_TO_TILE and self.object_cell_x and self.object_anim then
      local xpos, ypos = self.ui:WorldToScreen(self.object_cell_x, self.object_cell_y)
      local zoom = self.ui.zoom_factor
      if canvas:scale(zoom) then
        xpos = math.floor(xpos / zoom)
        ypos = math.floor(ypos / zoom)
      end
      self.object_anim:draw(canvas, xpos, ypos)
      if self.objects[self.active_index].object.slave_type then
        self.object_slave_anim:draw(canvas, xpos, ypos)
      end
      canvas:scale(1)
    end
  end

  Window.draw(self, canvas, x, y)

  local s = TheApp.config.ui_scale
  x, y = x + self.x * s, y + self.y * s
  self.white_font:draw(canvas, self.title_text, x + 17 * s, y + 21 * s, 153 * s, 0)
  self.white_font:drawWrapped(canvas, self.desc_text, x + 20 * s, y + 46 * s, 147 * s)

  for i, o in ipairs(self.objects) do
    local font = self.white_font
    local font_ypos = y + 136 * s + i * 29 * s

    if i == self.active_index then
      local frame_xpos = x + 20 * s
      local frame_ypos = y + 134 * s + i * 29 * s
      local frame_width = 119 * s
      local frame_height = 12 * s

      local red = canvas:mapRGB(221, 83, 0)
      canvas:drawRect(red, frame_xpos, frame_ypos, frame_width, s)
      canvas:drawRect(red, frame_xpos, frame_ypos + frame_height, frame_width + s, s)
      canvas:drawRect(red, frame_xpos, frame_ypos, s, frame_height)
      canvas:drawRect(red, frame_xpos + frame_width, frame_ypos, s, frame_height)
    end
    if i == self.active_hover_index then
      font = self.blue_font
    end
    font:draw(canvas, o.object.name, x + 15 * s, font_ypos, 130 * s, 0)
    font:draw(canvas, o.qty, x + 151 * s, font_ypos, 19 * s, 0)
  end
end

function UIPlaceObjects:onMouseMove(x, y, dx, dy)
  local s = TheApp.config.ui_scale
  local current_hover_id
  local header_height = 159 * s
  local bar_height = 29 * s
  local inside_objects_area = (x > 0 and x < 186 * s) and (y > header_height and y < header_height + bar_height * #self.objects)
  if inside_objects_area then
    -- Check if player hovers over a button, not between.
    if (header_height + y) % bar_height < 21 * s then
      current_hover_id = math_floor((y - header_height)/bar_height) + 1
      if self.active_hover_index ~= current_hover_id then
        if self.hover_sound then
          self.ui:stopSound(self.hover_sound)
        end
        self.hover_sound = self.ui:playSound("Hlight5.wav")
        self.active_hover_index = current_hover_id
      end
    else
      self.active_hover_index = 0
    end
  else
    self.active_hover_index = nil
  end
  return Window:onMouseMove(x, y, dx, dy)
end

function UIPlaceObjects:clearBlueprint()
  local map = self.map.th
  if self.object_anim then
    self.object_anim:setTile(nil)
  end
  if self.object_slave_anim then
    self.object_slave_anim:setTile(nil)
  end
  for _, xy in ipairs(self.object_footprint) do
    if xy[1] ~= 0 then
      map:setCell(xy[1], xy[2], 4, 0)
    end
  end
end

local flag_alpha75 = 256 * 8
local flag_altpal = 16

--! Method for displaying a blueprint when placing object by coordinates under the user cursor.
--! Draws a blueprint and determines its color depending on whether the object can be placed
--! at the given coordinates or not. A gray blueprint means placement is prohibited.
--! A colored blueprint means placement is permitted.
function UIPlaceObjects:setBlueprintCell(x, y)
  self:clearBlueprint()
  self.object_cell_x = x
  self.object_cell_y = y

  if not x or not y or #self.objects == 0 then
    self.object_footprint = {}
    return
  end

  local real_obj = self.objects[self.active_index]
  local object = real_obj.object
  local object_footprint = object.orientations[self.object_orientation].footprint
  local map = self.map.th
  if #object_footprint ~= #self.object_footprint then
    self.object_footprint = {}
    for i = 1, #object_footprint do
      self.object_footprint[i] = {}
    end
  end

  local room = self.room and self.room.id
  local valid_placement, room_id, passable_flag = self:_placementAvailableHereAndOtherObjectsDoNotInterfere(
      x, y, object, real_obj, object_footprint, room, map)

  if self.object_anim and object.class ~= "SideObject" then
    -- Not SideObject - object is occupying one or more tiles entirely
    -- (Drinks machine, plant, reception desk, room machines and etc).
    valid_placement = valid_placement and self:_isNonSideObjectPlacementValid(x, y, object, self.object_orientation, room_id)
    self:_setBlueprintPartialFlags(x, y, map, valid_placement)
  elseif object.class == "SideObject" then
    -- SideObject - an object that lives on the edge of a tile (radiator, bin, extinguisher).
    valid_placement = valid_placement and self:_isSideObjectPlacementValid(x, y, room_id, passable_flag, map)
    self:_setBlueprintPartialFlags(x, y, map, valid_placement)
  end
end

--! Function checks whether this space is available for placing an object and
-- whether the space is not occupied by other objects.
function UIPlaceObjects:_placementAvailableHereAndOtherObjectsDoNotInterfere(
    x, y, object, real_obj, object_footprint, room, map)
  local flags = {}
  local passable_flag
  local room_id = room
  local valid_placement = true
  local opt_tiles_blocked = 0
  local world = self.ui.app.world
  local player_id = self.ui.hospital:getPlayerIndex()
  local direction = self.object_orientation
  local direction_parameters = Object.directionParameters()
  local optional_tiles = 0
  for _, tile in ipairs(object_footprint) do
    if tile.optional then
      optional_tiles = optional_tiles + 1
    end
  end

  -- flags for the case of moving a previously placed object to a new location
  local existing_object = real_obj.existing_object
  local same_placement = false -- same cell and same orientation
  local moving_existing_object = existing_object and existing_object.picked_up and
      not existing_object.th:isVisible()

  -- The given footprint tile is not usable,
  -- update the external 'valid_placement' variable accordingly.
  local function updateResult(xy)
    if xy.optional then
      opt_tiles_blocked = opt_tiles_blocked + 1
      if opt_tiles_blocked >= optional_tiles then
        valid_placement = false
      end
    else
      valid_placement = false
    end
  end

  if moving_existing_object then
    -- moving a previously placed object to a new location
    local same_tile = existing_object.tile_x == x and existing_object.tile_y == y
    same_placement = same_tile and existing_object.direction == direction
  end

  if moving_existing_object and self.object_anim and object.class ~= "SideObject" then
    -- we placing previously 'grabbed' an existing object which actually just made invisible
    -- free up the space occupied by the object during the test for tiles availability
    existing_object:deoccupyTilesByObjectFootprintAt(existing_object.tile_x, existing_object.tile_y)
  end

  -- for each tile in object_footprint check map tile availability
  for i, tile in ipairs(object_footprint) do
    local xpos = x + tile[1]
    local ypos = y + tile[2]

    -- Check 1: Does the tile have valid map coordinates?:
    if not world:isOnMap(xpos, ypos) then
      updateResult(tile)
      xpos = 0
      ypos = 0
    else
      local flag = "buildable"
      local good_tile = 24 + flag_alpha75
      local bad_tile = 67 + flag_alpha75
      if tile.only_passable then
        flag = "passable"
      end
      if tile.only_side then
        -- 50 is a trash bin thob
        if object.thob == 50 and direction == "east" then
          direction = "west"
        end
        flag = direction_parameters[direction]["buildable_flag"]
        passable_flag = direction_parameters[direction]["passable_flag"]
      end

      -- Check 2: Is the tile in the object's allowed room?:
      local result = world:willObjectsFootprintTileBeWithinItsAllowedRoomIfLocatedAt(xpos, ypos, object, room_id)
      local is_object_allowed = result.within_room
      room_id = result.roomId

      -- Check 3: The footprint tile should either be buildable or passable, is it?:
      if not tile.only_side and is_object_allowed then
        is_object_allowed =
          world:isFootprintTileBuildableOrPassable(xpos, ypos, tile, object_footprint, flag, player_id)
      elseif is_object_allowed then
        is_object_allowed = (map:getCellFlags(xpos, ypos, flags)[flag] or same_placement) and
          (player_id == 0 or flags.owner == player_id)
      end

      -- ignore placed object tile if it is shareable
      if not tile.shareable and is_object_allowed then
        -- Check 4: only one object per tile allowed original TH
        -- can build on litter and unoccupied tiles and only placeable
        -- if not on another objects passable footprint unless that
        -- too is a shareable tile
        local objchk = map:getCellFlags(xpos, ypos, flags)["thob"]
        local objects_do_not_interfere =
          (objchk == 0) or (objchk == 62) or (objchk == 64) -- no other object, except litter/puke, ratholes
        local moving_existing_object_on_same_place =
          existing_object and (existing_object.tile_x == xpos) and (existing_object.tile_y == ypos)
        local tile_exclusively_passable = world:isTileExclusivelyPassable(xpos, ypos, 10)
        is_object_allowed = tile_exclusively_passable and
          (moving_existing_object_on_same_place or objects_do_not_interfere)
      end

      -- Having checked if the tile is good set its blueprint appearance flag:
      if is_object_allowed then
        if not tile.invisible then
          map:setCell(xpos, ypos, 4, good_tile)
        end
      else
        if not tile.invisible then
          map:setCell(xpos, ypos, 4, bad_tile)
        end
        updateResult(tile)
      end
    end
    self.object_footprint[i][1] = xpos
    self.object_footprint[i][2] = ypos
  end

  if moving_existing_object and self.object_anim and object.class ~= "SideObject" then
    -- we've tested the placement and now we need to re-occupy the cells.
    existing_object:occupyTilesByObjectFootprintAt(existing_object.tile_x, existing_object.tile_y)
  end

  return valid_placement, room_id, passable_flag
end

--! Function for checking the valid placement of "NonSideObject".
--! param x (integer) target tile x coordinate.
--! param y (integer) target tile y coordinate.
--! param object (object) target SideObject.
--! param object_orientation (string) footprint orientation name.
--! param room_id (integer) room id if we in a build mode.
--! return (bool) is this placement not going to break path finding.
function UIPlaceObjects:_isNonSideObjectPlacementValid(x, y, object, object_orientation, room_id)
  local world = self.ui.app.world
  local invalid_placement

  if room_id > 0 and x and y then
    -- placing an object in a room
    local room = world:getRoom(x, y)
    if room and not room.crashed and room.door and room.door.tile_x and room.door.tile_y then
      -- a valid room with a door
      if self.ui.app.config.blocking_off_areas == 2 then
        -- soft placing approach
        if not room.is_active then
          -- user in a room editing mode
          local object_layout = object.orientations[object_orientation]
          invalid_placement = world:wouldNonSideObjectsNotHaveAccessToRoomDoor(x, y, room, object_layout)
          if not invalid_placement then
            -- also check that after placing the object, all usage titles of objects already placed in the room
            -- will remain accessible from the door room tile.
            invalid_placement = world:wouldObjectBreakRoomObjectsAccessToTheRoomDoor(x, y, room, object_layout, false)
          end
        else
          -- user not in a room editing mode.
          -- as we are not in a room editing mode, then there may be humanoids in the room.
          -- this means that placing object can block a humanoid's passage to the door.
          -- To prevent that case we fallback to strict placing approach.
          invalid_placement = world:wouldNonSideObjectBreakPathfindingIfSpawnedAt(x, y, object, object_orientation, room_id)
        end
      else
        -- soft placing approach disabled. So follow strict placing approach.
        invalid_placement = world:wouldNonSideObjectBreakPathfindingIfSpawnedAt(x, y, object, object_orientation, room_id)
      end
    else
      -- not a valid room with a door. Possible corner case like a transition state.
      invalid_placement = true
    end
  else
    -- placing an object outside of any room.
    -- Fallback to strict placing approach.
    invalid_placement = world:wouldNonSideObjectBreakPathfindingIfSpawnedAt(x, y, object, object_orientation, room_id)
  end

  if not invalid_placement then
    return true
  elseif self.ui.app.config.blocking_off_areas == 3 then
    -- all-permissive placing approach.
    -- This could lead to crashes, so we'll record this in the log so that during investigation
    -- we'll be able to know that safe placement was disabled.
    TheApp.world:gameLog("Blocking off areas is allowed at " .. x .. ", " .. y .. ".")
    return true
  end
  return false
end

--! Function for checking the valid placement of "SideObject".
--! param x (integer) target tile x coordinate.
--! param y (integer) target tile y coordinate.
--! param room_id (integer) room id if we in a build mode.
--! param passable_flag (string) passable flag name. orientation of an object relative to the cardinal directions.
--! param map (object) map class instance.
--! return (bool) is this placement not going to break path finding.
function UIPlaceObjects:_isSideObjectPlacementValid(x, y, room_id, passable_flag, map)
  -- if we consider to place a SideObject against a wall, it is always a valid placement.
  -- also, for SideObject, do not change 'passable_flag according to the algorithm below in 1.,
  -- as this will make the wall passable through.
  local along_wall = map:getCellFlags(x, y)[passable_flag] == false
  if along_wall then return true end

  local world = self.ui.app.world
  local invalid_placement

  -- Depending on the orientation of the side object, we will check the accessibility of the cell
  -- that is located just behind the edge that this side object creates with its placement
  local to_check_x, to_check_y = x, y
  if passable_flag == "travelNorth" or passable_flag == "travelSouth" then
    to_check_y = to_check_y + (passable_flag == "travelNorth" and -1 or 1)
  else
    to_check_x = to_check_x + (passable_flag == "travelEast" and 1 or -1)
  end
  local opposite_passable_flag = Object.getComplementaryPassableFlag(passable_flag)

  local function hasNoConnectingPath(x1, y1, x2, y2)
    if not world.pathfinder:findDistance(x1, y1, x2, y2) then
      -- we need to check if the failure to get the distance is due to the presence of an object in the adjacent tile
      if map:getCellFlags(x2, y2)["passable"] then
        return true
      end
    end
    return false
  end

  -- 1. Let's simulate it as if we've already placed the object and make tile edge unpassable
  -- SideObject cell
  local flags_main = {}
  flags_main[passable_flag] = false
  map:setCellFlags(x, y, flags_main)
  -- adjacent cell to SideObject
  local flags_adjacent = {}
  flags_adjacent[opposite_passable_flag] = false
  map:setCellFlags(to_check_x, to_check_y, flags_adjacent)

  -- 2. Check if the passability to objects is broken.
  if room_id > 0 and x and y then
    -- placing an object in a room
    local room = world:getRoom(x, y)
    if room and not room.crashed and room.door and room.door.tile_x and room.door.tile_y then
      -- a valid room with a door
      if self.ui.app.config.blocking_off_areas == 2 then
        -- soft placing approach
        if not room.is_active then
          -- user in a room editing mode
          invalid_placement = world:wouldObjectBreakRoomObjectsAccessToTheRoomDoor(x, y, room, nil, true)
        else
          -- user not in a room editing mode.
          -- as we are not in a room editing mode, then there could be humanoids in the room.
          -- this means that placing object can block a humanoid's passage to the door.
          -- to prevent that case we fallback to strict placing approach.
          invalid_placement = hasNoConnectingPath(x, y, to_check_x, to_check_y)
        end
      else
        -- follow strict placing approach.
        invalid_placement = hasNoConnectingPath(x, y, to_check_x, to_check_y)
      end
    else
      -- not a valid room with a door. Possible corner case like a transition state.
      invalid_placement = true
    end
  else
    -- placing an object outside of any room.
    -- Fallback to strict placing approach.
    invalid_placement = hasNoConnectingPath(x, y, to_check_x, to_check_y)
  end

  -- 3. Restore the original tile's passable properties, thus undoing the actions taken in step 1.
  flags_main[passable_flag] = true
  map:setCellFlags(x, y, flags_main)
  flags_adjacent[opposite_passable_flag] = true
  map:setCellFlags(to_check_x, to_check_y, flags_adjacent)

  if not invalid_placement then
    return true
  elseif self.ui.app.config.blocking_off_areas == 3 then
    -- all-permissive placing approach.
    -- This could lead to crashes, so we'll record this in the log so that during investigation
    -- we'll be able to know that safe placement was disabled.
    TheApp.world:gameLog("Blocking off areas is allowed at " .. x .. ", " .. y .. ".")
    return true
  end
  return false
end

function UIPlaceObjects:_setBlueprintPartialFlags(x, y, map, valid_placement)
  if ATTACH_BLUEPRINT_TO_TILE then
    self.object_anim:setTile(map, x, y, 0)
  end
  self.object_anim:setPartialFlag(flag_altpal, not valid_placement)
  self.object_slave_anim:setPartialFlag(flag_altpal, not valid_placement)
  self.object_blueprint_good = valid_placement
  self.ui:tutorialStep(1, valid_placement and 5 or 4, valid_placement and 4 or 5)
end

local function NearestPointOnLine(lx1, ly1, lx2, ly2, px, py)
  -- Translate everything to make one line segment endpoint be on the origin
  local lx = lx1 - lx2
  local ly = ly1 - ly2
  px = px - lx2
  py = py - ly2

  -- Project point onto line (scale everything to make (lx, ly) be on the unit
  -- circle, then dot product).
  local d = (lx * px + ly * py) / (lx * lx + ly * ly)

  if d <= 0 then
    return lx2, ly2
  elseif d >= 1 then
    return lx1, ly1
  else
    return d * lx1 + (1 - d) * lx2, d * ly1 + (1 - d) * ly2
  end
end

function UIPlaceObjects:calculateBestPlacementPosition(x, y)
  local object = self.objects[self.active_index].object
  local room = self.room
  local wx, wy = self.ui:ScreenToWorld(self.x + x, self.y + y)
  local bestd = nil
  local bestx, besty = wx, wy
  local besto = self.object_orientation
  if room and object.locked_to_wall then
    if object.locked_to_wall.north then
      local px, py = NearestPointOnLine(room.x + 0.5, room.y + 0.5,
        room.x + room.width - 0.5, room.y + 0.5, wx, wy)
      local d = ((px - wx)^2 + (py - wy)^2)^0.5
      if not bestd or d < bestd then
        bestd, bestx, besty, besto = d, px, py, object.locked_to_wall.north
      end
    end
    if object.locked_to_wall.west then
      local px, py = NearestPointOnLine(room.x + 0.5, room.y + 0.5,
        room.x + 0.5, room.y + room.height - 0.5, wx, wy)
      local d = ((px - wx)^2 + (py - wy)^2)^0.5
      if not bestd or d < bestd then
        bestx, besty, besto = px, py, object.locked_to_wall.west
      end
    end
    -- TODO: East, South
  end
  bestx, besty = math_floor(bestx), math_floor(besty)
  if bestx < 1 or besty < 1 or
      bestx > self.map.width or besty > self.map.height then
    bestx, besty = nil, nil
  end
  return bestx, besty, besto
end

function UIPlaceObjects:onCursorWorldPositionChange(x, y)
  local repaint = Window.onCursorWorldPositionChange(self, x, y)
  if not self.place_objects then -- We don't want to place objects because we are selecting new objects for adding in a room being built/edited
    return repaint
  end

  repaint = true
  if self.world.user_actions_allowed then
    local wx, wy, wo = self:calculateBestPlacementPosition(x, y)
    if wx ~= self.object_cell_x or wy ~= self.object_cell_y then
      self:setBlueprintCell(wx, wy)
      repaint = true
    end
    if wo ~= self.object_orientation then
      self:setOrientation(wo)
      repaint = true
    end
  else
    self:clearBlueprint()
  end

  return repaint
end

function UIPlaceObjects:selectObjectType(object_type)
  for i, o in ipairs(self.objects) do
    if o.object.id == object_type.id then
      self:setActiveIndex(i)
      return
    end
  end
end

function UIPlaceObjects:afterLoad(old, new)
  Window.afterLoad(self, old, new)
  UIPlaceObjects.registerKeyHandlers(self)

  if old < 236 then
    local selected_label_color = { red = 40, green = 40, blue = 250 }
    self.white_font = TheApp.gfx:loadFontAndSpriteTable("QData", "Font01V", nil, nil, { apply_ui_scale = true })
    self.blue_font = TheApp.gfx:loadFontAndSpriteTable("QData", "Font02V", nil, nil, {ttf_color = selected_label_color, apply_ui_scale = true })
  end
end
