-- Copyright 2007-2022 Mitchell. See LICENSE.

local M = {}

--[[ This comment is for LuaDoc.
---
-- Textadept's core event structure and handlers.
--
-- Textadept emits events when you do things like create a new buffer, press a key, click on
-- a menu, etc. You can even emit events yourself using Lua. Each event has a set of event
-- handlers, which are simply Lua functions called in the order they were connected to an
-- event. For example, if you created a module that needs to do something each time Textadept
-- creates a new buffer, connect a Lua function to the [`events.BUFFER_NEW`]() event:
--
--     events.connect(events.BUFFER_NEW, function()
--       -- Do something here.
--     end)
--
-- Events themselves are nothing special. You do not have to declare one before using it. Events
-- are simply strings containing arbitrary event names. When either you or Textadept emits an
-- event, Textadept runs all event handlers connected to the event, passing any given arguments
-- to the event's handler functions. If an event handler explicitly returns a value that is not
-- `nil`, Textadept will not call subsequent handlers. This is useful if you want to stop the
-- propagation of an event like a keypress if your event handler handled it, or if you want to
-- use the event framework to pass values.
--
-- @field APPLEEVENT_ODOC (string)
--   Emitted when macOS tells Textadept to open a file.
--   Arguments:
--
--   * _`uri`_: The UTF-8-encoded URI to open.
-- @field AUTO_C_CHAR_DELETED (string)
--   Emitted after deleting a character while an autocompletion or user list is active.
-- @field AUTO_C_CANCELED (string)
--   Emitted when canceling an autocompletion or user list.
-- @field AUTO_C_COMPLETED (string)
--   Emitted after inserting an item from an autocompletion list into the buffer.
--   Arguments:
--
--   * _`text`_: The selection's text.
--   * _`position`_: The autocompleted word's beginning position.
-- @field AUTO_C_SELECTION (string)
--   Emitted after selecting an item from an autocompletion list, but before inserting that
--   item into the buffer.
--   Automatic insertion can be canceled by calling [`buffer:auto_c_cancel()`]() before returning
--   from the event handler.
--   Arguments:
--
--   * _`text`_: The selection's text.
--   * _`position`_: The autocompleted word's beginning position.
-- @field AUTO_C_SELECTION_CHANGE (string)
--   Emitted as items are highlighted in an autocompletion or user list.
--   Arguments:
--
--   * _`id`_: Either the *id* from [`buffer.user_list_show()`]() or `0` for an autocompletion list.
--   * _`text`_: The current selection's text.
--   * _`position`_: The position the list was displayed at.
-- @field BUFFER_AFTER_SWITCH (string)
--   Emitted right after switching to another buffer.
--   The buffer being switched to is `buffer`.
--   Emitted by [`view.goto_buffer()`]().
-- @field BUFFER_BEFORE_REPLACE_TEXT (string)
--   Emitted before replacing the contents of the current buffer.
--   Note that it is not guaranteed that [`events.BUFFER_AFTER_REPLACE_TEXT`]() will be emitted
--   shortly after this event.
--   The buffer **must not** be modified during this event.
-- @field BUFFER_BEFORE_SWITCH (string)
--   Emitted right before switching to another buffer.
--   The buffer being switched from is `buffer`.
--   Emitted by [`view.goto_buffer()`]().
-- @field BUFFER_AFTER_REPLACE_TEXT (string)
--   Emitted after replacing the contents of the current buffer.
--   Note that it is not guaranteed that [`events.BUFFER_BEFORE_REPLACE_TEXT`]() was emitted
--   previously.
--   The buffer **must not** be modified during this event.
-- @field BUFFER_DELETED (string)
--   Emitted after deleting a buffer.
--   Emitted by [`buffer.delete()`]().
-- @field BUFFER_NEW (string)
--   Emitted after creating a new buffer.
--   The new buffer is `buffer`.
--   Emitted on startup and by [`buffer.new()`]().
-- @field CALL_TIP_CLICK (string)
--   Emitted when clicking on a calltip.
--   Arguments:
--
--   * _`position`_: `1` if the up arrow was clicked, 2 if the down arrow was clicked, and
--     0 otherwise.
-- @field CHAR_ADDED (string)
--   Emitted after the user types a text character into the buffer.
--   Arguments:
--
--   * _`code`_: The text character's character code.
-- @field COMMAND_TEXT_CHANGED (string)
--   Emitted when the text in the command entry changes.
--   `ui.command_entry:get_text()` returns the current text.
-- @field DOUBLE_CLICK (string)
--   Emitted after double-clicking the mouse button.
--   Arguments:
--
--   * _`position`_: The position double-clicked.
--   * _`line`_: The line number of the position double-clicked.
--   * _`modifiers`_: A bit-mask of any modifier keys held down: `view.MOD_CTRL`,
--     `view.MOD_SHIFT`, `view.MOD_ALT`, and `view.MOD_META`. On macOS, the Command modifier
--     key is reported as `view.MOD_CTRL` and Ctrl is `view.MOD_META`. Note: If you set
--     `view.rectangular_selection_modifier` to `view.MOD_CTRL`, the "Control" modifier is
--     reported as *both* "Control" and "Alt" due to a Scintilla limitation with GTK.
-- @field CSI (string)
--   Emitted when the terminal version receives an unrecognized CSI sequence.
--   Arguments:
--
--   * _`cmd`_: The 24-bit CSI command value. The lowest byte contains the command byte. The
--     second lowest byte contains the leading byte, if any (e.g. '?'). The third lowest byte
--     contains the intermediate byte, if any (e.g. '$').
--   * _`args`_: Table of numeric arguments of the CSI sequence.
-- @field DWELL_END (string)
--   Emitted after `DWELL_START` when the user moves the mouse, presses a key, or scrolls the view.
--   Arguments:
--
--   * _`position`_: The position closest to *x* and *y*.
--   * _`x`_: The x-coordinate of the mouse in the view.
--   * _`y`_: The y-coordinate of the mouse in the view.
-- @field DWELL_START (string)
--   Emitted when the mouse is stationary for [`view.mouse_dwell_time`]() milliseconds.
--   Arguments:
--
--   * _`position`_: The position closest to *x* and *y*.
--   * _`x`_: The x-coordinate of the mouse in the view.
--   * _`y`_: The y-coordinate of the mouse in the view.
-- @field ERROR (string)
--   Emitted when an error occurs.
--   Arguments:
--
--   * _`text`_: The error message text.
-- @field FIND (string)
--   Emitted to find text via the Find & Replace Pane.
--   Arguments:
--
--   * _`text`_: The text to search for.
--   * _`next`_: Whether or not to search forward.
-- @field FIND_TEXT_CHANGED (string)
--   Emitted when the text in the "Find" field of the Find & Replace Pane changes.
--   `ui.find.find_entry_text` contains the current text.
-- @field FOCUS (string)
--   Emitted when Textadept receives focus.
--   This event is never emitted when Textadept is running in the terminal.
-- @field INDICATOR_CLICK (string)
--   Emitted when clicking the mouse on text that has an indicator present.
--   Arguments:
--
--   * _`position`_: The clicked text's position.
--   * _`modifiers`_: A bit-mask of any modifier keys held down: `view.MOD_CTRL`,
--     `view.MOD_SHIFT`, `view.MOD_ALT`, and `view.MOD_META`. On macOS, the Command modifier
--     key is reported as `view.MOD_CTRL` and Ctrl is `view.MOD_META`. Note: If you set
--     `view.rectangular_selection_modifier` to `view.MOD_CTRL`, the "Control" modifier is
--     reported as *both* "Control" and "Alt" due to a Scintilla limitation with GTK.
-- @field INDICATOR_RELEASE (string)
--   Emitted when releasing the mouse after clicking on text that has an indicator present.
--   Arguments:
--
--   * _`position`_: The clicked text's position.
--   * _`modifiers`_: A bit-mask of any modifier keys held down: `view.MOD_CTRL`,
--     `view.MOD_SHIFT`, `view.MOD_ALT`, and `view.MOD_META`. On macOS, the Command modifier
--     key is reported as `view.MOD_CTRL` and Ctrl is `view.MOD_META`. Note: If you set
--     `view.rectangular_selection_modifier` to `view.MOD_CTRL`, the "Control" modifier is
--     reported as *both* "Control" and "Alt" due to a Scintilla limitation with GTK.
-- @field INITIALIZED (string)
--   Emitted after Textadept finishes initializing.
-- @field KEYPRESS (string)
--   Emitted when pressing a key.
--   If any handler returns `true`, the key is not inserted into the buffer.
--   Arguments:
--
--   * _`code`_: The numeric key code.
--   * _`shift`_: The "Shift" modifier key is held down.
--   * _`ctrl`_: The "Control" modifier key is held down.
--   * _`alt`_: The "Alt"/"Option" modifier key is held down.
--   * _`cmd`_: The "Command" modifier key on macOS is held down.
--   * _`caps_lock`_: The "Caps Lock" modifier is on.
-- @field MARGIN_CLICK (string)
--   Emitted when clicking the mouse inside a sensitive margin.
--   Arguments:
--
--   * _`margin`_: The margin number clicked.
--   * _`position`_: The beginning position of the clicked margin's line.
--   * _`modifiers`_: A bit-mask of any modifier keys held down: `view.MOD_CTRL`,
--     `view.MOD_SHIFT`, `view.MOD_ALT`, and `view.MOD_META`. On macOS, the Command modifier
--     key is reported as `view.MOD_CTRL` and Ctrl is `view.MOD_META`. Note: If you set
--     `view.rectangular_selection_modifier` to `view.MOD_CTRL`, the "Control" modifier is
--     reported as *both* "Control" and "Alt" due to a Scintilla limitation with GTK.
-- @field MENU_CLICKED (string)
--   Emitted after selecting a menu item.
--   Arguments:
--
--   * _`menu_id`_: The numeric ID of the menu item, which was defined in [`ui.menu()`]().
-- @field MOUSE (string)
--   Emitted by the terminal version for an unhandled mouse event.
--   A handler should return `true` if it handled the event. Otherwise Textadept will try again.
--   (This side effect for a `false` or `nil` return is useful for sending the original mouse
--   event to a different view that a handler has switched to.)
--   Arguments:
--
--   * _`event`_: The mouse event: `view.MOUSE_PRESS`, `view.MOUSE_DRAG`, or `view.MOUSE_RELEASE`.
--   * _`button`_: The mouse button number.
--   * _`y`_: The y-coordinate of the mouse event, starting from 1.
--   * _`x`_: The x-coordinate of the mouse event, starting from 1.
--   * _`shift`_: The "Shift" modifier key is held down.
--   * _`ctrl`_: The "Control" modifier key is held down.
--   * _`alt`_: The "Alt"/"Option" modifier key is held down.
-- @field QUIT (string)
--   Emitted when quitting Textadept.
--   When connecting to this event, connect with an index of 1 if the handler needs to run
--   before Textadept closes all open buffers. If a handler returns `true`, Textadept does not
--   quit. It is not recommended to return `false` from a quit handler, as that may interfere
--   with Textadept's normal shutdown procedure.
--   Emitted by [`quit()`]().
-- @field REPLACE (string)
--   Emitted to replace selected (found) text.
--   Arguments:
--
--   * _`text`_: The replacement text.
-- @field REPLACE_ALL (string)
--   Emitted to replace all occurrences of found text.
--   Arguments:
--
--   * _`find_text`_: The text to search for.
--   * _`repl_text`_: The replacement text.
-- @field RESET_AFTER (string)
--   Emitted after resetting Textadept's Lua state.
--   Emitted by [`reset()`]().
--   Arguments:
--
--   * _`persist`_: Table of data persisted by `events.RESET_BEFORE`. All handlers will have
--     access to this same table.
-- @field RESET_BEFORE (string)
--   Emitted before resetting Textadept's Lua state.
--   Emitted by [`reset()`]().
--   Arguments:
--
--   * _`persist`_: Table to store persistent data in for use by `events.RESET_AFTER`. All
--     handlers will have access to this same table.
-- @field RESUME (string)
--   Emitted when resuming Textadept from a suspended state.
--   This event is only emitted by the terminal version.
-- @field SAVE_POINT_LEFT (string)
--   Emitted after leaving a save point.
-- @field SAVE_POINT_REACHED (string)
--   Emitted after reaching a save point.
-- @field SUSPEND (string)
--   Emitted when suspending Textadept. If any handler returns `true`, Textadept does not suspend.
--   This event is only emitted by the terminal version.
-- @field TAB_CLICKED (string)
--   Emitted when the user clicks on a buffer tab.
--   When connecting to this event, connect with an index of 1 if the handler needs to run
--   before Textadept switches between buffers.
--   Note that Textadept always displays a context menu on right-click.
--   Arguments:
--
--   * _`index`_: The numeric index of the clicked tab.
--   * _`button`_: The mouse button number that was clicked, either `1` (left button), `2`
--     (middle button), `3` (right button), `4` (wheel up), or `5` (wheel down).
--   * _`shift`_: The "Shift" modifier key is held down.
--   * _`ctrl`_: The "Control" modifier key is held down.
--   * _`alt`_: The "Alt"/"Option" modifier key is held down.
--   * _`cmd`_: The "Command" modifier key on macOS is held down.
-- @field UNFOCUS (string)
--   Emitted when Textadept loses focus.
--   This event is never emitted when Textadept is running in the terminal.
-- @field UPDATE_UI (string)
--   Emitted after the view is visually updated.
--   Arguments:
--
--   * _`updated`_: A bitmask of changes since the last update.
--
--     + `buffer.UPDATE_CONTENT`
--       Buffer contents, styling, or markers have changed.
--     + `buffer.UPDATE_SELECTION`
--       Buffer selection has changed (including caret movement).
--     + `view.UPDATE_V_SCROLL`
--       Buffer has scrolled vertically.
--     + `view.UPDATE_H_SCROLL`
--       Buffer has scrolled horizontally.
-- @field URI_DROPPED (string)
--   Emitted after dragging and dropping a URI into a view.
--   Arguments:
--
--   * _`text`_: The UTF-8-encoded URI dropped.
-- @field USER_LIST_SELECTION (string)
--   Emitted after selecting an item in a user list.
--   Arguments:
--
--   * _`id`_: The *id* from [`buffer.user_list_show()`]().
--   * _`text`_: The selection's text.
--   * _`position`_: The position the list was displayed at.
-- @field VIEW_NEW (string)
--   Emitted after creating a new view.
--   The new view is `view`.
--   Emitted on startup and by [`view.split()`]().
-- @field VIEW_BEFORE_SWITCH (string)
--   Emitted right before switching to another view.
--   The view being switched from is `view`.
--   Emitted by [`ui.goto_view()`]().
-- @field VIEW_AFTER_SWITCH (string)
--   Emitted right after switching to another view.
--   The view being switched to is `view`.
--   Emitted by [`ui.goto_view()`]().
-- @field ZOOM (string)
--   Emitted after changing [`view.zoom`]().
--   Emitted by [`view.zoom_in()`]() and [`view.zoom_out()`]().
module('events')]]

