#!/usr/bin/perl

### Copyright notice at the bottom of this file ###

# $Id: baseconv,v 1.37 2012/01/13 16:57:48 adamm Exp adamm $
#
# $Source: /home/adamm/src/baseconv/RCS/baseconv,v $

### ? option for 11..36 to be UC or lc ?

use strict;
use warnings;
use English qw( -no_match_vars );
use File::Basename;
use Getopt::Long;
use Math::BigInt;


my $ERROR;
my %I_DIGITS;
my $MAX_BASE = 64;
my $MIN_BASE = 2;
my @O_DIGITS;
my $PROG = basename $PROGRAM_NAME;


sub main {
    my $ibase;
    my $line;
    my $obase;
    my $save;
    my $verbose;

    $save = $SIG{__WARN__};
    $SIG{__WARN__} = \&trap;
    ($ibase, $obase, $verbose) = assertParseArgs();
    $SIG{__WARN__} = $save;
    fillDigits();

    if (scalar @ARGV) {
	foreach my $arg (@ARGV) {
	    convert($ibase, $obase, $verbose, $arg);
	}
    } else {
	while ($line = <STDIN>) {
	    chomp $line;
	    convert($ibase, $obase, $verbose, $line);
	}
    }
} # main

sub assertParseArgs {
    my %options;
    my $ret;

    $ret = GetOptions(
	\%options,
	'i=i',
	'o=i',
	'v',
	#
	'man'        => sub { manpage(); exit 0; },
	'version'    => sub { version(); exit 0; },
    );

    if (! $ret) {
	printf STDERR "%s: %s\n", $PROG, lcfirst($ERROR);
	usage();
        exit 1;
    }

    if ((! defined($options{'i'})) || (! defined($options{'o'}))) {
	print STDERR "%s: must supply -i # and -o #\n", $PROG;
	usage();
	exit 1;
    }

    if (! checkBases("ibase", $options{'i'})) {
	usage();
	exit 1;
    }
    if (! checkBases("obase", $options{'o'})) {
	usage();
	exit 1;
    }

    return (
	$options{'i'},
	$options{'o'},
	$options{'v'},
    );
} # assertParseArgs


sub checkBases {
    my ($arg, $val) = @_;

    if (($val >= $MIN_BASE) && ($val <= $MAX_BASE)) {
	return 1;
    } else {
	printf STDERR "%s: invalid %s: '%s'\n", $PROG, $arg, $val;
	return 0;
    }
} # checkBases

sub convert {
    my ($ibase, $obase, $verbose, $val) = @_;
    my $numIn;
    my @numOut;
    my @tmp;

    @tmp = split //, $val;
    $numIn = Math::BigInt->new("0");
    foreach my $digit (@tmp) {
	$numIn = ($numIn * $ibase) + $I_DIGITS{$digit};
    }

    # Math::BigInt way of saying "$numIn > $obase"
    while ($numIn->bcmp($obase) == 1) {
	unshift @numOut, ( $numIn % $obase );
	$numIn /= $obase;
    }
    unshift @numOut, $numIn->as_int();

    if ($verbose) {
	printf "%s ", $val;
    }
    foreach my $digit (@numOut) {
	printf "%s", $O_DIGITS[$digit];
    }
    printf "\n";
} # convert


sub fillDigits {
    my $chars =
	"0123456789"
	. "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	. "abcdefghijklmnopqrstuvwxyz"
	. "{}";

    my $n;

    @O_DIGITS = split //, $chars;
    $n = 0;
    for my $digit (@O_DIGITS) {
	$I_DIGITS{$digit} = $n;
	++$n;
    }
} # fillDigits


sub manpage {
    print <<'__EOF'
NAME
    baseconv -- convert integers between "special" bases

SYNOPSIS
    baseconv [ -v ] -i base -o base [ # ... ]

    baseconv --version

DESCRIPTION
    baseconv converts integers between bases 2 through 64.
    The first four are useful to programmers and system administrators;
    the others are used mainly when writing shell scripts for encoding
    large numbers (typically to be used as part of a file name).

    If no number is given on the command line, baseconv will read from
    stdin (one number per line); if multiple numbers are given on the
    comamnd line, the results will be printed one per line.

    For all bases greater than 10, the input is case-sensitive.

    The output "digits" for bases greater than 10 are as follows:

	base 11:  0 - 9 A
	base 12:  0 - 9 A B
	base 12:  0 - 9 A B C
	...
	base 16:  0 - 9 A - F
	...
	base 32:  0 - 9 A - U
	base 62:  0 - 9 A - Z a - z
	base 64:  0 - 9 A - Z a - z "{" "}"

OPTIONS
    -i #
	the input base

    -o #
	the output base

    -v
	verbose; show the number in the input base and the output base

    --version
	show the version then exit

SEE ALSO
    dc(1)

AUTHOR
    Adam Moskowitz <adamm@menlo.com>
__EOF
} # manpage


sub trap {
    $ERROR = shift @_;
    chomp $ERROR;
} # trap


sub usage {
    print STDERR <<__EOF;

Usage:
    $PROG [ -v ] -i base -o base [ # ... ]

    $PROG --version

"base" is one of 2 - 64

"#" is a number in the base specified by "-i"

If "#" is not given the program will read from stdin, one number per
line, again, in the base specified by "-i".

The default output is the number in the specified base; the -v switch
will print the number twice, first in the input base then in the output
base.
__EOF
} # usage


sub version {
    print
	"\n",
	'# $Id: baseconv,v 1.37 2012/01/13 16:57:48 adamm Exp adamm $',
	"\n\n",
	'# $Source: /home/adamm/src/baseconv/RCS/baseconv,v $',
	"\n";
} # version


main();


###
### Copyright, 2009 - 2011, Adam Moskowitz. All rights reserved.
###
### Any redistribution of this software must retain the above copyright
### notice, this list of conditions, and the following disclaimer.
###
### Without specific prior written permission from the copyright holder,
### you may not charge a fee for the redistribution of this software.
###
### All other redistribution and use is hereby permitted.
###
### This software is provided "as is" and without any express or implied
### warranties, including, without limitation, the implied warranties of
### merchantability and fitness for a particular purpose.
###
