#include <assert.h>
#include <lauxlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "klib/kvec.h"
#include "nvim/api/autocmd.h"
#include "nvim/api/keysets_defs.h"
#include "nvim/api/private/defs.h"
#include "nvim/api/private/dispatch.h"
#include "nvim/api/private/helpers.h"
#include "nvim/api/private/validate.h"
#include "nvim/autocmd.h"
#include "nvim/autocmd_defs.h"
#include "nvim/buffer.h"
#include "nvim/buffer_defs.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/globals.h"
#include "nvim/lua/executor.h"
#include "nvim/memory.h"
#include "nvim/memory_defs.h"
#include "nvim/strings.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"

#include "api/autocmd.c.generated.h"

#define AUCMD_MAX_PATTERNS 256

// Copy string or array of strings into an empty array.
// Get the event number, unless it is an error. Then do `or_else`.
#define GET_ONE_EVENT(event_nr, event_str, or_else) \
  event_T event_nr = \
    event_name2nr_str(event_str.data.string); \
  VALIDATE_S((event_nr < NUM_EVENTS), "event", event_str.data.string.data, { \
    or_else; \
  });

// ID for associating autocmds created via nvim_create_autocmd
// Used to delete autocmds from nvim_del_autocmd
static int64_t next_autocmd_id = 1;