-- Map of event names to tables of handler functions.
-- Handler tables are auto-created as needed.
-- @class table
-- @name handlers
local handlers = setmetatable({}, {
  __index = function(t, k)
    t[k] = {}
    return t[k]
  end
})

---
-- Adds function *f* to the set of event handlers for event *event* at position *index*.
-- If *index* not given, appends *f* to the set of handlers. *event* may be any arbitrary string
-- and does not need to have been previously defined.
-- @param event The string event name.
-- @param f The Lua function to connect to *event*.
-- @param index Optional index to insert the handler into.
-- @usage events.connect('my_event', function(msg) ui.print(msg) end)
-- @see disconnect
-- @name connect
function M.connect(event, f, index)
  assert_type(event, 'string', 1)
  assert_type(f, 'function', 2)
  assert_type(index, 'number/nil', 3)
  M.disconnect(event, f) -- in case it already exists
  table.insert(handlers[event], index or #handlers[event] + 1, f)
end

---
-- Removes function *f* from the set of handlers for event *event*.
-- @param event The string event name.
-- @param f The Lua function connected to *event*.
-- @see connect
-- @name disconnect
function M.disconnect(event, f)
  assert_type(f, 'function', 2)
  for i = 1, #handlers[assert_type(event, 'string', 1)] do
    if handlers[event][i] == f then
      table.remove(handlers[event], i)
      break
    end
  end
end

local error_emitted = false
---
-- Sequentially calls all handler functions for event *event* with the given arguments.
-- *event* may be any arbitrary string and does not need to have been previously defined. If
-- any handler explicitly returns a value that is not `nil`, `emit()` returns that value and
-- ceases to call subsequent handlers. This is useful for stopping the propagation of an event
-- like a keypress after it has been handled, or for passing back values from handlers.
-- @param event The string event name.
-- @param ... Arguments passed to the handler.
-- @return `nil` unless any any handler explicitly returned a non-`nil` value; otherwise returns
--   that value
-- @usage events.emit('my_event', 'my message')
-- @name emit
function M.emit(event, ...)
  local event_handlers = handlers[assert_type(event, 'string', 1)]
  local i = 1
  while i <= #event_handlers do
    local handler = event_handlers[i]
    local ok, result = pcall(handler, ...)
    if not ok then
      if not error_emitted then
        error_emitted = true
        M.emit(events.ERROR, result)
        error_emitted = false
      else
        io.stderr:write(result) -- prevent infinite loop
      end
    end
    if result ~= nil then return result end
    if event_handlers[i] == handler then i = i + 1 end -- unless M.disconnect()
  end
end

-- Handles Scintilla notifications.
M.connect('SCN', function(notification)
  local iface = _SCINTILLA.events[notification.code]
  local args = {}
  for i = 2, #iface do args[i - 1] = notification[iface[i]] end
  return M.emit(iface[1], table.unpack(args))
end)

-- Set event constants.
for _, v in pairs(_SCINTILLA.events) do M[v[1]:upper()] = v[1] end
-- LuaFormatter off
local textadept_events = {'appleevent_odoc','buffer_after_replace_text','buffer_after_switch','buffer_before_replace_text','buffer_before_switch','buffer_deleted','buffer_new','csi','command_text_changed','error','find','find_text_changed','focus','initialized','keypress','menu_clicked','mouse','quit','replace','replace_all','reset_after','reset_before','resume','suspend', 'tab_clicked','unfocus','view_after_switch','view_before_switch','view_new'}
-- LuaFormatter on
for _, v in pairs(textadept_events) do M[v:upper()] = v end

-- Implement `events.BUFFER_{BEFORE,AFTER}_REPLACE_TEXT` as a convenience in lieu of the
-- undocumented `events.MODIFIED`.
local DELETE, INSERT, UNDOREDO = _SCINTILLA.constants.MOD_BEFOREDELETE,
  _SCINTILLA.constants.MOD_INSERTTEXT, _SCINTILLA.constants.MULTILINEUNDOREDO
-- Helper function for emitting `events.BUFFER_AFTER_REPLACE_TEXT` after a full-buffer undo/redo
-- operation, e.g. after reloading buffer contents and then performing an undo.
local function emit_after_replace_text()
  events.disconnect(events.UPDATE_UI, emit_after_replace_text)
  events.emit(events.BUFFER_AFTER_REPLACE_TEXT)
end
-- Emits events prior to and after replacing buffer text.
M.connect(M.MODIFIED, function(position, mod, text, length)
  if mod & (DELETE | INSERT) == 0 or length ~= buffer.length then return end
  if mod & (INSERT | UNDOREDO) == INSERT | UNDOREDO then
    -- Cannot emit BUFFER_AFTER_REPLACE_TEXT here because Scintilla will do things like update
    -- the selection afterwards, which could undo what event handlers do.
    events.connect(events.UPDATE_UI, emit_after_replace_text)
    return
  end
  M.emit(mod & DELETE > 0 and M.BUFFER_BEFORE_REPLACE_TEXT or M.BUFFER_AFTER_REPLACE_TEXT)
end)

return M
