#!/usr/bin/perl -w

# Copyright 2020, 2021, 2022, 2023, 2024 Kevin Ryde

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


# Usage: ODIS-data-reader --anum A123456
#                         --action XXX
#                         --flag 0
#
# This program does the actual work for OEIS-data.gp.
#
# Global variables $anum and $flag are the --anum and --flag parameters.
# Action XXX dispatches to function action__XXX() which outputs the desired
# data or other information.
#
# If unable to get data etc, or any other problem, output_error() prints an
# GP error() call.

use 5.006;
use strict;
use warnings;
use FindBin;
use Math::OEIS;
use File::Slurp;

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

our $VERSION = 1;

my $have_locale = eval {
  require Encode;
  require Encode::Locale;
  1;
};
### $have_locale

# These could be "my" but keeping them as module "our" helps the test
# scripts exercise and display.
#
our ($anum, $action, %options);
our (@flags, $flag_bfile,$flag_offset);
our ($internal_basename, $bfile_basename);


# Return string $str with characters escaped as necessary to make a GP
# syntax string literal.  The return is ready to go inside "" quotes.
sub GP_string_escape {
  my ($str) = @_;
  if ($have_locale) {
    $str = Encode::encode('console_out',$str,Encode::FB_PERLQQ());
  }
  $str =~ s/([\\"])/\\$1/g;
  $str =~ s/\n/\\n/g;
  $str =~ s/\r/\\r/g;
  return $str;
}
sub output_error {
  print "error(\"",
    (map {GP_string_escape($_)} @_),
    "\")\n";
  exit 0;
}

my $internal_filename;
sub internal_filename {
  unless (defined $internal_filename) {
    ### internal_filename() ...
    $internal_filename = Math::OEIS->local_filename($internal_basename);
  }
  return $internal_filename;
}

my $internal_str;
sub internal_str {
  ### internal_str(): $anum
  ### $internal_filename
  unless (defined $internal_str) {
    unless (defined internal_filename()) { return undef; }
    $internal_str = File::Slurp::read_file
      ($internal_filename,
       { err_mode => 'quiet',
         ($have_locale ? (binmode => 'utf8') : ()) });
  }
  return $internal_str;
}

sub stripped_data {
  ### stripped_data(): $anum
  require Math::OEIS::Stripped;
  return Math::OEIS::Stripped->anum_to_values_str($anum);
}

our $number_re = qr/0|-?[1-9][0-9]*/;
our $numbers_re = qr/^$number_re(,\s*$number_re)*$/;

sub internal_data {
  internal_str() or return undef;
  $internal_str =~ /^%S A\d+ (.*)/m or die;

  my @data = map {
    ### $_
    $internal_str =~ /^\%$_ A\d+ (.*)/m
      ? $1 : undef
    } 'S','T','U';
  ### @data
  while (@data && ! defined $data[-1]) { pop @data }
  ### @data
  foreach my $i (0 .. $#data-1) {
    if (! defined $data[$i]
        && defined $data[$i+1]) {
      ### bad ...
      @data = ();
    }
  }
  my $data = join("\n",@data);
  unless ($data =~ $numbers_re) {
    output_error("OEIS-data-reader: $anum unrecognised data lines in $internal_filename");
  }
  $data =~ s/\s*+//g;
  return $data;
}

my $bfile_filename;
sub bfile_filename {
  ### bfile_filename() ...
  ### $bfile_basename
  $bfile_filename = (Math::OEIS->local_filename($bfile_basename)
                     || return undef);
  ### $bfile_filename
  return $bfile_filename;
}

sub bfile_offset_and_data {
  ### bfile_offset_and_data() ...
  bfile_filename() || return;
  ### read file: $bfile_filename
  my $str = File::Slurp::read_file($bfile_filename,
                                   { err_mode => 'quiet' });
  if(! defined $str) {
    ### cannot read b-file ...
    return;
  }
  my $offset;
  my @data;
  my $prev;
  while ($str =~ /^\s*(-?\d+)\s+(-?\d+)/mg) {
      my $n = $1;
      push @data, $2;
      if (defined $prev) {
        unless ($n == $prev+1) {
          output_error("OEIS-data-reader: indices not consecutive at n=$n in $bfile_filename");
        }
      } else {
        $offset = $n;
      }
      $prev = $n;
  }
  @data or output_error("OEIS-data-reader: unrecognised contents in $bfile_filename");
  ### return ...
  return ($offset, join(',',@data));
}

sub action__data {
  my $data;
  if ($flag_bfile) {
    (undef,$data) = bfile_offset_and_data();
    defined $data
      or output_error("OEIS-data-reader: OEIS_data() $anum need ~/OEIS/$bfile_basename for b-file data");

  } else {
    defined($data = internal_data())
      or defined($data = stripped_data())
      or output_error("OEIS-data-reader: no data files for $anum");
  }
  print "[$data]\n";
}

sub internal_offset {
  my $str = internal_str();
  return (defined(internal_str())
          && internal_str() =~ /^%O A\d+ (-?[0-9]+)/m ? $1 : undef);
}
sub bfile_offset {
  bfile_filename() || return;
  open my $fh, '<', $bfile_filename
    or return;
  while (defined(my $line = readline $fh)) {
    if ($line =~ /^\s*(-?\d+)\s+(-?\d+)/mg) {
      return $1;   # first n
    }
  }
  return;
}
sub action__offset {
  my $offset;
  defined($offset = internal_offset())
    or defined($offset = bfile_offset())
    or output_error("OEIS-data-reader: OEIS_offset() $anum need $internal_basename or $bfile_basename for OFFSET");
  print $offset,"\n";
}

sub action__offset_and_data {
  my ($offset,$data);
  ### action__offset_and_data() ...
  if ($flag_bfile) {
    ($offset,$data) = bfile_offset_and_data()
      or output_error("OEIS-data-reader: OEIS_data() $anum need ~/OEIS/$bfile_basename for b-file data");
  }
  if (defined $flag_offset) {
    $offset = $flag_offset;
  }
  if (! defined $offset) {
    defined($offset = internal_offset())
      or ($offset = bfile_offset());
  }
  if (! defined $data) {
    defined ($data = internal_data())
      or ($data = stripped_data())
  }
  ### $offset
  ### $data
  if (defined $offset && defined $data) {
    print "[$offset,[$data]]\n";
  } else {
    bfile_filename();
    output_error("OEIS-data-reader: OEIS_offset_and_data() $anum need $internal_basename, or stripped and $bfile_basename");
  }
}

sub action__name {
  ### action__name() ...
  #  --name A001317   Sierpinski
  my $name;
  if (my $str = internal_str()) {
    ### $str
    $str =~ /^%N A\d+ (.*)/mg
      or output_error("OEIS-data-reader: no %N name in $internal_filename");
    $name = $1;
  } else {
    require Math::OEIS::Names;
    if (! ($name = Math::OEIS::Names->anum_to_name($anum))) {
      $name = "unknown";
    }
  }
  print '"'.GP_string_escape($name),'"';
}

#------------------------------------------------------------------------------
# Main

sub next_ARGV {
  my ($key) = @_;
  @ARGV or output_error("OEIS-data-reader: missing argument for $key");
  return shift @ARGV;
}

sub main {
  ### main(): @ARGV
  while (defined(my $arg = shift @ARGV)) {
    if ($arg eq '--version') {
      print $VERSION,"\n";
      exit 0;

    } elsif ($arg eq '--check-version') {
      my $want_version = next_ARGV($arg);
      $VERSION == $want_version
        or output_error("OEIS-data-reader: program version $VERSION mismatch OEIS-data.gp version $want_version");

    } elsif ($arg eq '--anum') {
      $anum = next_ARGV($arg);
      $internal_basename = $anum.'.internal.txt';
      $bfile_basename = 'b'.substr($anum,1).'.txt';
    } elsif ($arg eq '--action') {
      $action = next_ARGV($arg);
    } elsif ($arg eq '--flag') {
      push @flags, next_ARGV($arg);
    } elsif ($arg eq '--option') {
      $options{next_ARGV($arg)} = 1;
    } else {
      output_error("OEIS-data-reader: unrecognised option: $arg");
    }
  }

  defined $anum
    or output_error("OEIS-data-reader: missing --anum");
  defined $action
    or output_error("OEIS-data-reader: missing --action");
  my $action_func = __PACKAGE__->can("action__$action")
    or output_error("OEIS-data-reader: unknown action $action");

  while (@flags) {
    my $flag = shift @flags;
    if ($flag eq '0') { }
    elsif ($flag eq 'bfile') { $flag_bfile = 1; }
    elsif ($flag eq 'offset') {
      @flags >= 1 or output_error("Missing second argument to \"offset\"");
      $flag_offset = shift @flags;
      $flag_offset =~ /^[-0-9]+$/
        or output_error("Bad offset $flag_offset");
    } else {
      output_error("Unrecognised flag: $flag");
    }
  }

  ### $anum
  ### $action
  ### @flags
  ### $flag_bfile
  ### $flag_offset
  ### $internal_basename
  ### $bfile_basename

  $action_func->();
}

main();
1;
