# This module provides personalised helpers for managing plugins
# using lazy.nvim and a set of custom runtime primitives.
{ pkgs, lib, config, ... }:
let
  inherit (lib) types;

  e = config.satellite.lib.lua.encoders;
  cfg = config.satellite.neovim;

  # {{{ Custom types 
  myTypes = {
    oneOrMany = t: types.either t (types.listOf t);
    zeroOrMore = t: types.nullOr (myTypes.oneOrMany t);

    neovimEnv = types.enum [ "firenvim" "vscode" "neovide" ];

    # {{{ Lua code 
    luaCode = types.nullOr (types.oneOf [
      types.path
      types.str
      myTypes.luaLiteral
    ]);

    luaLiteral = types.submodule {
      options.__luaEncoderTag = lib.mkOption {
        type = types.enum [ "lua" ];
      };
      options.value = lib.mkOption {
        type = types.str;
      };
    };

    luaValue = types.nullOr (types.oneOf [
      types.str
      types.number
      types.bool
      (types.attrsOf myTypes.luaValue)
      (types.listOf myTypes.luaValue)
    ]);
    # }}}
    # {{{ Lazy key
    lazyKey = types.oneOf [
      types.str
      (types.submodule
        {
          options = {
            mapping = lib.mkOption {
              type = types.str;
              description = "The lhs of the neovim mapping";
            };

            action = lib.mkOption {
              default = null;
              type = types.nullOr (types.oneOf [
                types.str
                myTypes.luaLiteral
              ]);
              description = "The rhs of the neovim mapping";
            };

            ft = lib.mkOption {
              default = null;
              type = myTypes.zeroOrMore types.str;
              description = "Filetypes on which this keybind should take effect";
            };

            mode = lib.mkOption {
              default = null;
              type = types.nullOr types.str;
              description = "The vim modes the mapping should take effect in";
            };

            desc = lib.mkOption {
              default = null;
              type = types.nullOr types.str;
              description = "Description for the current keymapping";
            };


            expr = lib.mkOption {
              default = null;
              example = true;
              type = types.nullOr types.bool;
              description = "If set to `true`, the mapping is treated as an action factory";
            };
          };
        })
    ];
    # }}} 
    # {{{ Tempest key
    tempestKey = types.submodule {
      options = {
        mapping = lib.mkOption {
          example = "<leader>a";
          type = types.str;
          description = "The lhs of the neovim mapping";
        };

        action = lib.mkOption {
          example = "<C-^>";
          type = types.either types.str myTypes.luaLiteral;
          description = "The rhs of the neovim mapping";
        };

        bufnr = lib.mkOption {
          default = null;
          example = true;
          type = types.nullOr
            (types.oneOf [
              types.bool
              types.integer
              myTypes.luaLiteral
            ]);
          description = ''
            The index of the buffer to apply local keymaps to. Can be set to 
            `true` to refer to the current buffer
          '';
        };

        mode = lib.mkOption {
          default = null;
          example = "nov";
          type = types.nullOr types.str;
          description = "The vim modes the mapping should take effect in";
        };

        silent = lib.mkOption {
          default = null;
          example = true;
          type = types.nullOr types.bool;
          description = "Whether the logs emitted by the keymap should be supressed";
        };

        expr = lib.mkOption {
          default = null;
          example = true;
          type = types.nullOr types.bool;
          description = "If set to `true`, the mapping is treated as an action factory";
        };

        desc = lib.mkOption {
          default = null;
          type = types.nullOr types.str;
          description = "Description for the current keymapping";
        };
      };
    };
    # }}}
    # {{{ Tempest autocmd
    tempestAutocmd = types.submodule {
      options = {
        event = lib.mkOption {
          example = "InsertEnter";
          type = myTypes.oneOrMany types.str;
          description = "Events to bind autocmd to";
        };

        pattern = lib.mkOption {
          example = "Cargo.toml";
          type = myTypes.oneOrMany types.str;
          description = "File name patterns to run autocmd on";
        };

        group = lib.mkOption {
          example = "CargoCmpSource";
          type = types.str;
          description = "Name of the group to create and assign autocmd to";
        };

        action = lib.mkOption {
          example.vim.opt.cmdheight = 1;
          type = types.oneOf [
            myTypes.tempestConfiguration
            myTypes.luaCode
          ];
          description = ''
            Code to run when the respctive event occurs. Will pass the event
            object as context, which might be used for things like assigning 
            a buffer number to local keymaps automatically.
          '';
        };
      };
    };
    # }}}
    # {{{ Tempest configuration
    tempestConfiguration = types.submodule {
      options = {
        vim = lib.mkOption {
          default = null;
          type = myTypes.luaValue;
          example.opt.cmdheight = 0;
          description = "Values to assign to the `vim` lua global object";
        };

        keys = lib.mkOption {
          default = null;
          type = myTypes.zeroOrMore myTypes.tempestKey;
          description = ''
            Arbitrary key mappings to create. The keymappings might 
            automatically be buffer specific depending on the context. For 
            instance, keymappings created inside autocmds will be local unless
            otherwise specified.
          '';
        };

        autocmds = lib.mkOption {
          default = null;
          type = myTypes.zeroOrMore myTypes.tempestAutocmd;
          description = "Arbitrary autocmds to create";
        };

        setup = lib.mkOption {
          default = null;
          type = types.nullOr (types.attrsOf myTypes.luaValue);
          example.lualine.opts.theme = "auto";
          description = ''
            Key-pair mappings for options to pass to .setup functions imported
            from different modules
          '';
        };

        callback = lib.mkOption {
          default = null;
          type = types.nullOr myTypes.luaCode;
          description = "Arbitrary code to run after everything else has been configured";
        };
      };
    };
    # }}}
    # {{{ Lazy module
    lazyModule = lib.fix (lazyModule: types.submodule ({ name ? null, ... }: {
      options = {
        package = lib.mkOption {
          default = null;
          type = types.nullOr types.str;
          description = "Package to configure the module around";
          example = "nvim-telescope/telescope.nvim";
        };

        dir = lib.mkOption {
          default = null;
          type = types.nullOr types.path;
          description = "Path to install pacakge from";
        };

        name = lib.mkOption {
          default = name;
          type = types.nullOr types.str;
          description = "Custom name to use for the module";
          example = "lualine";
        };

        main = lib.mkOption {
          default = null;
          type = types.nullOr types.str;
          description = "The name of the lua entrypoint for the plugin (usually auto-detected)";
          example = "lualine";
        };

        version = lib.mkOption {
          default = null;
          type = types.nullOr types.str;
          description = "Pin the package to a certain version (useful for non-nix managed packages)";
        };

        tag = lib.mkOption {
          default = null;
          type = types.nullOr types.str;
          description = "Pin the package to a certain git tag (useful for non-nix managed packages)";
        };

        lazy = lib.mkOption {
          default = null;
          type = types.nullOr types.bool;
          description = "Specifies whether this module should be lazy-loaded";
        };

        dependencies.lua = lib.mkOption {
          default = [ ];
          type = types.listOf (types.either types.str lazyModule);
          description = "Lazy.nvim module dependencies";
        };

        dependencies.nix = lib.mkOption {
          default = [ ];
          type = types.listOf types.package;
          description = "Nix packages to give nvim access to";
        };

        cond = lib.mkOption {
          default = null;
          type = myTypes.luaCode;
          description = "Condition based on which to enable/disbale loading the package";
        };

        env.blacklist = lib.mkOption {
          default = null;
          type = myTypes.zeroOrMore myTypes.neovimEnv;
          description = "Environments to blacklist plugin on";
        };

        env.whitelist = lib.mkOption {
          default = null;
          type = myTypes.zeroOrMore myTypes.neovimEnv;
          description = "Environments to whitelist plugin on";
        };

        setup = lib.mkOption {
          default = null;
          type = types.nullOr (types.oneOf [
            myTypes.tempestConfiguration
            myTypes.luaCode
            types.bool
          ]);
          description = ''
            Lua function (or module) to use for configuring the package.
            Used instead of the canonically named `config` because said name has a special meaning in nix
          '';
        };

        event = lib.mkOption {
          default = null;
          type = myTypes.zeroOrMore types.str;
          description = "Event on which the module should be lazy loaded";
        };

        ft = lib.mkOption {
          default = null;
          type = myTypes.zeroOrMore types.str;
          description = "Filetypes on which the module should be lazy loaded";
        };

        cmd = lib.mkOption {
          default = null;
          type = myTypes.zeroOrMore types.str;
          description = "Comands on which to load this plugin";
        };

        init = lib.mkOption {
          default = null;
          type = myTypes.luaCode;
          description = "Lua function (or module) to run right away (even if the package is not yet loaded)";
        };

        passthrough = lib.mkOption {
          default = null;
          type = myTypes.luaCode;
          description = "Attach additional things to the lazy module";
        };

        opts = lib.mkOption {
          default = null;
          type = myTypes.luaValue;
          description = "Custom data to pass to the plugin .setup function";
        };

        keys = lib.mkOption {
          default = null;
          type = myTypes.zeroOrMore myTypes.lazyKey;
          description = "Keybinds to lazy-load the module on";
        };
      };
    }));
    # }}}
  };
  # }}}
