# Converter.pm: Common code for Converters.
#
# Copyright 2011-2024 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 <http://www.gnu.org/licenses/>.
#
# Original author: Patrice Dumas <pertusus@free.fr>

package Texinfo::Convert::Converter;

use 5.006;
use strict;

# To check if there is no erroneous autovivification
#no autovivification qw(fetch delete exists store strict);

# for fileparse
use File::Basename;
# for file names portability
use File::Spec;
use Encode qw(decode);
# for dclone
use Storable;
#use Data::Dumper;

use Carp qw(cluck confess);

use Texinfo::Convert::ConvertXS;
use Texinfo::XSLoader;

use Texinfo::Options;
use Texinfo::Common;

use Texinfo::Report;

use Texinfo::Convert::Utils;
use Texinfo::Convert::Unicode;
use Texinfo::Convert::Texinfo;
use Texinfo::Convert::Text;
use Texinfo::Convert::NodeNameNormalization;
use Texinfo::OutputUnits;

use Texinfo::Translations;

require Exporter;
our @ISA = qw(Exporter);

our @EXPORT_OK = qw(
xml_protect_text
xml_comment
xml_accent
xml_accents
);

our $VERSION = '7.2';

my $XS_convert = Texinfo::XSLoader::XS_convert_enabled();

our $module_loaded = 0;

my %XS_overrides = (
  # fully overriden for all the converters
  "Texinfo::Convert::Converter::_generic_converter_init",
   => "Texinfo::Convert::ConvertXS::generic_converter_init",
  "Texinfo::Convert::Converter::set_document"
   => "Texinfo::Convert::ConvertXS::converter_set_document",
  "Texinfo::Convert::Converter::set_conf"
   => "Texinfo::Convert::ConvertXS::set_conf",
  "Texinfo::Convert::Converter::force_conf"
   => "Texinfo::Convert::ConvertXS::force_conf",
  "Texinfo::Convert::Converter::get_conf"
   => "Texinfo::Convert::ConvertXS::get_conf",
  "Texinfo::Convert::Converter::get_converter_errors"
   => "Texinfo::Convert::ConvertXS::get_converter_errors",
  "Texinfo::Convert::Converter::converter_line_error"
   => "Texinfo::Convert::ConvertXS::converter_line_error",
  "Texinfo::Convert::Converter::converter_line_warn"
   => "Texinfo::Convert::ConvertXS::converter_line_warn",
  "Texinfo::Convert::Converter::converter_document_error"
   => "Texinfo::Convert::ConvertXS::converter_document_error",
  "Texinfo::Convert::Converter::converter_document_warn"
   => "Texinfo::Convert::ConvertXS::converter_document_warn",
  "Texinfo::Convert::Converter::get_converter_indices_sorted_by_letter"
   => "Texinfo::Convert::ConvertXS::get_converter_indices_sorted_by_letter",
  "Texinfo::Convert::Converter::get_converter_indices_sorted_by_index"
   => "Texinfo::Convert::ConvertXS::get_converter_indices_sorted_by_index",
  "Texinfo::Convert::Converter::set_global_document_commands"
   => "Texinfo::Convert::ConvertXS::converter_set_global_document_commands",

  # XS only
  "Texinfo::Convert::Converter::reset_converter"
   => "Texinfo::Convert::ConvertXS::reset_converter",
  "Texinfo::Convert::Converter::destroy"
   => "Texinfo::Convert::ConvertXS::destroy",

  "Texinfo::Convert::Converter::XS_get_unclosed_stream"
   => "Texinfo::Convert::ConvertXS::get_unclosed_stream",
);

sub import {
  if (!$module_loaded) {
    if ($XS_convert) {
      foreach my $sub (keys %XS_overrides) {
        Texinfo::XSLoader::override ($sub, $XS_overrides{$sub});
      }
    }

    $module_loaded = 1;
  }
  # The usual import method
  goto &Exporter::import;
}

# values for integer and string options in code generated from
# Texinfo/Convert/converters_defaults.txt
my $regular_defaults
  = Texinfo::Options::get_converter_regular_options('converter');

my %defaults = %$regular_defaults;

# values for integer and string options in code generated from
# Texinfo/Convert/converters_defaults.txt
my $common_defaults = Texinfo::Options::get_converter_regular_options('common');

