# Copyright 2015, 2016, 2017, 2018, 2019, 2020 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/>.


package Math::PlanePath::HilbertSides;
use 5.004;
use strict;
use List::Util 'min'; # 'max'
*max = \&Math::PlanePath::_max;

use vars '$VERSION', '@ISA';
$VERSION = 129;

use Math::PlanePath;
use Math::PlanePath::Base::NSEW;
@ISA = ('Math::PlanePath::Base::NSEW',
        'Math::PlanePath');

*_divrem_mutate = \&Math::PlanePath::_divrem_mutate;

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

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

use Math::PlanePath::HilbertCurve;
my $hilbert_path = Math::PlanePath::HilbertCurve->new;


use constant n_start => 0;
use constant class_x_negative => 0;
use constant class_y_negative => 0;

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

#                              ---3
#                                 |
# state=0    3--2   plain         2
#               |                 |
#            0--1              0--1
#
# state=4    1--2  transpose     1--2--3
#            |  |                |     |
#            0  3                0     |
#
# state=8    1--0  rot180     1--0
#            |                |
#            2--3             2
#                             |
#                             3---
#
# state=12   3  0  rot180 + transpose    |     0
#            |  |                        |     |
#            2--1                        3--2--1
#
# generated by tools/hilbert-curve-table.pl
my @next_state = (4,0,0,12, 0,4,4,8, 12,8,8,4, 8,12,12,0);
my @digit_to_x = (0,1,1,0, 0,0,1,1, 1,0,0,1, 1,1,0,0);
my @digit_to_y = (0,0,1,1, 0,1,1,0, 1,1,0,0, 1,0,0,1);
my @state_to_frac = (0,1, undef,undef,
                        1,0, undef,undef,
                        0,-1, undef,undef,
                        -1,0, undef,undef);

sub n_to_xy {
  my ($self, $n) = @_;
  ### HilbertSides n_to_xy(): $n
  ### hex: sprintf "%#X", $n

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

  my $int = int($n);
  $n -= $int;   # fraction part

  my @ndigits = digit_split_lowtohigh($int,4);
  my $state = ($#ndigits & 1 ? 4 : 0);

  my (@xbits, @ybits);
  foreach my $i (reverse 0 .. $#ndigits) {    # digits high to low
    $state += $ndigits[$i];
    $xbits[$i] = $digit_to_x[$state];
    $ybits[$i] = $digit_to_y[$state];
    $state = $next_state[$state];
  }

  my $zero = ($int * 0); # inherit bigint 0
  # print "state $state  state $state\n";
  my $add = ($state >= 8 ? 1 : 0);

  return ($n * $state_to_frac[$state]  # frac
          + digit_join_lowtohigh (\@xbits, 2, $zero)
          + $add,

          $n * $state_to_frac[$state+1]  # frac
          + digit_join_lowtohigh (\@ybits, 2, $zero)
          + $add);
}


sub xy_to_n {
  return scalar((shift->xy_to_n_list(@_))[0]);
}
sub xy_to_n_list {
  my ($self, $x, $y) = @_;
  ### HilbertSides xy_to_n(): "$x, $y"

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

  my @n_list;
  foreach my $d (0,1) {
    ### try: ($x-$d).','.($y-$d)
    my $n = $hilbert_path->xy_to_n($x-$d,$y-$d);
    ### $n
    if (defined $n) {
      if (my ($gx,$gy) = $self->n_to_xy($n)) {
        ### is at: "$gx,$gy"
        if ($x == $gx && $y == $gy) {
          ### push: $n
          push @n_list, $n;
        }
      }
    }
  }
  # if (@n_list == 2 && $n_list[0] > $n_list[1]) {
  #   @n_list = reverse @n_list;
  # }
  @n_list = sort {$a <=> $b} @n_list;
  ### @n_list
  return @n_list;
}

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

  $x1 = round_nearest ($x1);
  $x2 = round_nearest ($x2);
  $y1 = round_nearest ($y1);
  $y2 = round_nearest ($y2);

  my ($pow, $exp) = round_down_pow (max(abs($x1),abs($y1), abs($x2),abs($y2)),
                                    2);
  return (0, 4*$pow*$pow);
}

