#! /usr/bin/env perl

# generate_code_convert_data.pl: generate perl hashes and C structures
# based on data information relevant for conversion, mainly to HTML.
#
# Copyright 2024-2026 Free Software Foundation, Inc.
#
# This program 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 of the License,
# or (at your option) any later version.
#
# This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.

# ../maintain/generate_code_convert_data.pl ../data/default_direction_strings.csv ../data/default_special_unit_info.csv ../data/html_style_commands_element.csv perl Texinfo/HTMLData.pm

use strict;

use warnings;

# in Perl core standard modules
use File::Basename;
use Text::Wrap;

my $program_name = basename($0);

my $default_direction_strings_file = $ARGV[0];
open (DDS, "<$default_direction_strings_file")
  or die "open $default_direction_strings_file failed: $!";

my $default_special_unit_info_file = $ARGV[1];
open (DSUI, "<$default_special_unit_info_file")
  or die "open $default_special_unit_info_file failed: $!";

my $style_commands_element_file = $ARGV[2];
open (SCE, "<$style_commands_element_file")
  or die "open $style_commands_element_file failed: $!";

my $lang = $ARGV[3];

my $perl_format = 0;
my $C_format = 0;
if ($lang eq 'perl') {
  $perl_format = 1;
} else {
  $C_format = 1;
}

my $out_file = $ARGV[4];

die "Need an output file\n" if (!defined($out_file));

open(OUT, ">$out_file") or die "Open $out_file: $!\n";

my $initial_notes_tab;
my $subsequent_notes_tab;

if ($perl_format) {
  $initial_notes_tab = ' # ';
  $subsequent_notes_tab = ' # ';

  print OUT "# Automatically generated by $program_name\n\n";

  print OUT "package Texinfo::HTMLData;\n\n";

  print OUT "use Texinfo::Common;\n\n";
} else {
  # C format

  my $header_file = $ARGV[5];

  die "Need an output header\n" if (!defined($header_file));

  open(HDR, ">$header_file") or die "Open $header_file: $!\n";

  $initial_notes_tab = '';
  $subsequent_notes_tab = '   ';

  print OUT "/* Automatically generated by $program_name */\n\n";

  print OUT "#include <config.h>\n\n";
  print OUT "#include \"html_conversion_data.h\"\n";
  print OUT "#include \"tree_types.h\"\n";
  print OUT "#include \"converter_types.h\"\n\n";
}

my $C_header_string = 'CONVERSION_DATA_H';
if ($perl_format) {
} else {
  print HDR "/* Automatically generated from $program_name */\n\n";
  print HDR "#ifndef $C_header_string\n"
         ."#define $C_header_string\n\n";
  print HDR 'typedef struct HTML_DEFAULT_DIRECTION_STRING_TRANSLATED {'."\n"
   .'    const char *converted;'."\n"
   .'    const char *to_convert;'."\n"
   .'} HTML_DEFAULT_DIRECTION_STRING_TRANSLATED;'."\n\n";
}



my @su_ordered_untranslated_hashes;
my @su_ordered_translated_hashes;

my @su_header;
while (1) {
  my $su_header_line = <DSUI>;
  chomp($su_header_line);
  #print STDERR "$su_header_line\n";
  @su_header = split(/\|/, $su_header_line);
  if (scalar(@su_header) > 1) {
    last;
  }
}

my $special_unit_header = shift @su_header;

my $su_header_index = 1;
my $su_type;
my %su_header_indices;
foreach my $header (@su_header) {
  if ($header =~ /^([^ ]+) ([^ ]+)$/) {
    my $spec = $2;
    if ($spec ne 'string') {
      die "Unknown special unit column spec $spec\n";
    }
    $su_type = $1;
    push @su_ordered_translated_hashes, $su_type;
    $su_header_indices{$su_type} = {$spec => $su_header_index};
  } elsif ($header eq '') {
    next;
  } else {
    push @su_ordered_untranslated_hashes, $header;
    $su_type = undef;
    $su_header_indices{$header} = $su_header_index;
  }
  $su_header_index++;
}

my $default_special_unit_info_base_name
  = basename($default_special_unit_info_file);

