{
  lib,
  helpers,
  config,
  pkgs,
  ...
}:
with lib;
let
  cfg = config.plugins.packer;
in
{
  options = {
    plugins.packer = {
      enable = mkEnableOption "packer.nvim";

      package = lib.mkPackageOption pkgs [
        "vimPlugins"
        "packer-nvim"
      ] { };

      gitPackage = lib.mkPackageOption pkgs "git" {
        nullable = true;
      };

      plugins =
        with types;
        let
          pluginType = either str (submodule {
            options = {
              name = mkOption {
                type = str;
                description = "Name of the plugin to install";
              };

              disable = helpers.mkNullOrOption bool "Mark plugin as inactive";

              as = helpers.mkNullOrOption str "Specifies an alias under which to install the plugin";

              installer = helpers.defaultNullOpts.mkLuaFn "nil" "A custom installer";

              updater = helpers.defaultNullOpts.mkLuaFn "nil" "A custom updater";

              after = helpers.mkNullOrOption (either str (listOf str)) "Plugins to load after this plugin";

              rtp = helpers.mkNullOrOption str "Specifies a subdirectory of the plugin to add to runtimepath";

              opt = helpers.mkNullOrOption bool "Marks a plugin as optional";

              branch = helpers.mkNullOrOption str "Git branch to use";

              tag = helpers.mkNullOrOption str "Git tag to use";

              commit = helpers.mkNullOrOption str "Git commit to use";

              lock = helpers.mkNullOrOption bool "Skip this plugin in updates";

              run = helpers.mkNullOrOption (oneOf [
                str
                rawLua
                (listOf (either str rawLua))
              ]) "Post-install hook";

              requires = helpers.mkNullOrOption (eitherRecursive str listOfPlugins) "Plugin dependencies";

              rocks = helpers.mkNullOrOption (either str (listOf (either str attrs))) "Luarocks dependencies";

              config = helpers.mkNullOrOption (either str rawLua) "Code to run after this plugin is loaded";

              setup = helpers.mkNullOrOption (either str rawLua) "Code to be run before this plugin is loaded";

              cmd = helpers.mkNullOrOption (either str (listOf str)) "Commands which load this plugin";

              ft = helpers.mkNullOrOption (either str (listOf str)) "Filetypes which load this plugin";

              keys = helpers.mkNullOrOption (either str (listOf str)) "Keymaps which load this plugin";

              event = helpers.mkNullOrOption (either str (listOf str)) "Autocommand events which load this plugin";

              fn = helpers.mkNullOrOption (either str (listOf str)) "Functions which load this plugin";

              cond = helpers.mkNullOrOption (oneOf [
                str
                rawLua
                (listOf (either str rawLua))
              ]) "Conditional test to load this plugin";

              module = helpers.mkNullOrOption (either str (listOf str)) "Patterns of module names which load this plugin";
            };
          });

          listOfPlugins = types.listOf pluginType;
        in
        mkOption {
          type = listOfPlugins;
          default = [ ];
          description = "List of plugins";
        };

      rockPlugins = mkOption {
        type = with types; listOf (either str attrs);
        description = "List of lua rock plugins";
        default = [ ];
        example = ''
          [
            "penlight"
            "lua-resty-http"
            "lpeg"
          ]
        '';
      };
    };
  };

  config = mkIf cfg.enable {
    extraPlugins = [ cfg.package ];

    extraPackages = [ cfg.gitPackage ];

    extraConfigLua =
      let
        luaRockPluginToLua =
          luaRockPlugin:
          if isAttrs luaRockPlugin then
            mapAttrs' (k: v: {
              name = if k == "name" then "__unkeyed" else k;
              value = v;
            }) luaRockPlugin
          else
            luaRockPlugin;
        luaRockListToLua = map luaRockPluginToLua;

        pluginToLua =
          plugin:
          if isAttrs plugin then
            {
              "__unkeyed" = plugin.name;

              inherit (plugin)
                disable
                as
                installer
                updater
                after
                rtp
                opt
                branch
                tag
                commit
                lock
                run
                ;

              requires = helpers.ifNonNull' plugin.requires (
                if isList plugin.requires then (pluginListToLua plugin.requires) else plugin.requires
              );

              rocks = helpers.ifNonNull' plugin.rocks (
                if isList plugin.rocks then luaRockListToLua plugin.rocks else plugin.rocks
              );

              inherit (plugin)
                config
                setup
                cmd
                ft
                keys
                event
                fn
                cond
                module
                ;
            }
          else
            plugin;

        pluginListToLua = map pluginToLua;

        plugins = pluginListToLua cfg.plugins;

        packedPlugins = if length plugins == 1 then head plugins else plugins;

        luaRockPlugins = luaRockListToLua cfg.rockPlugins;
        luaRocksString = optionalString (
          cfg.rockPlugins != [ ]
        ) "use_rocks ${helpers.toLuaObject luaRockPlugins}";
      in
      mkIf (cfg.plugins != [ ]) ''
        require('packer').startup(function()
          use ${helpers.toLuaObject packedPlugins}
          ${luaRocksString}
        end)
      '';
  };
}