/// Gets all autocommands matching ALL criteria in the {opts} query.
///
/// Note: When multiple patterns or events are provided they have "OR" semantics (any combination
/// is matched).
///
/// Examples:
///
/// ```lua
/// -- Matches all criteria
/// autocommands = vim.api.nvim_get_autocmds({
///   group = 'MyGroup',
///   event = {'BufEnter', 'BufWinEnter'},
///   pattern = {'*.c', '*.h'}
/// })
///
/// -- All commands from one group
/// autocommands = vim.api.nvim_get_autocmds({
///   group = 'MyGroup',
/// })
/// ```
///
/// @param opts Dict with at least one of these keys:
///        - buf: (`integer[]|integer?`) Buffer id or list of buffer ids, for buffer-local autocommands
///          |autocmd-buflocal|. Not allowed with {pattern}.
///        - event: (`vim.api.keyset.events|vim.api.keyset.events[]?`) Event(s) to match |autocmd-events|.
///        - group: (`string|table?`) Group name or id to match.
///        - id: (`integer?`) Autocommand ID to match.
///        - pattern: (`string|table?`) Pattern(s) to match |autocmd-pattern|. Not allowed with {buf}.
/// @return Array of matching autocommands, where each item has:
///         - buf (`integer?`): Buffer id (only for |autocmd-buffer-local|).
///         - buflocal (`boolean?`): true if the autocommand is buffer-local |autocmd-buffer-local|.
///         - callback: (`function|string?`): Event handler: a Lua function or Vimscript function name.
///         - command: (`string`) Event handler: an Ex-command. Empty if a `callback` is set.
///         - desc: (`string`) Description.
///         - event: (`vim.api.keyset.events`) Event name(s).
///         - group: (`integer`) Group id.
///         - group_name: (`string`) Group name.
///         - id: (`integer`) Autocommand id (only when defined with the API).
///         - once: (`boolean`) true if |autocmd-once| was set.
///         - pattern: (`string`) Autocommand pattern.
ArrayOf(DictAs(get_autocmds__ret)) nvim_get_autocmds(Dict(get_autocmds) *opts, Arena *arena,
                                                     Error *err)
  FUNC_API_SINCE(9)
{
  ArrayBuilder autocmd_list = KV_INITIAL_VALUE;
  kvi_init(autocmd_list);
  char *pattern_filters[AUCMD_MAX_PATTERNS];

  Array buffers = ARRAY_DICT_INIT;

  bool event_set[NUM_EVENTS] = { false };
  bool check_event = false;

  int group = 0;

  switch (opts->group.type) {
  case kObjectTypeNil:
    break;
  case kObjectTypeString:
    group = augroup_find(opts->group.data.string.data);
    VALIDATE_S((group >= 0), "group", opts->group.data.string.data, {
      goto cleanup;
    });
    break;
  case kObjectTypeInteger:
    group = (int)opts->group.data.integer;
    char *name = group == 0 ? NULL : augroup_name(group);
    VALIDATE_INT(augroup_exists(name), "group", opts->group.data.integer, {
      goto cleanup;
    });
    break;
  default:
    VALIDATE_EXP(false, "group", "String or Integer", api_typename(opts->group.type), {
      goto cleanup;
    });
  }

  int id = (HAS_KEY(opts, get_autocmds, id)) ? (int)opts->id : -1;

  if (HAS_KEY(opts, get_autocmds, event)) {
    check_event = true;

    Object v = opts->event;
    if (v.type == kObjectTypeString) {
      GET_ONE_EVENT(event_nr, v, goto cleanup);
      event_set[event_nr] = true;
    } else if (v.type == kObjectTypeArray) {
      FOREACH_ITEM(v.data.array, event_v, {
        VALIDATE_T("event item", kObjectTypeString, event_v.type, {
          goto cleanup;
        });

        GET_ONE_EVENT(event_nr, event_v, goto cleanup);
        event_set[event_nr] = true;
      })
    } else {
      VALIDATE_EXP(false, "event", "String or Array", NULL, {
        goto cleanup;
      });
    }
  }

  bool has_buf = HAS_KEY(opts, get_autocmds, buf) || HAS_KEY(opts, get_autocmds, buffer);
  Object buf = HAS_KEY(opts, get_autocmds, buf) ? opts->buf : opts->buffer;

  VALIDATE_CON((!HAS_KEY(opts, get_autocmds, buf) || !HAS_KEY(opts, get_autocmds, buffer)),
               "buf", "buffer", {
    goto cleanup;
  });

  VALIDATE_CON((!HAS_KEY(opts, get_autocmds, pattern) || !has_buf),
               "pattern", "buf", {
    goto cleanup;
  });

  int pattern_filter_count = 0;
  if (HAS_KEY(opts, get_autocmds, pattern)) {
    Object v = opts->pattern;
    if (v.type == kObjectTypeString) {
      pattern_filters[pattern_filter_count] = v.data.string.data;
      pattern_filter_count += 1;
    } else if (v.type == kObjectTypeArray) {
      VALIDATE((v.data.array.size <= AUCMD_MAX_PATTERNS),
               "Too many patterns (maximum of %d)", AUCMD_MAX_PATTERNS, {
        goto cleanup;
      });

      FOREACH_ITEM(v.data.array, item, {
        VALIDATE_T("pattern", kObjectTypeString, item.type, {
          goto cleanup;
        });

        pattern_filters[pattern_filter_count] = item.data.string.data;
        pattern_filter_count += 1;
      });
    } else {
      VALIDATE_EXP(false, "pattern", "String or Array", api_typename(v.type), {
        goto cleanup;
      });
    }
  }

  if (buf.type == kObjectTypeInteger || buf.type == kObjectTypeBuffer) {
    buf_T *b = find_buffer_by_handle((Buffer)buf.data.integer, err);
    if (ERROR_SET(err)) {
      goto cleanup;
    }

    String pat = arena_printf(arena, "<buffer=%d>", (int)b->handle);
    buffers = arena_array(arena, 1);
    ADD_C(buffers, STRING_OBJ(pat));
  } else if (buf.type == kObjectTypeArray) {
    VALIDATE((buf.data.array.size <= AUCMD_MAX_PATTERNS),
             "Too many buffers (maximum of %d)", AUCMD_MAX_PATTERNS, {
      goto cleanup;
    });

    buffers = arena_array(arena, kv_size(buf.data.array));
    FOREACH_ITEM(buf.data.array, bufnr, {
      VALIDATE_EXP((bufnr.type == kObjectTypeInteger || bufnr.type == kObjectTypeBuffer),
                   "buffer", "Integer", api_typename(bufnr.type), {
        goto cleanup;
      });

      buf_T *b = find_buffer_by_handle((Buffer)bufnr.data.integer, err);
      if (ERROR_SET(err)) {
        goto cleanup;
      }

      ADD_C(buffers, STRING_OBJ(arena_printf(arena, "<buffer=%d>", (int)b->handle)));
    });
  } else if (has_buf) {
    VALIDATE_EXP(false, "buffer", "Integer or Array", api_typename(buf.type), {
      goto cleanup;
    });
  }

  FOREACH_ITEM(buffers, bufnr, {
    pattern_filters[pattern_filter_count] = bufnr.data.string.data;
    pattern_filter_count += 1;
  });

  FOR_ALL_AUEVENTS(event) {
    if (check_event && !event_set[event]) {
      continue;
    }

    AutoCmdVec *acs = au_get_autocmds_for_event(event);
    for (size_t i = 0; i < kv_size(*acs); i++) {
      AutoCmd *const ac = &kv_A(*acs, i);
      AutoPat *const ap = ac->pat;

      if (ap == NULL) {
        continue;
      }

      if (id != -1 && ac->id != id) {
        continue;
      }

      // Skip autocmds from invalid groups if passed.
      if (group != 0 && ap->group != group) {
        continue;
      }

      // Skip 'pattern' from invalid patterns if passed.
      if (pattern_filter_count > 0) {
        bool passed = false;
        for (int j = 0; j < pattern_filter_count; j++) {
          assert(j < AUCMD_MAX_PATTERNS);
          assert(pattern_filters[j]);

          char *pat = pattern_filters[j];
          int patlen = (int)strlen(pat);

          char pattern_buflocal[BUFLOCAL_PAT_LEN];
          if (aupat_is_buflocal(pat, patlen)) {
            aupat_normalize_buflocal_pat(pattern_buflocal, pat, patlen,
                                         aupat_get_buflocal_nr(pat, patlen));
            pat = pattern_buflocal;
          }

          if (strequal(ap->pat, pat)) {
            passed = true;
            break;
          }
        }

        if (!passed) {
          continue;
        }
      }

      Dict autocmd_info = arena_dict(arena, 12);

      if (ap->group != AUGROUP_DEFAULT) {
        PUT_C(autocmd_info, "group", INTEGER_OBJ(ap->group));
        PUT_C(autocmd_info, "group_name", CSTR_AS_OBJ(augroup_name(ap->group)));
      }

      if (ac->id > 0) {
        PUT_C(autocmd_info, "id", INTEGER_OBJ(ac->id));
      }

      if (ac->desc != NULL) {
        PUT_C(autocmd_info, "desc", CSTR_AS_OBJ(ac->desc));
      }

      if (ac->handler_cmd) {
        PUT_C(autocmd_info, "command", CSTR_AS_OBJ(ac->handler_cmd));
      } else {
        PUT_C(autocmd_info, "command", STRING_OBJ(STRING_INIT));

        Callback *cb = &ac->handler_fn;
        switch (cb->type) {
        case kCallbackLua:
          if (nlua_ref_is_function(cb->data.luaref)) {
            PUT_C(autocmd_info, "callback", LUAREF_OBJ(api_new_luaref(cb->data.luaref)));
          }
          break;
        case kCallbackFuncref:
        case kCallbackPartial:
          PUT_C(autocmd_info, "callback", CSTR_AS_OBJ(callback_to_string(cb, arena)));
          break;
        case kCallbackNone:
          abort();
        }
      }

      PUT_C(autocmd_info, "pattern", CSTR_AS_OBJ(ap->pat));
      PUT_C(autocmd_info, "event", CSTR_AS_OBJ(event_nr2name(event)));
      PUT_C(autocmd_info, "once", BOOLEAN_OBJ(ac->once));

      if (ap->buflocal_nr) {
        PUT_C(autocmd_info, "buflocal", BOOLEAN_OBJ(true));
        PUT_C(autocmd_info, "buf", INTEGER_OBJ(ap->buflocal_nr));
        PUT_C(autocmd_info, "buffer", INTEGER_OBJ(ap->buflocal_nr));
      } else {
        PUT_C(autocmd_info, "buflocal", BOOLEAN_OBJ(false));
      }

      kvi_push(autocmd_list, DICT_OBJ(autocmd_info));
    }
  }

cleanup:
  return arena_take_arraybuilder(arena, &autocmd_list);
}

