/* 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 .
            check ruff check --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)