#!/usr/bin/perl

# Extract utility for SimH help text


#  Copyright (c) 2013, Timothe Litt

#  Permission is hereby granted, free of charge, to any person obtaining a
#  copy of this software and associated documentation files (the "Software"),
#  to deal in the Software without restriction, including without limitation
#  the rights to use, copy, modify, merge, publish, distribute, sublicense,
#  and/or sell copies of the Software, and to permit persons to whom the
#  Software is furnished to do so, subject to the following conditions:

#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.

#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
#  THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
#  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
#  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

#  Except as contained in this notice, the name of the author shall not be
#  used in advertising or otherwise to promote the sale, use or other dealings
#  in this Software without prior written authorization from the author.

use warnings;
use strict;

# This utility attempts to read the C source of an emulator device and convert
# its help function to the structured help format.  Manual editing of the result
# will be required, but the mechanical work is done by the tool.
#
# A template for organizing the help into standard topics/subtopics is inserted
# along with the translation.  At this writing, everything is experimental, so
# the 'standard' format may change.  Nonetheless, this is suitable for experimentation.

use File::Basename;

my $prg = basename $0;

my $rtn;
my $ifn = '-';
my $ofn = '-';
my $line;
my $update;

while (@ARGV) {
    if( $ARGV[0] eq '--' ) {
	last;
    }
    if( $ARGV[0] eq '-dev' ) {
	shift;
	$rtn = shift;
	$rtn .= "_help";
	next;
    }
    if( $ARGV[0] eq '-u' ) {
	$update = 1;
	shift;
	next;
    }
    last if( $ARGV[0] !~ /^-/ );

    printf STDERR << "USAGE_";
Usage:
$prg -u -dev devname -func rtn infile outfile

devname is used to look for the existing help function name.
E.g. if the routine is cr_help, use -dev cr.

Alternatively, use -func to specify the full function name.

infile and outfile default to - (stdin and stdout)

$prg will attempt to produce a sensible device help string, although you
should expect that the result will require manual editing.  Complex C
constructs (preprocessor conditionals, if statements that generate strings)
are not automatically translated, but the old code will be preserved.

However, as it may have been partially translated, the result may not compile.

A template is installed so that you can move your information into the standard
sections.

Source code in the help function is reformatted - not to any particular
style, but as a consequence of how it is tokenized and parsed.  Use your
favorite pretty-printer if you don't like the results.

Normally, just the help function is output.  -u will output the entire file
(-u = "update")

USAGE_

    exit (0);
}

unless( defined $rtn ) {
    die "The help function must be specified with -func or -dev; -help for usage\n";
}

if( @ARGV ) {
    $ifn = shift;
}
if( @ARGV ) {
    $ofn = shift;
}

open( STDIN, "<$ifn" ) or
    die "Unable to open $ifn for input: $!\n";

open( STDOUT, ">$ofn" ) or
    die "Unable to open $ofn for output: $!\n";

