#  Copyright (c) 1997-2024
#  Ewgenij Gawrilow, Michael Joswig, and the polymake team
#  Technische Universität Berlin, Germany
#  https://polymake.org
#
#  This program is free software; you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by the
#  Free Software Foundation; either version 2, or (at your option) any
#  later version: http://www.gnu.org/licenses/gpl.txt.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#-------------------------------------------------------------------------------

use strict;
use namespaces;
use warnings qw(FATAL void syntax misc);

package Polymake::Core::Preference;
use constant clock_start => 100000000;
my $clock = clock_start;
my $compile_clock = 0;
use constant settings_key => "_preferences";

use Polymake::Enum Mode => {
   strict => 0,
   create => 1,    # allow to create new sublevels
   rules => 2,     # 'prefer' statement comes from the rules
   imported => 4,  # 'prefer' statement comes from an imported settings file
};

####################################################################################
package Polymake::Core::Preference::ControlList;

use Polymake::Struct (
   '@items',                              # controlled items (rules, subs)
   '@labels',                             # corresponding Label
   [ '$ordered' => '0' ],                 # number of leading items which are known to be ordered due to the active preferences.
                                          #  The trailing items are considered equally ranked.
                                          #  Thus, a control list without any active preferences applied has ordered==0.
   [ '$cleanup_table' => 'undef' ],       # -> Scope::cleanup for lists changed by prefer_now
   [ '$destroy_on_change' => 'undef' ],   # optional list of things to be destroyed upon any change at the top position
                                          # their destructors are supposed to invalidate caches dependent on the top position item
);

# private:
sub register_copy {
   my ($self, $src) = @_;
   ++($_->controls->{$self}) for @{$src->labels};
}
####################################################################################
sub init {
   my ($label, $self, $item) = @_;
   @$self == 0
     or Carp::confess( "internal error: passing a non-empty anonymous array to Label::add_control" );
   @$self = ( [ $item ], [ $label ], defined($label->clock), undef, undef );
   bless $self;
}

sub clone {
   my ($self) = @_;
   inherit_class([ [ @{$self->items} ], [ @{$self->labels} ], $self->ordered, undef, undef ], $self);
}

sub dup {
   my ($self) = @_;
   my $copy = &clone;
   register_copy($copy, $self);
   $copy;
}
####################################################################################
# private:
sub merge_items {
   my ($self, $src) = @_;

   if (!$src->ordered ||
       $self->ordered && $self->labels->[0]->clock > $src->labels->[0]->clock) {
      # src unordered - attach to the tail
      push @{$self->items}, @{$src->items};
      push @{$self->labels}, @{$src->labels};

   } elsif (!$self->ordered  ||
            $src->ordered && $self->labels->[0]->clock < $src->labels->[0]->clock) {
      # self unordered - insert the new items at the beginning
      unshift @{$self->items}, @{$src->items};
      unshift @{$self->labels}, @{$src->labels};
      $self->ordered = $src->ordered;

   } else {
      # both ordered: merge carefully
      my $self_ord = $self->ordered;
      my ($s, $d) = (0, 0);
      for (; $s < $src->ordered && $d < $self_ord;  ++$d) {
         if ($self->labels->[$d]->rank > $src->labels->[$s]->rank) {
            splice @{$self->items}, $d, 0, $src->items->[$s];
            splice @{$self->labels}, $d, 0, $src->labels->[$s];
            ++$s;
            ++$self_ord;
         }
      }
      $self->ordered += $src->ordered;
      # insert the higher-ranked and unordered rest
      my $last = $#{$src->items};
      splice @{$self->items}, $self_ord, 0, @{$src->items}[$s..$last];
      splice @{$self->labels}, $self_ord, 0, @{$src->labels}[$s..$last];
   }
}
####################################################################################
# protected:
sub merge {
   my $src = shift;
   my $self = $src->dup;
   my ($clean_merge, @remerge);
   if (defined($src->cleanup_table)) {
      # $src has been temporarily modified by prefer_now: this list has to be re-merged after leaving the scope
      $self->cleanup_table = $src->cleanup_table;
      $self->cleanup_table->{$self} = $clean_merge = new ControlList;
      push @remerge, $src;
   }
   foreach $src (@_) {
      if (defined($src->cleanup_table)) {
         # $src has been temporarily modified by prefer_now: this list has to be re-merged after leaving the scope
         if (@remerge) {
            # record the cleanup table of the innermost scope
            if ($src->labels->[0]->clock > $self->labels->[0]->clock) {
               $self->cleanup_table = $src->cleanup_table;
            }
         } else {
            $clean_merge = clone($self);
            $self->cleanup_table = $src->cleanup_table;
         }
         $src->cleanup_table->{$self} //= $clean_merge;
         push @remerge, $src;
      } elsif (defined($clean_merge)) {
         merge_items($clean_merge, $src);
      }
      register_copy($self, $src);
      merge_items($self, $src);
   }
   $clean_merge->cleanup_table = \@remerge if defined($clean_merge);
   $self
}
####################################################################################
# called from Scope destructor
# merge the items afresh after the source lists have been reverted
sub cleanup {
   my ($self, $clean_merge) = @_;
   undef $self->cleanup_table;
   undef $self->destroy_on_change;
   my @remerge;
   foreach my $src (@{$clean_merge->cleanup_table}) {
      if (defined($src->cleanup_table)) {
         # $src has been temporarily modified in the outer scope
         if (@remerge) {
            # record the cleanup table of the innermost scope
            if ($src->labels->[0]->clock > $self->labels->[0]->clock) {
               $self->cleanup_table = $src->cleanup_table;
            }
         } else {
            $self->cleanup_table = $src->cleanup_table;
            @{$self->items} = @{$clean_merge->items};
            @{$self->labels} = @{$clean_merge->labels};
            $self->ordered = $clean_merge->ordered;
         }
         $src->cleanup_table->{$self} //= $clean_merge;
         push @remerge, $src;
      } else {
         merge_items($clean_merge, $src);
      }
      merge_items($self, $src) if @remerge;
   }
   if (@remerge) {
      $clean_merge->cleanup_table = \@remerge;
   } else {
      # $clean_merge can be recycled
      $self->items = $clean_merge->items;
      $self->labels = $clean_merge->labels;
      $self->ordered = $clean_merge->ordered;
   }
}
####################################################################################
sub DESTROY {
   my ($self)=@_;
   delete $_->controls->{$self} for @{$self->labels};
}

