#!/usr/bin/perl

sub canonical;

my %tile = 
  (A => [3, 11, 7],
   B => ['3*', 10, 8],
   C => [3, '0*', 7, 7],
   E => ['3*', 2, 10, 8, 8],
   F => [3, 3, 11, 9, 7],
   G => [3, '0*', 9, '6*'],
  );

expand_set(\%tile);
$tile{D} = $tile{C};

#my $target = canonical [6, 4, 4, 2, 2, 0, 10, 10, 8, 8];
my $target = canonical $tile{D}[0];

# A state consists of:
# 1. The current agglomeration of tiles so far
# 2. A hash indicating which primitive tiles have been used up
my $root = [[], {C => undef, D => undef}];

sub dfs {
  my ($root, $children, $aha) = @_;
  my @queue = $root;
  while (@queue) {
    my $next = shift @queue;
    my $s = tile_to_string($next->[0]);
    warn "> $s\n";
    next if $seen{$s}++;
    if ($aha->($next)) {
      return $next;
    } else {
      push @queue, $children->($next);
    }
  }
  return;
}

my $result = dfs($root, \&add_tile, \&win);

if (defined $result) {
  print "Yes: ", join(" ", keys %{$result->[1]}), "\n";

} else {
  print "No.\n";
}

################################################################


sub add_tile {
  my $state = shift;
  my ($c, $used) = @$state;
  my @new;
  my ($i, $n, $action);
  
  if (defined($i = next_to_eliminate($c, $target))) {
    $n = (6 + $c->[$i]) % 12;
    $action = "eliminate";
  } elsif (defined($n = next_to_introduce($c, $target))) {
    $action = "introduce";
  } else { "???" }

  for my $name (keys %tile) {
    next if $used->{$name};  # Can't use same tile twice
    my %used = (%$used, $name => 1);
    for my $tilepos (@{$tile{$name}}) {
      for my $tp (adjust_to($n, $tilepos)) {
        my $new_conf;
        if ($action eq "eliminate") {
          $new_conf = insert_into($c, $i, $tp);
        } elsif ($action eq "introduce") {
          $new_conf = $tp;
        }
        push @new, [$new_conf, \%used];
      }
    }
  }

  return @new;
}

sub win {
  my $state = shift;
  my ($c, $used) = @$state;
  return if defined next_to_eliminate($c, $target);
  return $#$c == $#$target;
}

################################################################

# Look through the tile for a specified value
# reorganize the tile so that that value is first
sub adjust_to {
  my ($val, $tile) = @_;
  my @adjusted;
  for my $i (0 .. $#$tile) {
    next unless $tile->[$i] eq $val;
    my @tile = @$tile;
    push @tile, splice(@tile, 0, $i);
    push @adjusted, [@tile];
  }
  return @adjusted;
}

# given configuration, insert position, and new tile, 
# insert new tile into configuration at given position
# eliminate superfluous motions
sub insert_into {
  my ($c, $pos, $t) = @_;
  my @c = @$c;
  splice @c, $pos+1, 0, @$t;
  my $reducing;
  do {
    $reducing = 0;
    for my $i (-1 .. $#c-1) {
      my ($a, $b) = @c[$i, $i+1];
      my ($an, $as) = ($a =~ /(\d+)(.*)/);
      my ($bn, $bs) = ($b =~ /(\d+)(.*)/);
      next unless abs($an - $bn) == 6;
      next unless $as eq $bs;
      if ($i == -1) {
        pop @c; shift @c;
      } else {
        splice @c, $i, 2;
      }
      $reducing = 1;
      last;
    }
  } while $reducing;
  \@c;
}

sub next_to_eliminate {
  my ($here, $there) = @_;
  for my $i (0 .. $#$here) {
    return $i if $i > $#$there || $here->[$i] ne $there->[$i];
  }
  return;
}

sub next_to_introduce {
  my ($here, $there) = @_;
  return $there->[@$here];
}

sub expand_set {
  my $tiles = shift;
  for my $name (keys %$tiles) {
    my @set;
    for (0 .. 11) {
      push @set,
        rotate($tiles->{$name}, $_),
        rotate(flip($tiles->{$name}), $_);
    }
    $tiles->{$name} = [tile_set(@set)];
  }
}

#convert a tile to a string 
sub tile_to_string {
  my $tile = canonical(shift());
  "@$tile";
}

# Rotate a tile by $rot*30 degrees
sub rotate {
  my ($tile, $rot) = @_;
  my @rot = @$tile;
  for (@rot) {
    my ($n, $star) = /(\d+)(.*)/;
    $_ = (($n + $rot) % 12) . $star;
  }
  \@rot;
}

# Reflect a tile
sub flip {
  my ($tile) = @_;
  my @flip = reverse @$tile;
  for (@flip) {
    my ($n, $star) = /(\d+)(.*)/;
    $_ = (12 - $n) . $star;
  }
  \@flip;
}

sub tile_set {
  my %seen;
  for my $tile (@_) {
    $seen{tile_to_string(canonical $tile)} = $tile;
  }
  values %seen;
}


# reorder a tile into canonical form
sub canonical {
  my $tile = [@{shift()}];
  my $canonical = [@$tile];
  for (0 .. $#$tile) {
    push @$tile, shift @$tile;
    @$canonical = @$tile if tilecmp($tile, $canonical) < 0;
  }
  $canonical;
}

sub tilecmp {
  my ($a, $b) = @_;
  for my $i (0 .. $#$a) {
    return 1 if $i > $#$b;
    my $cmp = $a->[$i] <=> $b->[$i];
    return $cmp if $cmp;
  }
  return -1 if $#$a < $#$b;
  return 0;
}