# defaults for all converters that are not defined elsewhere.
# undef values in general marks information passed by the caller that
# is valid in the initialization hash, but is not considered
# as "configuration/customization". It is not available through get_conf()
# but is available directly in the converter as a hash key.
# FIXME separate the two types of information and check that those
# items are not valid customization options?  Or make them customization
# variables that can only be set from init files, like buttons or icons?
# NOTE converters for now only set customization variables.
# It would be good to keep it that way.
my %common_converters_defaults = (
  # Following are set in the main program
  'deprecated_config_directories' => undef,

  # Not set in the main program
  'translated_commands'  => {'error' => 'error@arrow{}',},

# integer and string customization variables common for all the converters
# with values different from main program values
  %$common_defaults
);

my %all_converters_defaults
 = (%Texinfo::Options::converter_cmdline_options,
    %Texinfo::Options::converter_customization_options,
    %Texinfo::Options::unique_at_command_options,
    %Texinfo::Options::multiple_at_command_options,
    %common_converters_defaults
);

# For translation of in document string.
if (0) {
  my $self;
  # TRANSLATORS: expansion of @error{} as Texinfo code
  $self->cdt('error@arrow{}');
}

our %default_args_code_style = (
  'email' => [1],
  'anchor' => [1],
  'uref' => [1],
  'url' => [1],
  'math' => [1],
  'inforef' => [1,undef,1],
  'image' => [1, 1, 1, undef, 1],
# and type?
  'float' => [1],
);

foreach my $code_style_command (keys(%Texinfo::Commands::brace_code_commands)) {
  $default_args_code_style{$code_style_command} = [1];
}
foreach my $ref_cmd ('pxref', 'xref', 'ref') {
  $default_args_code_style{$ref_cmd} = [1, undef, undef, 1];
}

################################################################
# converter API

# convert_tree() and convert() should be implemented in converters.
# There is an implementation of output() below but in general
# output() should also be implemented by Converters.  The simple
# implementation of convert_output_unit() below is likely to be
# ok for most converters.

# Functions that should be defined in specific converters
sub converter_defaults($$)
{
  return \%defaults;
}

# should be redefined by specific converters
sub converter_initialize($)
{
}

sub conversion_initialization($;$)
{
  my $converter = shift;
  my $document = shift;

  if ($document) {
    $converter->set_document($document);
  }
}

sub conversion_finalization($)
{
  #my $converter = shift;
}

sub output_internal_links($)
{
  my $self = shift;
  return undef;
}

sub set_document($$)
{
  my $converter = shift;
  my $document = shift;

  $converter->{'document'} = $document;

  Texinfo::Common::set_output_encoding($converter, $document);

  $converter->{'convert_text_options'}
     = Texinfo::Convert::Text::copy_options_for_convert_text($converter);

  # In general, OUTPUT_PERL_ENCODING set below is needed for the output()
  # entry point through Texinfo::Convert::Utils::output_files_open_out.  It is
  # also sometime needed for the converter itself.  If not, in general it
  # is not needed for the convert() entry point, so the call could also be
  # done more finely in converters, but it is not really important.
  Texinfo::Common::set_output_perl_encoding($converter);
}

# initialization either in generic XS converter or in Perl
sub _generic_converter_init($$;$)
{
  my $converter = shift;
  my $format_defaults = shift;
  my $conf = shift;

  my %defaults = %all_converters_defaults;

  if ($format_defaults) {
    foreach my $key (keys(%$format_defaults)) {
      $defaults{$key} = $format_defaults->{$key};
    }
  }
  $converter->{'conf'} = {};
  foreach my $key (keys(%defaults)) {
    if (Texinfo::Common::valid_customization_option($key)) {
      $converter->{'conf'}->{$key} = $defaults{$key};
    } else {
      $converter->{$key} = $defaults{$key};
    }
  }

  $converter->{'configured'} = {};
  if (defined($conf)) {
    foreach my $key (keys(%$conf)) {
      if (Texinfo::Common::valid_customization_option($key)) {
        $converter->{'conf'}->{$key} = $conf->{$key};
      } elsif (!exists($defaults{$key})) {
        my $class = ref($converter);
        warn "$class: $key not a possible configuration\n";
      } else {
        $converter->{$key} = $conf->{$key};
      }
      # configuration set here, from the argument of the converter,
      # in general coming from command-line or from init files will not
      # be reset by set_conf.
      $converter->{'configured'}->{$key} = 1;
    }
  }
  # set $converter->{'converter_init_conf'} to the customization
  # options obtained after setting the defaults and applying
  # the customization passed as argument.
  $converter->{'converter_init_conf'} = { %{$converter->{'conf'}} };

  # used for output files information, to register opened
  # and not closed files.  Accessed through output_files_information()
  $converter->{'output_files'}
    = Texinfo::Convert::Utils::output_files_initialize();

  # setup expanded formats as a hash.
  my $expanded_formats = $converter->{'conf'}->{'EXPANDED_FORMATS'};
  $converter->{'expanded_formats'} = {};
  if (defined($expanded_formats)) {
    foreach my $expanded_format (@$expanded_formats) {
      $converter->{'expanded_formats'}->{$expanded_format} = 1;
    }
  }

  $converter->{'error_warning_messages'} = [];
}

