package DX::State;

use Return::MultiLevel qw(with_return);
use DX::Op::Backtrack;
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 { {} });

has rule_set => (is => 'ro');

has facts => (is => 'ro');

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

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

sub expand_vars {
  my ($self, %vars) = @_;
  my %by_id = %{$self->by_id};
  my %id_gen = %{$self->id_gen};
  foreach my $name (keys %vars) {
    unless (blessed($vars{$name})) {
      my $var = $vars{$name} = $self->allocate_var(
        $name, $vars{$name}, \%id_gen
      );
      $by_id{$var->id} = $var;
    }
  }
  $self->but(
    by_id => \%by_id, id_gen => \%id_gen,
  ), %vars;
}

sub assign_vars {
  my ($self, %vars) = @_;
  my ($state, %expanded) = $self->expand_vars(%vars);
  $state->but(
    scope => { %{$self->scope}, map +($_ => $expanded{$_}->id), keys %expanded }
  );
}

sub bind_value {
  my ($self, $var, $value) = @_;
  my $bound = $var->with_value($value);
  $self->but(
    by_id => { %{$self->by_id}, $var->id => $bound },
  );
}

sub bind_stream_then {
  my ($self, $var, $stream, $then) = @_;
  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 $Run_Return)->($return);
}

sub return_from_op {
  my (undef, $return) = @_;
  if (our $Op_Return) {
    $Op_Return->($return);
  }
}

sub run {
  my ($state) = @_;
  with_return {
    my ($return) = @_;
    local our $Run_Return = $return;
    while (my $op = $state->next_op) {
      my $backtrack = with_return {
        my ($return) = @_;
        local our $Op_Return = $return;
        $state = $op->run($state);
        return;
      };
      if ($backtrack) { $state = $state->backtrack }
    }
    return $state;
  }
}

sub push_backtrack {
  $_[0]->then(DX::Op::Backtrack->new);
}

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->[0]);
}

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

1;
