-- Copyright 2007-2022 Mitchell. See LICENSE.

local M = {}

--[[ This comment is for LuaDoc.
---
-- Session support for Textadept.
-- @field save_on_quit (bool)
--   Save the session when quitting.
--   The default value is `true` unless the user passed the command line switch `-n` or
--   `--nosession` to Textadept.
-- @field _G.events.SESSION_SAVE (string)
--   Emitted when saving a session.
--   Arguments:
--
--   * `session`: Table of session data to save. All handlers will have access to this same
--     table, and Textadept's default handler reserves the use of some keys.
--     Note that functions, userdata, and circular table values cannot be saved. The latter
--     case is not recognized at all, so beware.
-- @field _G.events.SESSION_LOAD (string)
--   Emitted when loading a session.
--   Arguments:
--
--   * `session`: Table of session data to load. All handlers will have access to this same table.
module('textadept.session')]]

M.save_on_quit = true

-- Events.
local session_events = {'session_save', 'session_load'}
for _, v in ipairs(session_events) do events[v:upper()] = v end

local session_file = _USERHOME .. (not CURSES and '/session' or '/session_term')

---
-- Loads session file *filename* or the user-selected session, returning `true` if a session
-- file was opened and read.
-- Textadept restores split views, opened buffers, cursor information, recent files, and bookmarks.
-- @param filename Optional absolute path to the session file to load. If `nil`, the user is
--   prompted for one.
-- @return `true` if the session file was opened and read; `nil` otherwise.
-- @usage textadept.session.load(filename)
-- @name load
function M.load(filename)
  local dir, name = session_file:match('^(.-[/\\]?)([^/\\]+)$')
  if not assert_type(filename, 'string/nil', 1) then
    filename = ui.dialogs.fileselect{
      title = _L['Load Session'], with_directory = dir, with_file = name
    }
    if not filename then return end
  end
  if session_file ~= filename then M.save(session_file) end
  local f = loadfile(filename, 't', {})
  if not f or not io.close_all_buffers() then return end -- fail silently
  local session = f()
  local not_found = {}

  -- Unserialize cwd.
  if session.cwd then lfs.chdir(session.cwd) end

  -- Unserialize buffers.
  for _, buf in ipairs(session.buffers) do
    if lfs.attributes(buf.filename) then
      io.open_file(buf.filename)
      buffer:set_sel(buf.anchor, buf.current_pos)
      view.first_visible_line = buf.top_line
      for _, line in ipairs(buf.bookmarks) do
        buffer:marker_add(line, textadept.bookmarks.MARK_BOOKMARK)
      end
    elseif buf.filename:find('^%[.+%]$') then
      buffer.new()._type = buf.filename
      buffer:set_save_point()
      events.emit(events.FILE_OPENED, buf.filename) -- close initial buffer
    else
      not_found[#not_found + 1] = buf.filename
    end
  end

  -- Unserialize UI state.
  ui.maximized = session.ui.maximized
  if not ui.maximized then ui.size = session.ui.size end

  -- Unserialize views.
  local function unserialize_split(split)
    if type(split) ~= 'table' then
      view:goto_buffer(_BUFFERS[math.min(split, #_BUFFERS)])
    else
      for i, view in ipairs{view:split(split.vertical)} do
        view.size = split.size
        ui.goto_view(view)
        unserialize_split(split[i])
      end
    end
  end
  unserialize_split(session.views[1])
  ui.goto_view(_VIEWS[math.min(session.views.current, #_VIEWS)])

  -- Unserialize recent files.
  io.recent_files = session.recent_files

  -- Unserialize user data.
  events.emit(events.SESSION_LOAD, session)

  if #not_found > 0 then
    ui.dialogs.msgbox{
      title = _L['Session Files Not Found'],
      text = _L['The following session files were not found'],
      informative_text = table.concat(not_found, '\n'):iconv('UTF-8', _CHARSET),
      icon = 'dialog-warning', width = CURSES and ui.size[1] - 2 or nil
    }
  end
  session_file = filename
  return true
end
-- Load session when no args are present.
local function load_default_session() if M.save_on_quit then M.load(session_file) end end
events.connect(events.ARG_NONE, load_default_session)

-- Returns value *val* serialized as a string.
-- This is a very simple implementation suitable for session saving only.
-- Ignores function, userdata, and thread types, and does not handle circular tables.
local function _tostring(val)
  if type(val) == 'table' then
    local t = {}
    for k, v in pairs(val) do t[#t + 1] = string.format('[%s]=%s,', _tostring(k), _tostring(v)) end
    return string.format('{%s}', table.concat(t))
  elseif type(val) == 'function' or type(val) == 'userdata' or type(val) == 'thread' then
    val = nil
  end
  return type(val) == 'string' and string.format('%q', val) or tostring(val)
end

---
-- Saves the session to file *filename* or the user-selected file.
-- Saves split views, opened buffers, cursor information, recent files, and bookmarks.
-- Upon quitting, the current session is saved to *filename* again, unless
-- `textadept.session.save_on_quit` is `false`.
-- @param filename Optional absolute path to the session file to save. If `nil`, the user is
--   prompted for one.
-- @usage textadept.session.save(filename)
-- @name save
function M.save(filename)
  local dir, name = session_file:match('^(.-[/\\]?)([^/\\]+)$')
  if not assert_type(filename, 'string/nil', 1) then
    filename = ui.dialogs.filesave{
      title = _L['Save Session'], with_directory = dir, with_file = name
    }
    if not filename then return end
  end
  local session = {}

  -- Serialize user data.
  events.emit(events.SESSION_SAVE, session)

  -- Serialize cwd.
  session.cwd = lfs.currentdir()

  -- Serialize buffers.
  session.buffers = {}
  for _, buffer in ipairs(_BUFFERS) do
    if not buffer.filename and not buffer._type then goto continue end
    local current = buffer == view.buffer
    session.buffers[#session.buffers + 1] = {
      filename = buffer.filename or buffer._type,
      anchor = current and buffer.anchor or buffer._anchor or 1,
      current_pos = current and buffer.current_pos or buffer._current_pos or 1,
      top_line = current and view.first_visible_line or buffer._top_line or 1
    }
    local bookmarks = {}
    local BOOKMARK_BIT = 1 << textadept.bookmarks.MARK_BOOKMARK - 1
    local line = buffer:marker_next(1, BOOKMARK_BIT)
    while line ~= -1 do
      bookmarks[#bookmarks + 1] = line
      line = buffer:marker_next(line + 1, BOOKMARK_BIT)
    end
    session.buffers[#session.buffers].bookmarks = bookmarks
    ::continue::
  end

  -- Serialize UI state.
  session.ui = {maximized = ui.maximized, size = ui.size}

  -- Serialize views.
  local function serialize_split(split)
    return split.buffer and _BUFFERS[split.buffer] or {
      serialize_split(split[1]), serialize_split(split[2]), vertical = split.vertical,
      size = split.size
    }
  end
  session.views = {serialize_split(ui.get_split_table()), current = _VIEWS[view]}

  -- Serialize recent files.
  session.recent_files = io.recent_files

  -- Write the session.
  assert(io.open(filename, 'wb')):write('return ', _tostring(session)):close()
  session_file = filename
end
-- Saves session on quit.
events.connect(events.QUIT, function() if M.save_on_quit then M.save(session_file) end end, 1)

-- Does not save session on quit.
args.register('-n', '--nosession', 0, function() M.save_on_quit = false end,
  'No session functionality')
-- Loads the given session on startup.
args.register('-s', '--session', 1, function(name)
  if not lfs.attributes(name) then name = string.format('%s/%s', _USERHOME, name) end
  M.load(name)
  events.disconnect(events.ARG_NONE, load_default_session)
end, 'Load session')

return M
