/*
This is the library of nix builders. Some rules to follow:
- Keep this code as minimal as possible. I'd rather write Haskell than Nix,
  wouldn't you?
- Try to reuse as much upstream Nix as possible.
*/
{
  analysisJSON,
  bild,
}:
with bild; let
  analysis = builtins.fromJSON analysisJSON;

  # common bash functions for the builder
  commonBash = builtins.toFile "common.bash" ''
    # Check that a command succeeds, fail and log if not.
    function check {
        $@ || { echo "fail:  $name:  $3"; exit 1; }
      }
  '';

  build = _: target: let
    name = target.out;
    root = builtins.getEnv "CODEROOT";
    mainModule = target.mainModule;
    compileLine =
      lib.strings.concatStringsSep " "
      ([target.compiler] ++ target.compilerFlags);

    allSources = target.srcs ++ [target.quapath];

    isEmpty = x: x == null || x == [];

    skip = ["_" ".direnv"];
    filter = file: type:
      if lib.lists.elem (builtins.baseNameOf file) skip
      then false
      # TODO: this means any new directory will cause a rebuild. this bad. i
      # should recurse into the directory and match against the srcs. for now I
      # just use preBuild to delete empty dirs
      else if type == "directory"
      then true
      else if type == "regular"
      then
        lib.trivial.pipe file [
          (f: lib.strings.removePrefix "${root}/" f)
          (f: lib.lists.elem f allSources)
        ]
      else false;

    # remove empty directories, leftover from the src filter
    preBuild = "find . -type d -empty -delete";

    src = lib.sources.cleanSourceWith {
      inherit filter;
      src = lib.sources.cleanSource root;
    };

    langdeps_ =
      if isEmpty target.langdeps
      then []
      else
        lib.attrsets.attrVals target.langdeps (lib.attrsets.getAttrFromPath
          (lib.strings.splitString "." target.packageSet)
          bild);

    sysdeps_ =
      if isEmpty target.sysdeps
      then []
      else lib.attrsets.attrVals target.sysdeps pkgs;

    rundeps_ =
      if isEmpty target.rundeps
      then []
      else lib.attrsets.attrVals target.rundeps pkgs;

    CODEROOT = ".";

    builders = {
      base = stdenv.mkDerivation rec {
        inherit name src CODEROOT preBuild;
        buildInputs = langdeps_ ++ sysdeps_;
        installPhase = "install -D ${name} $out/bin/${name}";
        buildPhase = compileLine;
      };

      haskell = stdenv.mkDerivation rec {
        inherit name src CODEROOT preBuild;
        nativeBuildInputs = [makeWrapper];
        buildInputs =
          sysdeps_
          ++ [
            (haskell.ghcWith (p: (lib.attrsets.attrVals target.langdeps p)))
          ];
        buildPhase = compileLine;
        installPhase = ''
          install -D ${name} $out/bin/${name}
          wrapProgram $out/bin/${name} \
            --prefix PATH : ${lib.makeBinPath rundeps_}
        '';
      };

      c = stdenv.mkDerivation rec {
        inherit name src CODEROOT preBuild;
        buildInputs = langdeps_ ++ sysdeps_;
        installPhase = "install -D ${name} $out/bin/${name}";
        buildPhase = lib.strings.concatStringsSep " " [
          compileLine
          (
            if isEmpty langdeps_
            then ""
            else "$(pkg-config --cflags ${
              lib.strings.concatStringsSep " " target.langdeps
            })"
          )
          (
            if isEmpty sysdeps_
            then ""
            else "$(pkg-config --libs ${
              lib.strings.concatStringsSep " " target.sysdeps
            })"
          )
        ];
      };

      python = python.buildPythonApplication rec {
        inherit name src CODEROOT;
        nativeBuildInputs = [makeWrapper];
        propagatedBuildInputs = langdeps_ ++ sysdeps_ ++ rundeps_;
        buildInputs = sysdeps_;
        nativeCheckInputs = [pkgs.ruff python.packages.mypy];
        checkPhase = ''
          . ${commonBash}
          cp ${../../pyproject.toml} ./pyproject.toml
          check ruff format --exclude 'setup.py' --check .
          # ignore EXE here to support run.sh shebangs
          check ruff check \
            --ignore EXE \
            --exclude 'setup.py' \
            --exclude '__init__.py' \
            .
          touch ./py.typed
          check python -m mypy \
            --explicit-package-bases \
            --no-error-summary \
            --exclude 'setup\.py$' \
            .
        '';
        installCheck = ''
          . ${commonBash}
          check python -m ${mainModule} test
        '';
        preBuild = ''
          # remove empty directories, leftover from the src filter
          find . -type d -empty -delete
          # initialize remaining dirs as python modules
          find . -type d -exec touch {}/__init__.py \;
          # generate a minimal setup.py
          cat > setup.py << EOF
          from setuptools import find_packages, setup
          setup(
              name="${name}",
              entry_points={"console_scripts":["${name} = ${mainModule}:main"]},
              version="0.0.0",
              url="git://simatime.com/omni.git",
              author="dev",
              author_email="dev@simatime.com",
              description="nil",
              packages=find_packages(),
              install_requires=[],
          )
          EOF
        '';
        pythonImportsCheck = [mainModule]; # sanity check
      };
    };
  in
    builders.${target.builder};
  # the bild caller gives us the Analysis type, which is a hashmap, but i need to
  # return a single drv, so just take the first one for now. ideally i would only
  # pass Target, one at a time, (perhaps parallelized in haskell land) and then i
  # wouldn't need all of this let nesting
in
  builtins.head (lib.attrsets.mapAttrsToList build analysis)