in
{
  # {{{ Option declaration 
  options.satellite.neovim = {
    lazy = lib.mkOption {
      default = { };
      description = "Record of plugins to install using lazy.nvim";
      type = types.attrsOf myTypes.lazyModule;
    };

    # {{{ Generated
    generated = {
      lazy = lib.mkOption {
        type = types.attrsOf (types.submodule {
          options = {
            raw = lib.mkOption {
              type = types.lines;
              description = "The lua script generated using the other options";
            };

            module = lib.mkOption {
              type = types.package;
              description = "The lua script generated using the other options";
            };
          };
        });
        description = "Attrset containing every module generated from the lazy configuration";
      };

      all = lib.mkOption {
        default = { };
        type = types.package;
        description = "Derivation building all the given nix modules";
      };

      lazySingleFile = lib.mkOption {
        type = types.package;
        description = "Derivation building all the given nix modules in a single file";
      };

      dependencies = lib.mkOption {
        default = [ ];
        type = types.listOf types.package;
        description = "List of packages to give neovim access to";
      };
    };
    # }}}
    # {{{ Lua generation lib 
    lib = {
      # {{{ Basic lua generators
      lua = lib.mkOption {
        default = value: { inherit value; __luaEncoderTag = "lua"; };
        type = types.functionTo myTypes.luaLiteral;
        description = "include some raw lua code inside module configuration";
      };

      import = lib.mkOption {
        default = path: tag: cfg.lib.lua (e.luaCode tag path);
        type = types.functionTo (types.functionTo myTypes.luaLiteral);
        description = "import some identifier from some module";
      };
      # }}}
      # {{{ Encoders 
      encode = lib.mkOption {
        default = e.anything;
        type = types.functionTo types.str;
        description = "Encode a nix value to a lua string";
      };

      encodeTempestConfiguration = lib.mkOption {
        default = given:
          e.attrset true [ ]
            {
              vim = e.anything;
              callback = e.nullOr e.luaString;
              setup = e.nullOr (e.attrsetOf e.anything);
              keys = e.zeroOrMany (e.attrset true [ ] {
                mapping = e.string;
                action = e.luaCodeOr e.string;
                desc = e.nullOr e.string;
                expr = e.nullOr e.bool;
                mode = e.nullOr e.string;
                silent = e.nullOr e.bool;
                buffer = e.nullOr (e.luaCodeOr (e.boolOr e.number));
              });
              autocmds = e.zeroOrMany (e.attrset true [ ] {
                event = e.oneOrMany e.string;
                pattern = e.oneOrMany e.string;
                group = e.string;
                action = e.conditional lib.isAttrs
                  cfg.lib.encodeTempestConfiguration
                  e.luaString;
              });
              mk_context = e.const (e.nullOr e.luaString (given.mkContext or null));
            }
            given;
        type = types.functionTo types.str;
        description = "Generate a lua object for passing to my own lua runtime for configuration";
      };
      # }}}
      # {{{ Thunks
      # This version of `nlib.thunk` is required in ceratain cases because 
      # of issues with `types.oneOf [types.submodule ..., types.submodule]` not
      # working as intended atm.
      thunkString = lib.mkOption {
        default = given: /* lua */ ''
          function() ${e.luaString given} end
        '';
        type = types.functionTo types.str;
        description = "Wrap a lua expression into a lua function as a string";
      };

      thunk = lib.mkOption {
        default = given: cfg.lib.lua (cfg.lib.thunkString given);
        type = types.functionTo myTypes.luaLiteral;
        description = "Wrap a lua expression into a lua function";
      };

      contextThunk = lib.mkOption {
        default = given: cfg.lib.lua /* lua */ ''
          function(context) ${e.luaString given} end
        '';
        type = types.functionTo myTypes.luaLiteral;
        description = "Wrap a lua expression into a lua function taking an argument named `context`";
      };

      lazy = lib.mkOption {
        default = given: cfg.lib.thunk "return ${e.luaCodeOr e.anything given}";
        type = types.functionTo myTypes.luaLiteral;
        description = "Wrap a lua expression into a function returning said expression";
      };

      withContext = lib.mkOption {
        default = given: cfg.lib.contextThunk "return ${e.luaCodeOr e.anything given}";
        type = types.functionTo myTypes.luaLiteral;
        description = "Wrap a lua expression into a lua function taking an argument named `context` and returning said expression";
      };

      tempest = lib.mkOption {
        default = given: cfg.lib.lua /* lua */ ''
          function(context)
            require(${e.string cfg.runtime.tempest}).configure(
              ${cfg.lib.encodeTempestConfiguration given},
              context
            )
          end
        '';
        type = types.functionTo myTypes.luaLiteral;
        description = "Wrap a lua expression into a lua function taking an argument named `context`";
      };
      # }}}
      # {{{ Language server on attach
      languageServerOnAttach = lib.mkOption {
        default = given: cfg.lib.lua /* lua */ ''
          function(client, bufnr)
            require(${e.string cfg.runtime.tempest}).configure(${cfg.lib.encodeTempestConfiguration given}, 
              { client = client; bufnr = bufnr; })

            require(${e.string cfg.runtime.languageServerOnAttach}).on_attach(client, bufnr)
          end
        '';
        type = types.functionTo myTypes.luaCode;
        description = "Attach a language server and run some additional code";
      };
      # }}}
    };
    # }}}
    # {{{ Neovim runtime module paths
    runtime = {
      tempest = lib.mkOption {
        type = types.str;
        example = "my.runtime.tempest";
        description = "Module to import the tempest runtime from";
      };

      languageServerOnAttach = lib.mkOption {
        type = types.str;
        example = "my.runtime.lspconfig";
        description = "Module to import langauge server .on_attach function from";
      };
    };
    # }}}
  };
  # }}}
  # {{{ Config generation 
  # {{{ Lazy module generation 
  config.satellite.neovim.generated.lazy =
    let
      # {{{ Lazy key encoder
      lazyKeyEncoder =
        e.stringOr (e.attrset true [ "mapping" "action" ] {
          mapping = e.string;
          action = e.nullOr (e.luaCodeOr e.string);
          mode = e.nullOr
            (e.map
              lib.strings.stringToCharacters
              (e.listAsOneOrMany e.string));
          desc = e.nullOr e.string;
          expr = e.nullOr e.bool;
          ft = e.zeroOrMany e.string;
        });
      # }}}
      # {{{ Lazy spec encoder 
      xor = a: b: (a || b) && (!a || !b);
      implies = a: b: !a || b;
      hasProp = obj: p: (obj.${p} or null) != null;
      propXor = a: b: obj: xor (hasProp obj a) (hasProp obj b);
      propImplies = a: b: obj: implies (hasProp obj a) (hasProp obj b);

      lazyObjectEncoder = e.bind
        (opts: e.attrset true [ "package" ]
          {
            package = e.nullOr e.string;
            dir = assert propXor "package" "dir" opts; e.nullOr e.string;
            tag = assert propImplies "tag" "package" opts; e.nullOr e.string;
            version = assert propImplies "version" "package" opts; e.nullOr e.string;
            name = e.nullOr e.string;
            main = e.nullOr e.string;
            dependencies = e.map (d: d.lua) (e.tryNonemptyList (e.stringOr lazyObjectEncoder));
            lazy = e.nullOr e.bool;
            cond = e.all [
              (e.nullOr (e.luaCode "cond"))
              (e.filter (_: opts.env.blacklist != [ ] && opts.env.blacklist != null)
                (e.const /* lua */ ''
                  require(${e.string cfg.runtime.tempest}).blacklist(${e.oneOrMany e.string opts.env.blacklist})
                ''))
              (e.filter (_: opts.env.whitelist != [ ] && opts.env.whitelist != null)
                (e.const /* lua */ ''
                  require(${e.string cfg.runtime.tempest}).whitelist(${e.oneOrMany e.string opts.env.blacklist})
                ''))
            ];

            config = _:
              let
                wrap = given: /* lua */''
                  function(lazy, opts)
                    require(${e.string cfg.runtime.tempest}).lazy(lazy, opts, ${given})
                  end
                '';
              in
              e.conditional lib.isAttrs
                (e.postmap wrap cfg.lib.encodeTempestConfiguration)
                (e.nullOr (e.boolOr (e.luaCode "config")))
                opts.setup;
            init = e.nullOr (e.luaCode "init");
            event = e.zeroOrMany e.string;
            cmd = e.zeroOrMany e.string;
            ft = e.zeroOrMany e.string;
            keys = e.nullOr (e.oneOrManyAsList lazyKeyEncoder);
            passthrough = e.anything;
            opts = e.anything;
          });
      # }}}
    in
    lib.attrsets.mapAttrs
      (name: opts: rec {
        raw = lazyObjectEncoder opts;
        module = config.satellite.lib.lua.writeFile "lua/nix/plugins" name raw;
      })
      cfg.lazy;

  config.satellite.neovim.generated.lazySingleFile =
    let
      makeFold = name: m: ''
        -- {{{ ${name}
        ${m.raw},
        -- }}}'';

      mkReturnList = objects: ''
        return {
          ${objects}
        }'';

      script = lib.pipe cfg.generated.lazy [
        (lib.attrsets.mapAttrsToList makeFold)
        (lib.concatStringsSep "\n")
        mkReturnList
      ];
    in
    config.satellite.lib.lua.writeFile
      "lua/nix/plugins" "init"
      script;

  config.satellite.neovim.generated.all =
    pkgs.symlinkJoin {
      name = "lazy-nvim-modules";
      paths = lib.attrsets.mapAttrsToList (_: m: m.module) cfg.generated.lazy;
    };
  # }}}

  config.satellite.neovim.generated.dependencies =
    lib.pipe cfg.lazy
      [
        (lib.attrsets.mapAttrsToList (_: m: m.dependencies.nix))
        lib.lists.flatten
      ];
  # }}}
}