# Thank you: https://gist.github.com/petabyteboy/558ffddb9aeb24e1eab2d5d6d021b5d7

with import <nixpkgs/lib>;

rec {
  # FIXME: add case for negative numbers
  pow = base: exponent: if exponent == 0 then 1 else fold (
    x: y: y * base
  ) base (
    range 2 exponent
  );

  fromHexString = hex: foldl (
    x: y: 16 * x + (
      (
        listToAttrs (
          map (
            x: nameValuePair (
              toString x
            ) x
          ) (
            range 0 9
          )
        ) // {
          "a" = 10;
          "b" = 11;
          "c" = 12;
          "d" = 13;
          "e" = 14;
          "f" = 15;
        }
      ).${y}
    )
  ) 0 (
    stringToCharacters (
      removePrefix "0x" (
        hex
      )
    )
  );

  ipv4 = rec {

    decode = address: foldl (
      x: y: 256 * x + y
    ) 0 (
      map toInt (
        splitString "." address
      )
    );

    encode = num: concatStringsSep "." (
      map (
        x: toString (mod (num / x) 256)
      ) (
        reverseList (
          genList (
            x: pow 2 (x * 8)
          ) 4
        )
      )
    );

    netmask = prefixLength: (
      foldl (
        x: y: 2 * x + 1
      ) 0 (
        range 1 prefixLength
      )
    ) * (
      pow 2 (
        32 - prefixLength
      )
    );

    reverseZone = net: (
      concatStringsSep "." (
        reverseList (
          splitString "." net
        )
      )
    ) + ".in-addr.arpa";

    eachAddress = net: prefixLength: genList (
      x: decode (
        x + (
          decode net
        )
      )
    ) (
      pow 2 (
        32 - prefixLength
      )
    );

    networkOf = address: prefixLength: encode (
      bitAnd (
        decode address
      ) (
        netmask prefixLength
      )
    );

    isInNetwork = net: address: networkOf address == net;

    /* nixos-specific stuff */

    findOwnAddress = config: net: head (
      filter (
        isInNetwork net
      ) (
        configuredAddresses config
      )
    );

    configuredAddresses = config: concatLists (
      mapAttrsToList (
        name: iface: iface.ipv4.addresses
      ) config.networking.interfaces
    );

  };

  ipv6 = rec {

    expand = address: (
      replaceStrings ["::"] [(
        concatStringsSep "0" (
          genList (x: ":") (
            9 - (count (x: x == ":") (stringToCharacters address))
          )
        )
      )] address
    ) + (
      if hasSuffix "::" address then
        "0"
      else
        ""
    );

    decode = address: map fromHexString (
      splitString ":" (
        expand address
      )
    );

    encode = address: toLower (
      concatStringsSep ":" (
        map toHexString address
      )
    );

    netmask = prefixLength: map (
      x: if prefixLength > x + 16 then
        (pow 2 16) - 1
      else if prefixLength < x then
        0
      else
        (
          foldl (
            x: y: 2 * x + 1
          ) 0 (
            range 1 (prefixLength - x)
          )
        ) * (
          pow 2 (
            16 - (prefixLength - x)
          )
        )
    ) (
      genList (
        x: x * 16
      ) 8
    );

    reverseZone = net: (
      concatStringsSep "." (
        concatLists (
          reverseList (
            map (
              x: stringToCharacters (fixedWidthString 4 "0" x)
            ) (
              splitString ":" (
                expand net
              )
            )
          )
        )
      )
    ) + ".ip6.arpa";

    networkOf = address: prefixLength: encode (
      zipListsWith bitAnd (
        decode address
      ) (
        netmask prefixLength
      )
    );

    isInNetwork = net: address: networkOf address == (expand net);

    /* nixos-specific stuff */

    findOwnAddress = config: net: head (
      filter (
        isInNetwork net
      ) (
        configuredAddresses config
      )
    );

    configuredAddresses = config: concatLists (
      mapAttrsToList (
        name: iface: iface.ipv6.addresses
      ) config.networking.interfaces
    );

  };
}