$line = "";
while( <STDIN> ) {

    # Look for the help function
    if( /^(?:static\s+)?t_stat\s+$rtn\s*\(/ ) {
	$line = $_;
	while( $line !~ /\{/ && $line !~ /\)\s*;/ ) {
	    my $cont = <STDIN>;
	    if( !defined $cont ) {
		die "EOF in function definition\n";
	    }
	    $line .= $cont;
	}
	if( $line =~ /\)\s*;/ ) { # Just a prototype
	    if( $update ) {
		print $line;
	    }
	    $line = "";
	    next;
	}
	# Process the function body
	my $f = $line;
	my $b = '';
	my $bl = 1;
	my( %vargs, @vargs );
	my $help = '';
	my $comments = '';

	# Each statement in the body
	while (1) {
	    my ($tok, $val) = gettok();
	    last if( !defined $tok );

	    if ($tok eq '{') {                  # Track brace level
		$bl++;
		$b .= $tok;
	    } elsif ($tok eq '}') {
		die "Unmatched }\n" if ( --$bl < 0 );
		$b .= $tok;
		last if (!$bl);                # End of function
	    } elsif ($tok eq 'word' && $val eq 'fprintf') {
		# fprintf ( st, "string" ,args );
		# Save embedded comments, but don't confuse the parse.
		($tok, $val) = gettok(\$comments);
		if( $tok ne '(' ) {
		    $b .= " $val";
		    next;
		}
		($tok, $val) = gettok(\$comments);
		if( $tok ne 'word' || $val ne 'st' ) {
		    $b .= "fprintf ($val";
		    next;
		}
		($tok, $val) = gettok(\$comments);
		if( $tok ne ',' ) {
		    $b .= "fprintf (st$val";
		    next;
		}
		($tok, $val) = gettok(\$comments);
		if( $tok ne 'QS' ) {
		    $b .= "fprintf (st, $val";
		    next;
		}
		# Concatenate adjacent strings
		my $string = '';
		while( $tok eq 'QS' ) {
		    $string .= substr( $val, 1, length( $ val ) -2);
		    ($tok, $val) = gettok(\$comments);
		}
		# Check for format codes.  plain %s is all that can be automated
		if ($string =~ /(%[^%s])/) {
		    print STDERR "Line $.: Unsupported format code $1 in help string.  Please convert to %s\n";
		}
		# Rework argument list
		my $arg = '';
		my @vlist;
		my $pl = 1;             # Paren level
		while( $tok eq ',' ) {
		    ($tok, $val) = gettok(\$comments);
		    while( $tok ne ',' ) {
			if( $tok eq '(' ) {
			    $pl++;
			} elsif( $tok eq ')' ) {
			    die "Unmatched )" if( --$pl < 0);
			    last if( !$pl );
			}
			$arg .= " $val";
			($tok, $val) = gettok(\$comments);
		    }
		    if( !length $arg ) {
			print STDERR "Line $.: null argument to fprintf in $rtn\n";
			$string = "<<NULL>>";
		    }
		    unless( exists $vargs{$arg} ) {   # Assign each unique arg an index
			$vargs{$arg} = @vargs;
			push @vargs, $arg;
		    }
		    push @vlist, $vargs{$arg};        # Remember offset in this list
		    $arg = '';
		}
		die "Line $.: Missing ')' in fprintf\n" if( $tok ne ')' );
		($tok, $val) = gettok(\$comments);
		die "Line $.: Missing ';' in fprintf\n" if( $tok ne ';' );

		# Replace each escape with positional %s in new list.
		my $n = 0;
		$string =~ s/%([.\dlhs# +Lqjzt-]*[diouxXeEfFgGaAcspnm%])/
		  sprintf "%%%us",$vlist[$n++]+1/eg;
		$help .= $string;
		next;
	    } elsif ($tok eq 'word' && $val =~ /^fprint_(set|show|reg)_help(?:_ex)?$/) {
		my %alt = ( set  => "\$Set commands", 
			    show => "\$Show commmands", 
			    reg  => "\$Registers" );
		$b .= "/* Use \"$alt{$1}\" topic instead:\n";
		do {
		    $b .= " $val";
		    ($tok, $val) = gettok (\$comments);
		} while ($tok ne ';');
		$b .= ";\n*/\n";
		next;
	    }

	    # Random function body content

	    $b .= " $val";
	}
	# End of function - output new one
	print $f;                       # Function header
	print "const char helpString[] =\n";

	print << 'TEMPLATE_';
/* Template for re-arranging your help.
 * Lines marked with '+' in the translation seemed to be indented and will
 * indent 4 columns for each '+'.  See scp_help.h for a worked-out example.
 * The '*'s in the next line represent the standard text width of a help line */
     /****************************************************************************/
    " Insert your device summary here.  Keep it short.  Be sure to put a leading\n"
    " space at the start of each line.  Blank lines do appear in the output;\n"
    " don't add extras.\n"
    "1 Hardware Description\n"
    " The details of the hardware.  Feeds & speeds are OK here.\n"
    "2 Models\n"
    " If the device was offered in distinct models, a subtopic for each\n"
    "3 Model A\n"
    " Description of model A\n"
    "3 Model B\n"
    " Description of model B\n"
    "2 $Registers\n"
    " The register list of the device will automagically display above this\n"
    " line.  Add any special notes.\n"
    "1 Configuration\n"
    " How to configure the device under SimH.  Use subtopics\n"
    " if there is a lot of detail.\n"
    "2 $Set commands\n"
    " The SET commands for the device will automagically display above\n"
    " this line.  Add any special notes.\n"
    "2 OSNAME1\n"
    " Operating System-specif configuration details\n"
    " If the device needs special configuration for a particular OS, a subtopic\n"
    " for each such OS goes here.\n"
    "2 Files\n"
    " If the device uses external files (tapes, cards, disks, configuration)\n"
    " Create a subtopic for each here.\n"
    "3 Config file 1\n"
    " Description.\n"
    "2 Examples\n"
    " Provide usable examples for configuring complex devices.\n"
    " If the examples are more than a couple of lines, make a subtopic for each.\n"
    "1 Operation\n"
    " How to operate the device under SimH.  Attach, runtime events\n"
    " (e.g. how to load cards or mount a tape)\n"
    "1 Monitoring\n"
    " How to obtain and interpret status\n"
    "2 $Show commands\n"
    " The SHOW commands for the device will automagically display above\n"
    " this line.  Add any special notes.\n"
    "1 Restrictions\n"
    " If some aspects of the device aren't emulated or some host\n"
    " host environments that aren't (fully) supported, list them here.\n"
    "1 Debugging\n"
    "  Debugging information - provided by the device.  Tips for common problems.\n"
    "1 Related Devices\n"
    " If devices are configured or used together, list the other devices here.\n"
    " E.G. The DEC KMC/DUP are two hardware devices that are closely related;\n"
    " The KMC controlls the DUP on behalf of the OS.\n"

    /* **** Your converted help text starts hare **** */

TEMPLATE_

	my @lines = split /(\\n|\n)/, $help;
	while( @lines ) {
	    my $line = shift @lines;
	    my $term = shift @lines;
	    if ($term eq "\\n") {
		$line .= $term;
		$term = "\n";
	    }
	    if( $line =~ s/^(\s+)// ) {
		$line = ('+' x ((length( $1 ) +3)/4)) . $line;
	    } else {
		$line = ' ' . $line;
	    }

	    print "    \"$line\"\n" ;
	}
	print "    ;\n";
	print $b;                       # Stuff from body of old function
	if( length $comments ) {
	    print "\n$comments";
	}

	# Call scp_help

	print "\nreturn scp_help (st, dptr, uptr, helpString, cptr";

	%vargs = reverse %vargs;
	while( @vargs ) {
	    print ",\n                " . shift( @vargs );
	}

	print  ");\n}\n";
    } else {
	if( $update ) {
	    print $_;
	}
	next;
    }
}

exit (0);

my @pending;
sub nextc {
    if( @pending ) {
	my $c = shift @pending;
	return $c;
    }
    return getc;
}

sub gettoken {
    my $c;
    my $ql = 0;
    my $cl = 0;
    my $tok = '';

    while( defined(($c = nextc())) ) {
	if( $cl ) {
	    if( $c eq '*' ) {
		$c = nextc;
		die "EOF in comment\n" if( !defined $c );

		if ($c eq '/') {
		    $tok .= '*/';
		    return ('comment', $tok);
		}
		push @pending, $c;
		$c = '*';
	    }
	    $tok .= $c;
	    next;
	} elsif( $c eq '/' ) {
	    $c = nextc;
	    if( $c eq '*' ) {
		if (length $tok) {
		    push @pending, '/', '*';
		    return ('word', $tok);
		}
		$cl = 1;
		$tok = '/*';
		next;
	    }
	    push @pending, $c;
	    $c = '/';
	}
	if( $ql ) {
	    if( $c eq '\\' ) {
		$c = nextc;
		die "EOF in string\n" if( !defined $c );

		$tok .= "\\$c"; # eval "\"\\$c\"";
		next;
	    }
	    if( $c eq $ql ) {
		$tok .= $ql;
		return ("QS", $tok);
	    }
	    $tok .= $c;
	    next;
	}
	if( $c eq '"' || $c eq "'" ) {
	    $ql = $c;
	    $tok = $c;
	    next;
	}
	if ($c =~ /^\s$/) {
	    if( length $tok ) {
		return ('word', $tok);
	    }
	    next;
	}
	if ($c =~ /^\w$/) {
	    $tok .= $c;
	    next;
	}
	if( length $tok ) {
	    push @pending, $c;
	    return ('word', $tok);
	}
	if ($c eq '-') {
	    $c = nextc;
	    if( $c =~ /^[>=-]$/ ) {
		return ('op', "-$c");
	    }
	    push @pending, $c;
	    return ('op', '-');
	}
	if( $c eq '<' ) {
	    $c = nextc;
	    if( $c eq '=' ) {
		return ('op', "<$c");
	    }
	    if( $c eq '<' ) {
		my $c2 = nextc;
		if( $c2 eq '=' ) {
		    return ('op', "<<=");
		}
		push @pending, $c2;
		return ('op', '<<');
	    }
	    push @pending, $c;
	    return ('op', '<');
	}
	if( $c eq '>' ) {
	    $c = nextc;
	    if( $c eq '=' ) {
		return ('op', ">$c");
	    }
	    if( $c eq '>' ) {
		my $c2 = nextc;
		if( $c2 eq '=' ) {
		    return ('op', ">>=");
		}
		push @pending, $c2;
		return ('op', '>>');
	    }
	    push @pending, $c;
	    return ('op', '>');
	}
	if( $c eq '=' ) {
	    $c = nextc;
	    if( $c eq '=' ) {
		return ('op', '==');
	    }
	    push @pending, $c;
	    return ('op', '=');
	}
	if ($c =~ m,^[!*+/%&^|]$,) {
	    my $c2 = nextc;
	    if( $c2 eq '=' ) {
		return ('op', "$c$c2");
	    }
	    push @pending, $c2;
	    return ('op', $c);
	}
	if( $c =~ /^[&|]$/ ) {
	    my $c2 = nextc;
	    if( $c2 eq $c ) {
		return ('op', "$c$c");
	    }
	    push @pending, $c2;
	    return ('op', $c);
	}

	if ($c =~ /^[#}]$/ ) {
	    return ($c, "\n$c");
	}
	return ($c, ($c =~ /^[{;]$/? "$c\n" : $c));
    }
    return (undef, '<<EOF>>');
}

sub gettok {
    my $comments = $_[0];

    while( 1 ) {
	my( $token, $value ) = gettoken();
	return ($token, $value) if( !defined $token );

	if( $token eq 'comment' && $comments ) {
	    $$comments .= $value . "\n";
	    next;
	}
	return ($token, $value);
    }
}