if ($C_format) {
  print HDR "enum special_unit_info_type {\n"
         ."   SUI_type_none = -1,\n";

  print OUT "/* Generated from $default_special_unit_info_base_name */\n";
  print OUT "const char *special_unit_info_type_names[] = {\n";
}

my %su_hash_lines;
foreach my $type (@su_ordered_untranslated_hashes, @su_ordered_translated_hashes) {
  $su_hash_lines{$type} = '';
  if ($C_format) {
    print HDR "   SUI_type_${type},\n";
    print OUT "   \"${type}\",\n";
  }
}

if ($C_format) {
  print OUT "};\n\n";
  print HDR "};\n\n";

  print OUT "const char *special_unit_info_tree_names[] = {\n";
  foreach my $type (@su_ordered_translated_hashes) {
    print OUT "   \"${type}\",\n";
  }
  print OUT "};\n\n";

  print HDR "#define SPECIAL_UNIT_INFO_TYPE_NR "
    .(scalar(@su_ordered_untranslated_hashes))."\n\n";

  print HDR "#define SPECIAL_UNIT_INFO_TREE_NR "
    .(scalar(@su_ordered_translated_hashes))."\n\n";

  print HDR "#define CUSTOMIZED_SPECIAL_UNIT_INFO_TYPE_NR "
  ."(SPECIAL_UNIT_INFO_TYPE_NR + SPECIAL_UNIT_INFO_TREE_NR)\n\n";

  print HDR "/* translated */\n";
  print HDR "enum special_unit_info_tree {\n"
         ."   SUIT_type_none = -1,\n\n";

  print OUT "const TRANSLATED_SUI_ASSOCIATION translated_special_unit_info[] = {\n";
  foreach my $type (@su_ordered_translated_hashes) {
    print HDR "   SUIT_type_${type},\n";
    print OUT "  {SUIT_type_${type}, SUI_type_${type}},\n";
  }
  print OUT "  /* these special types end the list */\n"
         ."  {SUIT_type_none, SUI_type_none},\n"
         ."};\n\n";

  print HDR "};\n\n";
}

# gather for direction structures below
my %su_directions;
my @ordered_su_directions;
my @su_ordered;
while (<DSUI>) {
  chomp;
  my @data = split (/\|/);
  my $special_unit = $data[0];
  push @su_ordered, $special_unit;
  foreach my $untranslated_type (@su_ordered_untranslated_hashes) {
    my $index = $su_header_indices{$untranslated_type};
    my $value = '';
    if (defined($data[$index])) {
      $value = $data[$index];
      if ($untranslated_type eq 'direction') {
        $su_directions{$value} = 1;
        push @ordered_su_directions, $value;
      }
    }
    if ($perl_format) {
      $su_hash_lines{$untranslated_type} .= "    '$special_unit' => '$value',\n";
    } else {
      my $value = '0';
      if (defined($data[$index])) {
        $value = "\"$data[$index]\"";
      }
      $su_hash_lines{$untranslated_type} .= "$value, ";
    }
  }
  foreach my $translated_type (@su_ordered_translated_hashes) {
    my $string = $data[$su_header_indices{$translated_type}->{'string'}];
    if (defined($string) and $string ne '') {
      # NOTE to be kept in sync with context in Texinfo::Convert::HTML
      # special_unit_info
      my $context = $special_unit . ' section heading';
      if ($perl_format) {
        $su_hash_lines{$translated_type}
         .= "    '$special_unit' => Texinfo::Common::pgdt('$context', '$string'),\n";
      } else {
        $su_hash_lines{$translated_type}
         .= "pgdt_noop(\"$context\", \"$string\"), ";
      }
    } else {
      print STDERR "REMARK: '$special_unit': no $translated_type\n";
      if ($C_format) {
        $su_hash_lines{$translated_type} .= '0, ';
      }
    }
  }
}
my $special_units_nr = scalar(@su_ordered);

