The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Kevin Ryde

# This file is part of Math-PlanePath.
#
# Math-PlanePath 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-PlanePath 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-PlanePath.  If not, see <http://www.gnu.org/licenses/>.


# math-image --path=SquareReplicate --lines --scale=10
# math-image --path=SquareReplicate --all --output=numbers_dash --size=80x50
# math-image --path=SquareReplicate,numbering_type=rotate-4 --all --output=numbers --size=48x9


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

use vars '$VERSION', '@ISA';
$VERSION = 126;
use Math::PlanePath;
@ISA = ('Math::PlanePath');

use Math::PlanePath::Base::Generic
  'is_infinite',
  'round_nearest';
use Math::PlanePath::Base::Digits
  'round_down_pow','round_up_pow',
  'digit_split_lowtohigh','digit_join_lowtohigh';

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


use constant parameter_info_array =>
  [ { name            => 'numbering_type',
      display         => 'Numbering',
      type            => 'enum',
      default         => 'fixed',
      choices         => ['fixed','rotate-4','rotate-8'],
      choices_display => ['Fixed','Rotate 4','Rotate 8'],
      description     => 'Fixed or rotating sub-part numbering.',
    },
  ];

use constant n_start => 0;
use constant xy_is_visited => 1;
use constant ddiffxy_maximum => 1;
use constant dir_maximum_dxdy => (0,-1); # South

# these don't vary with numbering_type since initial N=0to9 same
use constant x_negative_at_n => 4;
use constant y_negative_at_n => 6;

#------------------------------------------------------------------------------

sub new {
  my $self = shift->SUPER::new (@_);
  $self->{'numbering_type'} ||= 'fixed';  # default
  return $self;
}

sub _digits_rotate_lowtohigh {
  my ($self, $aref) = @_;
  my $rot = 0;
  my $mask = ($self->{'numbering_type'} eq 'rotate-4' ? 1 : 0);
  foreach my $digit (reverse @$aref) {
    if ($digit) {
      $digit--;
      my $delta_rot = $digit - ($digit & $mask);
      $digit = (($digit + $rot) % 8) + 1;  # mutate $aref
      $rot += $delta_rot;
    }
  }
}
sub _digits_unrotate_lowtohigh {
  my ($self, $aref) = @_;
  ### _digits_unrotate_lowtohigh(): @$aref
  my $rot = 0;
  my $mask = ($self->{'numbering_type'} eq 'rotate-4' ? 1 : 0);
  foreach my $digit (reverse @$aref) {
    ### at: "digit=$digit rot=$rot"
    if ($digit) {
      $digit = ($digit-1 - $rot) % 8;  # mutate $aref
      ### new digit 0-based: $digit
      $rot += $digit - ($digit & $mask);
      ### $rot
      $digit++;
      ### new digit 1-based: $digit
    }
  }
}

#  4 3 2
#  5 0 1
#  6 7 8
#
my @digit_to_x = (0,1, 1,0,-1, -1, -1, 0, 1);
my @digit_to_y = (0,0, 1,1, 1,  0, -1,-1,-1);

sub n_to_xy {
  my ($self, $n) = @_;
  ### SquareReplicate n_to_xy(): $n

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

  {
    my $int = int($n);
    ### $int
    ### $n
    if ($n != $int) {
      my ($x1,$y1) = $self->n_to_xy($int);
      my ($x2,$y2) = $self->n_to_xy($int+1);
      my $frac = $n - $int;  # inherit possible BigFloat
      my $dx = $x2-$x1;
      my $dy = $y2-$y1;
      return ($frac*$dx + $x1, $frac*$dy + $y1);
    }
    $n = $int;       # BigFloat int() gives BigInt, use that
  }

  my $x = my $y = ($n * 0);  # inherit bignum 0
  my $len = ($x + 1);        # inherit bignum 1

  my @digits = digit_split_lowtohigh($n,9);
  if ($self->{'numbering_type'} ne 'fixed') {
    _digits_rotate_lowtohigh($self, \@digits, 1);
  }
  foreach my $digit (@digits) {
    ### at: "$x,$y  digit=$digit"
    $x += $digit_to_x[$digit] * $len;
    $y += $digit_to_y[$digit] * $len;
    $len *= 3;
  }
  ### final: "$x,$y"
  return ($x,$y);
}