/// Creates an |autocommand| event handler, defined by `callback` (Lua function or Vimscript
/// function _name_ string) or `command` (Ex command string).
///
/// Example using Lua callback:
///
/// ```lua
/// vim.api.nvim_create_autocmd({'BufEnter', 'BufWinEnter'}, {
///   pattern = {'*.c', '*.h'},
///   callback = function(ev)
///     print(string.format('event fired: %s', vim.inspect(ev)))
///   end
/// })
/// ```
///
/// Example using an Ex command as the handler:
///
/// ```lua
/// vim.api.nvim_create_autocmd({'BufEnter', 'BufWinEnter'}, {
///   pattern = {'*.c', '*.h'},
///   command = "echo 'Entering a C or C++ file'",
/// })
/// ```
///
/// Note: `pattern` is NOT automatically expanded (unlike with |:autocmd|), thus names like "$HOME"
/// and "~" must be expanded explicitly:
///
/// ```lua
/// pattern = vim.fn.expand('~') .. '/some/path/*.py'
/// ```
///
/// @param event Event(s) that will trigger the handler (`callback` or `command`).
/// @param opts Options dict:
///        - buf (`integer?`) Buffer id for buffer-local autocommands |autocmd-buflocal|.
///          Not allowed with {pattern}.
///        - callback (`function|string?`) Lua function (or Vimscript function name, if string)
///          called when the event(s) is triggered. Lua callback can return |lua-truthy| to delete
///          the autocommand. Callback receives one argument, a table with keys: [event-args]()
///            - id: (`number`) Autocommand id
///            - event: (`vim.api.keyset.events`) Name of the triggered event |autocmd-events|
///            - group: (`number?`) Group id, if any
///            - file: (`string`) [<afile>] (not expanded to a full path)
///            - match: (`string`) [<amatch>] (expanded to a full path)
///            - buf: (`number`) [<abuf>]
///            - data: (`any`) Arbitrary data passed from [nvim_exec_autocmds()] [event-data]()
///        - command (string?) Vim command executed on event. Not allowed with {callback}.
///        - desc (`string?`) Description (for documentation and troubleshooting).
///        - group (`string|integer?`) Group name or id to match against.
///        - nested (`boolean?`, default: false) Run nested autocommands |autocmd-nested|.
///        - once (`boolean?`, default: false) Handle the event only once |autocmd-once|.
///        - pattern (`string|array?`) Pattern(s) to match literally |autocmd-pattern|.
///
/// @return Autocommand id (number)
/// @see |autocommand|
/// @see |nvim_del_autocmd()|
Integer nvim_create_autocmd(uint64_t channel_id, Object event, Dict(create_autocmd) *opts,
                            Arena *arena, Error *err)
  FUNC_API_SINCE(9)
{
  int64_t autocmd_id = -1;
  char *desc = NULL;
  char *handler_cmd = NULL;
  Callback handler_fn = CALLBACK_NONE;

  Array event_array = unpack_string_or_array(event, "event", true, arena, err);
  if (ERROR_SET(err)) {
    goto cleanup;
  }

  VALIDATE_CON((!HAS_KEY(opts, create_autocmd, callback) || !HAS_KEY(opts, create_autocmd,
                                                                     command)),
               "callback", "command", {
    goto cleanup;
  });

  if (HAS_KEY(opts, create_autocmd, callback)) {
    // NOTE: We could accept callable tables, but that isn't common in the API.

    Object *callback = &opts->callback;
    switch (callback->type) {
    case kObjectTypeLuaRef:
      VALIDATE_S((callback->data.luaref != LUA_NOREF), "callback", "<no value>", {
        goto cleanup;
      });
      VALIDATE_S(nlua_ref_is_function(callback->data.luaref), "callback", "<not a function>", {
        goto cleanup;
      });

      handler_fn.type = kCallbackLua;
      handler_fn.data.luaref = callback->data.luaref;
      callback->data.luaref = LUA_NOREF;
      break;
    case kObjectTypeString:
      handler_fn.type = kCallbackFuncref;
      handler_fn.data.funcref = string_to_cstr(callback->data.string);
      break;
    default:
      VALIDATE_EXP(false, "callback", "Lua function or Vim function name",
                   api_typename(callback->type), {
        goto cleanup;
      });
    }
  } else if (HAS_KEY(opts, create_autocmd, command)) {
    handler_cmd = string_to_cstr(opts->command);
  } else {
    VALIDATE_R(false, "'command' or 'callback'", {
      goto cleanup;
    });
  }

  int au_group = get_augroup_from_object(opts->group, err);
  if (au_group == AUGROUP_ERROR) {
    goto cleanup;
  }

  bool has_buf = HAS_KEY(opts, create_autocmd, buf) || HAS_KEY(opts, create_autocmd, buffer);
  Buffer buf = HAS_KEY(opts, create_autocmd, buf) ? opts->buf : opts->buffer;

  VALIDATE_CON((!HAS_KEY(opts, create_autocmd, buf) || !HAS_KEY(opts, create_autocmd, buffer)),
               "buf", "buffer", {
    goto cleanup;
  });

  VALIDATE_CON((!HAS_KEY(opts, create_autocmd, pattern) || !has_buf), "pattern", "buf", {
    goto cleanup;
  });

  Array patterns = get_patterns_from_pattern_or_buf(opts->pattern, has_buf, buf, "*",
                                                    arena, err);
  if (ERROR_SET(err)) {
    goto cleanup;
  }

  if (HAS_KEY(opts, create_autocmd, desc)) {
    desc = opts->desc.data;
  }

  VALIDATE_R((event_array.size > 0), "event", {
    goto cleanup;
  });

  autocmd_id = next_autocmd_id++;
  FOREACH_ITEM(event_array, event_str, {
    GET_ONE_EVENT(event_nr, event_str, goto cleanup);

    int retval;

    FOREACH_ITEM(patterns, pat, {
      WITH_SCRIPT_CONTEXT(channel_id, {
        retval = autocmd_register(autocmd_id,
                                  event_nr,
                                  pat.data.string.data,
                                  (int)pat.data.string.size,
                                  au_group,
                                  opts->once,
                                  opts->nested,
                                  desc,
                                  handler_cmd,
                                  &handler_fn);
      });

      if (retval == FAIL) {
        api_set_error(err, kErrorTypeException, "Failed to set autocmd");
        goto cleanup;
      }
    })
  });

cleanup:
  if (handler_cmd) {
    XFREE_CLEAR(handler_cmd);
  } else {
    callback_free(&handler_fn);
  }

  return autocmd_id;
}