1;
__END__

=for stopwords eg Ryde ie HilbertSides Math-PlanePath Dekking unrotated Ns HilbertCurve dX OEIS

=head1 NAME

Math::PlanePath::HilbertSides -- sides of Hilbert curve squares

=head1 SYNOPSIS

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

=head1 DESCRIPTION

This path is segments along the sides of the Hilbert curve squares as per

=over

F. M. Dekking, "Recurrent Sets", Advances in Mathematics, volume 44, 1982,
pages 79-104, section 4.8 "Hilbert Curve"

=back

The base pattern is N=0 to N=4.  That pattern repeats transposed as points
N=0,4,8,12,16, etc.

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

If each point of the C<HilbertCurve> path is taken to be a unit square the
effect here is to go along the sides of those squares.  The square for a
segment is on the left or right,

     -------3.       .
        v   |
            |>
            |
            2        .
            |
            |>
        ^   |
    0-------1        .

Some points are visited twice.  The first is at X=2,Y=3 which is N=7 and N=9
where the two segments N=7to8 and N=8to9 overlap.  These are consecutive
segments, and non-consecutive segments can overlap too, as for example
N=27to28 and N=36to37.  Double-visited points occur also as corners
touching, for example at X=4,Y=3 the two N=11 N=31 touch without overlapping
segments.

The HilbertCurve squares fall within 2^k x 2^k blocks and so likewise the
segments here.  The right side 1 to 2 and 2 to 3 don't touch the 2^k side.
This is so of the base figure N=0 to N=4 which doesn't touch X=2 and higher
levels are unrotated replications so for example in the N=0 to N=64 shown
above X=8 is not touched.  This creates rectangular columns up from the X
axis.  Likewise rectangular rows across from the Y axis, and both columns
and rows inside.

The sides which are N=0 to N=1 and N=3 to N=4 of the base pattern variously
touch in higher levels giving interesting patterns of squares, shapes,
notches, etc.

=head1 FUNCTIONS

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

=over 4

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

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

Return the point number for coordinates C<$x,$y>.  If there's nothing at
C<$x,$y> then return C<undef>.

The curve visits an C<$x,$y> twice for various points.  The smaller of the
two N values is returned.

=item C<@n_list = $path-E<gt>xy_to_n_list ($x,$y)>

Return a list of N point numbers for coordinates C<$x,$y>.  Points may have
up to two Ns for a given C<$x,$y>.

=back

=head1 FORMULAS

=head2 Coordinates

Difference X-Y is the same here as in the C<HilbertCurve>.  The base pattern
here has N=3 at 1,2 whereas the HilbertCurve is 0,1 so X-Y is the same.  The
two then have the same pattern of rotate 180 and/or transpose in subsequent
replications.

                      3
                      |
    HilbertSides      2         3----2    HilbertCurve
                      |              |
                 0----1         0----1

=head2 Abs dX,dY

abs(dY) is 1 for a vertical segment and 0 for a horizontal segment.  For the
curve here it is

    abs(dY) = count 1-bits of N, mod 2 = Thue-Morse binary parity
    abs(dX) = 1 - abs(dY)              = opposite

This is so for the base pattern N=0,1,2, and also at N=3 turning towards
N=4.  Replication parts 1 and 2 are transposes where there is a single extra
1-bit in N and dX,dY are swapped.  Replication part 3 is a 180 degree
rotation where there are two extra 1-bits in N and dX,dY are negated so
abs(dX),abs(dY) unchanged.

=head2 Turn

The path can turn left or right or go straight forward or 180 degree
reverse.  Straight,reverse vs left,right is given by

    N num trailing 0 bits               turn
    ---------------------      -----------------------
          odd                  straight or 180 reverse     (A096268)
          even                 left or right               (A035263)

The path goes straight ahead at 2 and reverses 180 at 8 and all subsequent
2*4^k.