END { undef &DESTROY; }
####################################################################################
# return the items from a control list, sorted by rank
# each bag starts with the rank value
# the last bag may contain unordered items (and rank is undef)
sub get_items_by_rank {
   my ($self)=@_;
   my (%seen, @bags);
   my $pos;
   for ($pos=0; $pos < $self->ordered; ++$pos) {
      my $cur_rank=$self->labels->[$pos]->rank;
      if (!@bags) {
         push @bags, [ $cur_rank ];
      } elsif (@{$bags[-1]}==1) {
         $bags[-1]->[0]=$cur_rank;
      } elsif ($bags[-1]->[0]<$cur_rank) {
         push @bags, [ $cur_rank ];
      }
      push @{$bags[-1]}, $self->items->[$pos] unless $seen{$self->items->[$pos]}++;
   }
   my $end = @{$self->items};
   if ($pos < $end) {
      if (!@bags || @{$bags[-1]}>1) {
         push @bags, [ undef ];
      } else {
         undef $bags[-1]->[0];
      }
      for (; $pos < $end; ++$pos) {
         push @{$bags[-1]}, $self->items->[$pos] unless $seen{$self->items->[$pos]}++;
      }
   }
   @bags;
}
####################################################################################
sub find_item_of_label {
   my ($self, $label)=@_;
   if ((my $pos=list_index($self->labels, $label)) >= 0) {
      $self->items->[$pos]
   } else {
      undef
   }
}
####################################################################################
package Polymake::Core::Preference::Label;

use Polymake::Struct (
   [ new => '$;$$$$' ],
   [ '$name' => '#1' ],
   [ '$parent' => 'weak(#2)' ],         # Label higher in the hierarchy
   [ '$wildcard_name' => '#3 || "*"' ],
   '%children',                         # Labels lower in the hierarchy
   '%controls',                         # control list -> number of items in this list
   [ '$clock' => '#4' ],                # sequential number of the last 'prefer' command
   [ '$rank' => '#5' ],                 # rank in this command
   [ '$application' => 'undef' ],       # Application and ...
   [ '$extension' => 'undef' ],         # Extension where this label occurs
);

