{ lib, stdenvNoCC }:

/**
  `replaceVarsWith` is a wrapper around the [bash function `substitute`](https://nixos.org/manual/nixpkgs/stable/#fun-substitute)
  in the stdenv. It allows for terse replacement of names in the specified path, while checking
  for common mistakes such as naming a replacement that does nothing or forgetting a variable which
  needs to be replaced.

  As with the [`--subst-var-by`](https://nixos.org/manual/nixpkgs/stable/#fun-substitute-subst-var-by)
  flag, names are encoded as `@name@` in the provided file at the provided path.

  Any unmatched variable names in the file at the provided path will cause a build failure.

  By default, any remaining text that matches `@[A-Za-z_][0-9A-Za-z_'-]@` in the output after replacement
  has occurred will cause a build failure. Variables can be excluded from this check by passing "null" for them.

  # Inputs

  `src` ([Store Path](https://nixos.org/manual/nix/latest/store/store-path.html#store-path) String)
  : The file in which to replace variables.

  `replacements` (AttrsOf String)
  : Each entry in this set corresponds to a `--subst-var-by` entry in [`substitute`](https://nixos.org/manual/nixpkgs/stable/#fun-substitute) or
    null to keep it unchanged.

  `dir` (String)
  : Sub directory in $out to store the result in. Commonly set to "bin".

  `isExecutable` (Boolean)
  : Whether to mark the output file as executable.

  Most arguments supported by mkDerivation are also supported, with some exceptions for which
  an error will be thrown.

  # Example

  ```nix
  { replaceVarsWith }:

  replaceVarsWith {
    src = ./my-setup-hook.sh;
    replacements = { world = "hello"; };
    dir = "bin";
    isExecutable = true;
  }
  ```

  See `../../test/replace-vars/default.nix` for tests of this function. Also see `replaceVars` for a short
  version with src and replacements only.
*/
{
  src,
  replacements,
  dir ? null,
  isExecutable ? false,
  ...
}@attrs:

let
  # We use `--replace-fail` instead of `--subst-var-by` so that if the thing isn't there, we fail.
  subst-var-by = name: value: [
    "--replace-fail"
    (lib.escapeShellArg "@${name}@")
    (lib.escapeShellArg (lib.defaultTo "@${name}@" value))
  ];

  substitutions = lib.concatLists (lib.mapAttrsToList subst-var-by replacements);

  left-overs = map ({ name, ... }: name) (
    builtins.filter ({ value, ... }: value == null) (lib.attrsToList replacements)
  );

  optionalAttrs =
    if (builtins.intersectAttrs attrs forcedAttrs == { }) then
      builtins.removeAttrs attrs [ "replacements" ]
    else
      throw "Passing any of ${builtins.concatStringsSep ", " (builtins.attrNames forcedAttrs)} to replaceVarsWith is not supported.";

  forcedAttrs = {
    doCheck = true;
    dontUnpack = true;
    preferLocalBuild = true;
    allowSubstitutes = false;

    buildPhase = ''
      runHook preBuild

      target=$out
      if test -n "$dir"; then
          target=$out/$dir/$name
          mkdir -p $out/$dir
      fi

      substitute "$src" "$target" ${lib.concatStringsSep " " substitutions}

      if test -n "$isExecutable"; then
          chmod +x $target
      fi

      runHook postBuild
    '';

    # Look for Nix identifiers surrounded by `@` that aren't substituted.
    checkPhase =
      let
        lookahead =
          if builtins.length left-overs == 0 then "" else "(?!${builtins.concatStringsSep "|" left-overs}@)";
        regex = lib.escapeShellArg "@${lookahead}[a-zA-Z_][0-9A-Za-z_'-]*@";
      in
      ''
        runHook preCheck
        if grep -Pqe ${regex} "$target"; then
          echo The following look like unsubstituted Nix identifiers that remain in "$target":
          grep -Poe ${regex} "$target"
          echo Use the more precise '`substitute`' function if this check is in error.
          exit 1
        fi
        runHook postCheck
      '';
  };
in

stdenvNoCC.mkDerivation (
  {
    name = baseNameOf (toString src);
  }
  // optionalAttrs
  // forcedAttrs
)