#   mod    digit
#  5 3 4   4 3 2     (x mod 3) + 3*(y mod 3)
#  2 0 1   5 0 1
#  8 6 7   6 7 8
#
my @mod_to_digit = (0,1,5, 3,2,4, 7,8,6);

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

  $x = round_nearest ($x);
  $y = round_nearest ($y);

  my ($len,$level_limit);
  {
    my $xa = abs($x);
    my $ya = abs($y);
    ($len,$level_limit) = round_down_pow (2*($xa > $ya ? $xa : $ya) || 1, 3);
    ### $level_limit
    ### $len
  }
  $level_limit += 2;
  if (is_infinite($level_limit)) {
    return $level_limit;
  }

  my $zero = ($x * 0 * $y);  # inherit bignum 0
  my @n; # digits low to high
  while ($x || $y) {
    if ($level_limit-- < 0) {
      ### oops, level limit reached ...
      return undef;
    }
    my $m = ($x % 3) + 3*($y % 3);
    my $digit = $mod_to_digit[$m];
    push @n, $digit;
    ### at: "$x,$y  m=$m digit=$digit"

    $x -= $digit_to_x[$digit];
    $y -= $digit_to_y[$digit];
    ### subtract: "$digit_to_x[$digit],$digit_to_y[$digit] to $x,$y"

    ### assert: $x!=$x || $x % 3 == 0
    ### assert: $y!=$y || $y % 3 == 0
    $x /= 3;
    $y /= 3;
  }
  ### n from xy: @n
  if ($self->{'numbering_type'} ne 'fixed') {
    _digits_rotate_lowtohigh($self, \@n, -1);
    ### @n
  }
  return digit_join_lowtohigh (\@n, 9, $zero);
}

# level   N    Xmax
#   1   9^1-1    1
#   2   9^2-1    1+3
#   3   9^3-1    1+3+9
# X <= 3^0+3^1+...+3^(level-1)
# X <= 1 + 3^0+3^1+...+3^(level-1)
# X <= (3^level - 1)/2
# 2*X+1 <= 3^level
# level >= log3(2*X+1)
#
# X < 1  +  3^0+3^1+...+3^(level-1)
# X < 1 + (3^level - 1)/2
# (3^level - 1)/2 > X-1
# 3^level - 1 > 2*X-2
# 3^level > 2*X-1
#
# not exact
sub rect_to_n_range {
  my ($self, $x1,$y1, $x2,$y2) = @_;
  ### SquareReplicate rect_to_n_range(): "$x1,$y1  $x2,$y2"

  my $max = abs(round_nearest($x1));
  foreach ($y1, $x2, $y2) {
    my $m = abs(round_nearest($_));
    if ($m > $max) { $max = $m }
  }
  my ($pow) = round_down_pow (2*($max||1)-1, 3);
  return (0, 9*$pow*$pow - 1);  # 9^level-1
}

#-----------------------------------------------------------------------------
# level_to_n_range()

# shared by Math::PlanePath::WunderlichMeander and more
sub level_to_n_range {
  my ($self, $level) = @_;
  return (0, 9**$level - 1);
}
sub n_to_level {
  my ($self, $n) = @_;
  if ($n < 0) { return undef; }
  if (is_infinite($n)) { return $n; }
  $n = round_nearest($n);
  my ($pow, $exp) = round_up_pow ($n+1, 9);
  return $exp;
}

#-----------------------------------------------------------------------------
1;
__END__

=for stopwords eg Ryde Math-PlanePath aabbccdd

=head1 NAME

Math::PlanePath::SquareReplicate -- replicating squares

=head1 SYNOPSIS

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

=head1 DESCRIPTION

This path is a self-similar replicating square,

    40--39--38  31--30--29  22--21--20         4
     |       |   |       |   |       |
    41  36--37  32  27--28  23  18--19         3
     |           |           |
    42--43--44  33--34--35  24--25--26         2

    49--48--47   4-- 3-- 2  13--12--11         1
     |       |   |       |   |       |
    50  45--46   5   0-- 1  14   9--10     <- Y=0
     |           |           |
    51--52--53   6-- 7-- 8  15--16--17        -1

    58--57--56  67--66--65  76--75--74        -2
     |       |   |       |   |       |
    59  54--55  68  63--64  77  72--73        -3
     |           |           |
    60--61--62  69--70--71  78--79--80        -4

                     ^
    -4  -3  -2  -1  X=0  1   2   3   4

The base shape is the initial N=0 to N=8 section,

   4  3  2
   5  0  1
   6  7  8