# this function is designed so as to be used in specific Converters
# and not redefined.
sub converter($;$)
{
  my $class = shift;
  my $conf = shift;

  my $converter = {};

  bless $converter, $class;

  my $format_defaults = $converter->converter_defaults($conf);

  _generic_converter_init($converter, $format_defaults, $conf);

  $converter->converter_initialize();

  return $converter;
}

sub convert_output_unit($$)
{
  my $self = shift;
  my $output_unit = shift;

  my $result = '';
  foreach my $element (@{$output_unit->{'unit_contents'}}) {
    $result .= $self->convert_tree($element);
  }
  return $result;
}

# should be redefined by specific converters
sub conversion_output_begin($;$$)
{
  my $self = shift;
  my $output_file = shift;
  my $output_filename = shift;

  return '';
}

sub conversion_output_end($)
{
  my $self = shift;

  return '';
}

sub output_tree($$)
{
  my $self = shift;
  my $document = shift;

  $self->conversion_initialization($document);

  my $root = $document->tree();

  my ($output_file, $destination_directory, $output_filename)
    = $self->determine_files_and_directory(
                              $self->get_conf('TEXINFO_OUTPUT_FORMAT'));

  my ($encoded_destination_directory, $dir_encoding)
    = $self->encoded_output_file_name($destination_directory);
  my $succeeded
    = $self->create_destination_directory($encoded_destination_directory,
                                          $destination_directory);
  unless ($succeeded) {
    $self->conversion_finalization();
    return undef;
  }

  my $fh;
  my $encoded_output_file;
  if (! $output_file eq '') {
    my $path_encoding;
    ($encoded_output_file, $path_encoding)
      = $self->encoded_output_file_name($output_file);
    my $error_message;
    # the third return information, set if the file has already been used
    # in this files_information is not checked as this cannot happen.
    ($fh, $error_message) = Texinfo::Convert::Utils::output_files_open_out(
                              $self->output_files_information(), $self,
                              $encoded_output_file);
    if (!$fh) {
      $self->converter_document_error(
           sprintf(__("could not open %s for writing: %s"),
                                    $output_file, $error_message));
      $self->conversion_finalization();
      return undef;
    }
  }

  my $output_beginning
    = $self->conversion_output_begin($output_file, $output_filename);

  my $result = '';
  $result .= $self->write_or_return($output_beginning, $fh);
  $result .= $self->write_or_return($self->convert_tree($root), $fh);

  my $output_end = $self->conversion_output_end();

  $result .= $self->write_or_return($output_end, $fh);

  if ($fh and $output_file ne '-') {
    Texinfo::Convert::Utils::output_files_register_closed(
                  $self->output_files_information(), $encoded_output_file);
    if (!close ($fh)) {
      $self->converter_document_error(
            sprintf(__("error on closing %s: %s"),
                                    $output_file, $!));
    }
  }
  $self->conversion_finalization();
  return $result;
}

# Nothing to do in Perl.  XS function resets converter
sub reset_converter($)
{
}

# Nothing to do in Perl.  XS function frees memory
sub destroy($)
{
}

sub XS_get_unclosed_stream($$)
{
  return undef;
}

sub output_files_information($)
{
  my $self = shift;
  return $self->{'output_files'};
}