/// Deletes an autocommand by id.
///
/// @param id Autocommand id returned by |nvim_create_autocmd()|
void nvim_del_autocmd(Integer id, Error *err)
  FUNC_API_SINCE(9)
{
  VALIDATE_INT((id > 0), "autocmd id", id, {
    return;
  });
  if (!autocmd_delete_id(id)) {
    api_set_error(err, kErrorTypeException, "Failed to delete autocmd");
  }
}

/// Clears all autocommands matching the {opts} query. To delete autocmds see |nvim_del_autocmd()|.
///
/// @param opts Optional parameters:
///        - event: (`vim.api.keyset.events|vim.api.keyset.events[]?`)
///          Examples:
///          - event: "pat1"
///          - event: { "pat1" }
///          - event: { "pat1", "pat2", "pat3" }
///        - pattern: (`string|table?`) Filter by patterns (exact match). Not allowed with {buf}.
///          - Example: if you have `*.py` as that pattern for the autocmd, you must pass `*.py`
///            exactly to clear it. `test.py` will not match the pattern.
///        - buf: (`integer?`) Select |autocmd-buflocal| autocommands. Not allowed with {pattern}.
///        - group: (`string|int?`) Group name or id.
///          - NOTE: If not given, matches autocmds *not* in any group.
///
void nvim_clear_autocmds(Dict(clear_autocmds) *opts, Arena *arena, Error *err)
  FUNC_API_SINCE(9)
{
  // TODO(tjdevries): Future improvements:
  //        - once: (boolean) - Only clear autocmds with once. See |autocmd-once|
  //        - nested: (boolean) - Only clear autocmds with nested. See |autocmd-nested|
  //        - group: Allow passing "*" or true or something like that to force doing all
  //        autocmds, regardless of their group.

  Array event_array = unpack_string_or_array(opts->event, "event", false, arena, err);
  if (ERROR_SET(err)) {
    return;
  }

  bool has_buf = HAS_KEY(opts, clear_autocmds, buf) || HAS_KEY(opts, clear_autocmds, buffer);
  int buf = HAS_KEY(opts, clear_autocmds, buf) ? opts->buf : opts->buffer;

  VALIDATE_CON((!HAS_KEY(opts, clear_autocmds, buf) || !HAS_KEY(opts, clear_autocmds, buffer)),
               "buf", "buffer", {
    return;
  });

  VALIDATE_CON((!HAS_KEY(opts, clear_autocmds, pattern) || !has_buf), "pattern", "buf", {
    return;
  });

  int au_group = get_augroup_from_object(opts->group, err);
  if (au_group == AUGROUP_ERROR) {
    return;
  }

  // When we create the autocmds, we want to say that they are all matched, so that's *
  // but when we clear them, we want to say that we didn't pass a pattern, so that's NUL
  Array patterns = get_patterns_from_pattern_or_buf(opts->pattern, has_buf, buf, "",
                                                    arena, err);
  if (ERROR_SET(err)) {
    return;
  }

  // If we didn't pass any events, that means clear all events.
  if (event_array.size == 0) {
    FOR_ALL_AUEVENTS(event) {
      FOREACH_ITEM(patterns, pat_object, {
        char *pat = pat_object.data.string.data;
        if (!clear_autocmd(event, pat, au_group, err)) {
          return;
        }
      });
    }
  } else {
    FOREACH_ITEM(event_array, event_str, {
      GET_ONE_EVENT(event_nr, event_str, return );

      FOREACH_ITEM(patterns, pat_object, {
        char *pat = pat_object.data.string.data;
        if (!clear_autocmd(event_nr, pat, au_group, err)) {
          return;
        }
      });
    });
  }
}