It then repeats with 3x3 blocks arranged in the same pattern, then 9x9
blocks, etc.

    36 --- 27 --- 18
     |             |
     |             |
    45      0 ---  9
     |
     |
    54 --- 63 --- 72

The replication means that the values on the X axis are those using only
digits 0,1,5 in base 9.  Those to the right have a high 1 digit and those to
the left a high 5 digit.  These digits are the values in the initial N=0 to
N=8 figure which fall on the X axis.

Similarly on the Y axis digits 0,3,7 in base 9, or the leading diagonal X=Y
0,2,6 and opposite diagonal 0,4,8.  The opposite diagonal digits 0,4,8 are
00,11,22 in base 3, so is all the values in base 3 with doubled digits
aabbccdd, etc.

=head2 Level Ranges

A given replication extends to

    Nlevel = 9^level - 1
    - (3^level - 1) <= X <= (3^level - 1)
    - (3^level - 1) <= Y <= (3^level - 1)

=head2 Complex Base

This pattern corresponds to expressing a complex integer X+i*Y with axis
powers of base b=3,

    X+Yi = a[n]*b^n + ... + a[2]*b^2 + a[1]*b + a[0]

using complex digits a[i] encoded in N in integer base 9,

    a[i] digit     N digit
    ----------     -------
          0           0
          1           1
        i+1           2
        i             3
        i-1           4
         -1           5
       -i-1           6
       -i             7
       -i+1           8

=head2 Numbering Rotate-4

Parameter C<numbering_type =E<gt> 'rotate-4'> applies a rotation to 4
directions E,N,W,S for each sub-part according to its position around the
preceding level.

         ^   ^
         |   |
       +---+---+---+
       | 4   3 | 2 |-->
       +---+---+   +
    <--| 5 | 0>| 1 |-->
       +   +---+---+
    <--| 6 | 7   8 |
       +---+---+---+
             |   |
             v   v

The effect can be illustrated by writing N in base-9.

=cut

# math-image --path=SquareReplicate,numbering_type=rotate-4 --all --output=numbers_dash --size=58x27

=pod

    42--41  48  32--31  38  24--23--22
     |   |   |   |   |   |   |       |
    43  40  47  33  30  37  25  20--21      numbering_type => 'rotate-4'
     |       |   |       |   |                  N shown in base-9
    44--45--46  34--35--36  26--27--28
                                   
    58--57--56   4---3---2  14--13--12
             |   |       |   |       |
    51--50  55   5   0---1  15  10--11
     |       |   |           |     
    52--53--54   6---7---8  16--17--18
                                   
    68--67--66  76--75--74  86--85--84
             |   |       |   |       |
    61--60  65  77  70  73  87  80  83
     |       |   |   |   |   |   |   |
    62--63--64  78  71--72  88  81--82

Parts 10-18 and 20-28 are the same as the middle 0-8.  Parts 30-38 and 40-48
have a rotation by +90 degrees.  Parts 50-58 and 60-68 rotation by +180
degrees, and so on.

Notice this means in each part the base-9 points 11, 21, 31, points are
directed away from the middle in the same way, relative to the sub-part
locations.  This gives a reasonably simple way to characterize points on the
boundary of a given expansion level.

Working through the directions and boundary sides gives a state machine for
which unit squares are on the boundary.  For level E<gt>= 1 a given unit
square has one of both of two sides on the boundary.

       B
    +-----+         
    |     |            unit square with expansion direction,   
    |     |->  A       one or both of sides A,B on the boundary    
    |     |
    +-----+

A further low base-9 digit expands the square to a block of 9, with squares
then boundary or not.  The result is 4 states, which can be expressed by
pairs of digits

    write N in base-9 using level many digits,
    delete all 2s in 2nd or later digit
    non-boundary =
      0 anywhere
      5 or 6 or 7 in 2nd or later digit
      pair 13,33,53,73, 14,34,54,74 anywhere
      pair 43,44, 81,88 at 2nd or later digit

Pairs 53,73,54,74 can be checked just at the start of the digits, since 5 or
7 anywhere later are non-boundary alone irrespective of what (if any) pair
they might make.

=cut

# boundary squares
# GP-DEFINE  B(k) = if(k==0,1, 4*(3^k-1));
# GP-Test  vector(6,k,k--; B(k)) == [1, 8, 32, 104, 320, 968]
# k>=1 half = A100774 2*(3^n - 1)

