# nixLib - A library of nix functions for vhack.eu
#
# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
# SPDX-License-Identifier: LGPL-3.0-or-later
#
# This file is part of vhack.eu's nix library.
#
# You should have received a copy of the License along with this program.
# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
{warn}:
# Adapted from this: https://github.com/NixOS/nixpkgs/blob/1814b56453c91192f6d5a6276079948f9fe96c18/pkgs/top-level/by-name-overlay.nix
# This file should not depend on `pkgs` and thus not use `lib`.
{
  baseDirectory,
  fileName ? null,
  fileRegex ? null,
  finalizeFunction ? name: value: value,
  useShards ? true,
}:
assert fileName == null -> fileRegex != null;
assert fileRegex == null -> fileName != null; let
  finalFileRegex =
    if fileRegex == null
    then "^${escapeRegex fileName}$"
    else fileRegex;
  fileDisplay =
    if fileName == null
    then "matching ${fileRegex}"
    else fileName;

  # From nixpkgs/lib {{{
  # These functions are taken straight out of the `nixpkgs/lib`.
  # We can't depended on `pkgs` (and thus on `lib`), because the `pkgs` module argument
  # is only defined in the `nixpkgs` module (which is imported through this function).
  mapAttrsToList = f: attrs:
    builtins.map (name: f name attrs.${name}) (builtins.attrNames attrs);

  stringToCharacters = s: builtins.genList (p: builtins.substring p 1 s) (builtins.stringLength s);
  escape = list: builtins.replaceStrings list (map (c: "\\${c}") list);
  escapeRegex = escape (stringToCharacters "\\[{()^$?*+|.");

  mapAttrs' = f: set: builtins.listToAttrs (map (attr: f attr set.${attr}) (builtins.attrNames set));

  nameValuePair = name: value: {inherit name value;};
  filterAttrs = pred: set:
    builtins.listToAttrs (builtins.concatMap (name: let
      v = set.${name};
    in
      if pred name v
      then [(nameValuePair name v)]
      else []) (builtins.attrNames set));
  # }}}

  rawByName = {
    baseDirectory,
    finalFileRegex,
    fileDisplay,
    finalizeFunction,
    useShards,
  }: let
    # Takes a list of attrs as input and returns one merged attr set.
    flattenAttrs = list:
      if builtins.isList list
      then
        builtins.foldl' (acc: elem:
          if builtins.isList elem
          # Merging them with `//` is okay here, as we can be sure that the attr names are
          # unique (they were separate dictionary after all)
          then acc // (flattenAttrs elem)
          else acc // elem) {}
        list
      else list;

    # Module files for a single shard
    # Type: String -> String -> ListOf Path
    namesForShard = shard: type:
      if type != "directory"
      then warn "Ignored non-directory, whilst importing by-name directory (${fileDisplay}): '${shard}'" {}
      else if useShards
      then namesForElementShard shard type
      else namesForElementDirect shard type;

    # Type: String -> String -> ListOf Path
    mkPath = name: toplevelType: let
      rawPath = baseDirectory + "/${name}";
      paths = filterAttrs (_: v: v != null) (mapAttrs' (name: value:
        if builtins.match finalFileRegex name != null
        then nameValuePair name value
        else nameValuePair name null) (builtins.readDir rawPath));

      checkPath = path: type:
        if builtins.pathExists "${rawPath}/${path}"
        then
          if toplevelType != "directory"
          then
            # The `namesForShard` function should have already printed a warning.
            [null]
          else path
        else warn "'${builtins.toString path}' does not exist. Skipped" null;
    in
      if toplevelType != "directory"
      then
        # The `namesForShard` function should have already printed a warning.
        [null]
      else if (builtins.attrValues paths) == []
      then warn "'${fileDisplay}' did not match anything in ${rawPath}. Skipped" [null]
      else mapAttrsToList checkPath paths;

    # Type: String -> String -> AttrSet
    namesForElementShard = shard: _type:
      filterAttrs (name: value: value != null)
      (builtins.mapAttrs
        mkPath
        (builtins.readDir (baseDirectory + "/${shard}")));

    # Type: String -> String -> AttrSet
    namesForElementDirect = name: type: {"${name}" = builtins.filter (value: value != null) (mkPath name type);};

    # A list of all paths.
    files = flattenAttrs (mapAttrsToList namesForShard (builtins.readDir baseDirectory));
    output =
      builtins.mapAttrs
      finalizeFunction
      files;
  in
    output;

  firstPass = rawByName {
    inherit
      baseDirectory
      finalFileRegex
      fileDisplay
      finalizeFunction
      useShards
      ;
  };

  secondPass =
    if fileName != null
    then
      builtins.mapAttrs (name: value: builtins.head value)
      (filterAttrs (_: value: value != []) firstPass)
    else firstPass;
in
  secondPass