####################################################################################
sub set_application {
   my ($self, $app, $ext)=@_;
   while ($self->parent) {
      if ($self->application) {
         if ($self->application != $app && $self->application->imported->{$app->name}) {
            $self->application=$app;
         }
         last;
      }
      $self->application=$app;
      $self->extension=$ext;
      $self=$self->parent;
   }
}
####################################################################################
sub child {
   my ($self, $name)=@_;
   $self->children->{$name} ||=
      new Label( $name, $self, $self->wildcard_name.".$name", $self->clock, $self->rank );
}
####################################################################################
sub add_control {
   my ($self, $list, $item)=@_;
   ++$self->controls->{$list};
   if (is_object($list)) {
      my $pos = @{$list->items};
      if (defined($self->clock)) {
         if ($list->ordered == 0  or
             (my $clock_diff = $list->labels->[0]->clock - $self->clock) < 0) {
            $pos = 0;
            $list->ordered = 1;
         } elsif ($clock_diff == 0) {
            for ($pos = 0;
                 $pos < $list->ordered && $list->labels->[$pos]->rank <= $self->rank;
                 ++$pos) { }
            ++$list->ordered;
         }
      }
      splice @{$list->items}, $pos, 0, $item;
      splice @{$list->labels}, $pos, 0, $self;
   } else {
      &ControlList::init;
   }
}
####################################################################################
sub list_all_rules {
   my ($self, $others) = @_;
   my @rules;
   while (my ($list, $cnt) = each %{$self->controls}) {
      for (my ($pos, $end) = (0, scalar @{$list->items}); $pos < $end; ++$pos) {
         if (instanceof Rule(my $rule=$list->items->[$pos])) {
            if ($others) {
               if ($list->labels->[$pos] != $self) {
                  push @rules, $rule;
               }
            } else {
               if ($list->labels->[$pos] == $self) {
                  push @rules, $rule;
                  --$cnt or last;
               }
            }
         }
      }
   }
   keys %{$self->controls};  # reset iterator
   (@rules, map { list_all_rules($_, $others) } values %{$self->children})
}
####################################################################################
# clock, rank => clock values of preference lists having lost effect
sub set_preferred {
   my $self=shift;
   my @out_of_effect;
   if (defined $self->clock) {
      if ($self->clock==$_[0]) {
         warn_print( $self->full_name, " occurs in the preference list at positions ", $self->rank, " and $_[1]" );
         return;
      }
      push @out_of_effect, $self->clock;
   }
   ($self->clock, $self->rank)=@_;
   while (my ($list, $cnt)=each %{$self->controls}) {
      # what has to be done with this control list?
      if ($list->ordered) {
         my $clock_cmp=$list->labels->[0]->clock <=> $self->clock;
         if ($clock_cmp<0) {
            # the control list is obsolete, current label moves to its head
            push @out_of_effect, $list->labels->[0]->clock;
            $list->ordered=0;
         } elsif ($clock_cmp>0) {
            # default preference from the rules defeated by an active setting
            push @out_of_effect, $self->clock;
            next;
         } elsif ($list->labels->[0]==$self) {
            # nothing changes
            $list->ordered=$cnt;
            next;
         }
      }

      # lift all occurences of the current label to the bottom end of the active region of the control list
      my $new_pos = $list->ordered;
      $list->destroy_on_change = undef if $new_pos == 0;
      $list->ordered += $cnt;
      for (my ($pos, $end) = ($new_pos, scalar @{$list->items}); $pos < $end; ++$pos) {
         if ($list->labels->[$pos] == $self) {
            if ($pos != $new_pos) {
               splice @{$list->items}, $new_pos, 0, splice @{$list->items}, $pos, 1;
               splice @{$list->labels}, $new_pos, 0, splice @{$list->labels}, $pos, 1;
            }
            --$cnt or last;
            ++$new_pos;
         }
      }
      Carp::confess( "corrupted control list for label ", $self->full_name ) if $cnt;
   }

   (@out_of_effect, map { $_->set_preferred(@_) } values %{$self->children})
}
####################################################################################
sub neutralize_controls {
   my ($self, $deep)=@_;
   foreach my $list (keys %{$self->controls}) {
      if ($list->ordered && $list->labels->[0]==$self) {
         $list->ordered=0;
      }
   }

   if ($deep) {
      foreach my $c (values %{$self->children}) {
         neutralize_controls($c, $deep);
      }
   }
}
####################################################################################
sub set_temp_preferred {
   my ($self, $scope, $clock, $rank) = @_;
   local with($scope->locals) {
      local scalar $self->rank = $rank;
      local scalar $self->clock = $clock;
   }

   while (my ($list, $cnt) = each %{$self->controls}) {
      my ($new_items, $new_labels, $new_ordered);
      if (@{$list->items} == $cnt) {
         # no competitors
         if ($list->ordered) {
	    # nothing to do
            next;
	 } else {
	    $new_items = [ @{$list->items} ];
            $new_labels = [ @{$list->labels} ];
            $new_ordered = $cnt;
         }
      } else {
         if ($rank && $list->labels->[0]->clock == $clock && $list->labels->[0]->rank < $rank) {
            # already modified this list via a higher-ranked label
            $list->cleanup_table == $scope->cleanup
              or Carp::confess( "internal error: cleanup table mismatch: ", $list->cleanup_table, " instead of ", $scope->cleanup );
            $new_items = $list->items;
            $new_labels = $list->labels;
            $new_ordered = $list->ordered;
         } else {
            $new_items = [ @{$list->items} ];
            $new_labels = [ @{$list->labels} ];
            $new_ordered = 0;
         }
         my $higher = $new_ordered;
         for (my ($i, $end) = ($new_ordered, scalar @$new_items); $i < $end; ++$i) {
            if ($new_labels->[$i] == $self) {
               if ($i != $new_ordered) {
                  # put the controlled item in the first position
                  splice @$new_items, $new_ordered, 0, splice @$new_items, $i, 1;
                  splice @$new_labels, $new_ordered, 0, splice @$new_labels, $i, 1;
               }
               ++$new_ordered;
               last unless --$cnt;
            }
         }
         $cnt == 0
           or Carp::confess( "corrupted control list for label ", $self->full_name );
      }
      undef $list->destroy_on_change;
      if ($list->cleanup_table == $scope->cleanup) {
         # already modified in this scope
         if ($new_items != $list->items) {
            $list->items = $new_items;
            $list->labels = $new_labels;
         }
         $list->ordered = $new_ordered;
      } else {
         local with($scope->locals) {
            local ref $list->items = $new_items;
            local ref $list->labels = $new_labels;
            local scalar $list->ordered = $new_ordered;
            local scalar $list->cleanup_table = $scope->cleanup;
            local scalar $list->destroy_on_change = undef;
         }
      }
   }

   foreach my $c (values %{$self->children}) {
      $c->set_temp_preferred($scope, $clock, $rank);
   }
}
####################################################################################
sub full_name {
   my ($self)=@_;
   my $n=$self->name;
   while (defined($self=$self->parent)) {
      $n=$self->name.".$n";
   }
   $n
}