if ($perl_format) {
  print OUT "# Generated from $default_special_unit_info_base_name\n";
  print OUT "my %default_special_unit_info = (\n";

  foreach my $type (@su_ordered_untranslated_hashes) {
    print OUT "\n  '$type' => {\n";
    print OUT $su_hash_lines{$type};
    print OUT "  },\n";
  }

  print OUT ");\n\n";

  print OUT 'sub get_default_special_unit_info() {'."\n"
   .'  return \%default_special_unit_info;'."\n"
   ."}\n\n";


  print OUT "my %default_translated_special_unit_info = (\n";
  foreach my $type (@su_ordered_translated_hashes) {
    print OUT "\n  '$type' => {\n";
    print OUT $su_hash_lines{$type};
    print OUT "  },\n";
  }
  print OUT ");\n\n";

  print OUT 'sub get_default_translated_special_unit_info() {'."\n"
   .'  return \%default_translated_special_unit_info;'."\n"
   ."}\n\n";
} else {
  # C format

  print OUT "static char *default_special_unit_varieties_array[] = {\n";
  foreach my $special_units (@su_ordered) {
    print OUT "  \"$special_units\",\n";
  }
  print OUT "};\n";

  print OUT "const STRING_LIST default_special_unit_varieties = {default_special_unit_varieties_array, $special_units_nr, $special_units_nr};\n\n";

  # For now, this define is not used anywhere else.  Keep it here only.
  print OUT "#define pgdt_noop(Context,String) String\n";

  print OUT "const char * const default_special_unit_info[SPECIAL_UNIT_INFO_TYPE_NR][$special_units_nr] = {\n";
  foreach my $type (@su_ordered_untranslated_hashes) {
    print OUT "  {".$su_hash_lines{$type}."}, /* $type */\n";
  }
  print OUT "};\n\n";

  print OUT "const char * const default_special_unit_tree_info[SPECIAL_UNIT_INFO_TREE_NR][$special_units_nr] = {\n";
  foreach my $type (@su_ordered_translated_hashes) {
    print OUT "  {".$su_hash_lines{$type}."}, /* $type */\n";
  }
  print OUT "};\n\n";

  print HDR "extern const char * const default_special_unit_info[SPECIAL_UNIT_INFO_TYPE_NR][$special_units_nr];\n\n";

  print HDR "extern const char * const default_special_unit_tree_info[SPECIAL_UNIT_INFO_TREE_NR][$special_units_nr];\n\n";
}




my @ordered_global_directions = ('First', 'Top', 'Index', 'Last', 'Space');
my %ordered_global_directions_hash;
foreach my $global_direction (@ordered_global_directions) {
  $ordered_global_directions_hash{$global_direction} = 1;
}

my %direction_orders = (
  'global' => \@ordered_global_directions,
  'relative' => [],
  'file' => []
);
my @orders_order = ('global', 'relative', 'file');

my @d_ordered_untranslated_hashes;
my @d_ordered_translated_hashes;

my @d_header;
while (1) {
  my $d_header_line = <DDS>;
  chomp($d_header_line);
  #print STDERR "$d_header_line\n";
  @d_header = split(/\|/, $d_header_line);
  if (scalar(@d_header) > 1) {
    last;
  }
}

my $direction_header = shift @d_header;

my $d_header_index = 1;
my $type;
my %d_header_indices;
foreach my $header (@d_header) {
  if ($header =~ /^([^ ]+) converted$/) {
    $type = $1;
    push @d_ordered_translated_hashes, $type;
    $d_header_indices{$type} = {'converted' => $d_header_index};
  } elsif ($header =~ /^([^ ]+) to_convert ([^ ]+)$/) {
    if ($1 ne $type) {
      die "Non matching type $1 ne $type\n";
    }
    my $spec = $2;
    if ($spec ne 'string') {
      die "Unknown to_convert spec $spec\n";
    }
    $d_header_indices{$type}->{'to_convert'} = $d_header_index;
  } elsif ($header eq '') {
    next;
  } else {
    push @d_ordered_untranslated_hashes, $header;
    $type = undef;
    $d_header_indices{$header} = $d_header_index;
  }
  $d_header_index++;
}

sub substitute_direction_value
{
  my $input = shift;
  $input =~ s/\$html_default_entity_nbsp/&nbsp;/;
  $input =~ s/\$\{bar\}/\|/;
  return $input;
}

