# Nixpkgs/NixOS properties.  Generalize the problem of delayable (not yet
# evaluable) properties like mkIf.

let lib = import ./default.nix; in

with { inherit (builtins) head tail; };
with import ./trivial.nix;
with import ./lists.nix;
with import ./misc.nix;
with import ./attrsets.nix;

rec {

  inherit (lib) typeOf;

  # Tell that nothing is defined.  When properties are evaluated, this type
  # is used to remove an entry.  Thus if your property evaluation semantic
  # implies that you have to mute the content of an attribute, then your
  # property should produce this value.
  isNotdef = attrs: (typeOf attrs) == "notdef";
  mkNotdef = {_type = "notdef";};

  # General property type, it has a property attribute and a content
  # attribute.  The property attribute refers to an attribute set which
  # contains a _type attribute and a list of functions which are used to
  # evaluate this property.  The content attribute is used to stack properties
  # on top of each other.
  # 
  # The optional functions which may be contained in the property attribute
  # are:
  #  - onDelay: run on a copied property.
  #  - onGlobalDelay: run on all copied properties.
  #  - onEval: run on an evaluated property.
  #  - onGlobalEval: run on a list of property stack on top of their values.
  isProperty = attrs: (typeOf attrs) == "property";
  mkProperty = p@{property, content, ...}: p // {
    _type = "property";
  };

  # Go through the stack of properties and apply the function `op' on all
  # property and call the function `nul' on the final value which is not a
  # property.  The stack is traversed in reversed order.  The `op' function
  # should expect a property with a content which have been modified.
  # 
  # Warning: The `op' function expects only one argument in order to avoid
  # calls to mkProperties as the argument is already a valid property which
  # contains the result of the folding inside the content attribute.
  foldProperty = op: nul: attrs:
    if isProperty attrs then
      op (attrs // {
        content = foldProperty op nul attrs.content;
      })
    else
      nul attrs;

  # Simple function which can be used as the `op' argument of the
  # foldProperty function.  Properties that you don't want to handle can be
  # ignored with the `id' function.  `isSearched' is a function which should
  # check the type of a property and return a boolean value.  `thenFun' and
  # `elseFun' are functions which behave as the `op' argument of the
  # foldProperty function.
  foldFilter = isSearched: thenFun: elseFun: attrs:
    if isSearched attrs.property then
      thenFun attrs
    else
      elseFun attrs;


  # Move properties from the current attribute set to the attribute
  # contained in this attribute set.  This trigger property handlers called
  # `onDelay' and `onGlobalDelay'.
  delayPropertiesWithIter = iter: path: attrs:
    let cleanAttrs = rmProperties attrs; in
    if isProperty attrs then
      iter (a: v:
        lib.addErrorContext "while moving properties on the attribute `${a}':" (
          triggerPropertiesGlobalDelay a (
            triggerPropertiesDelay a (
              copyProperties attrs v
      )))) path cleanAttrs
    else
      attrs;

  delayProperties = # implicit attrs argument.
    let
      # mapAttrs except that it also recurse into potential mkMerge
      # functions.  This may cause a strictness issue because looking the
      # type of a string implies evaluating it.
      iter = fun: path: value:
        lib.mapAttrs (attr: val:
          if isProperty val && isMerge val.property then
            val // { content = map (fun attr) val.content; }
          else
            fun attr val
        ) value;
    in
      delayPropertiesWithIter iter "";

  # Call onDelay functions.
  triggerPropertiesDelay = name: attrs:
    let
      callOnDelay = p@{property, ...}:
        lib.addErrorContext "while calling an `onDelay' function:" (
          if property ? onDelay then
            property.onDelay name p
          else
            p
        );
    in
      foldProperty callOnDelay id attrs;

  # Call onGlobalDelay functions.
  triggerPropertiesGlobalDelay = name: attrs:
    let
      globalDelayFuns = uniqListExt {
        getter = property: property._type;
        inputList = foldProperty (p@{property, content, ...}:
          if property ? onGlobalDelay then
            [ property ] ++ content
          else
            content
        ) (a: []) attrs;
      };

      callOnGlobalDelay = property: content:
        lib.addErrorContext "while calling an `onGlobalDelay' function:" (
          property.onGlobalDelay name content
        );
    in
      fold callOnGlobalDelay attrs globalDelayFuns;

  # Expect a list of values which may have properties and return the same
  # list of values where all properties have been evaluated and where all
  # ignored values are removed.  This trigger property handlers called
  # `onEval' and `onGlobalEval'.
  evalProperties = valList:
    if valList != [] then
      filter (x: !isNotdef x) (
        lib.addErrorContext "while evaluating properties:" (
          triggerPropertiesGlobalEval (
            evalLocalProperties valList
      )))
    else
      valList;

  evalLocalProperties = valList:
    filter (x: !isNotdef x) (
      lib.addErrorContext "while evaluating local properties:" (
        map triggerPropertiesEval valList
    ));

  # Call onEval function
  triggerPropertiesEval = val:
    foldProperty (p@{property, ...}:
      lib.addErrorContext "while calling an `onEval' function:" (
        if property ? onEval then
          property.onEval p
        else
          p
      )
    ) id val;

  # Call onGlobalEval function
  triggerPropertiesGlobalEval = valList:
    let
      globalEvalFuns = uniqListExt {
        getter = property: property._type;
        inputList =
          fold (attrs: list:
            foldProperty (p@{property, content, ...}:
              if property ? onGlobalEval then
                [ property ] ++ content
              else
                content
            ) (a: list) attrs
          ) [] valList;
      };

      callOnGlobalEval = property: valList:
        lib.addErrorContext "while calling an `onGlobalEval' function:" (
          property.onGlobalEval valList
        );
    in
      fold callOnGlobalEval valList globalEvalFuns;

  # Remove all properties on top of a value and return the value.
  rmProperties =
    foldProperty (p@{content, ...}: content) id;

  # Copy properties defined on a value on another value.
  copyProperties = attrs: newAttrs:
    foldProperty id (x: newAttrs) attrs;

  /* Merge. */

  # Create "merge" statement which is skipped by the delayProperty function
  # and interpreted by the underlying system using properties (modules).

  # Create a "Merge" property which only contains a condition.
  isMerge = attrs: (typeOf attrs) == "merge";
  mkMerge = content: mkProperty {
    property = {
      _type = "merge";
      onDelay = name: val: throw "mkMerge is not the first of the list of properties.";
      onEval = val: throw "mkMerge is not allowed on option definitions.";
    };
    inherit content;
  };

  /* If. ThenElse. Always. */

  # create "if" statement that can be delayed on sets until a "then-else" or
  # "always" set is reached.  When an always set is reached the condition
  # is ignore.

  # Create a "If" property which only contains a condition.
  isIf = attrs: (typeOf attrs) == "if";
  mkIf = condition: content: mkProperty {
    property = {
      _type = "if";
      onGlobalDelay = onIfGlobalDelay;
      onEval = onIfEval;
      inherit condition;
    };
    inherit content;
  };

  # Create a "ThenElse" property which contains choices being chosen by
  # the evaluation of an "If" statement.
  isThenElse = attrs: (typeOf attrs) == "then-else";
  mkThenElse = attrs:
    assert attrs ? thenPart && attrs ? elsePart;
    __trace "Obsolete usage of mkThenElse, replace it by mkMerge."
    mkProperty {
      property = {
        _type = "then-else";
        onEval = val: throw "Missing mkIf statement.";
        inherit (attrs) thenPart elsePart;
      };
      content = mkNotdef;
    };

  # Create an "Always" property removing/ ignoring all "If" statement.
  isAlways = attrs: (typeOf attrs) == "always";
  mkAlways = value:
    mkProperty {
      property = {
        _type = "always";
        onEval = p@{content, ...}: content;
        inherit value;
      };
      content = mkNotdef;
    };

  mkAssert = assertion: message: content:
    mkIf
      (if assertion then true else throw "\nFailed assertion: ${message}")
      content;

  # Remove all "If" statement defined on a value.
  rmIf = foldProperty (
      foldFilter isIf
        ({content, ...}: content)
        id
    ) id;

  # Evaluate the "If" statements when either "ThenElse" or "Always"
  # statement is encountered.  Otherwise it removes multiple If statements and
  # replaces them by one "If" statement where the condition is the list of all
  # conditions joined with a "and" operation.
  onIfGlobalDelay = name: content:
    let
      # extract if statements and non-if statements and repectively put them
      # in the attribute list and attrs.
      ifProps =
        foldProperty
          (foldFilter (p: isIf p || isThenElse p || isAlways p)
            # then, push the codition inside the list list
            (p@{property, content, ...}:
              { inherit (content) attrs;
                list = [property] ++ content.list;
              }
            )
            # otherwise, add the propertie.
            (p@{property, content, ...}:
              { inherit (content) list;
                attrs = p // { content = content.attrs; };
              }
            )
          )
          (attrs: { list = []; inherit attrs; })
          content;

      # compute the list of if statements.
      evalIf = content: condition: list:
        if list == [] then
          mkIf condition content
        else
          let p = head list; in

          # evaluate the condition.
          if isThenElse p then
            if condition then
              copyProperties content p.thenPart
            else
              copyProperties content p.elsePart
          # ignore the condition.
          else if isAlways p then
            copyProperties content p.value
          # otherwise (isIf)
          else
            evalIf content (condition && p.condition) (tail list);
    in
      evalIf ifProps.attrs true ifProps.list;

  # Evaluate the condition of the "If" statement to either get the value or
  # to ignore the value.
  onIfEval = p@{property, content, ...}:
    if property.condition then
      content
    else
      mkNotdef;

  /* mkOverride */

  # Create an "Override" statement which allow the user to define
  # priorities between values.  The default priority is 100. The lowest
  # priorities are kept.  The template argument must reproduce the same
  # attribute set hierarchy to override leaves of the hierarchy.
  isOverride = attrs: (typeOf attrs) == "override";
  mkOverrideTemplate = priority: template: content: mkProperty {
    property = {
      _type = "override";
      onDelay = onOverrideDelay;
      onGlobalEval = onOverrideGlobalEval;
      inherit priority template;
    };
    inherit content;
  };

  # Like mkOverrideTemplate, but without the template argument.
  mkOverride = priority: content: mkOverrideTemplate priority {} content;

  # Sugar to override the default value of the option by making a new
  # default value based on the configuration.
  mkDefaultValue = mkOverride 1000;
  mkDefault = mkOverride 1000;
  mkForce = mkOverride 50;
  mkStrict = mkOverride 0;

  # Make the template traversal in function of the property traversal.  If
  # the template define a non-empty attribute set, then the property is
  # copied only on all mentionned attributes inside the template.
  # Otherwise, the property is kept on all sub-attribute definitions.
  onOverrideDelay = name: p@{property, content, ...}:
    let inherit (property) template; in
    if isAttrs template && template != {} then
      if hasAttr name template then
        p // {
          property = p.property // {
            template = builtins.getAttr name template;
          };
        }
      # Do not override the attribute \name\
      else
        content
    # Override values defined inside the attribute \name\.
    else
      p;

  # Keep values having lowest priority numbers only throwing away those having
  # a higher priority assigned.
  onOverrideGlobalEval = valList:
    let
      defaultPrio = 100;

      inherit (builtins) lessThan;

      getPrioVal =
        foldProperty
          (foldFilter isOverride
            (p@{property, content, ...}:
              if content ? priority && lessThan content.priority property.priority then
                content
              else
                content // {
                  inherit (property) priority;
                }
            )
            (p@{property, content, ...}:
              content // {
                value = p // { content = content.value; };
              }
            )
          ) (value: { inherit value; });

      addDefaultPrio = x:
        if x ? priority then x
        else x // { priority = defaultPrio; };

      prioValList = map (x: addDefaultPrio (getPrioVal x)) valList;

      higherPrio =
        if prioValList == [] then
          defaultPrio
        else
          fold (x: min:
            if lessThan x.priority min then
              x.priority
            else
              min
          ) (head prioValList).priority (tail prioValList);
    in
      map (x:
        if x.priority == higherPrio then
          x.value
        else
          mkNotdef
      ) prioValList;

  /* mkOrder */

  # Order definitions based on there index value.  This property is useful
  # when the result of the merge function depends on the order on the
  # initial list.  (e.g. concatStrings) Definitions are ordered based on
  # their rank.  The lowest ranked definition would be the first to element
  # of the list used by the merge function.  And the highest ranked
  # definition would be the last.  Definitions which does not have any rank
  # value have the default rank of 100.
  isOrder = attrs: (typeOf attrs) == "order";
  mkOrder = rank: content: mkProperty {
    property = {
      _type = "order";
      onGlobalEval = onOrderGlobalEval;
      inherit rank;
    };
    inherit content;
  };

  mkHeader = mkOrder 10;
  mkFooter = mkOrder 1000;

  # Fetch the rank of each definition (add the default rank is none) and
  # sort them based on their ranking.
  onOrderGlobalEval = valList:
    let
      defaultRank = 100;

      inherit (builtins) lessThan;

      getRankVal =
        foldProperty
          (foldFilter isOrder
            (p@{property, content, ...}:
              if content ? rank then
                content
              else
                content // {
                  inherit (property) rank;
                }
            )
            (p@{property, content, ...}:
              content // {
                value = p // { content = content.value; };
              }
            )
          ) (value: { inherit value; });

      addDefaultRank = x:
        if x ? rank then x
        else x // { rank = defaultRank; };

      rankValList = map (x: addDefaultRank (getRankVal x)) valList;

      cmp = x: y:
        builtins.lessThan x.rank y.rank;
    in
      map (x: x.value) (sort cmp rankValList);

  /* mkFixStrictness */

  # This is a hack used to restore laziness on some option definitions.
  # Some option definitions are evaluated when they are not used.  This
  # error is caused by the strictness of type checking builtins.  Builtins
  # like 'isAttrs' are too strict because they have to evaluate their
  # arguments to check if the type is correct.  This evaluation, cause the
  # strictness of properties.
  #
  # Properties can be stacked on top of each other.  The stackability of
  # properties on top of the option definition is nice for user manipulation
  # but require to check if the content of the property is not another
  # property.  Such testing implies to verify if this is an attribute set
  # and if it possess the type 'property'. (see isProperty & typeOf)
  #
  # To avoid strict evaluation of option definitions, 'mkFixStrictness' is
  # introduced.  This property protects an option definition by replacing
  # the base of the stack of properties by 'mkNotDef', when this property is
  # evaluated it returns the original definition.
  #
  # This property is useful over any elements which depends on options which
  # are raising errors when they get evaluated without the proper settings.
  #
  # Plain list and attribute set are lazy structures, which means that the
  # container gets evaluated but not the content.  Thus, using this property
  # on top of plain list or attribute set is pointless.
  #
  # This is a Hack, you should avoid it!

  # This property has a long name because you should avoid it.
  isFixStrictness = attrs: (typeOf attrs) == "fix-strictness";
  mkFixStrictness = value:
    mkProperty {
      property = {
        _type = "fix-strictness";
        onEval = p: value;
      };
      content = mkNotdef;
    };

}
