# Copyright 2011 Kevin Ryde

# This file is part of Math-Image.
#
# Math-Image 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 3, or (at your option) any later
# version.
#
# Math-Image 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.
#
# You should have received a copy of the GNU General Public License along
# with Math-Image.  If not, see <http://www.gnu.org/licenses/>.


# http://www.springerlink.com/content/y1l60g7125038668/  [pay]
#
# Google Books LATIN'95 link: page 44
# http://books.google.com.au/books?id=_aKhJUJunYwC&lpg=PA44&ots=ARyDkP_hjU&dq=%22Space-Filling%20Curves%20and%20Their%20Use%20in%20the%20Design%20of%20Geometric%20Data%20Structures%22&pg=PA44#v=onepage&q&f=false

package Math::PlanePath::MathImageAR2W2Curve;
use 5.004;
use strict;

use vars '$VERSION', '@ISA';
$VERSION = 83;
use Math::PlanePath 54; # v.54 for _max()
@ISA = ('Math::PlanePath');
*_max = \&Math::PlanePath::_max;
*_is_infinite = \&Math::PlanePath::_is_infinite;
*_round_nearest = \&Math::PlanePath::_round_nearest;

use Math::PlanePath::KochCurve 42;
*_round_down_pow = \&Math::PlanePath::KochCurve::_round_down_pow;


use constant n_start => 0;
use constant x_negative => 0;
use constant y_negative => 0;

use constant parameter_info_array =>
  [
   {
    name    => 'start_shape',
    type    => 'enum',
    default => 'A1',
    choices => ['A1','D2','D1'],
   },
  ];


# tables generated by tools/ar2w2-curve-table.pl
#
my @next_state;
my @digit_to_x;
my @digit_to_y;
my @yx_to_digit;
my @min_digit;
my @max_digit;