=head2 Segments on Axes

The number of line segments on the X and Y axes 0 to 2^k, which is N=0 to
4^k, is

    Xsegs[k] = 1/3*2^k + 1/2 + 1/6*(-1)^k
             = 1, 1, 2, 3, 6, 11, 22, 43, 86             (A005578)
             = Ysegs[k] + 1

    Ysegs[k] = 1/3*2^k - 1/2 + 1/6*(-1)^k
             = 0, 0, 1, 2, 5, 10, 21, 42, 85,...         (A000975)
             = binary 101010...   k-1 many bits alternating

=cut

# GP-DEFINE  Xsegs(k) = 1/3*2^k + 1/2 + 1/6*(-1)^k;
# GP-DEFINE  Ysegs(k) = 1/3*2^k - 1/2 + 1/6*(-1)^k;
# GP-Test  vector(9,k,k--; Xsegs(k)) == [1,1,2,3,6,11,22,43,86]
# GP-Test  vector(9,k,k--; Ysegs(k)) == [0,0,1,2,5,10,21,42,85]
# GP-DEFINE  from_binary_vector(v) = subst(Polrev(v),'x,2);
# GP-Test  from_binary_vector([1,1,0,1]) == 11
# GP-DEFINE  Ysegs_by_binary(k) = \
# GP-DEFINE    from_binary_vector(vector(max(0,k-1),i, (k-i)%2));
# GP-Test  vector(100,k,k--; Ysegs_by_binary(k)) == \
# GP-Test  vector(100,k,k--; Ysegs(k))

=pod

These counts can be calculated from the curve sub-parts

      k odd              k even

    +---+   .          .   .   .
      R |>T              T   T
    .   .   .          +---+---+
        |>T            |>    R<|
    o---+   .          o   .   +

The block at the origin is X and Y segments of the k-1 level.  For k odd,
the X axis then has a transposed block which means the Y segments of k-1.
The Y axis has a 180 degree rotated block R.  The curve is symmetric in
mirror image across its start to end so the count of segments it puts on the
Y axis is the same as Y of level k-1.

    Xsegs[k] = Xsegs[k-1] +   Ysegs[k-1]       for k odd
    Ysegs[k] =              2*Ysegs[k-1]

=for GP-Test  vector(100,k,k=2*k-1; Xsegs(k)) == vector(100,k,k=2*k-1; Xsegs(k-1) + Ysegs(k-1))  /* k>=1 odd */

=for GP-Test  vector(100,k,k=2*k-1; Ysegs(k)) == vector(100,k,k=2*k-1; 2*Ysegs(k-1))  /* k>=1 odd */

Then similarly for k even, but the other way around
the 2*Y.

    Xsegs[k] = 2*Xsegs[k-1]                    for k even
    Ysegs[k] =   Xsegs[k-1] + Ysegs[k-1]

=for GP-Test  vector(100,k,k=2*k; Xsegs(k)) == vector(100,k,k=2*k; 2*Xsegs(k-1))  /* k>=2 even */

=for GP-Test  vector(100,k,k=2*k; Ysegs(k)) == vector(100,k,k=2*k; Xsegs(k-1) + Ysegs(k-1))  /* k>=2 even */

=head1 OEIS

Entries in Sloane's Online Encyclopedia of Integer Sequences related to this
path include

=over

L<http://oeis.org/A059285> (etc)

=back

    A059285    X-Y
    A010059    abs(dX), 1 - Thue-Morse binary parity
    A010060    abs(dY), Thue-Morse binary parity

    A096268    turn 1=straight or reverse, 0=left or right
    A035263    turn 0=straight or reverse, 1=left or right

    A062880    N values on diagonal X=Y (digits 0,2 in base 4)

    A005578    count segments on X axis, level k
    A000975    count segments on Y axis, level k

=head1 SEE ALSO

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

=head1 HOME PAGE

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

=head1 LICENSE

Copyright 2015, 2016, 2017, 2018, 2019, 2020 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
