-- Copyright 2007-2013 Mitchell mitchell.att.foicica.com. See LICENSE.

local M = {}

--[[ This comment is for LuaDoc.
---
-- Compile and run/execute source files with Textadept.
-- Typically, [language-specific modules][] populate the `compile_command`,
-- `run_command`, and `error_detail` tables for a particular language's file
-- extension.
--
-- [language-specific modules]: _M.html#Compile.and.Run
-- @field MARK_ERROR_BACK (number)
--   The background color, in "0xBBGGRR" format, used for a line containing a
--   recognized run or compile error.
-- @field cwd (string, Read-only)
--   The working directory for the most recently executed compile or run
--   command.
--   It is used for going to error messages with relative file paths.
-- @field _G.events.COMPILE_OUTPUT (string)
--   Emitted after executing a language's compile command.
--   By default, compiler output is printed to the message buffer. To override
--   this behavior, connect to the event with an index of `1` and return `true`.
--   Arguments:
--
--   * `lexer`: The lexer language name.
--   * `output`: The string output from the command.
-- @field _G.events.RUN_OUTPUT (string)
--   Emitted after executing a language's run command.
--   By default, output is printed to the message buffer. To override this
--   behavior, connect to the event with an index of `1` and return `true`.
--   Arguments:
--
--   * `lexer`: The lexer language name.
--   * `output`: The string output from the command.
module('_M.textadept.run')]]

M.MARK_ERROR_BACK = not CURSES and 0x8080CC or 0x0000FF

-- Events.
events.COMPILE_OUTPUT, events.RUN_OUTPUT = 'compile_output', 'run_output'

local preferred_view

-- Executes a compile or run command.
-- Emits a `COMPILE_OUTPUT` or `RUN_OUTPUT` event based on the `compiling` flag.
-- @param cmd_table Either `compile_command` or `run_command`.
-- @param compiling Flag indicating whether or not the command is a compiler
--   command. The default value is `false`.
-- @see _G.events
local function command(cmd_table, compiling)
  if not buffer.filename then return end
  buffer:annotation_clear_all()
  buffer:save()
  local command = cmd_table[buffer.filename:match('[^.]+$')]
  if not command then return end
  if type(command) == 'function' then command = command() end

  preferred_view = view
  local filepath = buffer.filename:iconv(_CHARSET, 'UTF-8')
  local filedir, filename = '', filepath
  if filepath:find('[/\\]') then
    filedir, filename = filepath:match('^(.+[/\\])([^/\\]+)$')
  end
  local filename_noext = filename:match('^(.+)%.')
  command = command:gsub('%%%b()', {
    ['%(filepath)'] = filepath, ['%(filedir)'] = filedir,
    ['%(filename)'] = filename, ['%(filename_noext)'] = filename_noext,
  })
  local current_dir = lfs.currentdir()
  lfs.chdir(filedir)
  local event = compiling and events.COMPILE_OUTPUT or events.RUN_OUTPUT
  local events_emit = events.emit
  local lexer = buffer:get_lexer()
  events_emit(event, lexer, '> '..command:iconv('UTF-8', _CHARSET))
  local p = io.popen(command..' 2>&1')
  for line in p:lines() do
    events_emit(event, lexer, line:iconv('UTF-8', _CHARSET))
  end
  local ok, status, code = p:close()
  if ok and code then events_emit(event, lexer, status..': '..code) end
  M.cwd = filedir
  lfs.chdir(current_dir)
end

-- Parses the given message for an error description and returns a table of the
-- error's details.
-- @param message The message to parse for errors.
-- @see error_detail
local function get_error_details(message)
  for _, error_detail in pairs(M.error_detail) do
    local captures = {message:match(error_detail.pattern)}
    if #captures > 0 then
      local details = {}
      for detail, i in pairs(error_detail) do details[detail] = captures[i] end
      return details
    end
  end
  return nil
end

local MARK_ERROR = _SCINTILLA.next_marker_number()

-- Prints the output from a run or compile command.
-- If the output is a recognized error message, mark it.
-- @param lexer The current lexer.
-- @param output The output to print.
local function print_output(lexer, output)
  gui.print(output)
  if get_error_details(output) then
    -- Current position is one line below the error due to gui.print()'s '\n'.
    buffer:marker_add(buffer.line_count - 2, MARK_ERROR)
  end
end

---
-- Map of file extensions (excluding the leading '.') to their associated
-- "compile" shell command line strings or functions returning such strings.
-- Command line strings may have the following macros:
--
--   + `%(filepath)`: The full path of the current file.
--   + `%(filedir)`: The current file's directory path.
--   + `%(filename)`: The name of the file, including its extension.
--   + `%(filename_noext)`: The name of the file, excluding its extension.
--
-- This table is typically populated by [language-specific modules][].
--
-- [language-specific modules]: _M.html#Compile.and.Run
-- @class table
-- @name compile_command
M.compile_command = {}