sub parent_name {
   my ($self)=@_;
   $self=$self->parent while defined($self->parent);
   $self->name
}
####################################################################################
sub descend {
   my $self=shift;
   foreach (@_) {
      $self=$self->children->{$_} or return;
   }
   $self
}

sub list_completions {
   my $prefix=pop;
   my $self=&descend or return ();
   grep { /^\Q$prefix\E/ } keys %{$self->children}
}
####################################################################################
#
#  Subtraction of preference lists

# => 1 - nothing more in effect
# => 0 - partially (not all controls or not all children)
# => 2 - fully
sub status {
   my ($self, $clock) = @_;
   my $status = 3;
   foreach my $list (keys %{$self->controls}) {
      $status &= ($list->ordered && $list->labels->[0]->clock == $clock) ? 2 : 1
        or do { keys %{$self->controls}; return 0; };
   }

   foreach my $c (values %{$self->children}) {
      $status &= status($c, $clock)
        or do { keys %{$self->children}; return 0; };
   }

   $status
}
####################################################################################
sub add_to_pref_tree {
   my ($self, $list) = @_;
   if (is_array($list)) {
      push @$list, $self;
   } else {
      while (my ($name, $c) = each %{$self->children}) {
         if (!exists $list->{$name} || is_array($list->{$name})) {
            push @{$list->{$name}}, $c;
         } elsif ($list->{$name}) {
            add_to_pref_tree($c, $list->{$name});
         }
      }
   }
}
####################################################################################
sub subtract {
   my ($self, $clock, $new_wildcard, $wildcard_cmp, $tree) = @_;

   if ($self->clock != $clock) {
      # already involved in the new pref list - nothing to do
      return;
   }

   if (defined(my $subtree = $tree->{$self->name})) {
      if ($subtree) {
         # positive result already known
         add_to_pref_tree($self, $subtree);
      } else {
         # negative result already known
         neutralize_controls($self, 1);
      }
      return;
   }

   if ($wildcard_cmp <= 0  and
       ($wildcard_cmp = prefix_cmp($self->wildcard_name, $new_wildcard, ".")) == 2) {
      # no intersection with new pref list - remains in effect
      $tree->{$self->name} = [ $self ];
      return;
   }

   my $status = status($self, $clock);
   if ($status & 1) {
      # completely out of control
      $tree->{$self->name} = 0;
      neutralize_controls($self, 1);

   } elsif ($status != 0) {
      if ($wildcard_cmp > 0) {
         # this branch has survived
         $tree->{$self->name} = [ $self ];
      }

   } else {
      # injured - handle children individually
      my $subtree = $tree->{$self->name} = { };
      neutralize_controls($self);
      subtract($_, $clock, $new_wildcard, $wildcard_cmp, $subtree) for values %{$self->children};
   }
}
####################################################################################
package Polymake::Core::Preference::List;

