package DX::State;

use Return::MultiLevel qw(with_return);
use DX::Op::FromCode;
use Scalar::Util qw(blessed);
use Moo;

has next_op => (is => 'ro', required => 1);

has return_stack => (is => 'ro', required => 1);

has by_id => (is => 'ro', required => 1);

has scope => (is => 'ro', required => 1);

has last_choice => (is => 'ro', required => 1);

has id_gen => (is => 'ro', default => sub { {} });

sub scope_var {
  my ($self, $name) = @_;
  $self->by_id->{$self->scope->{$name}};
}

sub allocate_var {
  my ($self, $name, $var) = @_;
  my $id = join('_', $name, ++($self->id_gen->{$name}));
  DX::Var->new(id => $id, %$var);
}

sub assign_vars {
  my ($self, %vars) = @_;
  my %by_id = %{$self->by_id};
  foreach my $name (keys %vars) {
warn "assign: ${name}";
    unless (blessed($vars{$name})) {
      my $var = $vars{$name} = $self->allocate_var($name, $vars{$name});
      $by_id{$var->id} = $var;
    }
  }
  $self->but(
    by_id => \%by_id,
    scope => { %{$self->scope}, map +($_ => $vars{$_}->id), keys %vars }
  );
}

sub bind_var_then {
  my ($self, $var, $value, $then) = @_;
  warn "Binding ".$var->id." to $value";
  my $bound = $var->with_value($value);
  $self->but(
    by_id => { %{$self->by_id}, $var->id => $bound },
    next_op => $then
  );
}

sub bind_stream_then {
  my ($self, $var, $stream, $then) = @_;
  warn "Binding ".$var->id." to $stream";
  my $bound = $var->with_stream($stream);
  $self->but(
    by_id => { %{$self->by_id}, $var->id => $bound },
    next_op => $then
  )->mark_choice($bound);
}

sub mark_choice {
  my ($self, $var) = @_;
  $self->but(last_choice => [ $self, $var ]);
}

sub backtrack {
  my ($self) = @_;
  my ($state, $var) = ($self);
  while (($state, $var) = @{$state->last_choice}) {
    $var->bound_value; $var->clear_bound_value;
    return $state->mark_choice($var) unless $var->bound_stream->is_exhausted;
  }
  $self->return_from_run(undef);
}

sub then {
  my ($self, $then) = @_;
  $self->but(next_op => $then);
}

sub return_from_run {
  my (undef, $return) = @_;
  (our $Nonlocal_Return)->($return);
}

sub run {
  my ($state) = @_;
  with_return {
    my ($return) = @_;
    local our $Nonlocal_Return = $return;
    while (my $op = $state->next_op) {
#::Dwarn($op);
      $state = $op->run($state);
    }
    return $state;
  }
}

sub push_backtrack {
  $_[0]->then(DX::Op::FromCode->new(code => sub { $_[1]->backtrack }));
}

sub but {
  my ($self, @but) = @_;
  $self->new(%$self, @but);
}

sub pop_return_stack {
  my ($self) = @_;
  my @stack = @{$self->return_stack};
  my $top = pop @stack;
  $self->but(return_stack => \@stack, next_op => $top);
}

sub push_return_then {
  my ($self, $return, $then) = @_;
  $self->but(
    return_stack => [ @{$self->return_stack}, $return ],
    next_op => $then
  );
}

1;
