# A partial and basic implementation of GVariant formatted strings.
#
# Note, this API is not considered fully stable and it might therefore
# change in backwards incompatible ways without prior notice.

{ lib }:

let
  inherit (lib)
    concatMapStringsSep concatStrings escape hasPrefix head replaceStrings;

  mkPrimitive = t: v: {
    _type = "gvariant";
    type = t;
    value = v;
    __toString = self: "@${self.type} ${toString self.value}";
  };

  type = {
    arrayOf = t: "a${t}";
    maybeOf = t: "m${t}";
    tupleOf = ts: "(${concatStrings ts})";
    dictionaryEntryOf = ts: "{${concatStrings ts}}";
    string = "s";
    boolean = "b";
    uchar = "y";
    int16 = "n";
    uint16 = "q";
    int32 = "i";
    uint32 = "u";
    int64 = "x";
    uint64 = "t";
    double = "d";
    variant = "v";
  };

  # Returns the GVariant type of a given Nix value. If no type can be
  # found for the value then the empty string is returned.
  typeOf = v:
    with type;
    if builtins.isBool v then
      boolean
    else if builtins.isInt v then
      int32
    else if builtins.isFloat v then
      double
    else if builtins.isString v then
      string
    else if builtins.isList v then
      let elemType = elemTypeOf v;
      in if elemType == "" then "" else arrayOf elemType
    else if builtins.isAttrs v && v ? type then
      v.type
    else
      "";

  elemTypeOf = vs:
    if builtins.isList vs then
      if vs == [ ] then "" else typeOf (head vs)
    else
      "";

  mkMaybe = elemType: elem:
    mkPrimitive (type.maybeOf elemType) elem // {
      __toString = self:
        if self.value == null then
          "@${self.type} nothing"
        else
          "just ${toString self.value}";
    };

in rec {

  inherit type typeOf;

  isGVariant = v: v._type or "" == "gvariant";

  isArray = hasPrefix "a";
  isDictionaryEntry = hasPrefix "{";
  isMaybe = hasPrefix "m";
  isTuple = hasPrefix "(";

  # Returns the GVariant value that most closely matches the given Nix
  # value. If no GVariant value can be found then `null` is returned.

  mkValue = v:
    if builtins.isBool v then
      mkBoolean v
    else if builtins.isInt v then
      mkInt32 v
    else if builtins.isFloat v then
      mkDouble v
    else if builtins.isString v then
      mkString v
    else if builtins.isList v then
      if v == [ ] then mkArray type.string [ ] else mkArray (elemTypeOf v) v
    else if builtins.isAttrs v && (v._type or "") == "gvariant" then
      v
    else
      null;

  mkArray = elemType: elems:
    mkPrimitive (type.arrayOf elemType) (map mkValue elems) // {
      __toString = self:
        "@${self.type} [${concatMapStringsSep "," toString self.value}]";
    };

  mkEmptyArray = elemType: mkArray elemType [ ];

  mkVariant = elem:
    let gvarElem = mkValue elem;
    in mkPrimitive type.variant gvarElem // {
      __toString = self: "@${self.type} <${toString self.value}>";
    };

  mkDictionaryEntry = elems:
    let
      gvarElems = map mkValue elems;
      dictionaryType = type.dictionaryEntryOf (map (e: e.type) gvarElems);
    in mkPrimitive dictionaryType gvarElems // {
      __toString = self:
        "@${self.type} {${concatMapStringsSep "," toString self.value}}";
    };

  mkNothing = elemType: mkMaybe elemType null;

  mkJust = elem: let gvarElem = mkValue elem; in mkMaybe gvarElem.type gvarElem;

  mkTuple = elems:
    let
      gvarElems = map mkValue elems;
      tupleType = type.tupleOf (map (e: e.type) gvarElems);
    in mkPrimitive tupleType gvarElems // {
      __toString = self:
        "@${self.type} (${concatMapStringsSep "," toString self.value})";
    };

  mkBoolean = v:
    mkPrimitive type.boolean v // {
      __toString = self: if self.value then "true" else "false";
    };

  mkString = v:
    let sanitize = s: replaceStrings [ "\n" ] [ "\\n" ] (escape [ "'" "\\" ] s);
    in mkPrimitive type.string v // {
      __toString = self: "'${sanitize self.value}'";
    };

  mkObjectpath = v:
    mkPrimitive type.string v // {
      __toString = self: "objectpath '${escape [ "'" ] self.value}'";
    };

  mkUchar = mkPrimitive type.uchar;

  mkInt16 = mkPrimitive type.int16;

  mkUint16 = mkPrimitive type.uint16;

  mkInt32 = v:
    mkPrimitive type.int32 v // {
      __toString = self: toString self.value;
    };

  mkUint32 = mkPrimitive type.uint32;

  mkInt64 = mkPrimitive type.int64;

  mkUint64 = mkPrimitive type.uint64;

  mkDouble = v:
    mkPrimitive type.double v // {
      __toString = self: toString self.value;
    };

}