# 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;
    };

}