my %hash_lines;
my %hash_values;
foreach my $type (@d_ordered_untranslated_hashes, @d_ordered_translated_hashes) {
  $hash_lines{$type} = '';
  $hash_values{$type} = {};
}

my %type_contexts_map = (
   'text' => 'string',
   'description' => 'description',
   'button' => 'button label',
);

my $default_direction_strings_base_name
  = basename($default_direction_strings_file);
if ($C_format) {
  print HDR "extern const char *direction_type_translation_context[];\n\n";

  # we define preprocessor macros
  print HDR "#define TDS_TRANSLATED_TYPES_LIST \\\n";
  foreach my $type (@d_ordered_translated_hashes) {
    print HDR "  tds_type(${type}) \\\n";
  }
  print HDR "\n\n";

  print HDR "#define TDS_NON_TRANSLATED_TYPES_LIST \\\n";
  foreach my $type (@d_ordered_untranslated_hashes) {
    print HDR "  tds_type(${type}) \\\n";
  }
  print HDR "\n\n";

  print HDR "#define TDS_TRANSLATED_MAX_NR TDS_type_"
   .$d_ordered_translated_hashes[-1]." +1\n\n";
  print HDR "#define TDS_TYPE_MAX_NR TDS_type_"
   .$d_ordered_untranslated_hashes[-1]." +1\n\n";

  print OUT "/* Generated from $default_direction_strings_base_name */\n";
  print OUT "const char *direction_type_translation_context[] = {\n";
  foreach my $type (@d_ordered_translated_hashes) {
    print OUT '"'.$type_contexts_map{$type}.'", '."/* $type */\n";
  }
  print OUT "};\n\n";
}

while (<DDS>) {
  chomp;
  my @data = split (/\|/);
  my $direction = $data[0];
  if (!$ordered_global_directions_hash{$direction}
      and !$su_directions{$direction}) {
    if ($direction =~ /File/) {
      push @{$direction_orders{'file'}}, $direction;
    } else {
      push @{$direction_orders{'relative'}}, $direction;
    }
  }
  foreach my $untranslated_type (@d_ordered_untranslated_hashes) {
    my $index = $d_header_indices{$untranslated_type};
    my $value = '';
    if (defined($data[$index])) {
      $value = substitute_direction_value($data[$index]);
    }
    if ($perl_format) {
      $hash_lines{$untranslated_type} .= "    '$direction' => '$value',\n";
    } else {
      if ($value ne '') {
        $hash_values{$untranslated_type}->{$direction} = $value;
      }
    }
  }
  foreach my $translated_type (@d_ordered_translated_hashes) {
    my $converted_value = $data[$d_header_indices{$translated_type}->{'converted'}];
    if (defined($converted_value) and $converted_value ne '') {
      my $value = substitute_direction_value($converted_value);
      if ($perl_format) {
        $hash_lines{$translated_type}
          .= "    '$direction' => {'converted' => '$value'},\n";
      } else {
        $hash_values{$translated_type}->{$direction} = {'converted' => $value};
      }
    } else {
      my $string = $data[$d_header_indices{$translated_type}->{'to_convert'}];
      if (defined($string) and $string ne '') {
        my $context;
        my $value = substitute_direction_value($string);
        # as an exception 'This' becomes 'This (current section)' in contexts
        if ($direction eq 'This') {
          $context = 'This (current section)';
        } else {
          $context = $direction;
        }
        $context .= ' direction '.$type_contexts_map{$translated_type};
        if ($perl_format) {
          $hash_lines{$translated_type}
           .= "    '$direction' => {'to_convert' "
                       ."=> Texinfo::Common::pgdt('$context', '$value')},\n";
        } else {
          $hash_values{$translated_type}->{$direction} = {'string' => $value,
                                                          'context' => $context};
        }
      } else {
        print STDERR "REMARK: '$direction': no $translated_type\n";
      }
    }
  }
}

my @ordered_directions = (@{$direction_orders{'global'}}, @{$direction_orders{'relative'}},
                          @{$direction_orders{'file'}}, @ordered_su_directions);