# GP-DEFINE  BpredRot4(n,k) = {
# GP-DEFINE    my(v=digits(n,9));
# GP-DEFINE    while(#v<k,v=concat([0],v));
# GP-DEFINE    if(#v>=2,
# GP-DEFINE       v=concat([v[1]],select(d->d!=2, v[2..#v])));
# GP-DEFINE    for(i=1,#v, if(v[i]==0,return(0)));
# GP-DEFINE    for(i=2,#v, if(v[i]==5||v[i]==6||v[i]==7,return(0)));
# GP-DEFINE    for(i=1,#v-1,
# GP-DEFINE        if((v[i]==1||v[i]==3||v[i]==5||v[i]==7)
# GP-DEFINE           && (v[i+1]==3||v[i+1]==4), return(0)));
# GP-DEFINE    for(i=2,#v-1,
# GP-DEFINE        if(v[i]==4
# GP-DEFINE           && (v[i+1]==3||v[i+1]==4), return(0));
# GP-DEFINE        if(v[i]==8
# GP-DEFINE           && (v[i+1]==1||v[i+1]==8), return(0)));
# GP-DEFINE    1;
# GP-DEFINE  }
# GP-Test  vector(6,k,k--; B(k)) == \
# GP-Test  vector(6,k,k--; sum(n=0,9^k-1,BpredRot4(n,k)))

# GP-DEFINE  to_base9(n) = fromdigits(digits(n,9));
# my(k=2); for(n=0,9^k-1,if(BpredRot4(n,k),print1(to_base9(n)","))); print();
# my(k=2); for(n=0,9^k-1,if(BpredRot4(n,k),print1(n","))); print();
# not in OEIS: 10,11,17,19,20,21,22,26,28,29,35,37,38,39,40,44,46,47,53,55,56,57,58,62,64,65,71,73,74,75,76,80
# not in OEIS: 11,12,18,21,22,23,24,28,31,32,38,41,42,43,44,48,51,52,58,61,62,63,64,68,71,72,78,81,82,83,84,88

=pod

=head2 Numbering Rotate 8

Parameter C<numbering_type =E<gt> 'rotate-8'> applies a rotation to 8
directions for each sub-part according to its position around the preceding
level.

     ^       ^       ^
      \      |      /
       +---+---+---+
       | 4 | 3 | 2 |
       +---+---+---+
    <--| 5 | 0>| 1 |-->
       +---+---+---+
       | 6 | 7 | 8 |
       +---+---+---+
      /      |      \
     v       v       v

The effect can be illustrated again by N in base-9.

=cut

# math-image --path=SquareReplicate,numbering_type=rotate-8 --all --output=numbers_dash --size=80x50

=pod

    41 48-47 32-31 38 23-22-21
     |\    |  |  |  |  |   /
    42 40 46 33 30 37 24 20 28      numbering_type => 'rotate'
     |     |  |     |  |     |          N shown in base-9
    43-44-45 34-35-36 25-26-27

    58-57-56  4--3--2 14-13-12
           |  |     |  |     |
    51-50 55  5  0--1 15 10-11
     |     |  |        |
    52-53-54  6--7--8 16-17-18

    67-66-65 76-75-74 85-84-83
     |     |  |     |  |     |
    68 60 64 77 70 73 86 80 82
      /    |  |  |  |  |   \ |
    61-62-63 78 71-72 87-88 81

Notice this means in each part the 11, 21, 31, etc, points are directed
away from the middle in the same way, relative to the sub-part locations.

=head1 FUNCTIONS

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

=over 4

=item C<$path = Math::PlanePath::SquareReplicate-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

=head2 Level Methods

=over

=item C<($n_lo, $n_hi) = $path-E<gt>level_to_n_range($level)>

Return C<(0, 9**$level - 1)>.

=back

=head1 SEE ALSO

L<Math::PlanePath>,
L<Math::PlanePath::CornerReplicate>,
L<Math::PlanePath::LTiling>,
L<Math::PlanePath::GosperReplicate>,
L<Math::PlanePath::QuintetReplicate>

=head1 HOME PAGE

L<http://user42.tuxfamily.org/math-planepath/index.html>

=head1 LICENSE

Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Kevin Ryde

This file is part of Math-PlanePath.

Math-PlanePath 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-PlanePath 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-PlanePath.  If not, see <http://www.gnu.org/licenses/>.

=cut