/*
 * \brief  Tools and utility functions
 * \author Emery Hemingway
 * \date   2014-09-30
 */

{ nixpkgs ? import <nixpkgs> { } }:

with builtins;

rec {
  inherit nixpkgs;
  inherit (nixpkgs) fetchurl;

  ##
  # Add a prefix to a list of strings.
  addPrefix = prefix: map (s: prefix+s);

  ##
  # Determine if any of the following libs are shared.
  anyShared = libs:
    let h = head libs; in
    if libs == [] then false else
    if h.shared or false then true else anyShared (tail libs);

  ##
  # Drop a suffix from the end of a string.
  dropSuffix = suf: str:
    let
      strL = stringLength str;
      sufL = stringLength suf;
    in
    if lessThan strL sufL || substring (sub strL sufL) strL str != suf
    then abort "${str} does not have suffix ${suf}"
    else substring 0 (sub strL sufL) str;

  ##
  # Generate a list of file paths from a directory and
  # filenames.
  fromDir = dir: map (s: dir+("/"+s));

  ##
  # Utility functions for gathering sources.
  fromGlob =
  dir: glob:
  let
    dirName = dir.name or baseNameOf (toString dir);
  in
  import (shellDerivation {
    name = "${dirName}-glob.nix";
    script = ./from-glob.sh;
    #PATH="${nixpkgs.coreutils}/bin";
    inherit dir glob;
  });

  fromPath = path: [ [ path (baseNameOf (toString path)) ] ];
  fromPaths = paths: map (p: [ p (baseNameOf (toString p)) ]) paths;

  ##
  # Filter out libs that are not derivations
  filterFakeLibs = libs: filter (lib: hasAttr "shared" lib) libs;

  ##
  # Test if a string ends in '.h'.
  hasDotH = s: substring (sub (stringLength s) 2) 2 s == ".h";
  hasDotHH = s: substring (sub (stringLength s) 3) 3 s == ".hh";
  hasDotHPP = s: substring (sub (stringLength s) 4) 4 s == ".hpp";

  ##
  # Filter out everything but *.h on a path.
  # Prevents files that exist alongside headers from changing header path hash.
  filterHeaders = dir: filterSource
    (path: type: hasDotH path || hasDotHH path || hasDotHPP path || type == "directory")
    dir;

  ##
  # Find a filename in a search path.
  findFile = fn: searchPath:
    if searchPath == [] then []
    else
      let
        sp = head searchPath;
        fn' = sp + "/${fn}";
      in
      if builtins.typeOf fn' != "path" then [] else
      if pathExists fn' then
        [ { key = fn'; relative = fn; } ]
      else findFile fn (tail searchPath);

  findIncludes = main: path:
    map (x: [ x.key x.relative ]) (genericClosure {
      startSet = [ { key = main; relative = baseNameOf (toString main); } ];
      operator =
        { key, ... }:
        let
          includes = import (includesOf key);
          includesFound =
            nixpkgs.lib.concatMap
              (fn: findFile fn ([ (dirOf main) ] ++ path))
              includes;
        in includesFound;
    });

  ##
  # Recursively find libraries.
  findLibraries = libs:
    let
      list = map (lib: { key = lib.name; inherit lib; });
    in
    map (x: x.lib) (genericClosure {
      startSet = list libs;
      operator = { key, lib }: list lib.libs or [];
    });

  ##
  # Recursively find libraries to link.
  findLinkLibraries = libs:
    let
      list = libs: map
        (lib: { key = lib.name; inherit lib; })
        (filter (lib: hasAttr "drvPath" lib) libs);
    in
    map (x: x.lib) (genericClosure {
      startSet = list libs;
      operator =
        { key, lib }:
        if lib.shared then []
        else list lib.libs or [];
      });

  findLocalIncludes = main: path:
    let path' = [ (dirOf main) ] ++ path; in
    map (x: [ x.key x.relative ]) (genericClosure {
      startSet = [ { key = main; relative = baseNameOf (toString main); } ];
      operator =
        { key, ... }:
        let
          includes = import (localIncludesOf key);
          includesFound =
            nixpkgs.lib.concatMap
              (fn: findFile fn path')
              includes;
        in includesFound;
    });

  ##
  # Recursively find shared libraries.
  findRuntimeLibraries = libs:
    let
      filter = libs: builtins.filter (lib: lib.shared) libs;
      list = libs:
        map (lib: { key = lib.name; inherit lib; }) libs;
    in
  filter (map (x: x.lib) (genericClosure {
    startSet = list libs;
    operator =
      { key, lib }:
      list ([lib ] ++ lib.libs);
  }));

  ##
  # Determine if a string has the given suffix.
  hasSuffix = suf: str:
    let
      strL = stringLength str;
      sufL = stringLength suf;
    in
    if lessThan strL sufL then false else
      substring (sub strL sufL) strL str == suf;

  includesOf = file:
    import (derivation {
      name =
        if typeOf file == "path"
        then "${baseNameOf (toString file)}-includes"
        else "includes";
      system = currentSystem;
      preferLocalBuild = true;
      builder = "${nixpkgs.perl}/bin/perl";
      args = [ ./find-includes.pl ];
      inherit file;
    });

  ##
  # Create a bootable ISO.
  iso =
  { name, contents, kernel, kernelArgs }:
  shellDerivation {
    name = "${name}.iso";
    script = ./iso.sh;
    PATH="${nixpkgs.coreutils}/bin:${nixpkgs.cdrkit}/bin:${nixpkgs.binutils}/bin";
    inherit kernel kernelArgs;
    inherit (nixpkgs) syslinux cdrkit;
    sources = map (x: x.source) contents;
    targets = map (x: x.target) contents;
  };

  ##
  # Generate a contents list of runtime libraries for a package.
  # This will go away as tool.runtime matures.
  libContents = contents: builtins.concatLists (map (
    content:
      map (source: { target = "/"; inherit source; }) content.source.runtime.libs or []
  ) contents);

  localIncludesOf = main: derivation {
    name =
      if typeOf main == "path"
      then "${baseNameOf (toString main)}-local-includes"
      else "local-includes";
    system = currentSystem;
    preferLocalBuild = true;
    builder = "${nixpkgs.perl}/bin/perl";
    args = [ ./find-local-includes.pl ];
    inherit main;
  };

  ##
  # Merge an attr between two sets.
  mergeAttr =
  name: s1: s2:
  let
    a1 = getAttr name s1;
    a2 = getAttr name s2;
    type1 = typeOf a1;
    type2 = typeOf a2;
  in
  if type1 == "null" then a2 else if type2 == "null" then a1 else
  if type1 != type2 then abort "cannot merge ${name}s of type ${type1} and ${type2}" else
  if type1 == "set" then mergeSet a1 a2 else
  if type1 == "list" then a1 ++ a2 else
  if type1 == "string" then "${a1} ${a2}" else
  #if type1 == "int" then add a1 a2 else
  abort "cannot merge ${type1} ${name} ${toString a1} ${toString a2}";

  ##
  # Merge two sets together.
  mergeSet = s1: s2:
    s1 // s2 // (listToAttrs (map
      (name: { inherit name; value = mergeAttr name s1 s2; })
      (attrNames (intersectAttrs s1 s2))
    ));

  ##
  # Merge a list of sets.
  mergeSets =
  sets:
  if sets == [] then {} else
    mergeSet (head sets) (mergeSets (tail sets));

  newDir =
  name: contents:
  derivation {
    inherit name contents;
    system = builtins.currentSystem;
    preferLocalBuild = true;
    builder = shell;
    PATH="${nixpkgs.coreutils}/bin";
    args = [ "-e" "-c" ''
      mkdir -p $out ; \
      for i in $contents; do cp -Hr $i $out; done
    '' ];
  };

  ##
  # Generate a list of paths from a path and a shell glob.
  pathsFromGlob = dir: glob:
    let path = toString dir; in
    import (derivation {
      name = "${baseNameOf path}-glob.nix";
      args = [ "-e" "-O" "nullglob" ./path-from-glob.sh ];
      inherit dir glob path;
      preferLocalBuild = true;
    });

  preparePort = import ./prepare-port { inherit nixpkgs; };

  # Concatenate the named attr found in pkgs.
  propagate = attrName: pkgs:
    let
      pkg = head pkgs;
    in
    if pkgs == [] then [] else
    ( if hasAttr attrName pkg
      then getAttr attrName pkg
      else []
    ) ++
    (propagate attrName (tail pkgs));


  ##
  # Replace substring a with substring b in string s.
  replaceInString =
  a: b: s:
  let
    al = stringLength a;
    bl = stringLength b;
    sl = stringLength s;
  in
  if al == 0 then s else
  if sl == 0 then "" else
  if ((substring 0 al s) == a) then
    b+(replaceInString a b (substring al sl s))
  else
    (substring 0 1 s) + (replaceInString a b (substring 1 sl s));

  shell = nixpkgs.bash + "/bin/sh";

  # Save some typing when creating derivation that use our shell.
  shellDerivation = { script, ... } @ args:
    derivation ( (removeAttrs args [ "script" ]) //
      { system = builtins.currentSystem;
        builder = shell;
        args = [ "-e" script ];
      }
    );

  singleton = x: [x];

  # Bootability is not assured, so its really system image.
  systemImage = import ./system-image { inherit nixpkgs; };
  bootImage = systemImage; # get rid of this


  wildcard =
  path: glob:
  let
    relativePaths = import (shellDerivation {
      name = "files.nix";
      PATH="${nixpkgs.coreutils}/bin";
      script = ./wildcard.sh;
      inherit path glob;

    });
  in
  map (rp: (path+"/${rp}")) relativePaths;

  # Appends string context from another string
  addContextFrom = a: b: substring 0 0 a + b;

  # Compares strings not requiring context equality
  # Obviously, a workaround but works on all Nix versions
  eqStrings = a: b: addContextFrom b a == addContextFrom a b;


  ##
  # Cut a string with a separator and produces a list of strings which were
  # separated by this separator. e.g.,
  # `splitString "." "foo.bar.baz"' returns ["foo" "bar" "baz"].
  # From nixpkgs.
  splitString = _sep: _s:
    let
      sep = addContextFrom _s _sep;
      s = addContextFrom _sep _s;
      sepLen = stringLength sep;
      sLen = stringLength s;
      lastSearch = sLen - sepLen;
      startWithSep = startAt:
        substring startAt sepLen s == sep;

      recurse = index: startAt:
        let cutUntil = i: [(substring startAt (i - startAt) s)]; in
        if index < lastSearch then
          if startWithSep index then
            let restartAt = index + sepLen; in
            cutUntil index ++ recurse restartAt restartAt
          else
            recurse (index + 1) startAt
        else
          cutUntil sLen;
    in
      recurse 0 0;
  ##
  # What I thought builtins.match would do.
  matchPattern = pat: str:
    concatLists (
      map
        ( l:
          let m = match pat l; in
          if m == null then [] else m
        )
        (splitString "\n" str)
    );

  ##
  # Generate a set of local ("") and system (<>)
  # preprocessor include directives from a file.
  relativeIncludes = file:
    let
      matches = pattern: lines:
        concatLists (filter (x: x != null) (map (match pattern) lines));
      lines = splitString "\n" (readFile file);
    in
    { local  = matches ''.*#include\s*"([^>]*)".*'' lines;
      system = matches ''.*#include\s*<([^>]*)>.*'' lines;
    };

  ##
  # Find a file in a set of directories.
  findFile' = key: dirs:
    if substring 0 1 key == "!" then builtins.trace "found a !key" key else
    if dirs == [] then null else
    let abs = (builtins.head dirs) +"/${key}"; in
    if builtins.pathExists abs then abs
    else findFile' key (builtins.tail dirs);

  ##
  # Generate a set of relative to absolute include mappings from a file.
  # This set includes a mapping from the orginal file basename to its
  # absolute path.
  #
  # The genericClosure primative applies an operation to a list of sets that
  # contain the attribute 'key'. This operation returns a similar list of sets,
  # and genericClosure appends elements of that list that contain a key
  # that does not already exist in the previous set. All sets returned by this
  # operation contain a function to resolve the relative path at 'key' into an
  # absolute one at 'abs', and a function to parse the absolute path at 'abs'
  # into a list of relative includes at 'inc'. GenericClosure discards any set
  # with a relative path at 'key' that it has already been seen, and thus due
  # to lazy evaulation, no relative path is resolved or parsed twice.
  #
  # A ! is prepended to the files of the initial set, to differentiate them from
  # files with unsolved locations and to satisfy the requirement that strings
  # not directly reference store paths in findFile'
  includesOfFiles = files: searchPath:
    let
      concat = sets:
        if sets == [] then {} else
        let x = head sets; in
        (if x.abs == null || substring 0 1 x.key == "!" then {} else { "${x.key}" = x.abs; }) // concat (tail sets);
    in
    concat (genericClosure {
      # Can the startSet really be filled with elements sharing a key?
      startSet = map (abs: { key = "!${abs}"; inherit abs; inc = includesOf abs; }) files;
      operator =
        { key, abs, inc }: if abs == null then [] else let abs' = abs; in
          (map 
            (key: rec { inherit key; abs = (findFile' key searchPath); inc = includesOf abs; })
            inc.system)
          ++
          (map
            (key: rec { inherit key; abs = (findFile' key (if typeOf abs' == "path" then searchPath ++ [ (dirOf abs') ] else searchPath)); inc = relativeIncludes abs; })
            inc.local);
    });

  ##
  # Load expressions from a directory path and apply func.
  loadExpressions = func: path:
    let
      dirSet  = builtins.readDir path;
      default =
        if builtins.hasAttr "default.nix" dirSet
        then func (import (path + "/default.nix"))
        else {};
    in
    default // (builtins.listToAttrs (builtins.filter (x: x != {}) (map
      (name:
        let
          type = builtins.getAttr name dirSet;
          more = loadExpressions func (path + "/${name}");
        in
        if type == "directory"
        then { inherit name; value = more; }
        else
        if
          (type == "regular" || type == "symlink") &&
          (hasSuffix ".nix" name)
        then
          { name = dropSuffix ".nix" name;
            value = func (import (path+"/${name}"));
          }
        else { }
      )
      (builtins.attrNames dirSet)
    )));

}
