#! @PERL@ -w
#
#  Man page to help file converter
#  Copyright (C) 1994, 1995, 1998, 2000, 2001, 2002, 2003, 2004, 2005,
#  2007, 2010, 2011
#  The Free Software Foundation, Inc.
#
#  Originally written by:
#   Andrew V. Samoilov, 2002
#   Pavel Roskin, 2002
#   Andrew Borodin <aborodin@vmail.ru>, 2010
#
#  Completely rewritten in Perl by:
#   Alexandr Prenko, 2010
#
# 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/>.
#
# \file man2hlp
# \brief Source: man page to help file converter

use strict;
use warnings;

# Perl have no static variables, so this hash emulates them
my %static = (
    "string_len anchor_flag"    => 0,
    "string_len lc_link_flag"   => 0,
    "handle_link old"           => undef
);

# Imported constants
my $CHAR_LINK_START     = chr(01);      # Ctrl-A
my $CHAR_LINK_POINTER   = chr(02);      # Ctrl-B
my $CHAR_LINK_END       = chr(03);      # Ctrl-C
my $CHAR_NODE_END       = chr(04);      # Ctrl-D
my $CHAR_ALTERNATE      = chr(05);      # Ctrl-E
my $CHAR_NORMAL         = chr(06);      # Ctrl-F
my $CHAR_VERSION        = chr(07);      # Ctrl-G
my $CHAR_FONT_BOLD      = chr(010);     # Ctrl-H
my $CHAR_FONT_NORMAL    = chr(013);     # Ctrl-K
my $CHAR_FONT_ITALIC    = chr(024);     # Ctrl-T
# end of import

my $col = 0;            # Current output column
my $out_row = 1;        # Current output row
my $in_row = 0;         # Current input row
my $no_split_flag = 0;  # Flag: Don't split section on next ".SH"
my $skip_flag = 0;      # Flag: Skip this section.
                        #       0 = don't skip,
                        #       1 = skipping title,
                        #       2 = title skipped, skipping text
my $link_flag = 0;      # Flag: Next line is a link
my $verbatim_flag = 0;  # Flag: Copy input to output verbatim
my $node = 0;           # Flag: This line is an original ".SH"

my $c_out;              # Output filename
my $f_out;              # Output file

my $c_in;               # Current input filename

my $indentation;        # Indentation level, n spaces
my $tp_flag;            # Flag: .TP paragraph
                        #       1 = this line is .TP label,
                        #       2 = first line of label description.
my $topics = undef;

# Emulate C strtok()
my $strtok;

sub strtok($$) {
    my ($str, $chars) = @_;

    if (! defined $chars || $chars eq "")
    {
        my $result = $strtok;
        $strtok = undef;
        return $result;
    }

    $str = $strtok unless defined $str;
    return undef unless defined $str;

    my $result;
    $str =~ s/^[$chars]+//;
    ($result, $strtok) = split /[$chars]+/, $str, 2;
    ($result, $strtok) = split /[$chars]+/, $strtok, 2 if defined $result && $result eq "";
    $strtok = undef if ! defined $strtok || $strtok eq "";
    return $result;
}

sub struct_node() {
    return {
        "node"          => undef,   # Section name
        "lname"         => undef,   # Translated .SH, undef if not translated
        "next"          => undef,
        "heading_level" => undef
    }
}

my $nodes = struct_node();
my $cnode;              # Current node

# Report error in input
sub print_error($)
{
    my ($message) = @_;
    warn sprintf  "man2hlp: %s in file \"%s\" on line %d\n", $message, $c_in, $in_row;
}

# Do open, exit if it fails
sub fopen_check ($$)
{
    my ($mode, $filename) = @_;
    my $f;

    unless (open $f, $mode, $filename)
    {
        warn sprintf("man2hlp: Cannot open file \"%s\" ($!)\n", $filename);
        exit 3;
    }
    return $f;
}

# Do close, exit if it fails
sub fclose_check($)
{
    my ($f) = @_;
    unless (close $f) 
    {
        warn "man2hlp: Cannot close file ($!)\n";
        exit 3;
    }
}