use Polymake::Struct (
   [ new => '$@' ],
   [ '$clock' => '#1' ],
   [ '$provenience' => '0' ],
   [ '@labels' => '@' ],
);

sub activate {
   my ($self)=@_;
   my $rank=0;
   map { $_->set_preferred($self->clock, $rank++) } @{$self->labels};
}

sub deactivate {
   my ($self)=@_;
   foreach (@{$self->labels}) {
      if ($_->clock == $self->clock) {
         $_->neutralize_controls(1);
      }
   }
}
####################################################################################
sub compare {
   my ($p1, $p2)=@_;
   my $l=$#{$p1->labels};
   return 2 if $l != $#{$p2->labels};
   my $result=0;
   for (my $i=0; $i<=$l; ++$i) {
      if ($p1->labels->[$i] != $p2->labels->[$i]) {
         my $cmp=prefix_cmp($p1->labels->[$i]->full_name, $p2->labels->[$i]->full_name, ".");
         return 2 if $cmp==2  or  $result && $result != $cmp;
         $result=$cmp;
      }
   }
   $result;
}
####################################################################################
sub subtract {
   my ($self, $new_wildcard) = @_;
   my (@result, %tree);
   $_->subtract($self->clock, $new_wildcard, -1, \%tree) for @{$self->labels};
   my @sublists = values %tree;
   for (my $i = 0; $i <= $#sublists; ++$i) {
      my $list = $sublists[$i];
      if (is_array($list)) {
         push @result, new List($self->clock, @$list);
      } elsif ($list) {
         push @sublists, values %$list;
      }
   }
   @result;
}
####################################################################################
sub toString {
   my ($self) = @_;
   if (@{$self->labels} == 1) {
      $self->labels->[0]->full_name
   } else {
      $self->labels->[0]->wildcard_name . " " . join(", ", map { $_->parent_name } @{$self->labels})
   }
}
sub toQuotedString {
   '"' . &toString . '"'
}
####################################################################################
sub belongs_to {
   my ($self, $app) = @_;
   my $answer = false;
   foreach my $label (@{$self->labels}) {
      if (defined($label->application))  {
         if ($label->application == $app) {
            $answer = true;
         } else {
            $app->imported->{$label->application->name} or return false;
         }
      } else {
         warn_print( "label ", $label->full_name, " might to be obsolete" ) if $DeveloperMode && ! -d "$InstallTop/bundled/".($label->parent_name);
         return false;
      }
   }
   $answer;
}

sub visible_from {
   my ($self, $app)=@_;
   foreach my $label (@{$self->labels}) {
      next unless defined($label->application);
      return 0 unless $label->application==$app || $app->imported->{$label->application->name};
   }
   1
}
####################################################################################
package Polymake::Core::Preference::perApplication;

use Polymake::Struct (
   [ new => '$' ],
   [ '$application' => 'weak(#1)' ],
   '@imported',                      # perApplication objects of imported applications
   '%labels',
   '@default_prefs',
);

