#!/usr/bin/ruby

require 'cgi'

def succ?(*a)
   begin
     (0..(a.size-2)).each {|i| return false if a[i].succ != a[i+1]}
     return true
   rescue
     return false
   end
end

class Deck
   def initialize
     @deck = [:A, :B] + (1..52).to_a
     #@deck = [:A, :B] + (14..26).to_a + (1..13).to_a + (27..52).to_a
   end

   def set_deck(id)
     if id == 1
       @deck = (1..52).to_a + [:A, :B]
     elsif id == 2
       @deck = [:A, :B] + (1..52).to_a
     elsif id == 3
       @deck = [:A, :B] + (14..26).to_a + (1..13).to_a + (27..52).to_a
     elsif id == 4
       @deck = (14..26).to_a + (1..13).to_a + (27..52).to_a + [:A, :B]
     elsif id == 5
       @deck = (1..52).to_a + [:A, :B]
       @deck = @deck.reverse
     elsif id == 6
       @deck = [:B, :A] + (1..52).to_a
       @deck = @deck.reverse
     elsif id == 7
       @deck = [:A, :B] + (14..26).to_a + (1..13).to_a + (27..52).to_a
       @deck = @deck.reverse
     elsif id == 8
       @deck = (14..26).to_a + (1..13).to_a + (27..52).to_a + [:B, :A]
       @deck = @deck.reverse
     end
   end

   def move_a
     move_down(@deck.index(:A))
   end

   def move_b
     move_down(move_down(@deck.index(:B)))
   end

   def move_down(index)
     if index < 53
       new = index + 1
       @deck[new], @deck[index] = @deck[index], @deck[new]
     else
       @deck = @deck[0,1] + @deck[-1,1] + @deck[1..-2]
       new = 1
     end
     return new
   end

   def triple_cut
     jokers = [@deck.index(:A), @deck.index(:B)]
     top, bottom = jokers.min, jokers.max
     @deck = @deck[(bottom+1)..-1] + @deck[top..bottom] + @deck[0...top]
   end

   def number_value(x)
     return 53 if x == :A or x == :B
     return x
   end

   def count_cut
     count = number_value( @deck[-1] )
     @deck = @deck[count..-2] + @deck[0,count] + @deck[-1,1]
   end

   def output
     return @deck[ number_value(@deck[0]) ]
   end

   def update
     move_a
     move_b
     triple_cut
     count_cut
   end

   def count_cut2(letter)
     count = letter-64 
     @deck = @deck[count..-2] + @deck[0,count] + @deck[-1,1]
   end
  
   def key(phrase, joker)
     if joker
       
       if phrase.length >= 2
	 joker1 = phrase[-2]-65
         joker2 = phrase[-1]-65
       else
         joker = false
       end
       
       if phrase.length > 2
         phrase = phrase[0..-3]
       else
         phrase = ""
       end
     end
     
     phrase.each_byte { |l|
       move_a
       move_b
       triple_cut
       count_cut
       count_cut2(l)
     }

     if joker
       @deck.delete(:A)
       @deck.delete(:B)
       @deck[joker1+1,0] = :A
       @deck[joker2+1,0] = :B
     end
   end

   def get
     while true
       update
       c = output
       if c != :A and c != :B
         letter = ( ((c-1) % 26) + 65 ).chr
         return letter
       end
     end
   end

   def to_s
     a = []
     @deck.each_index {|i|
       if  succ?(a[-1], @deck[i], @deck[i+1])
         a << "..."
       elsif a[-1] == "..." and succ?(@deck[i-1], @deck[i], @deck[i+1])
         # nop
       else
         a << @deck[i]
       end
     }
     return a.join(" ")
   end
end

class Encrypter
   def initialize(keystream)
     @keystream = keystream
   end

   def sanitize(s)
     s = s.upcase
     s = s.gsub(/[^A-Z]/, "")
     s = s + "X" * ((5 - s.size % 5) % 5)
     out = ""
     (s.size / 5).times {|i| out << s[i*5,5] << " "}
     return out
   end

   def mod(c)
     return c - 26 if c > 26
     return c + 26 if c < 1
     return c
   end

   def process(s, &combiner)
     s = sanitize(s)
     out = ""
     s.each_byte { |c|
       if c >= ?A and c <= ?Z
         key = @keystream.get
         res = combiner.call(c, key[0])
         out << res.chr
       else
         out << c.chr
       end
     }
     return out
   end

   def encrypt(s)
     return process(s) {|c, key| 64 + mod(c + key - 128)}
   end

   def decrypt(s)
     return process(s) {|c, key| 64 + mod(c -key)}
   end
end

cgi = CGI.new("html4")

ciphertext = cgi.params['ciphertext'][0]

if !ciphertext 
  ciphertext = "WBBMCHGFIBLXCQYWEZFLITHPJLFHWYETKWYLJOTYYNGYJBIOGIFUVMRXIHGURAGXHNQHRSXAWJUFJTAMSMMOSMVBAAKPGVVWXOVMYKZPLLUL"
end

passphrase = cgi.params['passphrase'][0]

if !passphrase 
  passphrase = ""
end

jokers = cgi.params['jokers'][0]

if !jokers
  jokers = false
else
  jokers = true
end

passphrase.upcase!

deck = Deck.new()

result = ""
keydecks = ""
origdecks = ""

for num in (1..8)
  deck.set_deck(num)

  origdecks = origdecks + "Orig " + num.to_s + ": " + deck.to_s + "\n"

  if passphrase != ""
    deck.key(passphrase, jokers)
  end

  keydecks = keydecks + "Keyed " + num.to_s + ": " + deck.to_s + "\n"

  e = Encrypter.new( deck )

  result = result + "Deck " + num.to_s + ": " + e.decrypt( ciphertext )+"\n" 
end

cgi.out {
  CGI.pretty(
    cgi.html { cgi.title{"Shuffled"} } +
    cgi.body {
      cgi.form {
        "Ciphertext: "+cgi.text_field(name="ciphertext", value=ciphertext)+
	cgi.br+
        "Passphrase: "+cgi.text_field(name="passphrase", value=passphrase)+
        cgi.br+
	"Use Jokers: "+cgi.checkbox(name="jokers", value="1", checked=jokers)+
	cgi.br+
        cgi.submit
      } +
      cgi.hr + 
      "<pre>" + result + "\n" +
      ""+ origdecks + "\n" +
      "" + keydecks + "</pre>"
    }
  )
}
