--=============================================================================
-- repl.lua --- REPL for spacevim
-- Copyright (c) 2016-2022 Wang Shidong & Contributors
-- Author: Wang Shidong < wsdjeg@outlook.com >
-- URL: https://spacevim.org
-- License: GPLv3
--=============================================================================

local job = require('spacevim.api.job')
local nt = require('spacevim.api.notify')
local vopt = require('spacevim.api.vim.option')
local str = require('spacevim.api.data.string')
local spi = require('spacevim.api.unicode.spinners')

local log = require('spacevim.logger').derive('repl')

local lines = 0
local bufnr = -1
local winid = -1
local status = {}
local start_time
local end_time
local job_id = 0
local exes = {}
local repl_spinners = ''

local M = {}

local function close()
  if job_id > 0 then
    job.stop(job_id)
    job_id = 0
  end
  if vim.api.nvim_buf_is_valid(bufnr) then
    vim.cmd('bd ' .. bufnr)
  end
end

local function insert()
  vim.fn.inputsave()
  local input = vim.fn.input('input >')
  if vim.fn.empty(input) == 0 then
    if job_id == 0 then
      nt.notify('please restart the REPL', 'WarningMsg')
    else
      job.send(job_id, input)
    end
  end
  vim.api.nvim_echo({}, false, {})
  vim.fn.inputrestore()
end

local function close_repl()
  if job_id > 0 then
    job.stop(job_id)
    job_id = 0
  end
end

local function open_windows()
  if vim.api.nvim_buf_is_valid(bufnr) then
    vim.cmd('bd ' .. bufnr)
  end
  local previous_win = vim.api.nvim_get_current_win()
  vim.cmd('botright split __REPL__')
  bufnr = vim.api.nvim_get_current_buf()
  winid = vim.api.nvim_get_current_win()
  local l = math.floor(vim.o.lines * 30 / 100)
  vim.cmd('resize ' .. l)
  vim.api.nvim_set_current_win(previous_win)
  vopt.setlocalopt(bufnr, winid, {
    buftype = 'nofile',
    bufhidden = 'wipe',
    buflisted = false,
    list = false,
    swapfile = false,
    wrap = false,
    cursorline = true,
    spell = false,
    number = false,
    relativenumber = false,
    winfixheight = true,
    modifiable = false,
    filetype = 'SpaceVimREPL',
  })
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'q', '', {
    callback = close,
  })
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'i', '', {
    callback = insert,
  })
  local id = vim.api.nvim_create_augroup('spacevim_repl', {
    clear = true,
  })
  vim.api.nvim_create_autocmd({ 'BufWipeout' }, {
    group = id,
    buffer = bufnr,
    callback = close_repl,
  })
end

local function on_stdout(_, data)
  if vim.api.nvim_buf_is_valid(bufnr) then
    vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
    vim.api.nvim_buf_set_lines(bufnr, lines, lines + 1, false, data)
    vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
    lines = lines + #data
    local cursor = vim.api.nvim_win_get_cursor(winid)
    if cursor[1] == vim.api.nvim_buf_line_count(bufnr) - #data then
      vim.api.nvim_win_set_cursor(winid, { vim.api.nvim_buf_line_count(bufnr), 0 })
    end
  end
end

local function on_stderr(_, data)
  status.has_errors = true
  on_stdout(_, data)
end

local function on_exit(id, code, single)
  end_time = vim.fn.reltime(start_time)
  status.is_exit = true
  status.is_running = false
  status.exit_code = code
  local done = {
    '',
    '[Done] exited with code='
      .. code
      .. ' in '
      .. str.trim(vim.fn.reltimestr(end_time))
      .. ' seconds',
  }
  if vim.api.nvim_buf_is_valid(bufnr) then
    vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
    vim.api.nvim_buf_set_lines(bufnr, lines, lines + 1, false, done)
    vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
  end
  job_id = 0
  spi.stop()
end

local function start(exe)
  lines = 0
  status = {
    is_running = true,
    is_exit = false,
    has_errors = false,
    exit_code = 0,
  }

  start_time = vim.fn.reltime()
  open_windows()
  vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
  vim.api.nvim_buf_set_lines(
    bufnr,
    lines,
    lines + 3,
    false,
    { '[REPL executable] ' .. vim.fn.string(exe), '', string.rep('-', 20) }
  )
  vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
  vim.api.nvim_win_set_cursor(winid, { vim.api.nvim_buf_line_count(bufnr), 0 })
  lines = lines + 3
  job_id = job.start(exe, {
    on_stdout = on_stdout,
    on_stderr = on_stderr,
    on_exit = on_exit,
  })
  if job_id > 0 then
    spi.apply('dot1', function(v)
      repl_spinners = v
      if vim.api.nvim_win_is_valid(winid) then
        vim.fn.win_execute(winid, 'redrawstatus')
      end
    end)
  end
end

function M.start(ft)
  log.info('start repl for filetype:' .. ft)
  local exe = exes[ft] or ''
  log.debug('get the command:' .. vim.inspect(exe))
  if exe ~= '' then
    start(exe)
  else
    vim.api.nvim_echo({ { 'no REPL executable for ' .. ft, 'WarningMsg' } }, false, {})
  end
end

function M.send(t, ...)
  if job_id == 0 then
    nt.notify('please restart the REPL', 'WarningMsg')
  else
    if t == 'line' then
      job.send(job_id, { vim.api.nvim_get_current_line(), '' })
    elseif t == 'buffer' then
      local data = vim.fn.getline(1, '$')
      table.insert(data, '')
      job.send(job_id, data)
    elseif t == 'raw' then
      local context = select(1, ...)
      if type(context) == 'string' then
        job.send(job_id, context)
      end
    elseif t == 'selection' then
      local b = vim.fn.getpos("'<")
      local e = vim.fn.getpos("'>")
      if b[2] ~= 0 and e[2] ~= 0 then
        local data = vim.fn.getline(b[2], e[2])
        table.insert(data, '')
        job.send(job_id, data)
      else
        nt.notify('no selection text', 'WarningMsg')
      end
    end
  end
end

function M.reg(ft, execute)
  exes[ft] = execute
end

function M.status()
  if status.is_running then
    return 'running ' .. repl_spinners
  elseif status.is_exit then
    return 'exit code:' .. status.exit_code .. '   time:' .. str.trim(vim.fn.reltimestr(end_time))
  end
end

return M