####################################################################################
sub find_label {
   my ($self, $name, $create) = @_;
   ($name, my @sublevels) = split /\./, $name;
   my $label = $self->labels->{$name};
   unless ($label) {
      foreach (@{$self->imported}) {
         $label=$_->labels->{$name}  and  last;
      }
      return unless $label;
   }
   foreach $name (@sublevels) {
      $label = $create ? $label->child($name) : $label->children->{$name}
      or return;
   }
   $label;
}
####################################################################################
# private:
sub list_completions {
   my ($self, $expr) = @_;
   if (length($expr)) {
      if ($expr =~ s/^\*\.($hier_id_re)\s+//o) {
         my @sublevels=split /\./, $1, -1;
         my %ignore;
         while ($expr =~ s/^($id_re)\s*,\s*//go) {
            $ignore{$1}=1;
         }
         grep { !$ignore{$_} && /^\Q$expr\E/ } map { $_->descend(@sublevels) ? ($_->name) : () } values %{$self->labels}
      } else {
         my ($top, @sublevels)=split /\./, $expr, -1;
         if (@sublevels) {
            if ($top eq "*") {
               map { $_->list_completions(@sublevels) } values %{$self->labels}
            } else {
               my $label=$self->labels->{$top} or return ();
               $label->list_completions(@sublevels)
            }
         } else {
            grep { /^\Q$top\E/ } keys %{$self->labels}
         }
      }
   } else {
      keys %{$self->labels}
   }
}
####################################################################################
# private:
sub parse_label_expr {
   my ($self, $expr, $mode) = @_;
   my (@err, @l);
   my $create_sublevels = $mode & Mode::create;

   if ($expr =~ /^ $hier_id_re $/xo) {
      if (defined (my $label = find_label($self, $expr, $create_sublevels))) {
         return $label;
      } else {
         push @err, $expr;
      }
   } elsif (my ($sublevel, $list) = $expr =~ /^ \*\.($hier_id_re) \s+ ($hier_ids_re) $/xo) {
      @l=map { find_label($self, "$_.$sublevel", $create_sublevels) or
               push @err, "$_.$sublevel" and ()
         } split /\s*,\s*/, $list;
   } else {
      croak( "syntax error in preference list" );
   }

   if (@err) {
      if ($mode & Mode::rules) {
         croak( "unknown label", @err > 1 && "s", " @err" );
      } else {
         warn_print( "stored preference statements for label", @err > 1 && "s", " @err\n",
                     "are not in effect - probably excluded by auto-configuration" );
         $Prefs->changed = true;
      }
   }

   @l
}
####################################################################################
sub add_preference {
   my ($self, $expr, $mode) = @_;
   my @l = &parse_label_expr or return;
   my $pref = new List($mode & Mode::rules ? ++$compile_clock : ++$clock, @l);
   $pref->provenience = $mode & (Mode::rules + Mode::imported);

   if ($mode & Mode::rules) {
      push @{$self->default_prefs}, $pref;
      # activate this preference right now if the application
      # is already active, otherwise end_loading will do this
      if (contains($Prefs->applications,$self)) {
         $Prefs->activate(false, $pref);
      }
   } else {
      if (defined(my $dominating = $Prefs->check_repeating($pref))) {
         if ($mode == Mode::create) {
            # loading private file
            warn_print( "preference list ", $pref->toQuotedString, " ignored since another list ", $dominating->toQuotedString, " is already in effect" );
            $Prefs->changed = true;
         }
         return;
      }
      $Prefs->changed = true if $mode == Mode::strict;
      $Prefs->activate(false, $pref);
   }
}
####################################################################################
sub set_temp_preference {
   my ($self, $scope, $expr) = @_;
   my @l = parse_label_expr($self, $expr, Mode::rules);
   local with($scope->locals) {
      local scalar ++$clock;
   }
   my $rank = 0;
   $_->set_temp_preferred($scope, $clock, $rank++) for @l;
}
####################################################################################
# private:
sub matching_default_prefs {
   my ($self, $expr) = @_;
   my @matched;
   if ($expr =~ /^ $hier_id_re $/xo) {
      foreach my $pref (@{$self->default_prefs}) {
         my $cmp = prefix_cmp($expr, $pref->labels->[0]->full_name, ".");
         if ($cmp == 0) {
            # exact match
            return $pref;
         }
         if ($cmp == 1) {
            # re-activating at a sublevel
            if (defined (my $label = find_label($self, $expr))) {
               return new List(++$clock, $label);
            }
         } elsif ($cmp == -1) {
            # re-activating this list and maybe more others
            push @matched, $pref;
         }
      }

   } elsif ($expr =~ /^ \*\.$hier_id_re $/xo) {
      foreach my $pref (@{$self->default_prefs}) {
         my $cmp = prefix_cmp($expr, $pref->labels->[0]->wildcard_name, ".");
         if ($cmp == 0) {
            # exact match
            return $pref;
         }
         if ($cmp == 1) {
            # re-activating at a sublevel
            my @sublevels = split /\./, substr($expr, length($pref->labels->[0]->wildcard_name)+1);
            if (my @sublabels = map { $_->descend(@sublevels) } @{$pref->labels}) {
               return new List(++$clock, @sublabels);
            }
         } elsif ($cmp == -1) {
            push @matched, $pref;
         }
      }

   } else {
      croak( "invalid label expression '$expr'" );
   }
   @matched;
}
####################################################################################
sub list_active {
   my ($self) = @_;
   map { $_->toQuotedString . (($_->provenience == Mode::rules) ? " (#)" : ($_->provenience == Mode::imported) ? " (I)" : "") }
   grep { $_->visible_from($self->application) } @{$Prefs->active_prefs};
}
####################################################################################
sub end_loading {
   my ($self) = @_;
   push @{$Prefs->applications}, $self;

   $Prefs->activate(false, @{$self->default_prefs});
   if (defined(my $imported_list = $Prefs->imported_prefs->{$self->application->name})) {
      add_preference($self, $_, Mode::create + Mode::imported) for @$imported_list;
   }
   if (defined(my $private_list = $Prefs->private_prefs->{$self->application->name})) {
      add_preference($self, $_, Mode::create) for @$private_list;
   }
}
####################################################################################