---
-- Compiles the file based on its extension using the command from the
-- `compile_command` table.
-- Emits a `COMPILE_OUTPUT` event.
-- @see compile_command
-- @see _G.events
-- @name compile
function M.compile() command(M.compile_command, true) end
events.connect(events.COMPILE_OUTPUT, print_output)

---
-- Map of file extensions (excluding the leading '.') to their associated
-- "run" shell command line strings or functions returning such strings.
-- Command line strings may have the following macros:
--
--   + `%(filepath)`: The full path of the current file.
--   + `%(filedir)`: The current file's directory path.
--   + `%(filename)`: The name of the file, including its extension.
--   + `%(filename_noext)`: The name of the file, excluding its extension.
--
-- This table is typically populated by [language-specific modules][].
--
-- [language-specific modules]: _M.html#Compile.and.Run
-- @class table
-- @name run_command
M.run_command = {}

---
-- Runs/executes the file based on its extension using the command from the
-- `run_command` table.
-- Emits a `RUN_OUTPUT` event.
-- @see run_command
-- @see _G.events
-- @name run
function M.run() command(M.run_command) end
events.connect(events.RUN_OUTPUT, print_output)

---
-- Map of lexer names to their error string details, tables containing the
-- following fields:
--
--   + `pattern`: A Lua pattern that matches the language's error string,
--     capturing the filename the error occurs in, the line number the error
--     occurred on, and optionally the error message.
--   + `filename`: The numeric index of the Lua capture containing the filename
--      the error occurred in.
--   + `line`: The numeric index of the Lua capture containing the line number
--      the error occurred on.
--   + `message`: (Optional) The numeric index of the Lua capture containing the
--     error's message. An annotation will be displayed if a message was
--     captured.
--
-- When an error message is double-clicked, the user is taken to the point of
-- error.
-- This table is usually populated by [language-specific modules][].
--
-- [language-specific modules]: _M.html#Compile.and.Run
-- @class table
-- @name error_detail
M.error_detail = {}

-- Returns whether or not the given buffer is a message buffer.
local function is_msg_buf(buf) return buf._type == _L['[Message Buffer]'] end
---
-- Goes to the source of the recognized compile/run error on line number *line*
-- in the message buffer or the next or previous recognized error depending on
-- boolean *next*.
-- Displays an annotation with the error message, if available.
-- @param line The line number in the message buffer that contains the
--   compile/run error to go to.
-- @param next Optional flag indicating whether to go to the next recognized
--   error or the previous one. Only applicable when *line* is `nil` or `false`.
-- @see error_detail
-- @see cwd
-- @name goto_error
function M.goto_error(line, next)
  local cur_buf, msg_view, msg_buf = _BUFFERS[buffer], nil, nil
  for i = 1, #_VIEWS do
    if is_msg_buf(_VIEWS[i].buffer) then msg_view = i break end
  end
  for i = 1, #_BUFFERS do
    if is_msg_buf(_BUFFERS[i]) then msg_buf = i break end
  end
  if not msg_view and not msg_buf then return end
  if msg_view then gui.goto_view(msg_view) else view:goto_buffer(msg_buf) end

  -- If no line was given, find the next error marker.
  if not line and next ~= nil then
    local f = buffer['marker_'..(next and 'next' or 'previous')]
    line = f(buffer, buffer:line_from_position(buffer.current_pos) +
                     (next and 1 or -1), 2^MARK_ERROR)
    if line == -1 then
      line = f(buffer, next and 0 or buffer.line_count, 2^MARK_ERROR)
      if line == -1 then if CURSES then view:goto_buffer(cur_buf) end return end
    end
  end
  buffer:goto_line(line)

  -- Goto the error and show an annotation.
  local err = get_error_details(buffer:get_line(line))
  if not err then if CURSES then view:goto_buffer(cur_buf) end return end
  _M.textadept.editing.select_line()
  gui.goto_file(M.cwd..err.filename, true, preferred_view, true)
  local line, message = err.line, err.message
  buffer:goto_line(line - 1)
  if message then
    buffer.annotation_text[line - 1] = message
    buffer.annotation_style[line - 1] = 8 -- error
  end
end
events.connect(events.DOUBLE_CLICK, function(pos, line) M.goto_error(line) end)

local CURSES_MARK = _SCINTILLA.constants.SC_MARK_CHARACTER + string.byte(' ')
-- Sets view properties for error markers.
local function set_error_properties()
  if CURSES then buffer:marker_define(MARK_ERROR, CURSES_MARK) end
  buffer.marker_back[MARK_ERROR] = M.MARK_ERROR_BACK
end
if buffer then set_error_properties() end
events.connect(events.VIEW_NEW, set_error_properties)

return M
