# Additional theming primitives not provided by stylix
{ pkgs, lib, config, ... }:
let
  inherit (lib) types;

  cfg = config.satellite.neovim;

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

    fileTypes = types.nullOr (types.oneOf [
      types.str
      (types.listOf types.str)
    ]);

    # {{{ Key type
    lazyKey = types.oneOf [
      types.str
      (types.submodule
        (_: {
          options.mapping = lib.mkOption {
            type = types.str;
          };

          options.action = lib.mkOption {
            type = types.str;
          };

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

          options.mode = lib.mkOption {
            default = null;
            # Only added the types I'm using in my config atm
            type = types.nullOr (types.enum [ "n" "v" ]);
          };

          options.desc = lib.mkOption {
            default = null;
            type = types.nullOr types.str;
          };
        }))
    ];
    # }}} 
    # {{{ Lazy module type 
    lazyModule = lib.fix (lazyModule: types.submodule (_: {
      options = {
        package = lib.mkOption {
          type = types.oneOf [
            types.package
            types.str
          ];
          description = "Package to configure the module around";
          example = "nvim-telescope/telescope.nvim";
        };

        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 lazyModule;
        };

        dependencies.nix = lib.mkOption {
          default = [ ];
          type = types.listOf types.package;
        };

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

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

        event = lib.mkOption {
          default = null;
          type = types.nullOr (types.oneOf [
            types.str
            (types.listOf types.str)
          ]);
          description = "Event on which the module should be lazy loaded";
        };

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

        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";
        };

        keys = lib.mkOption {
          default = null;
          type =
            types.nullOr (types.oneOf [
              myTypes.lazyKey
              (types.listOf myTypes.lazyKey)
            ]);
        };

        opts = { };
      };
    }));
    # }}}
  };
  # }}}
  # {{{ Lua encoders
  mkRawLuaObject = chunks:
    ''
      {
        ${lib.concatStringsSep "," (lib.filter (s: s != "") chunks)}
      }
    '';

  # An encoder is a function from some nix value to a string containing lua code. 
  # This object provides combinators for writing such encoders.
  luaEncoders = {
    # {{{ General helpers 
    identity = given: given;
    bind = encoder: given: encoder given given;
    conditional = predicate: caseTrue: caseFalse:
      luaEncoders.bind (given: if predicate given then caseTrue else caseFalse);
    map = f: encoder: given: encoder (f given);
    trace = message: luaEncoders.map (f: lib.traceSeq message f);
    # }}}
    # {{{ Base types
    # TODO: figure out escaping and whatnot
    string = string: ''"${string}"'';
    bool = bool: if bool then "true" else "false";
    nil = _: "nil";
    stringOr = luaEncoders.conditional lib.isString luaEncoders.string;
    boolOr = luaEncoders.conditional lib.isBool luaEncoders.bool;
    nullOr = luaEncoders.conditional (e: e == null) luaEncoders.nil;
    # }}}
    # {{{ Advanced types
    luaCode = tag:
      luaEncoders.conditional lib.isPath
        (path: "require(${path}).${tag}")
        luaEncoders.identity;

    listOf = encoder: list:
      mkRawLuaObject (lib.lists.map encoder list);
    tryNonemptyList = encoder: luaEncoders.conditional
      (l: l == [ ])
      luaEncoders.nil
      (luaEncoders.listOf encoder);
    oneOrMany = encoder:
      luaEncoders.conditional
        lib.isList
        (luaEncoders.listOf encoder)
        encoder;
    oneOrManyAsList = encoder: luaEncoders.map
      (given: if lib.isList given then given else [ given ])
      (luaEncoders.listOf encoder);

    attrset = noNils: listOrder: listSpec: spec: attrset:
      let
        shouldKeep = given:
          if noNils then
            given != "nil"
          else
            true;

        listChunks = lib.lists.map
          (attr:
            let result = listSpec.${attr} (attrset.${attr} or null);
            in
            lib.optionalString (shouldKeep result) result
          )
          listOrder;
        objectChunks = lib.mapAttrsToList
          (attr: encoder:
            let result = encoder (attrset.${attr} or null);
            in
            lib.optionalString (shouldKeep result)
              "${attr} = ${result}"
          )
          spec;
      in
      mkRawLuaObject (listChunks ++ objectChunks);
    # }}}
  };

  e = luaEncoders;
  # }}}

  # Format and write a lua file to disk
  writeLuaFile = path: name: text:
    let
      directory = "lua/${path}";
      destination = "${directory}/${name}.lua";
      unformatted = pkgs.writeText "raw-lua-${name}" text;
    in
    pkgs.runCommand "formatted-lua-${name}" { } ''
      mkdir -p $out/${directory}
      cp --no-preserve=mode ${unformatted} $out/${destination}
      ${lib.getExe pkgs.stylua} --config-path ${cfg.styluaConfig} $out/${destination}
    '';
in
{
  options.satellite.neovim = {
    lazy = lib.mkOption {
      default = { };
      description = "Record of persistent locations (eg: /persist)";
      type = types.attrsOf myTypes.lazyModule;
    };

    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";
    };

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

    styluaConfig = lib.mkOption {
      type = types.path;
      description = "Config to use for formatting lua modules";
    };
  };

  config.satellite.neovim.generated.lazy =
    let
      lazyKeyEncoder =
        e.stringOr (e.attrset true [ "mapping" "action" ]
          {
            mapping = e.string;
            action = e.nullOr e.string;
          }
          {
            mode = e.nullOr e.string;
            desc = e.nullOr e.string;
            ft = e.nullOr (e.oneOrMany e.string);
          });

      renameKey = from: to: lib.mapAttrs' (name: value:
        if name == from then { inherit value; name = to; }
        else { inherit name value; });


      lazyObjectEncoder = e.map (renameKey "setup" "config")
        (e.attrset true [ "package" ]
          {
            package = e.string;
          }
          {
            tag = e.nullOr e.string;
            version = e.nullOr e.string;
            dependencies = e.map (d: d.lua) (e.tryNonemptyList lazyObjectEncoder);
            lazy = e.nullOr e.bool;
            # TODO: add sugar for enabling/disabling under certain envs
            cond = e.nullOr (e.luaCode "cond");
            config = e.nullOr (e.boolOr (e.luaCode "config"));
            init = e.nullOr (e.luaCode "init");
            event = e.nullOr (e.oneOrMany e.string);
            ft = e.nullOr (e.oneOrMany e.string);
            keys = e.nullOr (e.oneOrManyAsList lazyKeyEncoder);
            # TODO: passthrough
          });

      makeLazyScript = opts: ''
        -- This file was generated by nix
        return ${lazyObjectEncoder opts}
      '';
    in
    lib.attrsets.mapAttrs
      (name: opts: rec {
        raw = makeLazyScript opts;
        module = writeLuaFile "nix/plugins" name raw;
      })
      cfg.lazy;

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