/// Create or get an autocommand group |autocmd-groups|.
///
/// To get an existing group id, do:
///
/// ```lua
/// local id = vim.api.nvim_create_augroup('my.lsp.config', {
///   clear = false
/// })
/// ```
///
/// @param name Group name
/// @param opts Optional parameters:
///        - clear (`boolean?`, default: true) Clear existing commands in the group |autocmd-groups|.
/// @return Group id.
/// @see |autocmd-groups|
Integer nvim_create_augroup(uint64_t channel_id, String name, Dict(create_augroup) *opts,
                            Error *err)
  FUNC_API_SINCE(9)
{
  char *augroup_name = name.data;
  bool clear_autocmds = GET_BOOL_OR_TRUE(opts, create_augroup, clear);

  int augroup = -1;
  WITH_SCRIPT_CONTEXT(channel_id, {
    augroup = augroup_add(augroup_name);
    if (augroup == AUGROUP_ERROR) {
      api_set_error(err, kErrorTypeException, "Failed to set augroup");
      return -1;
    }

    if (clear_autocmds) {
      FOR_ALL_AUEVENTS(event) {
        aucmd_del_for_event_and_group(event, augroup);
      }
    }
  });

  return augroup;
}

/// Delete an autocommand group by id.
///
/// To get a group id one can use |nvim_get_autocmds()|.
///
/// NOTE: behavior differs from |:augroup-delete|. When deleting a group, autocommands contained in
/// this group will also be deleted and cleared. This group will no longer exist.
/// @param id Group id.
/// @see |nvim_del_augroup_by_name()|
/// @see |nvim_create_augroup()|
void nvim_del_augroup_by_id(Integer id, Error *err)
  FUNC_API_SINCE(9)
{
  TRY_WRAP(err, {
    char *name = id == 0 ? NULL : augroup_name((int)id);
    augroup_del(name, false);
  });
}