{
  use List::Util 'min','max';
  sub min_maybe {
    return min(grep {defined} @_);
  }
  sub max_maybe {
    return max(grep {defined} @_);
  }

  sub print_table {
    my ($name, $aref) = @_;
    print "my \@$name\n  = (";
    my $entry_width = max (map {defined $_ ? length : 0} @$aref);

    foreach my $i (0 .. $#$aref) {
      printf "%*s", $entry_width, $aref->[$i]//'undef';
      if ($i == $#$aref) {
        print ");\n";
      } else {
        print ",";
        if (($i % 16) == 15) {
          print "\n     ";
        } elsif (($i % 4) == 3) {
          print " ";
        }
      }
    }
  }

  sub print_table12 {
    my ($name, $aref) = @_;
    print "my \@$name = (";
    my $entry_width = max (map {length($_//'')} @$aref);

    foreach my $i (0 .. $#$aref) {
      printf "%*s", $entry_width, $aref->[$i]//'undef';
      if ($i == $#$aref) {
        print ");\n";
      } else {
        print ",";
        if (($i % 12) == 11) {
          my $state = ($i-11)/3;
          print "   # 3* $state";
          print "\n        ".(" " x length($name));
        } elsif (($i % 3) == 2) {
          print " ";
        }
      }
    }
  }

    sub make_state {
      my ($part, $rot, $rev) = @_;
      $rot %= 4;
      return 4*($rot + 4*($rev + 2*$part));
    }
    sub state_string {
      my ($state) = @_;
      my $digit = $state % 4; $state = int($state/4);
      my $rot   = $state % 4; $state = int($state/4);
      my $rev = $state % 2; $state = int($state/2);
      my $part  = $state;
      return "part=$part rot=$rot rev=$rev digit=$digit";
    }

    use constant A1 => 0;
    use constant A2 => 1;
    use constant B1 => 2;
    use constant B2 => 3;
    use constant C1 => 4;
    use constant C2 => 5;
    use constant D1 => 6;
    use constant D2 => 7;

    foreach my $part (A1, A2, B1, B2, C1, C2, D1, D2) {
      foreach my $rot (0, 1, 2, 3) {
        foreach my $rev (0, 1) {
          my $state = make_state ($part, $rot, $rev);

          foreach my $orig_digit (0, 1, 2, 3) {
            my $digit = $orig_digit;
            if ($rev) {
              $digit = 3-$digit;
            }

            my $xo = 0;
            my $yo = 0;
            my $new_part = $part;
            my $new_rot = $rot;
            my $new_rev = $rev;

            if ($part == A1) {
              if ($digit == 0) {
                $new_part = D2;
              } elsif ($digit == 1) {
                $xo = 1;
                $new_part = B1;
                $new_rev ^= 1;
                $new_rot = $rot - 1;
              } elsif ($digit == 2) {
                $yo = 1;
                $new_part = C1;
                $new_rot = $rot + 1;
              } elsif ($digit == 3) {
                $xo = 1;
                $yo = 1;
                $new_part = B2;
                $new_rev ^= 1;
                $new_rot = $rot + 2;
              }

            } elsif ($part == A2) {
              if ($digit == 0) {
                $new_part = B1;
                $new_rev ^= 1;
                $new_rot = $rot - 1;
              } elsif ($digit == 1) {
                $yo = 1;
                $new_part = C2;
              } elsif ($digit == 2) {
                $xo = 1;
                $new_part = B2;
                $new_rev ^= 1;
                $new_rot = $rot + 2;
              } elsif ($digit == 3) {
                $xo = 1;
                $yo = 1;
                $new_part = D1;
                $new_rot = $rot + 1;
              }

            } elsif ($part == B1) {
              if ($digit == 0) {
                $new_part = D1;
                $new_rev ^= 1;
                $new_rot = $rot - 1;
              } elsif ($digit == 1) {
                $yo = 1;
                $new_part = C2;
              } elsif ($digit == 2) {
                $xo = 1;
                $yo = 1;
                $new_part = B1;
              } elsif ($digit == 3) {
                $xo = 1;
                $new_part = B2;
                $new_rev ^= 1;
                $new_rot = $rot + 1;
              }

            } elsif ($part == B2) {
              if ($digit == 0) {
                $new_part = B1;
                $new_rev ^= 1;
                $new_rot = $rot - 1;
              } elsif ($digit == 1) {
                $yo = 1;
                $new_part = B2;
              } elsif ($digit == 2) {
                $xo = 1;
                $yo = 1;
                $new_part = C1;
              } elsif ($digit == 3) {
                $xo = 1;
                $new_part = D2;
                $new_rev ^= 1;
                $new_rot = $rot + 1;
              }

            } elsif ($part == C1) {
              if ($digit == 0) {
                $new_part = A2;
              } elsif ($digit == 1) {
                $yo = 1;
                $new_part = B1;
                $new_rot = $rot + 1;
              } elsif ($digit == 2) {
                $xo = 1;
                $yo = 1;
                $new_part = A1;
                $new_rot = $rot - 1;
              } elsif ($digit == 3) {
                $xo = 1;
                $new_part = B2;
                $new_rev ^= 1;
                $new_rot = $rot + 1;
              }

            } elsif ($part == C2) {
              if ($digit == 0) {
                $new_part = B1;
                $new_rev ^= 1;
                $new_rot = $rot - 1;
              } elsif ($digit == 1) {
                $yo = 1;
                $new_part = A2;
              } elsif ($digit == 2) {
                $xo = 1;
                $yo = 1;
                $new_part = B2;
                $new_rot = $rot - 1;
              } elsif ($digit == 3) {
                $xo = 1;
                $new_part = A1; 
                $new_rot = $rot - 1;
              }

            } elsif ($part == D1) {
              if ($digit == 0) {
                $new_part = D1;
                $new_rev ^= 1;
                $new_rot = $rot - 1;
              } elsif ($digit == 1) {
                $yo = 1;
                $new_part = A2;
              } elsif ($digit == 2) {
                $xo = 1;
                $yo = 1;
                $new_part = C2;
                $new_rot = $rot - 1;
              } elsif ($digit == 3) {
                $xo = 1;
                $new_part = A2;
                $new_rot = $rot - 1;
              }

            } elsif ($part == D2) {
              if ($digit == 0) {
                $new_part = A1;
              } elsif ($digit == 1) {
                $yo = 1;
                $new_part = C1;
                $new_rot = $rot + 1;
              } elsif ($digit == 2) {
                $xo = 1;
                $yo = 1;
                $new_part = A1;
                $new_rot = $rot - 1;
              } elsif ($digit == 3) {
                $xo = 1;
                $new_part = D2;
                $new_rev ^= 1;
                $new_rot = $rot + 1;
              }

            } else {
              die;
            }

            ### base: "$xo, $yo"

            if ($rot & 2) {
              $xo ^= 1;
              $yo ^= 1;
            }
            if ($rot & 1) {
              ($xo,$yo) = ($yo^1,$xo);
            }
            ### rot to: "$xo, $yo"

            $digit_to_x[$state+$orig_digit] = $xo;
            $digit_to_y[$state+$orig_digit] = $yo;
            $yx_to_digit[$state + $yo*2 + $xo] = $orig_digit;

            my $next_state = make_state
              ($new_part, $new_rot, $new_rev);
            $next_state[$state+$orig_digit] = $next_state;
          }


          foreach my $x1pos (0 .. 1) {
            foreach my $x2pos ($x1pos .. 1) {
              my $xr = ($x1pos ? 2 : $x2pos ? 1 : 0);
              ### $xr

              foreach my $y1pos (0 .. 1) {
                foreach my $y2pos ($y1pos .. 1) {
                  my $yr = ($y1pos ? 6 : $y2pos ? 3 : 0);
                  ### $yr

                  my $min_digit = undef;
                  my $max_digit = undef;
                  foreach my $digit (0 .. 3) {
                    my $x = $digit_to_x[$state+$digit];
                    my $y = $digit_to_y[$state+$digit];
                    next unless $x >= $x1pos;
                    next unless $x <= $x2pos;
                    next unless $y >= $y1pos;
                    next unless $y <= $y2pos;
                    $min_digit = min_maybe($digit,$min_digit);
                    $max_digit = max_maybe($digit,$max_digit);
                  }

                  my $key = 3*$state + $xr + $yr;
                  ### $key

                  if (defined $min_digit[$key]) {
                    die "oops min_digit[] already: state=$state key=$key y1p=$y1pos y2p=$y2pos value=$min_digit[$key], new=$min_digit";
                  }
                  $min_digit[$key] = $min_digit;
                  $max_digit[$key] = $max_digit;
                }
              }
              ### @min_digit
            }
          }

        }
      }
    }

    sub check_used {
      my @pending_state = @_;
      my $count = 0;
      my @seen_state;
      my $depth = 1;
      while (@pending_state) {
        my $state = pop @pending_state;
        $count++;
        ### consider state: $state

        foreach my $digit (0 .. 3) {
          my $next_state = $next_state[$state+$digit];
          if (! $seen_state[$next_state]) {
            $seen_state[$next_state] = $depth;
            push @pending_state, $next_state;
            ### push: "$next_state  depth $depth"
          }
        }
        $depth++;
      }
      for (my $state = 0; $state < @next_state; $state += 4) {
        if (! defined $seen_state[$state]) { $seen_state[$state] = 'none'; }
        my $str = state_string($state);
        print "# used state $state   depth $seen_state[$state]    $str\n";
      }
      print "used state count $count\n";
    }

  print_table ("next_state", \@next_state);
  print_table ("digit_to_x", \@digit_to_x);
  print_table ("digit_to_y", \@digit_to_y);
  print_table ("yx_to_digit", \@yx_to_digit);
  print_table12 ("min_digit", \@min_digit);
  print_table12 ("max_digit", \@max_digit);
  print "# state length ",scalar(@next_state)," in each of 4 tables\n\n";

  print "# state A1=",make_state(A1,0,0),"\n";
  print "# state D2=",make_state(D2,0,0),"\n";
  print "# state D1=",make_state(D1,0,0),"\n";

  print "from A1/D2\n";
  check_used (make_state(A1,0,0), make_state(D2,0,0));
  print "from D1\n";
  check_used (make_state(D1,0,0));
}

sub new {
  my $class = shift;
  return $class->SUPER::new (start_shape => 'A1', # default
                             @_);
}

# uncomment this to run the ### lines
#use Smart::Comments;

my %start_state = (A1 => [make_state(A1,0,0), make_state(D2,0,0)],
                   D2 => [make_state(D2,0,0), make_state(A1,0,0)],
                   D1 => [make_state(D1,0,0), make_state(D1,0,1)]);
sub n_to_xy {
  my ($self, $n) = @_;
  ### AR2W2Curve n_to_xy(): $n

  if ($n < 0) { return; }
  if (_is_infinite($n)) { return ($n,$n); }

  my $int = int($n);
  $n -= $int;

  my @digits;
  my $len = $int*0 + 1;   # inherit bignum 1
  do {
    push @digits, $int % 4;
    $len *= 2;
  } while (($int = int($int/4)) || ($#digits&1));
  ### digits: join(', ',@digits)."   count ".scalar(@digits)
  ### $len

  # $dir default if all $digit==3
  my ($state,$dir) = @{$start_state{$self->{'start_shape'}}};
  if ($#digits & 1) {
    ($state,$dir) = ($dir,$state);
  }

  ### initial ...
  ### $state
  ### $dir

  my $x = 0;
  my $y = 0;
  while (@digits) {
    $len /= 2;
    $state += (my $digit = pop @digits);
    if ($digit != 3) {
      $dir = $state;  # lowest non-3 digit
    }

    ### $len
    ### $state
    ### state: state_string($state)
    ### digit_to_x: $digit_to_x[$state]
    ### digit_to_y: $digit_to_y[$state]
    ### next_state: $next_state[$state]

    $x += $len * $digit_to_x[$state];
    $y += $len * $digit_to_y[$state];
    $state = $next_state[$state];
  }

  ### $dir
  ### frac: $n

  # with $n fractional part
  return ($n * ($digit_to_x[$dir+1] - $digit_to_x[$dir]) + $x,
          $n * ($digit_to_y[$dir+1] - $digit_to_y[$dir]) + $y);
}

sub xy_to_n {
  my ($self, $x, $y) = @_;
  ### AR2W2Curve xy_to_n(): "$x, $y"

  $x = _round_nearest ($x);
  $y = _round_nearest ($y);
  if ($x < 0 || $y < 0) {
    return undef;
  }
  if (_is_infinite($x)) {
    return $x;
  }
  if (_is_infinite($y)) {
    return $y;
  }

  my ($len, $level) = _round_down_pow (($x > $y ? $x : $y),
                                       2);
  ### $len
  ### $level

  my $n = ($x * 0 * $y);  # inherit bignum 0
  my $state = $start_state{$self->{'start_shape'}}->[$level & 1];
  while ($level-- >= 0) {
    ### at: "$x,$y  len=$len level=$level"
    ### assert: $x >= 0
    ### assert: $y >= 0
    ### assert: $x < 2*$len
    ### assert: $y < 2*$len

    my $xo = int ($x / $len);
    my $yo = int ($y / $len);
    ### assert: $xo >= 0
    ### assert: $xo <= 1
    ### assert: $yo >= 0
    ### assert: $yo <= 1

    $x %= $len;
    $y %= $len;
    ### xy bits: "$xo, $yo"

    my $digit = $yx_to_digit[$state + 2*$yo + $xo];
    $state = $next_state[$state+$digit];
    $n = 4*$n + $digit;
    $len /= 2;
  }

  ### assert: $x == 0
  ### assert: $y == 0

  return $n;
}

# exact
sub rect_to_n_range {
  my ($self, $x1,$y1, $x2,$y2) = @_;
  ### AR2W2Curve rect_to_n_range(): "$x1,$y1, $x2,$y2"

  $x1 = _round_nearest ($x1);
  $x2 = _round_nearest ($x2);
  $y1 = _round_nearest ($y1);
  $y2 = _round_nearest ($y2);
  ($x1,$x2) = ($x2,$x1) if $x1 > $x2;
  ($y1,$y2) = ($y2,$y1) if $y1 > $y2;

  if ($x2 < 0 || $y2 < 0) {
    return (1, 0);
  }

  my ($len, $level) = _round_down_pow (_max($x2,$y2), 2);
  ### len/level: "$len  $level"
  if (_is_infinite($level)) {
    return (0, $level);
  }

  my $n_min = my $n_max
    = my $y_min = my $y_max
      = my $x_min = my $x_max = 0;
  my $min_state = my $max_state
    = $start_state{$self->{'start_shape'}}->[$level & 1];
  ### $x_min
  ### $y_min

  while ($level >= 0) {
    ### $level
    ### $len
    {
      my $x_cmp = $x_min + $len;
      my $y_cmp = $y_min + $len;
      my $digit = $min_digit[3*$min_state
                             + ($x1 >= $x_cmp ? 2 : $x2 >= $x_cmp ? 1 : 0)
                             + ($y1 >= $y_cmp ? 6 : $y2 >= $y_cmp ? 3 : 0)];

      my $xr = ($x1 >= $x_cmp ? 2 : $x2 >= $x_cmp ? 1 : 0);
      my $yr = ($y1 >= $y_cmp ? 6 : $y2 >= $y_cmp ? 3 : 0);
      ### $min_state
      ### min_state: state_string($min_state)
      ### $xr
      ### $yr
      ### $digit

      $n_min = 4*$n_min + $digit;
      $min_state += $digit;
      if ($digit_to_x[$min_state]) { $x_min += $len; }
      if ($digit_to_y[$min_state]) { $y_min += $len; }
      $min_state = $next_state[$min_state];
    }
    {
      my $x_cmp = $x_max + $len;
      my $y_cmp = $y_max + $len;
      my $digit = $max_digit[3*$max_state
                             + ($x1 >= $x_cmp ? 2 : $x2 >= $x_cmp ? 1 : 0)
                             + ($y1 >= $y_cmp ? 6 : $y2 >= $y_cmp ? 3 : 0)];

      $n_max = 4*$n_max + $digit;
      $max_state += $digit;
      if ($digit_to_x[$max_state]) { $x_max += $len; }
      if ($digit_to_y[$max_state]) { $y_max += $len; }
      $max_state = $next_state[$max_state];
    }

    $len = int($len/2);
    $level--;
  }

  return ($n_min, $n_max);
}


# not exact
sub XXrect_to_n_range {
  my ($self, $x1,$y1, $x2,$y2) = @_;
  ### AR2W2Curve rect_to_n_range(): "$x1,$y1, $x2,$y2"

  $x1 = _round_nearest ($x1);
  $x2 = _round_nearest ($x2);
  ($x1,$x2) = ($x2,$x1) if $x1 > $x2;

  $y1 = _round_nearest ($y1);
  $y2 = _round_nearest ($y2);
  ($y1,$y2) = ($y2,$y1) if $y1 > $y2;

  if ($x2 < 0 || $y2 < 0) {
    return (1, 0);
  }

  my ($len, $level) = _round_down_pow (($x2 > $y2 ? $x2 : $y2),
                                       2);
  ### $len
  ### $level
  return (0, 4*$len*$len-1);
}

1;
__END__

=for stopwords eg Ryde ie AR2W2Curve Math-PlanePath Asano Ranjan Roos Welzl Widmayer HilbertCurve ZOrderCurve Informatics

=head1 NAME

Math::PlanePath::MathImageAR2W2Curve -- 2x2 self-similar of four pattern parts

=head1 SYNOPSIS

 use Math::PlanePath::MathImageAR2W2Curve;
 my $path = Math::PlanePath::MathImageAR2W2Curve->new;
 my ($x, $y) = $path->n_to_xy (123);

=head1 DESCRIPTION

I<In progress ...>

This is an integer version of the AR2W2 curve by Asano, Ranjan, Roos, Welzl
and Widmayer.

                                         | 
      7     42--43--44  47--48--49  62--63   
              \      |   |       |   |     
      6     40--41  45--46  51--50  61--60 
             |               |           | 
      5     39  36--35--34  52  55--56  59 
             |   |    /      |   |   |   | 
      4     38--37  33--32  53--54  57--58 
                          \                
      3      6-- 7-- 8  10  31  28--27--26 
             |       |/  |   |   |       | 
      2      5-- 4   9  11  30--29  24--25 
                 |       |           |     
      1      2-- 3  13--12  17--18  23--22 
              \      |       |   |       | 
    Y=0 ->   0-- 1  14--15--16  19--20--21 
          
            X=0  1   2   3   4   5   6   7 

It makes a 2x2 expanding pattern with a mixture of "U" and "Z" shapes.  The
mixture is designed to improve some locality measures.

There's four base patterns A to D.  A2 is a reversal of A1, and B2 a
reversal of B1, etc.

          C1---B2             C2   D1
    A1       \          A2     | \  |
          D2---B1             B1   B2
                          
          C2---B1             B2---C1
    B1     |    |       B2     |    |
          D1   B2             B1   D2
                          
          B1---A1             A2---B2
    C1     |    |       C2     |    |
          A2   B2             B1   A1
                          
           A2---C2             C1---A1
    D1      |    |       D2     |    |
           D1   A2             A1   D2
         trans
                            
The starting pattern is the A1 "Z" shape and above that D2.  Notice the
starting sub-part of D2 is A1 and in turn the starting sub-part of A1 is D2,
so those two alternate at progressively higher levels.  The sub-parts of
those then reach all of the others, and rotated in four directions.

The curve is conceived by the authors as going downwards into ever-smaller
sub-parts and for that any of the patterns can be the top-level start.  But
for the expanding outwards version here the start must occur as the first
part of the pattern above it, which means the A1/D2 pair, or alternatively
the D1/D1trans which is its own start with a transpose.

Other parts can't be the N=0 starting point.  B2, C1 and C2 aren't the start
of anything, so if one of them was the initial N=0 to N=3 then there'd be
nothing which could be above.  A2 is the start of C1, but nothing there.
And B1 is the start of C2, B2 or A2, but then they don't lead anywhere.

=head1 FUNCTIONS

See L<Math::PlanePath/FUNCTIONS> for the behaviour common to all path
classes.

=over 4

=item C<$path = Math::PlanePath::MathImageAR2W2Curve-E<gt>new ()>

Create and return a new path object.

=item C<($x,$y) = $path-E<gt>n_to_xy ($n)>

Return the X,Y coordinates of point number C<$n> on the path.  Points begin
at 0 and if C<$n E<lt> 0> then the return is an empty list.

=back

=head1 SEE ALSO

L<Math::PlanePath>,
L<Math::PlanePath::HilbertCurve>,
L<Math::PlanePath::ZOrderCurve>

Asano, Ranjan, Roos, Welzl and Widmayer "Space-Filling Curves and Their Use
in the Design of Geometric Data Structures", Theoretical Computer Science,
181(1):3-15, 1997.  And LATIN'95 Theoretical Informatics which is at Google
Books

    http://books.google.com/books?isbn=3540591753

=cut


# Local variables:
# compile-command: "math-image --path=MathImageAR2W2Curve --lines --scale=20"
# End:
#
# math-image --path=MathImageAR2W2Curve --all --output=numbers_dash
