local Class = require 'lib/class'

local dialog_box = Class:new{

  -- Dialog box properties.
  dialog = nil,                -- Dialog being displayed or nil.
  first = true,                -- Whether this is the first dialog of a sequence.
  style = nil,                 -- "box" or "empty".
  vertical_position = "auto",  -- "auto", "top" or "bottom".
  skip_mode = nil,             -- "none", "current", "all" or "unchanged".
  icon_index = nil,            -- Index of the 16x16 icon in hud/dialog_icons.png or nil.
  info = nil,                  -- Parameter passed to start_dialog().
  skipped = false,             -- Whether the player skipped the dialog.
  selected_answer = nil,       -- Selected answer (1 or 2) or nil if there is no question.

  -- Displaying text gradually.
  next_line = nil,             -- Next line to display or nil.
  line_it = nil,               -- Iterator over of all lines of the dialog.
  lines = {},                  -- Array of the text of the 3 visible lines.
  line_surfaces = {},          -- Array of the 3 text surfaces.
  line_index = nil,            -- Line currently being shown.
  char_index = nil,            -- Next character to show in the current line.
  char_delay = nil,            -- Delay between two characters in milliseconds.
  full = false,                -- Whether the 3 visible lines have shown all content.
  need_letter_sound = false,   -- Whether a sound should be played with the next character.
  gradual = true,              -- Whether text is displayed gradually.

  -- Graphics.
  dialog_surface = nil,
  box_img = nil,
  icons_img = nil,
  end_lines_sprite = nil,
  box_dst_position = nil,      -- Destination coordinates of the dialog box.
  question_dst_position = nil, -- Destination coordinates of the question icon.
  icon_dst_position = nil,     -- Destination coordinates of the icon.
}

-- Constants.
local nb_visible_lines = 3     -- Maximum number of lines in the dialog box.
local char_delays = {
  slow = 60,
  medium = 40,
  fast = 20  -- Default.
}
local letter_sound_delay = 100
local box_width = 220
local box_height = 60
local dialog_font = 'la'

-- Initializes the dialog box system.
function dialog_box:initialize_dialog_box()

  -- Initialize dialog box data.
  for i = 1, nb_visible_lines do
    self.lines[i] = ""
    self.line_surfaces[i] = sol.text_surface.create{
      horizontal_alignment = "left",
      vertical_alignment = "top",
    }
  end
  self.dialog_surface = sol.surface.create(sol.video.get_quest_size())
  self.box_img = sol.surface.create("hud/dialog_box.png")
  self.icons_img = sol.surface.create("hud/dialog_icons.png")
  self.end_lines_sprite = sol.sprite.create("hud/dialog_box_message_end")
  self:set_dialog_style("box")
end

-- Exits the dialog box system.
function dialog_box:quit_dialog_box()

    if self.game:is_dialog_enabled() then
      sol.menu.stop(dialog_box)
    end
end

-- Sets the style of the dialog box for subsequent dialogs.
-- style must be one of:
-- - "box" (default): Usual dialog box.
-- - "empty": No decoration.
function dialog_box:set_dialog_style(style)

  self.style = style
  if style == "box" then
    -- Make the dialog box slightly transparent.
    self.dialog_surface:set_opacity(255)
  end
end

-- Sets the vertical position of the dialog box for subsequent dialogs.
-- vertical_position must be one of:
-- - "auto" (default): Choose automatically so that the hero is not hidden.
-- - "top": Top of the screen.
-- - "bottom": Botton of the screen.
function dialog_box:set_dialog_position(vertical_position)
  self.vertical_position = vertical_position
end

local function repeat_show_character(dialog_box)
    return function ()

        dialog_box:check_full()
        while not dialog_box:is_full()
            and dialog_box.char_index > #dialog_box.lines[dialog_box.line_index] do
            -- The current line is finished.
            dialog_box.char_index = 1
            dialog_box.line_index = dialog_box.line_index + 1
            dialog_box:check_full()
        end

        if not dialog_box:is_full() then
            dialog_box:add_character()
        else
            sol.audio.play_sound("message_end")
            if dialog_box:has_more_lines()
                or dialog_box.dialog.next ~= nil
                or dialog_box.selected_answer ~= nil then
                dialog_box.end_lines_sprite:set_animation("next")
                dialog_box.game:set_custom_command_effect("action", "next")
            else
                dialog_box.end_lines_sprite:set_animation("last")
                dialog_box.game:set_custom_command_effect("action", "return")
            end
        end
    end
end

-- The first dialog of a sequence starts.
function dialog_box:on_started()

  -- Set the initial properties.
  -- Subsequent dialogs in the same sequence do not reset them.
  self.icon_index = nil
  self.skip_mode = "none"
  self.char_delay = char_delays["fast"]
  self.selected_answer = nil

  -- Determine the position of the dialog box on the screen.
  local map = self.game:get_map()
  local camera_x, camera_y, camera_width, camera_height = map:get_camera():get_bounding_box()
  local top = false
  if self.vertical_position == "top" then
    top = true
  elseif self.vertical_position == "auto" then
    local hero_x, hero_y = map:get_entity("hero"):get_position()
    if hero_y >= camera_y + (camera_height / 2 + 10) then
      top = true
    end
  end

  -- Set the coordinates of graphic objects.
  local x = camera_width / 2 - 110
  local y = top and 32 or (camera_height - 96)

  if self.style == "empty" then
    y = y + (top and -24 or 24)
  end

  self.box_dst_position = { x = x, y = y }
  self.question_dst_position = { x = x + 18, y = y + 27 }
  self.icon_dst_position = { x = x + 18, y = y + 22 }

  self:show_dialog()
end

-- The dialog box is being closed.
function dialog_box:on_finished()

  -- Remove overriden command effects.
  self.game:set_custom_command_effect("action", nil)
  self.game:set_custom_command_effect("attack", nil)
end

-- A dialog starts (not necessarily the first one of its sequence).
function dialog_box:show_dialog()

  -- Initialize this dialog.
  local dialog = self.dialog

  local text = dialog.text
  if self.info ~= nil then
    -- There is a "$v" sequence to substitute.
    text = text:gsub("%$v", self.info)
  end
  -- Split the text in lines.
  text = text:gsub("\r\n", "\n"):gsub("\r", "\n")
  self.line_it = text:gmatch("([^\n]*)\n")  -- Each line including empty ones.

  self.next_line = self.line_it()
  self.line_index = 1
  self.char_index = 1
  self.skipped = false
  self.full = false
  self.need_letter_sound = self.style ~= "empty"

  if dialog.skip ~= nil then
    -- The skip mode changes for this dialog.
    self.skip_mode = dialog.skip
  end

  if dialog.icon ~= nil then
    -- The icon changes for this dialog ("-1" means none).
    if dialog.icon == "-1" then
      self.icon_index = nil
    else
      self.icon_index = dialog.icon
    end
  end

  if dialog.question == "1" then
    -- This dialog is a question.
    self.selected_answer = 1  -- The answer will be 1 or 2.
  end

  -- Start displaying text.
  self:show_more_lines()
end

-- Returns whether there are more lines remaining to display after the current
-- 3 lines.
function dialog_box:has_more_lines()
  return self.next_line ~= nil
end

-- Updates the result of is_full().
function dialog_box:check_full()
  if self.line_index >= nb_visible_lines
      and self.char_index > #self.lines[nb_visible_lines] then
    self.full = true
  else
    self.full = false
  end
end

-- Returns whether all 3 current lines of the dialog box are entirely
-- displayed.
function dialog_box:is_full()
  return self.full
end

-- Shows the next dialog of the sequence.
-- Closes the dialog box if there is no next dialog.
function dialog_box:show_next_dialog()

  local next_dialog_id
  if self.selected_answer ~= 2 then
    -- No question or first answer
    next_dialog_id = self.dialog.next
  else
    -- Second answer.
    next_dialog_id = self.dialog.next2
  end

  if next_dialog_id ~= nil and next_dialog_id ~= "_unknown" then
    -- Show the next dialog.
    self.first = false
    self.selected_answer = nil
    self.dialog = sol.language.get_dialog(next_dialog_id)
    self:show_dialog()
  else
    -- Finish the dialog, returning the answer or nil if there was no question.
    local status = self.selected_answer

    -- Conform to the built-in handling of shop items.
    if self.dialog.id == "_shop.question" then
      -- The engine expects a boolean answer after the "do you want to buy"
      -- shop item dialog.
      status = self.selected_answer == 1
    end

    if self.game:is_dialog_enabled() then
        self.game:stop_dialog(status)
    else
        sol.menu.stop(self)
    end
  end
end

-- Starts showing a new group of 3 lines in the dialog.
-- Shows the next dialog (if any) if there are no remaining lines.
function dialog_box:show_more_lines()

  self.gradual = true

  if not self:has_more_lines() then
    self:show_next_dialog()
    return
  end

  -- Hide the action icon and change the sword icon.
  self.game:set_custom_command_effect("action", nil)
  if self.skip_mode ~= "none" then
    self.game:set_custom_command_effect("attack", "skip")
    self.game:set_custom_command_effect("action", "next")
  else
    self.game:set_custom_command_effect("attack", nil)
  end

  -- Prepare the 3 lines.
  for i = 1, math.min(nb_visible_lines, #self.line_surfaces) do
    self.line_surfaces[i]:set_font(dialog_font)
    self.line_surfaces[i]:set_text("")
    if self:has_more_lines() then
      self.lines[i] = self.next_line
      self.next_line = self.line_it()
    else
      self.lines[i] = ""
    end
  end
  self.line_index = 1
  self.char_index = 1

  if self.gradual then
    sol.timer.start(self, self.char_delay, repeat_show_character(self))
  end
end

-- Adds the next character to the dialog box.
-- If this is a special character (like $0, $v, etc.),
-- the corresponding action is performed.
function dialog_box:add_character()

  local line = self.lines[self.line_index]
  local current_char = line:sub(self.char_index, self.char_index)
  if current_char == "" then
    error("No remaining character to add on this line")
  end
  self.char_index = self.char_index + 1
  local additional_delay = 0
  local text_surface = self.line_surfaces[self.line_index]

  -- Special characters:
  -- - $1, $2 and $3: slow, medium and fast
  -- - $0: pause
  -- - $v: variable
  -- - space: don't add the delay
  -- - 110xxxx: multibyte character

  local special = false
  if current_char == "$" then
    -- Special character.

    special = true
    current_char = line:sub(self.char_index, self.char_index)
    self.char_index = self.char_index + 1

    if current_char == "0" then
      -- Pause.
      additional_delay = 1000

    elseif current_char == "1" then
      -- Slow.
      self.char_delay = char_delays["slow"]

    elseif current_char == "2" then
      -- Medium.
      self.char_delay = char_delays["medium"]

    elseif current_char == "3" then
      -- Fast.
      self.char_delay = char_delays["fast"]

    else
      -- Not a special char, actually.
      text_surface:set_text(text_surface:get_text() .. "$")
      special = false
    end
  end

  if not special then
    -- Normal character to be displayed.
    text_surface:set_text(text_surface:get_text() .. current_char)

    -- If this is a multibyte character, also add the next byte.
    local byte = current_char:byte()
    if byte >= 192 and byte < 224 then
      -- The first byte is 110xxxxx: the character is stored with
      -- two bytes (utf-8).
      current_char = line:sub(self.char_index, self.char_index)
      self.char_index = self.char_index + 1
      text_surface:set_text(text_surface:get_text() .. current_char)
    end

    if current_char == " " then
      -- Remove the delay for whitespace characters.
      additional_delay = -self.char_delay
    end
  end

  if not special and current_char ~= nil and self.need_letter_sound then
    -- Play a letter sound sometimes.
    sol.audio.play_sound("message_letter")
    self.need_letter_sound = false
    sol.timer.start(self, letter_sound_delay, function()
      self.need_letter_sound = true
    end)
  end

  if self.gradual then
    sol.timer.start(self, self.char_delay + additional_delay, repeat_show_character(self))
  end
end

-- Stops displaying gradually the current 3 lines, shows them immediately.
-- If the 3 lines were already finished, the next group of 3 lines starts
-- (if any).
function dialog_box:show_all_now()

  if self:is_full() then
    self:show_more_lines()
  else
    self.gradual = false
    -- Check the end of the current line.
    self:check_full()
    while not self:is_full() do

      while not self:is_full()
          and self.char_index > #self.lines[self.line_index] do
        self.char_index = 1
        self.line_index = self.line_index + 1
        self:check_full()
      end

      if not self:is_full() then
        self:add_character()
      end
      self:check_full()
    end
  end
end

function dialog_box:on_command_pressed(command)

  if command == "action" then

    -- Display more lines.
    if self:is_full() then
      self:show_more_lines()
    elseif self.skip_mode ~= "none" then
      self:show_all_now()
    end

  elseif command == "attack" then

    -- Attempt to skip the dialog.
    if self.skip_mode == "all" then
      self.skipped = true
      self.game:stop_dialog("skipped")
    elseif self:is_full() then
      self:show_more_lines()
    elseif self.skip_mode == "current" then
      self:show_all_now()
    end

  elseif command == "up" or command == "down" then

    if self.selected_answer ~= nil
        and not self:has_more_lines()
        and self:is_full() then
      sol.audio.play_sound("cursor")
      self.selected_answer = 3 - self.selected_answer  -- Switch between 1 and 2.
      self.question_dst_position.y = self.box_dst_position.y +
          (self.selected_answer == 1 and 27 or 40)
    end
  end

  -- Don't propagate the event to anything below the dialog box.
  return true
end

function dialog_box:on_draw(dst_surface)

  local x, y = self.box_dst_position.x, self.box_dst_position.y

  self.dialog_surface:clear()

  if self.style == "empty" then
    -- Draw a dark rectangle.
    dst_surface:fill_color({0, 0, 0}, x, y, 220, 60)
  else
    -- Draw the dialog box.
    self.box_img:draw_region(0, 0, box_width, box_height, self.dialog_surface, x, y)
  end

  -- Draw the text.
  local text_x = x + (self.icon_index == nil and 16 or 48)
  local text_y = y - 1
  for i = 1, nb_visible_lines do
    text_y = text_y + 13
    if self.selected_answer ~= nil
        and i == nb_visible_lines - 1
        and not self:has_more_lines() then
      -- The last two lines are the answer to a question.
      text_x = text_x + 24
    end
    self.line_surfaces[i]:draw(self.dialog_surface, text_x, text_y)
  end

  -- Draw the icon.
  if self.icon_index ~= nil then
    local row, column = math.floor(self.icon_index / 10), self.icon_index % 10
    self.icons_img:draw_region(16 * column, 16 * row, 16, 16,
        self.dialog_surface,
        self.icon_dst_position.x, self.icon_dst_position.y)
    self.question_dst_position.x = x + 50
  else
    self.question_dst_position.x = x + 18
  end

  -- Draw the question arrow.
  if self.selected_answer ~= nil
      and self:is_full()
      and not self:has_more_lines() then
    self.box_img:draw_region(96, 60, 8, 8, self.dialog_surface,
        self.question_dst_position.x, self.question_dst_position.y)
  end

  -- Draw the end message arrow.
  if self:is_full() then
    self.end_lines_sprite:draw(self.dialog_surface, x + 103, y + 56)
  end

  -- Final blit.
  self.dialog_surface:draw(dst_surface)
end

return dialog_box
