{ config, lib, options, ... }:

let
  envConfig = config;

  toStringType = with lib.types; coercedTo anything (x: builtins.toString x) str;
  envSubmodule = { config, lib, name, ... }: {
    options = {
      action = lib.mkOption {
        type = lib.types.enum [ "unset" "set" "set-default" ];
        description = ''
          Sets the appropriate action for the environment variable.

          * `unset`... unsets the given variable.
          * `set-default` only sets the variable with the given value if
          not already set.
          * `set` forcibly sets the variable with given value.
        '';
        default = "set";
        example = "unset";
      };

      value = lib.mkOption {
        type = toStringType;
        description = ''
          The value of the variable that is holding.
        '';
        example = "HELLO THERE";
      };

      isEscaped = lib.mkEnableOption "escaping of the value" // {
        default = true;
      };
    };
  };

  wrapperType = { name, lib, config, pkgs, ... }:
    let
      flagType = with lib.types; listOf toStringType;
    in
    {
      options = {
        prependArgs = lib.mkOption {
          type = flagType;
          description = ''
            A list of arguments to be prepended to the user-given argument for the
            wrapper script.
          '';
          default = [ ];
          example = lib.literalExpression ''
            [
              "--config" ./config.conf
            ]
          '';
        };

        appendArgs = lib.mkOption {
          type = flagType;
          description = ''
            A list of arguments to be appended to the user-given argument for the
            wrapper script.
          '';
          default = [ ];
          example = lib.literalExpression ''
            [
              "--name" "doggo"
              "--location" "Your mom's home"
            ]
          '';
        };

        arg0 = lib.mkOption {
          type = lib.types.str;
          description = ''
            The first argument of the wrapper script.
          '';
          example = lib.literalExpression "lib.getExe' pkgs.neofetch \"neofetch\"";
        };

        executableName = lib.mkOption {
          type = lib.types.nonEmptyStr;
          description = "The name of the executable.";
          default = name;
          example = "custom-name";
        };

        env = options.environment.variables;
        pathAdd = options.environment.pathAdd;

        preScript = lib.mkOption {
          type = lib.types.lines;
          description = ''
            Script fragments to run before the main executable.

            ::: {.note}
            This option is only used when {option}`build.isBinary` is set to
            `false`.
            :::
          '';
          default = "";
          example = lib.literalExpression ''
            echo "HELLO WORLD!"
          '';
        };

        makeWrapperArgs = lib.mkOption {
          type = with lib.types; listOf str;
          description = ''
            A list of extra arguments to be passed as part of `makeWrapper`
            build step.
          '';
          example = [ "--inherit-argv0" ];
        };
      };

      config = lib.mkMerge [
        {
          env = envConfig.environment.variables;
          pathAdd = envConfig.environment.pathAdd;

          makeWrapperArgs = [
            "--argv0" config.arg0
          ]
          ++ (lib.mapAttrsToList
              (n: v:
                if v.action == "unset"
                then "--${v.action} ${lib.escapeShellArg n}"
                else "--${v.action} ${lib.escapeShellArg n} ${if v.isEscaped then lib.escapeShellArg v.value else v.value}")
              config.env)
          ++ (builtins.map (v: "--add-flags ${lib.escapeShellArg v}") config.prependArgs)
          ++ (builtins.map (v: "--append-flags ${lib.escapeShellArg v}") config.appendArgs)
          ++ (lib.optionals (!envConfig.build.isBinary && config.preScript != "") (
            let
              preScript =
                pkgs.runCommand "wrapper-script-prescript-${config.executableName}" { } config.preScript;
            in
              [ "--run" preScript ]));
        }

        (lib.mkIf (config.pathAdd != [ ]) {
          env.PATH.value = lib.concatStringsSep ":" config.pathAdd;
        })
      ];
    };
in
{
  options = {
    wrappers = lib.mkOption {
      type = with lib.types; attrsOf (submodule wrapperType);
      description = ''
        A set of wrappers to be included in the resulting derivation from
        wrapper-manager evaluation.
      '';
      default = { };
      example = lib.literalExpression ''
        {
          yt-dlp-audio = {
            arg0 = lib.getExe' pkgs.yt-dlp "yt-dlp";
            prependArgs = [
              "--config-location" ./config/yt-dlp/audio.conf
            ];
          };
        }
      '';
    };

    basePackages = lib.mkOption {
      type = with lib.types; listOf package;
      description = ''
        A list of packages to be included in the wrapper package.

        ::: {.note}
        This can override some of the binaries included in this list which is
        typically intended to be used as a wrapped package.
        :::
      '';
      default = [ ];
      example = lib.literalExpression ''
        with pkgs; [
          yt-dlp
        ]
      '';
    };

    environment.variables = lib.mkOption {
      type = with lib.types; attrsOf (submodule envSubmodule);
      description = ''
        A global set of environment variables and their actions to be applied
        per-wrapper.
      '';
      default = { };
      example = {
        "FOO_TYPE".value = "custom";
        "FOO_LOG_STYLE" = {
          action = "set-default";
          value = "systemd";
        };
        "USELESS_VAR".action = "unset";
      };
    };

    environment.pathAdd = lib.mkOption {
      type = with lib.types; listOf path;
      description = ''
        A global list of paths to be added per-wrapper as part of the `PATH`
        environment variable.
      '';
      default = [ ];
      example = lib.literalExpression ''
        wrapperManagerLib.getBin (with pkgs; [
          yt-dlp
          gallery-dl
        ])
      '';
    };
  };
}