#!/usr/bin/perl -w

use strict;
use Getopt::Long;
use vars qw(@left @right @discard),
  qw($opt_decrypt $opt_verbose $opt_iv $opt_key $opt_input $output);

sub canonicalise_string {
  my $s = uc shift;
  my $t;
  $s =~ s[(\d+)][($t = $1) =~ tr|0-9|ZA-I|;"X${t}X"]ego;
  $s =~ tr/A-Z//cd;
  return $s;
}

sub break_up_output {
  my $s = shift;
  $s =~ s/([A-Z]{5})\B/$1 /g;
  $s =~ s/([A-Z ]{29}) /$1\n/g;
  print $s, "\n";
}

sub string_to_pile {
  return map {ord(uc $_) - ord('A') +1} split(//, shift);
}

sub p2s { # pile to string
  return join('', map {chr(ord('A') + $_ -1)} @_);
}

sub print_state {
  if ($opt_verbose) {
    print "Discard: ", p2s(@discard), "\n";
    print "Right:   ", p2s(@right), "\n";
    print "Left:    ", p2s(@left), "\n\n";
  }
}

sub swap_piles {
  push @discard, @right;
  @right = @left;
  @left = @discard;
  @discard = ();
  print_state();
}

sub get_right {
  # Take cards from the bottom
  my $card = pop @right;
  unshift @discard, $card;
  print "Top card of discard pile: ", p2s($card), "\n" if $opt_verbose;
  return $card;
}

sub make_left {
  if ($opt_verbose) {
    print 
      "Putting together pile: ", 
      join(" + ", map {p2s @$_} \@left, @_), "\n"; 
  }
  push @left, map {@$_} @_;
}

sub count_cut {
  print "Count cut:\n" if $opt_verbose;
  for (;;) {
    make_left [splice @left, 0, get_right()];
    return unless @right == 0;
    print_state();
    print "Right pile exhausted, swapping:\n" if $opt_verbose;
    swap_piles();
    print "Repeating count cut:\n" if $opt_verbose;
  }
}

sub counted_rev {
  my $count = shift;
  my $p1 = [];
  my $p2 = [];
  print "Dealing cards: ", p2s(@left[0..$count -1]), "\n" if $opt_verbose;
  foreach my $i (1..$count) {
    ($p1, $p2) = ($p2, $p1);
    unshift @$p1, shift @left;
  }
  make_left $p1, $p2;
}

sub find_card {
  my $target = shift;
  my $i = 0;
  foreach (@left) {
    $i++;
    return $i if $_ == $target;
  }
  die "Failed to find expected card in deck: internal error";
}

sub encrypt_letter {
  my $ptext = shift;
  my $ctext = find_card($ptext);
  counted_rev $ctext;
  return $ctext;
}

sub decrypt_letter {
  my $ctext = shift;
  my $ptext = $left[$ctext -1];
  counted_rev $ctext;
  return $ptext;
}

sub crypt_letter  {
  my $in = shift;
  my $out;
  if ($opt_decrypt) {
    print "Decrypting letter: ", p2s($in), "\n" if $opt_verbose;
    $out = decrypt_letter($in);
    print "Plaintext letter: ", p2s($out), "\n" if $opt_verbose;
  } else {
    print "Encrypting letter: ", p2s($in), "\n" if $opt_verbose;
    $out = encrypt_letter($in);
    print "Ciphertext letter: ", p2s($out), "\n" if $opt_verbose;
  }
  print_state();
  return $out;
}


sub use_iv {
  my $iv = shift;
  @right = string_to_pile($iv);
  unless (@right == 25) {
    print "Invalid IV: should be exactly 25 characters\n";
  }
  my %missing_hash;
  @missing_hash{1..26} = (1) x 26;
  foreach (@right) {
    unless ($missing_hash{$_}) {
      die "IV invalid: ", p2s($_), " repeated."
    }
    delete $missing_hash{$_};
  }
  push @right, keys %missing_hash;
  @left = (1..26);
  @discard = ();
  print "Initial state of piles:\n" if $opt_verbose;
  print_state();
}

sub use_key {
  my $keyphrase = shift;
  foreach my $letter (string_to_pile($keyphrase)) {
    count_cut();
    print_state();
    print "Mixing in ", p2s($letter), " from the passphrase:\n" 
      if $opt_verbose;
    encrypt_letter($letter);
    print_state();
  }
}

sub mix_piles {
  print "Discarding rest of right pile and swapping:\n" if $opt_verbose;
  swap_piles();
  
  while (@right) {
    print "Mixing left pile using right:\n" 
      if $opt_verbose;
    encrypt_letter(get_right());
    print_state();
  }
  print "Swapping piles:\n" if $opt_verbose;
  swap_piles();
}

sub cryptit {
  my $input = shift;
  my @output;
  foreach my $letter (string_to_pile($input)) {
    count_cut();
    print_state();
    push @output, crypt_letter($letter);
  }
  return p2s(@output);
}

$opt_decrypt = 0;
$opt_verbose = 0;
GetOptions(qw(decrypt! verbose! key=s iv=s input=s))
  or die "Failed to parse command line options, stopped";

unless (defined $opt_key) {
  die "Can't work without --key argument, stopped";
}

$opt_key = canonicalise_string $opt_key;

unless (defined $opt_input) {
  $opt_input = join("", <>);
}
$opt_input = canonicalise_string $opt_input;

if (defined $opt_iv) {
  $opt_iv = canonicalise_string $opt_iv;
} else {
  if ($opt_decrypt) {
    $opt_input =~ s/^[A-Z]{25}//;
    $opt_iv = $&;
  } else {
    my @source = (1..26);
    my @iv = ();
    while (@source) {
      push @iv, splice(@source, int(rand(scalar(@source))), 1)
    }
    pop @iv;
    $opt_iv = p2s(@iv);
  }
}

if ($opt_verbose) {
  print "Initialisation vector: ";
  break_up_output($opt_iv);
}

unless ($opt_decrypt) {
  $opt_input .= "X" x (4 - ((length($opt_input) + 4) % 5));
}

if ($opt_verbose) {
  print "Key: $opt_key\n";
  print(($opt_decrypt ? "Cipher" : "Plain"), "text:\n");
  break_up_output $opt_input;
  print "\n";
}

use_iv($opt_iv);
use_key($opt_key);
mix_piles();
$output = cryptit($opt_input);
if ($opt_decrypt) {
  print "Plaintext:\n";
  break_up_output $output;
} else {
  print "Ciphertext: \n";
  break_up_output ($opt_iv . $output);
}