/// Delete an autocommand group by name.
///
/// NOTE: behavior differs from |:augroup-delete|. When deleting a group, autocommands contained in
/// this group will also be deleted and cleared. This group will no longer exist.
/// @param name Group name.
/// @see |autocmd-groups|
void nvim_del_augroup_by_name(String name, Error *err)
  FUNC_API_SINCE(9)
{
  TRY_WRAP(err, {
    augroup_del(name.data, false);
  });
}

/// Executes handlers for {event} that match the corresponding {opts} query. |autocmd-execute|
/// @param event Event(s) to execute.
/// @param opts Optional filters:
///        - group (`string|integer?`) Group name or id to match against. |autocmd-groups|.
///        - pattern (`string|array?`, default: current file name) |autocmd-pattern|. Not allowed with {buf}.
///        - buf (`integer?`) Buffer id |autocmd-buflocal|. Not allowed with {pattern}.
///        - modeline (`boolean?`, default: true) Process the modeline after the autocommands
///          [<nomodeline>].
///        - data (`any`): Arbitrary data passed to the callback. See |nvim_create_autocmd()|.
/// @see |:doautocmd|
void nvim_exec_autocmds(Object event, Dict(exec_autocmds) *opts, Arena *arena, Error *err)
  FUNC_API_SINCE(9)
{
  int au_group = AUGROUP_ALL;
  bool modeline = true;

  buf_T *b = curbuf;

  Object *data = NULL;

  Array event_array = unpack_string_or_array(event, "event", true, arena, err);
  if (ERROR_SET(err)) {
    return;
  }

  switch (opts->group.type) {
  case kObjectTypeNil:
    break;
  case kObjectTypeString:
    au_group = augroup_find(opts->group.data.string.data);
    VALIDATE_S((au_group != AUGROUP_ERROR), "group", opts->group.data.string.data, {
      return;
    });
    break;
  case kObjectTypeInteger:
    au_group = (int)opts->group.data.integer;
    char *name = au_group == 0 ? NULL : augroup_name(au_group);
    VALIDATE_INT(augroup_exists(name), "group", (int64_t)au_group, {
      return;
    });
    break;
  default:
    VALIDATE_EXP(false, "group", "String or Integer", api_typename(opts->group.type), {
      return;
    });
  }

  bool has_buf = HAS_KEY(opts, exec_autocmds, buf) || HAS_KEY(opts, exec_autocmds, buffer);
  Buffer buf = HAS_KEY(opts, exec_autocmds, buf) ? opts->buf : opts->buffer;

  VALIDATE_CON((!HAS_KEY(opts, exec_autocmds, buf) || !HAS_KEY(opts, exec_autocmds, buffer)),
               "buf", "buffer", {
    return;
  });

  if (has_buf) {
    VALIDATE_CON((!HAS_KEY(opts, exec_autocmds, pattern)), "pattern", "buf", {
      return;
    });

    b = find_buffer_by_handle(buf, err);
    if (ERROR_SET(err)) {
      return;
    }
  }

  Array patterns = get_patterns_from_pattern_or_buf(opts->pattern, has_buf, buf, "",
                                                    arena, err);
  if (ERROR_SET(err)) {
    return;
  }

  if (HAS_KEY(opts, exec_autocmds, data)) {
    data = &opts->data;
  }

  modeline = GET_BOOL_OR_TRUE(opts, exec_autocmds, modeline);

  bool did_aucmd = false;
  FOREACH_ITEM(event_array, event_str, {
    GET_ONE_EVENT(event_nr, event_str, return )

    FOREACH_ITEM(patterns, pat, {
      char *fname = !has_buf ? pat.data.string.data : NULL;
      did_aucmd |= apply_autocmds_group(event_nr, fname, NULL, true, au_group, b, NULL, data);
    })
  })

  if (did_aucmd && modeline) {
    do_modelines(0);
  }
}