# Change output line
sub newline()
{
    $out_row++;
    $col = 0;
    print $f_out "\n";
}

# Calculate the length of string
sub string_len
{
    my ($buffer) = @_;
    my $anchor_flag = \$static{"string_len anchor_flag"}; # Flag: Inside hypertext anchor name ho4u_v_Ariom
    my $lc_link_flag = \$static{"string_len lc_link_flag"}; # Flag: Inside hypertext link target name
    my $backslash_flag = 0;     # Flag: Backslash quoting
    my $len = 0;                # Result: the length of the string


    foreach my $c (split //, $buffer)
    {
        if ($c eq $CHAR_LINK_POINTER)
        {
            $$lc_link_flag = 1;   # Link target name starts
        }
        elsif ($c eq $CHAR_LINK_END)
        {
            $$lc_link_flag = 0;   # Link target name ends
        }
        elsif ($c eq $CHAR_NODE_END)
        {
            # Node anchor name starts
            $$anchor_flag = 1;
            # Ugly hack to prevent loss of one space
            $len++;
        }
        # Don't add control characters to the length
        next if ord($c) >= 0 && ord($c) < 32;
        # Attempt to handle backslash quoting
        if ($c eq '\\' && !$backslash_flag)
        {
            $backslash_flag = 1;
            next;
        }
        $backslash_flag = 0;
        # Increase length if not inside anchor name or link target name
        $len++ if !$$anchor_flag && !$$lc_link_flag;
        if ($$anchor_flag && $c eq ']')
        {
            # Node anchor name ends
            $$anchor_flag = 0;
        }
    }
    return $len;
}

# Output the string
sub print_string($)
{
    my ($buffer) = @_;
    my $len;                    # The length of current word
    my $backslash_flag = 0;
    my $font_change_flag = 0;

    # Skipping lines?
    return if $skip_flag;
    # Copying verbatim?
    if ($verbatim_flag)
    {
        # Attempt to handle backslash quoting
        foreach (split //, $buffer)
        {
            if ($_ eq '\\' && !$backslash_flag)
            {
                $backslash_flag = 1;
                next;
            }
            $backslash_flag = 0;
            print $f_out $_;
        }
    }
    else
    {
        # Split into words
        $buffer = strtok($buffer, " \t\n");
        # Repeat for each word
        while (defined $buffer)
        {
            # Skip empty strings
            if ($buffer ne '')
            {
                $len = string_len($buffer);
                # Words are separated by spaces
                if ($col > 0)
                {
                    print $f_out ' ';
                    $col++;
                }
                elsif ($indentation)
                {
                    print $f_out ' ' while $col++ < $indentation;
                }
                # Attempt to handle backslash quoting
                foreach (split //, $buffer)
                {
                    # handle \fR, \fB, \fI and \fP commands
                    if ($font_change_flag)
                    {
                        if ($_ eq 'B')
                        {
                            print $f_out $CHAR_FONT_BOLD;
                        }
                        elsif ($_ eq 'I')
                        {
                            print $f_out $CHAR_FONT_ITALIC;
                        }
                        elsif ($_ eq 'R' || $_ eq 'P')
                        {
                            print $f_out $CHAR_FONT_NORMAL;
                        }
                        else
                        {
                            print $f_out 'f' . $_;
                            print_error "Syntax error: unsupported \\f" . $_ . " command";
                        }

                        $font_change_flag = 0;
                        next;
                    }
                    if ($_ eq 'f' && $backslash_flag)
                    {
                        $font_change_flag = 1;
                        $backslash_flag = 0;
                        next;
                    }
                    if ($_ eq '\\' && !$backslash_flag)
                    {
                        $backslash_flag = 1;
                        next;
                    }
                    $backslash_flag = 0;
                    $font_change_flag = 0;
                    print $f_out $_;
                }
                # Increase column
                $col += $len;
            }
            # Get the next word
            $buffer = strtok(undef, " \t\n");
        }                       # while
    }
}

# Like print_string but with printf-like syntax
sub printf_string
{
    print_string sprintf shift, @_;
}

# Handle NODE and .SH commands.  is_sh is 1 for .SH, 0 for NODE
# FIXME: Consider to remove first parameter
sub handle_node($$)
{
    my ($buffer, $is_sh) = @_;
    my ($len, $heading_level);

    # If we already skipped a section, don't skip another
    $skip_flag = 0 if $skip_flag == 2;

    # Get the command parameters
    $buffer = strtok(undef, "");
    if (! defined $buffer)
    {
        print_error "Syntax error: .SH: no title";
        return;
    }
    else
    {
        # Remove quotes
        $buffer =~ s/^"// and $buffer =~ s/"$//;
        # Calculate heading level
        $heading_level = 0;
        $heading_level++ while substr($buffer, $heading_level, 1) eq ' ';
        # Heading level must be even
        if ($heading_level % 2)
        {
            print_error "Syntax error: .SH: odd heading level";
        }
        if ($no_split_flag)
        {
            # Don't start a new section
            newline;
            print_string $buffer;
            newline;
            newline;
            $no_split_flag = 0;
        }
        elsif ($skip_flag)
        {
            # Skipping title and marking text for skipping
            $skip_flag = 2;
        }
        else
        {
            $buffer = substr($buffer, $heading_level);
            if (! $is_sh || ! $node)
            {
                # Start a new section, but omit empty section names
                if ($buffer ne '')
                {
                    printf $f_out "%s[%s]", $CHAR_NODE_END, $buffer;
                    newline;
                }

                # Add section to the linked list
                if (! defined $cnode)
                {
                    $cnode = $nodes;
                }
                else
                {
                    $cnode->{'next'} = struct_node();
                    $cnode = $cnode->{'next'};
                }
                $cnode->{'node'} = $buffer;
                $cnode->{'lname'} = undef;
                $cnode->{'next'} = undef;
                $cnode->{'heading_level'} = $heading_level;
            }
            if ($is_sh)
            {
                $cnode->{'lname'} = $buffer;
                print_string $buffer;
                newline;
                newline;
            }
        }                       # Start new section
    }                         # Has parameters
    $node = ! $is_sh;
}

# Convert character from the macro name to the font marker
sub char_to_font($)
{
    my ($c) = @_;
    my %font = (
        'R' => $CHAR_FONT_NORMAL,
        'B' => $CHAR_FONT_BOLD,
        'I' => $CHAR_FONT_ITALIC
    );
    return exists $font{$c} ? $font{$c} : chr(0);
}

#
# Handle alternate font commands (.BR, .IR, .RB, .RI, .BI, .IB)
# Return 0 if the command wasn't recognized, 1 otherwise
#
sub handle_alt_font($)
{
    my ($buffer) = @_;
    my $in_quotes = 0;
    my $alt_state = 0;

    return 0 if length($buffer) != 3;
    return 0 if substr($buffer, 0, 1) ne '.';

    my @font = (
        char_to_font substr($buffer, 1, 1),
        char_to_font substr($buffer, 2, 1)
    );

    # Exclude names with unknown characters, .BB, .II and .RR
    if ($font[0] eq chr(0) || $font[1] eq chr(0) || $font[0] eq $font[1])
    {
        return 0;
    }

    my $p = strtok(undef, "");
    return 1 unless defined $p;

    $buffer = $font[0];

    my @p = split //, $p;
    while (@p)
    {

        if ($p[0] eq '"')
        {
            $in_quotes = !$in_quotes;
            shift @p;
            next;
        }

        if ($p[0] eq ' ' && !$in_quotes)
        {
            shift @p;
            # Don't change font if we are at the end
            if (@p)
            {
                $alt_state = $alt_state ? 0 : 1;
                $buffer .= $font[$alt_state];
            }

            # Skip more spaces
            shift @p while @p && $p[0] eq ' ';

            next;
        }

        $buffer .= shift @p;
    }

    # Turn off attributes if necessary
    if ($font[$alt_state] ne $CHAR_FONT_NORMAL)
    {
        $buffer .= $CHAR_FONT_NORMAL;
    }

    print_string $buffer;

    return 1;
}

# Handle .IP and .TP commands.  is_tp is 1 for .TP, 0 for .IP
sub handle_tp_ip($)
{
    my ($is_tp) = @_;
    newline if $col > 0;
    newline;
    if ($is_tp)
    {
        $tp_flag = 1;
        $indentation = 0;
    }
    else
    {
        $indentation = 8;
    }
}

# Handle all the roff dot commands.  See man groff_man for details
sub handle_command($)
{
    my ($buffer) = @_;
    my $len;

    # Get the command name
    $buffer = strtok($buffer, " \t");

    if ($buffer eq ".SH")
    {
        $indentation = 0;
        handle_node $buffer, 1;
    }
    elsif ($buffer eq ".\\\"NODE")
    {
        handle_node $buffer, 0;
    }
    elsif ($buffer eq ".\\\"DONT_SPLIT\"")
    {
        $no_split_flag = 1;
    }
    elsif ($buffer eq ".\\\"SKIP_SECTION\"")
    {
        $skip_flag = 1;
    }
    elsif ($buffer eq ".\\\"LINK2\"")
    {
        # Next two input lines form a link
        $link_flag = 2;
    }
    elsif ($buffer eq ".PP" || $buffer eq ".P" || $buffer eq ".LP")
    {
        $indentation = 0;
        # End of paragraph
        newline if $col > 0;
        newline;
    }
    elsif ($buffer eq ".nf")
    {
        # Following input lines are to be handled verbatim
        $verbatim_flag = 1;
        newline if $col > 0;
    }
    elsif ($buffer eq ".I" || $buffer eq ".B" || $buffer eq ".SB")
    {
        # Bold text or italics text
        my $backslash_flag = 0;

        # .SB [text]
        # Causes the text on the same line or the text on the
        # next  line  to  appear  in boldface font, one point
        # size smaller than the default font.
        #

        # FIXME: text is optional, so there is no error

        my $p = strtok(undef, "");
        if (! defined $p)
        {
            print_error "Syntax error: .I | .B | .SB : no text";
            return;
        }

        $buffer = substr($buffer, 1, 1) eq 'I' ? $CHAR_FONT_ITALIC : $CHAR_FONT_BOLD;

        # Attempt to handle backslash quoting
        foreach (split //, $p)
        {
            if ($_ eq '\\' && !$backslash_flag)
            {
                $backslash_flag = 1;
                next;
            }
            $backslash_flag = 0;
            $buffer .= $_;
        }
        print_string $buffer . $CHAR_FONT_NORMAL;
    }
    elsif ($buffer eq ".TP")
    {
        handle_tp_ip 1;
    }
    elsif ($buffer eq ".IP")
    {
        handle_tp_ip 0;
    }
    elsif ($buffer eq ".\\\"TOPICS")
    {
        if ($out_row > 1)
        {
            print_error "Syntax error: .\\\"TOPICS must be first command";
            return;
        }
        $buffer = strtok(undef, "");
        if (! defined $buffer)
        {
            print_error "Syntax error: .\\\"TOPICS: no text";
            return;
        }
        # Remove quotes
        $buffer =~ s/^"// and $buffer =~ s/"$//;
        $topics = $buffer;
    }
    elsif ($buffer eq ".br")
    {
        newline if $col;
    }
    elsif ($buffer =~  /^\.\\"/)
    {
        # Comment { Hello from K.O. ;-) }
    }
    elsif ($buffer eq ".TH")
    {
        # Title header
    }
    elsif ($buffer eq ".SM")
    {
        # Causes the text on the same line or the text on the
        # next  line  to  appear  in a font that is one point
        # size smaller than the default font.
        $buffer = strtok(undef, "");
        print_string $buffer if defined $buffer;
    }
    elsif (handle_alt_font($buffer) == 1)
    {
        return;
    }
    elsif ($buffer eq ".RE")
    {
        newline;
    }
    else
    {
        # Other commands are ignored
        print_error sprintf "Warning: unsupported command %s", $buffer;
        return;
    }
}

sub struct_links()
{
    return {
        'linkname'  => undef,   # Section name
        'line'      => undef,   # Input line in ...
        'filename'  => undef,
        'next'      => undef
    }
}

my $links = struct_links();
my $current_link;


sub handle_link($)
{
    my ($buffer) = @_;
    my $old = \$static{"handle_link old"};
    my $len;
    my $amp;
    my $amp_arg;

    if ($link_flag == 1)
    {
        # Old format link, not supported
    }
    elsif ($link_flag == 2)
    {
        # First part of new format link
        # Bold text or italics text
        if (substr($buffer, 0, 2) eq '.I' || substr($buffer, 0, 2) eq '.B')
        {
            $buffer =~ s/^..[\s\t]*//;
        }
        $$old = $buffer;
        $link_flag = 3;

    }
    elsif ($link_flag == 3)
    {
        # Second part of new format link
        $buffer =~ s/^\.//;
        $buffer =~ s/^\\//;
        $buffer =~ s/^"//;
        $buffer =~ s/"$//;

        # "Layout\&)," -- "Layout" should be highlighted, but not "),"
        ($$old, $amp_arg) = split /\\&/, $$old, 2;
        $amp_arg = "" unless defined $amp_arg;
        printf_string "%s%s%s%s%s%s\n", $CHAR_LINK_START, $$old,
                       $CHAR_LINK_POINTER, $buffer, $CHAR_LINK_END, $amp_arg;
        $link_flag = 0;
        # Add to the linked list
        if (defined $current_link)
        {
            $current_link->{'next'} = struct_links();
            $current_link = $current_link->{'next'};
            $current_link->{'next'} = undef;
        }
        else
        {
            $current_link = $links;
        }
        $current_link->{'linkname'} = $buffer;
        $current_link->{'filename'} = $c_in;
        $current_link->{'line'} = $in_row;
    }
}

sub main
{
    my $len;                    # Length of input line
    my $c_man;                  # Manual filename
    my $c_tmpl;                 # Template filename
    my $f_man;                  # Manual file
    my $f_tmpl;                 # Template file
    my $buffer;                 # Full input line
    my $lc_node = undef;
    my $outfile_buffer;         # Large buffer to keep the output file
    my $cont_start;             # Start of [Contents]
    my $file_end;               # Length of the output file

    # Validity check for arguments
    if (@ARGV != 3)
    {
        warn "Usage: man2hlp file.man template_file helpfile\n";
        return 3;
    }

    $c_man  = $ARGV[0];
    $c_tmpl = $ARGV[1];
    $c_out  = $ARGV[2];

    # First stage - process the manual, write to the output file

    $f_man = fopen_check "<", $c_man;
    $f_out = fopen_check ">", $c_out;
    $c_in = $c_man;

    # Repeat for each input line
    while (<$f_man>)
    {
        # Remove terminating newline
        chomp;
        $buffer = $_;
        my $input_line;       # Input line without initial "\&"

        if (substr($buffer, 0, 2) eq '\\&')
        {
            $input_line = substr($buffer, 2);
        }
        else
        {
            $input_line = $buffer;
        }

        $in_row++;
        $len = length($input_line);

        if ($verbatim_flag)
        {
            # Copy the line verbatim
            if ($input_line eq ".fi")
            {
                $verbatim_flag = 0;
            }
            else
            {
                print_string $input_line;
                newline;
            }
        }
        elsif ($link_flag)
        {
            # The line is a link
            handle_link $input_line;
        }
        elsif (substr($buffer, 0, 1) eq '.')
        {
            # The line is a roff command
            handle_command $input_line;
        }
        else
        {
            #A normal line, just output it
            print_string $input_line;
        }
        # .TP label processed as usual line
        if ($tp_flag)
        {
            if ($tp_flag == 1)
            {
                $tp_flag = 2;
            }
            else
            {
                $tp_flag = 0;
                $indentation = 8;
                if ($col >= $indentation)
                {
                    newline;
                }
                else
                {
                    print $f_out " " while ++$col < $indentation;
                }
            }
        }
    }

    newline;
    fclose_check $f_man;
    # First stage ends here, closing the manual

    # Second stage - process the template file
    $f_tmpl = fopen_check "<", $c_tmpl;
    $c_in = $c_tmpl;

    # Repeat for each input line
    # Read a line
    while (<$f_tmpl>)
    {
        $buffer = $_;
        if (defined $lc_node)
        {
            if ($buffer ne "\n")
            {
                $cnode->{'lname'} = $buffer;
                chomp $cnode->{'lname'};
            }
            $lc_node = undef;
        }
        else
        {
            my $char_node_end = index($buffer, $CHAR_NODE_END);
            $lc_node = $char_node_end < 0 ? undef : substr($buffer, $char_node_end);

            if (defined $lc_node && substr($lc_node, 1, 1) eq '[')
            {
                my $p = index($lc_node, ']');
                if ($p >= 0) {
                    if (substr($lc_node, 1, 6) eq '[main]')
                    {
                        $lc_node = undef;
                    }
                    else
                    {
                        if (! defined $cnode)
                        {
                            $cnode = $nodes;
                        }
                        else
                        {
                            $cnode->{'next'} = struct_node();
                            $cnode = $cnode->{'next'};
                        }
                        $cnode->{'node'} = substr($lc_node, 2, $p-2);
                        $cnode->{'lname'} = undef;
                        $cnode->{'next'} = undef;
                        $cnode->{'heading_level'} = 0;
                    }
                }
                else
                {
                  $lc_node = undef;
                }
            }
            else
            {
                $lc_node = undef;
            }
        }
        print $f_out $buffer;
    }

    $cont_start = tell $f_out;
    if ($cont_start <= 0)
    {
        perror $c_out;
        return 1;
    }

    if ($topics)
    {
        printf $f_out "\004[Contents]\n%s\n\n", $topics;
    }
    else
    {
        print $f_out "\004[Contents]\n";
    }

    for ($current_link = $links; defined $current_link && defined $current_link->{'linkname'};)
    {
        my $found = 0;
        my $next = $current_link->{'next'};

        if ($current_link->{'linkname'} eq "Contents")
        {
            $found = 1;
        }
        else
        {
            for ($cnode = $nodes; defined $cnode && defined $cnode->{'node'}; $cnode = $cnode->{'next'})
            {
                if ($cnode->{'node'} eq $current_link->{'linkname'})
                {
                    $found = 1;
                    last;
                }
            }
        }
        if (! $found)
        {
            $buffer = sprintf "Stale link \"%s\"", $current_link->{'linkname'};
            $c_in = $current_link->{'filename'};
            $in_row = $current_link->{'line'};
            print_error $buffer;
        }

        $current_link = $next;
    }

    for ($cnode = $nodes; defined $cnode && defined $cnode->{'node'};)
    {
        my $next = $cnode->{'next'};
        $lc_node = $cnode->{'node'};

        if (defined $lc_node && $lc_node ne '') {
            printf $f_out "  %*s\001%s\002%s\003", $cnode->{'heading_level'},
                "", $cnode->{'lname'} ? $cnode->{'lname'} : $lc_node, $lc_node;
        }
        print $f_out "\n";
        $cnode = $next;
    }

    $file_end = tell $f_out;

    # Sanity check
    if (($file_end <= 0) || ($file_end - $cont_start <= 0))
    {
        warn $c_out ."\n";
        return 1;
    }

    fclose_check $f_out;
    fclose_check $f_tmpl;
    # Second stage ends here, closing all files, note the end of output

    #
    # Third stage - swap two parts of the output file.
    # First, open the output file for reading and load it into the memory.
    #
    $outfile_buffer = '';
    $f_out = fopen_check '<', $c_out;
    $outfile_buffer .= $_ while <$f_out>;
    fclose_check $f_out;
    # Now the output file is in the memory

    # Again open output file for writing
    $f_out = fopen_check '>', $c_out;

    # Write part after the "Contents" node
    print $f_out substr($outfile_buffer, $cont_start, $file_end - $cont_start);

    # Write part before the "Contents" node
    print $f_out substr($outfile_buffer, 0, $cont_start-1);
    print $f_out "\n";
    fclose_check $f_out;

    return 0;
}

exit main();