if ($perl_format) {
  print OUT "# Generated from $default_direction_strings_base_name\n";
  print OUT 'sub get_directions_order() {'."\n"
    . 'return [';
  foreach my $order (@orders_order) {
    print OUT '['.join(', ', map{"'$_'"} @{$direction_orders{$order}})."],\n";
  }
  print OUT "];\n}\n\n";


  print OUT "my %default_converted_directions_strings = (\n";

  foreach my $type (@d_ordered_untranslated_hashes) {
    print OUT "\n  '$type' => {\n";
    print OUT $hash_lines{$type};
    print OUT "  },\n";
  }

  print OUT ");\n\n";

  print OUT 'sub get_default_converted_directions_strings() {'."\n"
   .'  return \%default_converted_directions_strings;'."\n"
   ."}\n\n";

  print OUT "my %default_translated_directions_strings = (\n";
  foreach my $type (@d_ordered_translated_hashes) {
    print OUT "\n  '$type' => {\n";
    print OUT $hash_lines{$type};
    print OUT "  },\n";
  }
  print OUT ");\n\n";

  print OUT 'sub get_default_translated_directions_strings() {'."\n"
   .'  return \%default_translated_directions_strings;'."\n"
   ."}\n\n";
} else {
  # C format

  print HDR "#define HTML_GLOBAL_DIRECTIONS_LIST \\\n";
  foreach my $direction (@{$direction_orders{'global'}}) {
    print HDR "   hgdt_name(${direction}) \\\n";
  }
  print HDR "\n\n";

  print HDR "/* relative output unit directions */\n";
  print HDR "#define RUD_DIRECTIONS_TYPES_LIST \\\n";
  foreach my $direction (@{$direction_orders{'relative'}}) {
    print HDR "   rud_type(${direction}) \\\n";
  }
  print HDR "\n\n";

  print HDR "/* relative output unit file directions */\n";
  print HDR "#define RUD_FILE_DIRECTIONS_TYPES \\\n";
  foreach my $direction (@{$direction_orders{'file'}}) {
    print HDR "   rud_type(${direction}) \\\n";
  }
  print HDR "\n\n";

  print HDR "/* for buttons directions */\n";
  print HDR "#define FIRSTINFILE_MIN_IDX D_direction_FirstInFile"
     .$direction_orders{'relative'}[0]."\n";
  print HDR "#define FIRSTINFILE_MAX_IDX D_direction_FirstInFile"
     .$direction_orders{'relative'}[-1]."\n\n";

  my $nr_string_directions = scalar(@ordered_directions);

  print HDR "extern const char * const default_converted_directions_strings[]["
                            ."$nr_string_directions];\n\n";

  print OUT "const char * const default_converted_directions_strings[]["
                            ."$nr_string_directions] = {\n";
  foreach my $type (@d_ordered_untranslated_hashes) {
    print OUT "  { /* $type */\n";
    foreach my $direction (@ordered_directions) {
       print OUT '    ';
      if ($hash_values{$type} and $hash_values{$type}->{$direction}) {
        print OUT '"'.$hash_values{$type}->{$direction}.'",';
      } else {
        print OUT '0,';
      }
      print OUT " /* $direction */\n";
    }
    print OUT "  },\n";
  }
  print OUT "};\n\n";

  print HDR "extern const HTML_DEFAULT_DIRECTION_STRING_TRANSLATED "
     ."default_translated_directions_strings[][$nr_string_directions];\n\n";

  print OUT "const HTML_DEFAULT_DIRECTION_STRING_TRANSLATED "
     ."default_translated_directions_strings[][$nr_string_directions] = {\n";
  foreach my $type (@d_ordered_translated_hashes) {
    print OUT "  { /* $type */\n";
    foreach my $direction (@ordered_directions) {
      if ($hash_values{$type} and $hash_values{$type}->{$direction}) {
        print OUT '    {';
        if (defined($hash_values{$type}->{$direction}->{'converted'})) {
          print OUT '"'.$hash_values{$type}->{$direction}->{'converted'}.'", 0';
        } else {
          my $to_convert = $hash_values{$type}->{$direction};
          my $context = $to_convert->{'context'};
          my $string = $to_convert->{'string'};
          my $value = "pgdt_noop(\"$context\", \"$string\")";
          print OUT "0, $value";
        }
        print OUT '},';
      } else {
        print OUT '    {0, 0},';
      }
      print OUT " /* $direction */\n";
    }
    print OUT "  },\n";
  }
  print OUT "};\n\n";
}