static Array unpack_string_or_array(Object v, char *k, bool required, Arena *arena, Error *err)
{
  if (v.type == kObjectTypeString) {
    Array arr = arena_array(arena, 1);
    ADD_C(arr, v);
    return arr;
  } else if (v.type == kObjectTypeArray) {
    if (!check_string_array(v.data.array, k, true, err)) {
      return (Array)ARRAY_DICT_INIT;
    }
    return v.data.array;
  } else {
    VALIDATE_EXP(!required && v.type == kObjectTypeNil, k, "Array or String", api_typename(v.type),
    {
      return (Array)ARRAY_DICT_INIT;
    });
  }

  return (Array)ARRAY_DICT_INIT;
}

// Returns AUGROUP_ERROR if there was a problem with {group}
static int get_augroup_from_object(Object group, Error *err)
{
  int au_group = AUGROUP_ERROR;

  switch (group.type) {
  case kObjectTypeNil:
    return AUGROUP_DEFAULT;
  case kObjectTypeString:
    au_group = augroup_find(group.data.string.data);
    VALIDATE_S((au_group != AUGROUP_ERROR), "group", group.data.string.data, {
      return AUGROUP_ERROR;
    });

    return au_group;
  case kObjectTypeInteger:
    au_group = (int)group.data.integer;
    char *name = au_group == 0 ? NULL : augroup_name(au_group);
    VALIDATE_INT(augroup_exists(name), "group", (int64_t)au_group, {
      return AUGROUP_ERROR;
    });
    return au_group;
  default:
    VALIDATE_EXP(false, "group", "String or Integer", api_typename(group.type), {
      return AUGROUP_ERROR;
    });
  }
}

