#!/usr/bin/env perl

my $copyright = <<'COPYRIGHT';
# Copyright 2021 by Christian Jaeger <ch@christianjaeger.ch>
# Published under the same terms as perl itself
COPYRIGHT

use strict;
use warnings;
use warnings FATAL => 'uninitialized';
use experimental 'signatures';

my ($email_full) = $copyright =~ / by ([^\n]*)/s;

my ($mydir, $myname);

BEGIN {
    $0 =~ /(.*?)([^\/]+)\z/s or die "?";
    ($mydir, $myname) = ($1, $2);
}
use lib "$mydir/../lib";

use Text::CSV qw( csv );
use JSON;
use Scalar::Util qw(looks_like_number);
use Getopt::Long;
use FunctionalPerl ":all";
use Scalar::Util qw(blessed);
use Chj::xIOUtil qw(stdout_utf8 stdin_utf8);

#use FP::Text::CSV qw(csv_file_to_rows);

sub usage {
    print STDERR map {"$_\n"} @_ if @_;
    print "$myname file.csv file.mint

  Convert CSV to JSON or Mint record literal syntax. It expects the
  input file to have a title row, and uses the fields in that as the
  field names (with Mint compatible mangling).

  file.csv or file.mint can also be '-' for stdin or stdout,
  respectively

  Options:
    --json  output JSON (default)
    --mint  output Mint record literal syntax
    --auto-numbers
            recognize strings that look like numbers
    --auto-integers
            strings that look like numbers and only have zeroes after
            the dot are treated as integers; currently implies
            --auto-numbers
    --repl
            open a repl instead of carrying out the identity
            conversion
    --conversion 'code'
            code is Perl that must evaluate to a function that
            receives the inputs and translate to the outputs.
            Example code:
            'sub(\$records) { groupByNumber(\$records, \"Quadrat\")}'
            or
            'sub(\$records) { groupByNumber(\$records, \"Quadrat\")->map(sub (\$group){ \$group->sort(on(hashkey(\"Frequency\"), \\&number_cmp))})}'


  ($email_full)
";
    exit(@_ ? 1 : 0);
}

my $verbose = 0;
our ($opt_json, $opt_mint, $opt_auto_numbers, $opt_auto_integers, $opt_repl,
    $opt_conversion);
GetOptions(
    "verbose"       => \$verbose,
    "help"          => sub {usage},
    "json"          => \$opt_json,
    "mint"          => \$opt_mint,
    "auto-numbers"  => \$opt_auto_numbers,
    "auto-integers" => \$opt_auto_integers,
    "repl"          => \$opt_repl,
    "conversion=s"  => \$opt_conversion,
) or exit 1;
usage unless @ARGV == 2;

our $mode
    = $opt_json
    ? ($opt_mint ? usage "both --json and --mint given" : "JSON")
    : ($opt_mint ? "Mint"                               : "JSON");

sub hashmap_pair_op() {
    $mode eq "JSON" ? ": " : " = "
}

# Not sure this is ideal but people will be confused if giving
# --auto-integers and it doesn't do anything.
$opt_auto_numbers = 1 if $opt_auto_integers;

my ($file_csv, $file_mint) = @ARGV;

#my $rows= csv_file_to_rows $file_csv;

sub fieldname_to_mint($str) {
    my $s = lcfirst($str);
    $s =~ s/^\s+//;
    $s =~ s/\s+\z//;
    $s =~ s/[^\w\d_]/_/sg;    # XX would mint support e.g. "-" ?
    if ($s =~ m/^\d/) {
        $s = "_$s";           # prepend an underscore if fieldname starts with a
                              # digit; XX needed?
    }
    $s
}

TEST { fieldname_to_mint "Quadrat 'o'Hara" } 'quadrat__o_Hara';

my $json = JSON->new->allow_nonref;

sub scalar_to_mint($val) {
    if ($opt_auto_numbers and looks_like_number $val) {

        # XX *assumes* that mint has the same number syntax as Perl
        $val =~ s/\.0*\z// if $opt_auto_integers;
        $val
    } else {

        # XX mint may *not* use all of the same syntax as JSON. For
        # now assume it does:
        $json->encode($val)
    }
}

TEST {
    local ($opt_auto_numbers, $opt_auto_integers) = (0, 0);
    scalar_to_mint "152.00"
}
"\"152.00\"";
TEST {
    local ($opt_auto_numbers, $opt_auto_integers) = (1, 0);
    scalar_to_mint "152.00"
}
"152.00";
TEST {
    local ($opt_auto_numbers, $opt_auto_integers) = (1, 1);
    scalar_to_mint "152.00"
}
"152";
TEST { scalar_to_mint "foo bar" } "\"foo bar\"";

#run_tests __PACKAGE__

sub hashmap_to_mint($hashmap) {
    "{\n" . [sort keys %$hashmap]->map(
        sub ($title) {
            my $value = $hashmap->{$title};
            fieldname_to_mint($title) . hashmap_pair_op . to_mint($value)
        }
    )->strings_join(",\n")
        . "\n}"
}

sub sequence_to_mint($l) {
    "[\n" . $l->map(\&to_mint)->strings_join(",\n") . "\n]"
}

sub to_mint($value) {
    if (defined(my $class = blessed $value)) {
        if ($value->isa("FP::Abstract::Sequence")) {
            return sequence_to_mint($value->purearray)
        }
    } elsif (my $r = ref($value)) {
        if ($r eq "ARRAY") {
            return sequence_to_mint($value->purearray)
        } elsif ($r eq "HASH") {
            return hashmap_to_mint($value)
        }
    } else {
        return scalar_to_mint($value)
    }
    die "don't know how to map this to Mint: " . show($value)
}

sub convertfile ($fn) {
    my $csvinput = $file_csv eq "-" ? stdin_utf8 : $file_csv;
    my $rows     = csv(in => $csvinput);

    my $titles = $rows->first;

    my $body      = $rows->rest;
    my $body_hms  = $body->map(sub ($row) { ziphash $titles, $row });
    my $body_hms2 = $fn->($body_hms);

    my $mint
        = "[\n" . $body_hms2->map(\&to_mint)->strings_join(",\n") . "\n]\n";

    my $out;
    if ($file_mint eq "-") {
        $out = stdout_utf8;
    } else {
        open $out, ">:encoding(UTF-8)", $file_mint
            or die "can't open file for writing: '$file_mint'";
    }
    print $out $mint or die "print: $!";
    close $out       or die "close($file_mint): $!";
}

sub groupByNumber ($records, $key) {
    $records->sort(on(hashkey($key), \&number_cmp))
        ->group(on(hashkey($key), \&number_eq))
}

if ($opt_repl) {
    repl;
} elsif ($opt_conversion) {
    my $function = eval $opt_conversion;
    die $@ if $@;
    ref($function) eq "CODE"
        or die
        "expecting --conversion's argument to evaluate to a function, but got: $function";
    convertfile($function);
} else {
    convertfile(\&identity);
}

#use Chj::ruse;
#use Chj::Backtrace;