my @sce_header;
while (1) {
  my $sce_header_line = <SCE>;
  chomp($sce_header_line);
  #print STDERR "$sce_header_line\n";
  @sce_header = split(/\|/, $sce_header_line);
  if (scalar(@sce_header) > 1) {
    last;
  }
}

my ($sce_command_index, $sce_html_element_index, $sce_notes_index);
my $sce_header_index = 0;
foreach my $header (@sce_header) {
  if ($header eq 'command') {
    $sce_command_index = $sce_header_index;
  } elsif ($header eq 'html_element') {
    $sce_html_element_index = $sce_header_index;
  } elsif ($header eq 'notes') {
    $sce_notes_index = $sce_header_index;
  }
  $sce_header_index++;
}
if (!defined($sce_command_index) or !defined($sce_html_element_index)
    or !defined($sce_notes_index)) {
  die "missing header column ($sce_command_index, "
                   ."$sce_html_element_index, $sce_notes_index)\n";
}

my $style_commands_element_base_name = basename($style_commands_element_file);
if ($perl_format) {
  print OUT "# Generated from $style_commands_element_base_name\n";
  print OUT "my %html_style_commands_element = (\n";
}

my %command_element;
my $line_nr = 1;
while (<SCE>) {
  $line_nr++;
  chomp;
  my @data = split (/\|/);
  my $notes = $data[$sce_notes_index];
  if (defined($notes) and $notes ne '') {
    my $lines;
    if ($perl_format) {
      $lines = wrap($initial_notes_tab, $subsequent_notes_tab, ($notes));
      print OUT $lines."\n";
    }
  }
  my $sce_command = $data[$sce_command_index];
  if (!defined($sce_command) or $sce_command eq '') {
    die "$style_commands_element_file: $line_nr: Bad command\n";
  }
  my $sce_html_element = $data[$sce_html_element_index];
  if (!defined($sce_html_element) or $sce_html_element eq '') {
    die "$style_commands_element_file: $line_nr: Bad html_element\n";
  }
  if ($perl_format) {
    print OUT "    '$sce_command'    => '$sce_html_element',\n";
  } else {
    $command_element{$sce_command} = $sce_html_element;
  }
}

if ($perl_format) {
  print OUT ");\n\n";

  print OUT 'sub get_html_style_commands_element() {'."\n"
   .'  return \%html_style_commands_element;'."\n"
   ."}\n\n";

  print OUT "1;\n";
} else {
  # C format

  print HDR "extern const char *html_style_commands_element[];\n\n";

  print HDR "#endif\n";

  my @commands_order = ('');

  my %commands_map = (
   '\\t' => "\t",
   '\\n' => "\n",
   '\\x20' => ' ',
    '\"' => '"',
    '\\\\' => '\\',
  );

  my %name_commands;
  while (<STDIN>) {
    my $command;
    if (/^{"([^"]+?)", /) {
      $command = $1;
    } elsif (/^{"\\"", /) {
      $command = '\"';
    }
    next if (!defined($command));
    my $command_name = $command;
    if (exists $commands_map{$command}) {
      $command_name = $commands_map{$command};
      $name_commands{$command_name} = $command;
    }
    push @commands_order, $command_name;
    #print STDERR "$command\n";
  }

  if ($C_format) {
    print OUT "/* Generated from $style_commands_element_base_name */\n";
    print OUT "const char *html_style_commands_element[] = {\n";
    foreach my $command_name (@commands_order) {
      my $command = $command_name;
      if (exists($name_commands{$command_name})) {
        $command = $name_commands{$command_name};
      }

      if (exists($command_element{$command})) {
        print OUT "\"$command_element{$command}\",  /* $command */\n";
      } else {
        print OUT "0,\n";
      }
    }
    print OUT "};\n\n";
  }
}

close(OUT);