static Array get_patterns_from_pattern_or_buf(Object pattern, bool has_buf, Buffer buf,
                                              char *fallback, Arena *arena, Error *err)
{
  ArrayBuilder patterns = ARRAY_DICT_INIT;
  kvi_init(patterns);

  if (pattern.type != kObjectTypeNil) {
    if (pattern.type == kObjectTypeString) {
      const char *pat = pattern.data.string.data;
      size_t patlen = aucmd_span_pattern(pat, &pat);
      while (patlen) {
        kvi_push(patterns, CBUF_TO_ARENA_OBJ(arena, pat, patlen));
        patlen = aucmd_span_pattern(pat + patlen, &pat);
      }
    } else if (pattern.type == kObjectTypeArray) {
      if (!check_string_array(pattern.data.array, "pattern", true, err)) {
        return (Array)ARRAY_DICT_INIT;
      }

      Array array = pattern.data.array;
      FOREACH_ITEM(array, entry, {
        const char *pat = entry.data.string.data;
        size_t patlen = aucmd_span_pattern(pat, &pat);
        while (patlen) {
          kvi_push(patterns, CBUF_TO_ARENA_OBJ(arena, pat, patlen));
          patlen = aucmd_span_pattern(pat + patlen, &pat);
        }
      })
    } else {
      VALIDATE_EXP(false, "pattern", "String or Table", api_typename(pattern.type), {
        return (Array)ARRAY_DICT_INIT;
      });
    }
  } else if (has_buf) {
    buf_T *b = find_buffer_by_handle(buf, err);
    if (ERROR_SET(err)) {
      return (Array)ARRAY_DICT_INIT;
    }

    kvi_push(patterns, STRING_OBJ(arena_printf(arena, "<buffer=%d>", (int)b->handle)));
  }

  if (kv_size(patterns) == 0 && fallback) {
    kvi_push(patterns, CSTR_AS_OBJ(fallback));
  }

  return arena_take_arraybuilder(arena, &patterns);
}

static bool clear_autocmd(event_T event, char *pat, int au_group, Error *err)
{
  if (do_autocmd_event(event, pat, false, false, "", true, au_group) == FAIL) {
    api_set_error(err, kErrorTypeException, "Failed to clear autocmd");
    return false;
  }

  return true;
}

#undef GET_ONE_EVENT
