# if evaluating outside of the store, copy the current directory to the store and import it
# filtering out .gitignore files and .git directories
# if evaluating inside the store, import the outputs.nix file

let
  # Ideally this file should be selfcontained, but I like the utilities in nixpkgs lib
  lib = (import "${(import ./inputs.nix {}).nixpkgs}/lib");

  # function that takes gitignore file pattern and returns filter function
  # true - include file
  # false - exclude file
  # null - no match
  # string -> string -> [(string -> string -> (bool | null))]
  toGitignoreMatcher = gitignorePath: pattern: lib.pipe pattern [
    (v: { pattern = v; invalid = false; })
    # trim whitespaces not preceded by backslash
    (v: v // { pattern = let
      stringLength = builtins.stringLength v.pattern;
      leftPaddingLength = builtins.stringLength (lib.trimWith { start = true; end = false; } v.pattern) - stringLength;
      rightPaddingLength = builtins.stringLength (lib.trimWith { start = false; end = true; } v.pattern) - stringLength;
      isLastCharBackslash = if stringLength == 0 then false
        else builtins.substring (stringLength - rightPaddingLength - 1) 1 v.pattern == "\\";
      trimmedString = builtins.substring leftPaddingLength (stringLength - leftPaddingLength - rightPaddingLength) v.pattern;
    in if isLastCharBackslash && rightPaddingLength > 0 then trimmedString + " " else trimmedString; })
    # ignore empty lines
    (v: if v.pattern != "" then v else v // { invalid = true; })
    # ignore comments
    (v: if !v.invalid && builtins.substring 0 1 v.pattern != "#" then v else v // { invalid = true; })
    # mark negated patterns
    (v:
      if !v.invalid && builtins.substring 0 1 v.pattern == "!"
      then v // {
        negated = true;
        pattern = builtins.substring 1 (builtins.stringLength v) v;
      }
      else v // { negated = false; }
    )
    # ignore escapes
    (v: if v.invalid then v else v // { pattern = builtins.replaceStrings ["\\"] [""] v.pattern; })
    # convert parsed pattern to matchers
    ({ pattern, negated, invalid }: {
      __functor = _: path: type: let
        relative = builtins.match "^/.+[^/]$" pattern == [];
        directory = builtins.match "/$" pattern == [];
        regexPattern = lib.pipe pattern [
          (v: if relative then "${gitignorePath}/${v}" else v)
          (builtins.split "/")
          (builtins.filter (v: v != []))
          (builtins.map (builtins.split "(\\*\\*|\\*)"))
          (builtins.concatMap (v:
            # v: (string | [string])[]
            if v == [ "" ] then []
            # TODO: check and add support for .. <directory-up> if git supports
            else if v == [ "." ] then []
            else [( builtins.foldl' (acc: vp:
              # vp: string | [string]
              if builtins.isString vp then acc + lib.escapeRegex vp
              else if vp == [ "**" ] then acc + ".*"
              else if vp == [ "*" ] then acc + "[^/]*"
              else throw "unreachable"
            ) "" v )]
          ))
          (builtins.concatStringsSep "/" )
          (v: if relative then v else ".*/${v}")
        ];
        matches = (!directory || type == "directory")
          && (builtins.match regexPattern path == []);
      in if invalid then null
        else if matches then negated
        else null;
      # for debug purposes
      inherit pattern negated;
      # for filtering purposes
      inherit invalid;
    })
  ];

  # TODO: optimize this so if match is found in a given gitignore,
  #       no further checks in gitignores in parent directories are performed

  parseGitignore = gitRepositoryPath: filePath: lib.pipe filePath [
    (builtins.dirOf)
    (builtins.split "/" )
    (builtins.filter (v: v != [] && v != ""))
    # ["a" "b" "c"] -> ["/" "/a/" "/a/b/" "/a/b/c/"]
    (
      builtins.foldl' (acc: v: acc ++ [(
        (builtins.elemAt acc (builtins.length acc - 1)) + "${v}/"
      )] ) ["/"]
    )
    (builtins.map (v: "${v}.gitignore"))
    # Filter out paths that are not part of git repository and don't exist
    (builtins.filter (v: lib.hasPrefix gitRepositoryPath v && builtins.pathExists v))
    (builtins.map (v: {
      path = v;
      # Split gitignore files into lines
      contents = lib.pipe v [
        builtins.readFile
        (builtins.split "\n")
        # builtins.split uses lists for matches
        (builtins.filter (v: v != []))
      ];
    }))
    # Convert gitignore patterns to matchers
    (builtins.map (v:
      builtins.map (toGitignoreMatcher v.path) v.contents)
    )
    lib.flatten
    (lib.filter (v: !v.invalid))
  ];

  runGitignoreFilter = filters: path: type: lib.pipe filters [
    (builtins.map (v: v path type))
    (builtins.filter (v: v != null))
    # If any filter didn't match anything, include the file
    (v: if v == [] then [ true ] else v)
    (v: builtins.elemAt v (builtins.length v - 1))
  ];

  currentFilePath = (builtins.unsafeGetAttrPos "any" { any = "any"; }).file;
  storePathLength = builtins.stringLength (builtins.toString builtins.storeDir);
  evaluatingInStore = (builtins.substring 0 storePathLength currentFilePath) == builtins.storeDir;

  selfInStore = builtins.filterSource (path: type:
      let
        selfPath = builtins.dirOf currentFilePath;
        gitIgnoreFilters = parseGitignore selfPath path;
        result = type != "unknown"
          && type != "symlink"
          && builtins.baseNameOf path != ".git"
          && runGitignoreFilter gitIgnoreFilters path type;
      in result
    ) ./.;
in
if !(evaluatingInStore) then { ... }@args: import selfInStore ({ selfPath = selfInStore; } // args )
else { ... }@args: import ./outputs.nix ({ selfPath = selfInStore; } // args)