package Polymake::Core::Preference;

use Polymake::Struct (
   '@applications',                  # perApplication
   '$changed',                       # boolean
   '@active_prefs',
   '%imported_prefs',
   '%private_prefs',
);

add_settings_callback sub {
   my ($settings) = @_;
   $settings->add_item(settings_key, $Prefs->private_prefs,
      "Active non-default preferences by application",
      UserSettings::Item::Flags::hidden,
      exporter => sub {
         $Prefs->export_active($_[0])
      },
      importer => sub {
         my ($value, $is_imported) = @_;
         if ($is_imported) {
            while (my ($app, $list) = each %$value) {
               push @{$Prefs->imported_prefs->{$app} //= [ ]}, @$list;
            }
         } else {
            push %{$Prefs->private_prefs}, %$value;
         }
      });
   add AtEnd("Preference", sub { $Prefs->prepare_stored if $Prefs->changed }, before => "Settings");
};

sub app_handler { new perApplication($_[1]) }

####################################################################################
sub activate {
   my ($self, $incr_clock) = splice @_, 0, 2;
   foreach my $pref (@_) {
      push @{$self->active_prefs}, $pref;
      $pref->clock = ++$clock if $incr_clock;

      if (my %old_clocks = map { $_ => 1 } $pref->activate) {
         # some older preference lists need modification or even must be discarded
         my $new_wildcard = $pref->labels->[0]->wildcard_name;
         for (my $i = $#{$self->active_prefs}-1; $i >= 0; --$i) {
            my $old_pref = $self->active_prefs->[$i];
            if ($old_clocks{$old_pref->clock}) {
               splice @{$self->active_prefs}, $i, 1, $old_pref->subtract($new_wildcard);
            }
         }
      }
   }
}
####################################################################################
sub check_repeating {
   my ($self, $pref) = @_;
   foreach my $p (@{$self->active_prefs}) {
      my $cmp = $p->compare($pref);
      if ($cmp <= 0) {
         return ($cmp, $p);
      } elsif ($cmp == 1) {
         # has absorbed some existing preference list - can't have duplicates
         last;
      }
   }
   ()
}
####################################################################################
sub reset {
   my ($self, $app, $expr)=@_;
   if (my @prefs = map { $_->matching_default_prefs($expr) } reverse($app->prefs, @{$app->prefs->imported})) {
      activate($self, true,
               grep {
                  if (my ($cmp, $p) = check_repeating($self, $_)) {
                     $p->provenience = Mode::rules if $cmp == 0;
                     false
                  } else {
                     $self->changed = true
                  }
               } @prefs);

   } else {
      my $matched = 0;
      my @sublevels = split /\./, $expr;
      my $use_wildcard = $sublevels[0] eq "*" and shift @sublevels;
      for (my $i = $#{$self->active_prefs}; $i >= 0; --$i) {
         my $pref = $self->active_prefs->[$i];
         if ($pref->provenience != Mode::rules && $pref->visible_from($app)) {
            if ($use_wildcard
                ? prefix_cmp($expr, $pref->labels->[0]->wildcard_name, ".") <= 0
                : @{$pref->labels} == 1 && prefix_cmp($expr, $pref->labels->[0]->full_name, ".") <= 0) {
               $pref->deactivate;
               splice @{$self->active_prefs}, $i, 1;
               $matched = true;
            } elsif ($use_wildcard and
                     $pref->labels->[0]->wildcard_name eq "*"
                     ? $pref->labels->[0]->descend(@sublevels)
                     : prefix_cmp($pref->labels->[0]->wildcard_name, $expr, ".") <= 0) {
               splice @{$self->active_prefs}, $i, 1, $pref->subtract($expr);
               $matched = true;
            }
         }
      }
      if ($matched) {
         $self->changed = true;
         ++$clock;
      } else {
         croak( "no active or default preferences matching '$expr'" );
      }
   }
}
####################################################################################
sub reset_all {
   my ($self, $app) = @_;
   for (my $i = $#{$self->active_prefs}; $i >= 0; --$i) {
      my $pref = $self->active_prefs->[$i];
      if ($pref->provenience != Mode::rules && $pref->visible_from($app)) {
         $pref->deactivate;
         splice @{$self->active_prefs}, $i, 1;
         $self->changed = true;
      }
   }
   foreach (reverse($app->prefs, @{$app->prefs->imported})) {
      activate($self, true, grep { !check_repeating($self,$_) and $self->changed = true } @{$_->default_prefs});
   }
}
####################################################################################
sub obliterate_extension {
   my ($self, $ext) = @_;
   for (my $i = $#{$self->active_prefs}; $i >= 0; --$i) {
      my $pref = $self->active_prefs->[$i];
      my $cnt = 0;
      foreach (@{$pref->labels}) {
         ++$cnt if $_->extension == $ext;
      }
      if ($cnt == @{$pref->labels}) {
         $pref->deactivate;
         splice @{$self->active_prefs}, $i, 1;
         $self->changed = true;
      } elsif ($cnt) {
         splice @{$self->active_prefs}, $i, 1;
         activate($self, false, new List(++$clock, grep { $_->extension != $ext } @{$pref->labels}));
         $self->changed = true;
      }
   }
}

sub obliterate_application {
   my ($self, $per_app) = @_;
   delete_from_list($self->applications, $per_app)
     and $self->changed = true;
}
####################################################################################
sub serialize_active {
   my ($self, $out, $exclude_provenience) = @_;
   my @active_prefs = grep { !($_->provenience & $exclude_provenience) } @{$self->active_prefs};
   foreach my $per_app (@{$self->applications}) {
      my $app = $per_app->application;
      my @list;
      for (my $i = 0; $i <= $#active_prefs; ) {
         my $pref = $active_prefs[$i];
         if ($pref->belongs_to($app)) {
            push @list, $pref->toString;
            splice @active_prefs, $i, 1;
         } else {
            ++$i;
         }
      }
      if (@list) {
         $out->{$app->name} = \@list;
      } else {
         delete $out->{$app->name};
      }
   }
}

sub prepare_stored {
   my ($self) = @_;
   serialize_active($self, $self->private_prefs, Mode::rules + Mode::imported);
   $self->changed = false;
}

sub export_active {
   my ($self, $include_imported) = @_;
   my %exported;
   serialize_active($self, \%exported, $include_imported ? Mode::rules : Mode::rules + Mode::imported);
   keys %exported ? \%exported : ()
}

####################################################################################
# merge several control lists together
# If some of input lists is temporarily changed by prefer_now,
# the resulting list will be re-merged after the changes are reverted when the enclosing scope is left.
sub merge_controls {
   @_ <= 1 ? $_[0] : &ControlList::merge
}

1

# Local Variables:
# cperl-indent-level:3
# indent-tabs-mode:nil
# End:
