local core = require "core"
local common = require "core.common"
local style = require "core.style"
local Widget = require "widget"
local Button = require "widget.button"
local Label = require "widget.label"

---@class widget.filepicker : widget
---@overload fun(parent?:widget, path?:string):widget.filepicker
---@field public pick_mode integer
---@field public filters table<integer,string>
---@field private path string
---@field private file widget.label
---@field private textbox widget.textbox
---@field private button widget.button
local FilePicker = Widget:extend()

---Operation modes for the file picker.
---@type table<string,integer>
FilePicker.mode = {
  ---Opens file browser the selected file does not has to exist.
  FILE = 1,
  ---Opens file browser the selected file has to exist.
  FILE_EXISTS = 2,
  ---Opens directory browser the selected directory does not has to exist.
  DIRECTORY = 4,
  ---Opens directory browser the selected directory has to exist.
  DIRECTORY_EXISTS = 8
}

---@param text string
---@return string[]
local function suggest_directory(text)
  text = common.home_expand(text)
  if not common.is_absolute_path(text) then
    text = core.root_project().path .. PATHSEP .. text
  end
  return common.home_encode_list(
    common.dir_path_suggest(text, core.root_project().path)
  )
end

---@param path string
local function check_directory_path(path)
  local abs_path = core.project_absolute_path(path)
  local info = abs_path and system.get_file_info(abs_path)
  if not info or info.type ~= 'dir' then return nil end
  return abs_path
end

---@param str string
---@param find string
---@param replace string
local function str_replace(str, find, replace)
  local start, ending = str:find(find, 1, true)
  if start == 1 then
    return replace .. str:sub(ending + 1)
  else
    return str:sub(1, start - 1) .. replace .. str:sub(ending + 1)
  end
end

---@alias widget.filepicker.modes
---| `FilePicker.mode.FILE`
---| `FilePicker.mode.FILE_EXISTS`
---| `FilePicker.mode.DIRECTORY`
---| `FilePicker.mode.DIRECTORY_EXISTS`

---Constructor
---@param parent widget
---@param path? string
function FilePicker:new(parent, path)
  FilePicker.super.new(self, parent)

  local this = self

  self.type_name = "widget.filepicker"

  self.filters = {}
  self.border.width = 0
  self.pick_mode = FilePicker.mode.FILE

  self.file = Label(self, "")
  self.file.clickable = true
  self.file:set_border_width(1)
  function self.file:on_click(button)
    if button == "left" then
      this:show_picker()
    end
  end
  function self.file:on_mouse_enter(...)
    Label.super.on_mouse_enter(self, ...)
    self.border.color = style.caret
  end
  function self.file:on_mouse_leave(...)
    Label.super.on_mouse_leave(self, ...)
    self.border.color = style.text
  end

  self.button = Button(self, "")
  self.button:set_icon("D")
  self.button:set_tooltip("open file browser")
  function self.button:on_click(button)
    if button == "left" then
      this:show_picker()
    end
  end

  local label_width = self.file:get_width()
  if label_width <= 10 then
    label_width = 200 + (self.file.border.width * 2)
    self.file:set_size(
      200, self.button:get_height() - self.button.border.width * 2
    )
  end

  self:set_size(
    label_width + self.button:get_width(),
    math.max(self.file:get_height(), self.button:get_height())
  )

  self:set_path(path)
end

---Set the filepicker size
---@param width? number
---@param height? number
function FilePicker:set_size(width, height)
  FilePicker.super.set_size(self, width, height)

  self.file:set_position(0, 0)
  self.file:set_size(
    self:get_width() - self.button:get_width(),
    self.button:get_height()
  )

  self.button:set_position(self.file:get_right(), 0)

  self.size.y = math.max(
    self.file:get_height(),
    self.button:get_height()
    -- something is off on calculation since adding border width should not
    -- be needed to display whole rendered control at all...
  ) + self.button.border.width
end

---Add a lua pattern to the filters list
---@param pattern string
function FilePicker:add_filter(pattern)
  table.insert(self.filters, pattern)
end

---Clear the filters list
function FilePicker:clear_filters()
  self.filters = {}
end

---Set the operation mode for the file picker.
---@param mode widget.filepicker.modes | string | integer
function FilePicker:set_mode(mode)
  if type(mode) == "string" then
    ---@type integer
    local intmode = FilePicker.mode[mode:upper()]
    self.pick_mode = intmode
  else
    self.pick_mode = mode
  end
end

---Set the full path including directory and filename.
---@param path? string
function FilePicker:set_path(path)
  if path then
    self.path = path or ""
    if common.path_belongs_to(path, core.root_project().path) then
      self.file.label = path ~= "" and
        common.relative_path(core.root_project().path, path)
        or
        ""
    else
      self.file.label = path
    end
  else
    self.path = ""
    self.file.label = ""
  end
end

---Get the full path including directory and filename.
---@return string | nil
function FilePicker:get_path()
  if self.path ~= "" then
    return self.path
  end
  return nil
end

---Get the full path relative to current project dir or absolute if it doesn't
---belongs to the current project directory.
---@return string
function FilePicker:get_relative_path()
  if
    self.path ~= ""
    and
    common.path_belongs_to(self.path, core.root_project().path)
  then
    return common.relative_path(core.root_project().path, self.path)
  end
  return self.path or ""
end

---Set the filename part only.
---@param name string
function FilePicker:set_filename(name)
  local dir_part = common.dirname(self.path)
  if dir_part then
    self:set_path(dir_part .. PATHSEP .. name)
  else
    self:set_path(name)
  end
end

---Get the filename part only.
---@return string | nil
function FilePicker:get_filename()
  local dir_part = common.dirname(self.path)
  if dir_part then
    local filename = str_replace(self.path, dir_part .. PATHSEP, "")
    return filename
  elseif self.path ~= "" then
    return self.path
  end
  return nil
end

---Set the directory part only.
---@param dir string
function FilePicker:set_directory(dir)
  local filename = self:get_filename()
  if filename then
    self:set_path(dir:gsub("[\\/]$", "") .. PATHSEP .. filename)
  else
    self:set_path(dir:gsub("[\\/]$", ""))
  end
end

---Get the directory part only.
---@return string | nil
function FilePicker:get_directory()
  if self.path ~= "" then
    local dir_part = common.dirname(self.path)
    if dir_part then return dir_part end
  end
  return nil
end

---Filter a list of directories by applying currently set filters.
---@param self widget.filepicker
---@param list table<integer, string>
---@return table<integer,string>
local function filter(self, list)
  for i=1, #list do
    local path = common.home_expand(list[i])
    if common.path_belongs_to(path, core.root_project().path) then
      list[i] = common.relative_path(core.root_project().path, path)
    end
  end
  if #self.filters > 0 then
    local new_list = {}
    for _, value in ipairs(list) do
      if common.match_pattern(value, self.filters) then
        table.insert(new_list, value)
      elseif
        self.pick_mode == FilePicker.mode.FILE
        or
        self.pick_mode == FilePicker.mode.FILE_EXISTS
      then
        local path = common.home_expand(value)
        local abs_path = check_directory_path(path)
        if abs_path then
          table.insert(new_list, value)
        end
      end
    end
    return new_list
  end
  return list
end

---@param self widget.filepicker
local function show_file_picker(self)
  core.command_view:enter("Choose File", {
    text = self:get_relative_path(),
    submit = function(text)
      ---@type string
      local filename = text
      local dirname = common.dirname(common.home_expand(text))
      if dirname then
        filename = common.home_expand(text)
        filename = core.project_absolute_path(dirname)
          .. PATHSEP
          .. str_replace(filename, dirname .. PATHSEP, "")
      elseif filename ~= "" then
        filename = core.root_project().path .. PATHSEP .. filename
      end
      self:set_path(filename)
      self:on_change(filename ~= "" and filename or nil)
    end,
    suggest = function (text)
      return filter(
        self,
        common.home_encode_list(
          common.path_suggest(
            common.home_expand(text), core.root_project().path
          )
        )
      )
    end,
    validate = function(text)
      if
        #self.filters > 0 and text ~= ""
        and
        not common.match_pattern(text, self.filters)
      then
        core.error(
          "File does not match the filters: %s",
          table.concat(self.filters, ", ")
        )
        return false
      end
      local filename = common.home_expand(text)
      local path_stat, err = system.get_file_info(filename)
      if path_stat and path_stat.type == 'dir' then
        core.error("Cannot open %s, is a folder", text)
        return false
      end
      if self.pick_mode == FilePicker.mode.FILE_EXISTS then
        if not path_stat then
          core.error("Cannot open file %s: %s", text, err)
          return false
        end
      else
        local dirname = common.dirname(filename)
        local dir_stat = dirname and system.get_file_info(dirname)
        if dirname and not dir_stat then
          core.error("Directory does not exists: %s", dirname)
          return false
        end
      end
      return true
    end,
  })
end

---@param self widget.filepicker
local function show_dir_picker(self)
  core.command_view:enter("Choose Directory", {
    text = self:get_relative_path(),
    submit = function(text)
      local path = common.home_expand(text)
      local abs_path = check_directory_path(path)
      self:set_path(abs_path or text)
      self:on_change(abs_path or (text ~= "" and text or nil))
    end,
    suggest = function(text)
      return filter(self, suggest_directory(text))
    end,
    validate = function(text)
      if
        #self.filters > 0 and text ~= ""
        and
        not common.match_pattern(text, self.filters)
      then
        core.error(
          "Directory does not match the filters: %s",
          table.concat(self.filters, ", ")
        )
        return false
      end
      if self.pick_mode == FilePicker.mode.DIRECTORY_EXISTS then
        local path = common.home_expand(text)
        local abs_path = check_directory_path(path)
        if not abs_path then
          core.error("Cannot open directory %q", path)
          return false
        end
      end
      return true
    end
  })
end

---Show the command view file or directory browser depending on the
---current file picker mode.
function FilePicker:show_picker()
  if
    self.pick_mode == FilePicker.mode.FILE
    or
    self.pick_mode == FilePicker.mode.FILE_EXISTS
  then
    show_file_picker(self)
  else
    show_dir_picker(self)
  end
end

function FilePicker:update_size_position()
  FilePicker.super.update_size_position(self)
  self:set_size(
    self.file:get_width() + self.button:get_width(),
    self.button:get_height()
  )
end

return FilePicker
