{
  lib,
  config,
  options,
  ...
}:
let
  inherit (lib) types mkOption;

  pluginWithConfigType = types.submodule {
    options = {
      config = mkOption {
        type = types.lines;
        description = "vimscript for this plugin to be placed in init.vim";
        default = "";
      };

      optional = lib.mkEnableOption "optional" // {
        description = "Don't load by default (load with :packadd)";
      };

      plugin = mkOption {
        type = types.package;
        description = "vim plugin";
      };
    };
  };

  isVimPluginPackage = package: package.vimPlugin or false;

  mkVimPluginPackageAssertion = opt: {
    assertion = !(lib.any isVimPluginPackage opt.value);
    message =
      let
        vimPluginDefs = lib.pipe opt.definitionsWithLocations [
          # Flatten to [{file, package}]
          (lib.concatMap ({ file, value }: map (package: { inherit file package; }) value))
          # Select offending definitions
          (lib.filter (def: isVimPluginPackage def.package))
          # Group definition files by plugin name
          (lib.groupBy (def: lib.getName def.package))
          (lib.mapAttrs (_: map (def: def.file)))
          (lib.mapAttrs (_: lib.uniqueStrings))
        ];
      in
      ''
        `${opt}` is for executable packages added to Neovim's PATH, but it contains Vim plugin package(s):
        ${lib.concatMapAttrsStringSep "\n" (
          name: files: "  - ${name} defined in ${lib.options.showFiles files}"
        ) vimPluginDefs}

        Use `${options.extraPlugins}` for Vim plugin packages:
          ${options.extraPlugins} = [ pkgs.vimPlugins.<plugin> ];
      '';
  };
in
{
  options = {
    env = mkOption {
      type =
        with types;
        lazyAttrsOf (oneOf [
          str
          path
          int
          float
        ]);
      description = "Environment variables to set in the neovim wrapper.";
      default = { };
      example = {
        FOO = 1;
        PI = 3.14;
        BAR_PATH = "/home/me/.local/share/bar";
        INFER_MODE = "local";
        BAZ_MAX_COUNT = 1000;
      };
    };

    extraPlugins = mkOption {
      type = with types; listOf (nullOr (either package pluginWithConfigType));
      default = [ ];
      description = "List of vim plugins to install";
      apply = builtins.filter (p: p != null);
    };

    extraPackages = mkOption {
      type = with types; listOf (nullOr package);
      default = [ ];
      description = "Extra packages to be made available to neovim, added to the start of `PATH`";
      apply = builtins.filter (p: p != null);
    };

    extraPackagesAfter = mkOption {
      type = with types; listOf (nullOr package);
      default = [ ];
      description = "Extra packages to be made available to neovim, added to the end of `PATH`";
      apply = builtins.filter (p: p != null);
    };

    extraPython3Packages = mkOption {
      type = with types; functionTo (listOf package);
      default = p: [ ];
      defaultText = lib.literalExpression "p: with p; [ ]";
      description = "Python packages to add to the `PYTHONPATH` of neovim.";
      example = lib.literalExpression ''
        p: [ p.numpy ]
      '';
    };

    extraConfigLua = mkOption {
      type = types.lines;
      default = "";
      description = "Extra contents for the file";
    };

    extraConfigLuaPre = mkOption {
      type = types.lines;
      default = "";
      description = "Extra contents for the file before everything else";
    };

    extraConfigLuaPost = mkOption {
      type = types.lines;
      default = "";
      description = "Extra contents for the file after everything else";
    };

    extraConfigVim = mkOption {
      type = types.lines;
      default = "";
      description = "Extra contents for the file, in vimscript";
    };

    type = mkOption {
      type = types.enum [
        "vim"
        "lua"
      ];
      default = "lua";
      description = ''
        Whether the generated file is a vim or a lua file

        Read-only outside of `files` submodules.
      '';
      readOnly = config.isTopLevel;
    };

    path = mkOption {
      type = types.str;
      description = "Path of the file relative to the config directory";
    };

    content = mkOption {
      type = types.str;
      description = "The content of the config file";
      readOnly = true;
      visible = false;
    };

    extraLuaPackages = mkOption {
      type = types.functionTo (types.listOf types.package);
      description = "Extra lua packages to include with neovim";
      default = _: [ ];
    };
  };

  config = {
    assertions = lib.nixvim.mkAssertions "output" [
      (mkVimPluginPackageAssertion options.extraPackages)
      (mkVimPluginPackageAssertion options.extraPackagesAfter)
    ];

    content =
      if config.type == "lua" then
        # Lua
        lib.nixvim.concatNonEmptyLines [
          config.extraConfigLuaPre
          (lib.nixvim.wrapVimscriptForLua config.extraConfigVim)
          config.extraConfigLua
          config.extraConfigLuaPost
        ]
      else
        # Vimscript
        lib.nixvim.concatNonEmptyLines [
          (lib.nixvim.wrapLuaForVimscript config.extraConfigLuaPre)
          config.extraConfigVim
          (lib.nixvim.wrapLuaForVimscript (
            lib.nixvim.concatNonEmptyLines [
              config.extraConfigLua
              config.extraConfigLuaPost
            ]
          ))
        ];
  };
}
