=begin rdoc
  
== RandomLab

Random number generators and associated methods.

=end

=begin
  TODO number line -- mini-histogram, e.g. second tick drawn above first? else explain in lab manual / reference why two values per tickmark
  TODO another thing for documentation: diff between p.random(x,y) and random(x,y) [latter uses Ruby's PRNG]
=end

module RubyLabs
	
module RandomLab
  
  require "permute.rb"
  
=begin rdoc
  Pseudo-random number generator.  Constraints on a, c, and m:
  *  c and m must be relatively prime
  *  a-1 divisible by prime factors of m
  *  if m is multiple of 4, a-1 must also be a multiple of 4

  Note: some good values (for small m) from Numerical Recipes:
      m = 53125, a = 171, c = 11213
=end

  class PRNG
  
    attr_accessor :a, :c, :m, :x

    def initialize(a, c, m)
      @a = a
      @c = c
      @m = m
      @x = 0
    end
  
    def state
      return @x
    end
  
    def seed(val)
      @x = val
    end
  
    # :begin :advance
    def advance
      @x = (@x * @a + @c) % @m    
    end
    # :end :advance
  
    # :begin :random
    def random(min, max)
      return nil if max <= min
      range = max - min + 1
      return (advance() % range) + min
    end
    # :end :random
  
    def inspect
      sprintf "#<RandomLab::PRNG a: #{@a} c: #{@c} m: #{@m}>"
    end
  
    alias to_s inspect
  
  end # class PRNG

# :begin :prng_sequence
  def prng_sequence(a, c, m)
    seq = [0]
    (m-1).times do
      seq << (a * seq.last + c) % m
    end
    return seq
  end
# :end :prng_sequence

=begin rdoc
  Make a new deck of cards
=end

  def new_deck
    (0..51).map { |i| Card.new(i) }
  end

=begin rdoc
  A "helper method" that can be called via a probe, to print the contents
  of an array during the execution of the permute! method
=end

# Note: permute! moved to own source file, permute.rb

  def brackets(a, i, r)
    res = "#{r}: "
    if i <= 0
      res += ("[" + a.join("  ") + "]")
    elsif i >= a.length
      res += (" " + a.join("  ") + "  [ ]")
    else
      pre = a.slice(0..(i-1))
      post = a.slice(i..-1)
      res += (" " + pre.join("  ") + " [" + post.join("  ") + "]")
    end
    return res
  end


=begin rdoc
  Classify a poker hand -- returns a symbol describing a hand.  Return values:
    :pair
    :two_pair
    :three_of_a_kind
    :full_house
    :straight
    :flush
    :four_of_a_kind
    :straight_flush
=end

  def poker_rank(a)
    rcount = Array.new(Card::Ranks.length, 0)
    scount = Array.new(Card::Suits.length, 0)
    a.each do |x|
      rcount[ Card::Ranks.index(x.rank) ] += 1
      scount[ Card::Suits.index(x.suit) ] += 1
    end
    if rcount.max == 1
      straight = (rcount.rindex(1) - rcount.index(1) == 4)
      flush = scount.max == 5
      return :straight_flush if (straight && flush) 
      return :straight if straight
      return :flush if flush
      return :high_card
    else
      rcount.reject! { |x| x == 0 }
      rcount.sort! { |x,y| y <=> x }
      return :four_of_a_kind if rcount[0] == 4
      return :full_house if (rcount[0] == 3 && rcount[1] == 2)
      return :three_of_a_kind if rcount[0] == 3
      return :two_pair if (rcount[0] == 2 && rcount[1] == 2)
      return :pair
    end
  end

  def poker_rankings
    return [:high_card, :pair, :two_pair, :three_of_a_kind, :straight, :flush, :full_house, :four_of_a_kind, :straight_flush]
  end

  def poker_counts
    h = Hash.new
    poker_rankings.each { |x| h[x] = 0 }
    return h
  end


=begin rdoc
  Class to represent cards from a standard 52-card deck.  Includes comparators
  to sort by rank or suit.
  
  Call Card.new to get a random card, or Card.new(id) to get a specific card
  where id is a number between 0 and 51.
=end

# To test the expression that assigns suits and ranks:
#   52.times { |x| puts (x/13).to_s + "  " + (x%13).to_s }

  class Card
    attr_accessor :rank, :suit
  
    unless defined? Suits
      Suits = [:spades, :hearts, :diamonds, :clubs]
    end
  
    unless defined? Ranks
      Ranks = [:ace, :king, :queen, :jack, :ten, :nine, :eight, :seven, :six, :five, :four, :three, :two]
    end
  
    def initialize(id = nil)
      id = rand(52) if id.nil?
      raise "card must be between 0 and 51" if id < 0 || id > 51
      @suit = Suits[id / 13]
      @rank = Ranks[id % 13]
    end
  
    def ==(x)
      return @suit == x.suit && @rank == x.rank
    end
  
    def <=>(x)
      r0 = Ranks.index(@rank); r1 = Ranks.index(x.rank)
      s0 = Suits.index(@suit); s1 = Suits.index(x.suit) 
      if (res = (s0 <=> s1)) == 0
        return (r0 <=> r1)
      else
        return res
      end
    end
  
    @@outputform = :utf8
  
    def inspect
      s = ""
      s << case @rank
        when :ace     :  "A"
        when :king    :  "K"
        when :queen   :  "Q"
        when :jack    :  "J"
        when :ten     :  "10"
        when :nine    :  "9"
        when :eight   :  "8"
        when :seven   :  "7"
        when :six     :  "6"
        when :five    :  "5"
        when :four    :  "4"
        when :three   :  "3"
        when :two     :  "2"      
      end
      if $KCODE[0] == ?U
        if @@outputform == :utf8
          s << case @suit
            when :spades    : "\xe2\x99\xa0"
            when :hearts    : "\xe2\x99\xa5"
            when :clubs     : "\xe2\x99\xa3"
            when :diamonds  : "\xe2\x99\xa6"
          end
        else
          s << "!irb" + @suit.to_s.chop
        end
      else
        s << case @suit
          when :spades    : "S"
          when :hearts    : "H"
          when :clubs     : "C"
          when :diamonds  : "D"
        end
      end
      return s
      # "#{@rank} #{@suit}"
    end
  
    alias to_s inspect
  
    def Card.print_latex
      @@outputform = :latex
    end
  
    def Card.print_utf8
      @@outputform = :utf8
    end 
  
  end # class Card

=begin rdoc
	Visualization methods called by students to test a PRNG object:

		view_numberline(n)	      make a number line for integers between 0 and n-1
		tick_mark(i)			        draw a tick mark at location i on the number line

    view_histogram(n, max)    make a histogram with n bins for value from 0 to max
    view_histogram(a)         make a histogram with one bin for each item in a
    update_bin(x)             add 1 to the count of items in bin x
    get_counts                get a copy of the bins (array of counts)
    
    view_dotplot(n)           initialize an n x n dotplot
    plot_point(x,y)           add a dot at (x,y) to the dotplot
=end

  def view_numberline(npoints, userOptions = {})
    Canvas.init(500, 100, "RandomLab::NumberLine")
    options = @@numberLineOptions.merge(userOptions)
    line = Canvas.line(0, 70, 500, 70, :width => options[:lineThickness], :fill => options[:lineColor])
    @@drawing = NumberLine.new(line, npoints, options)
    return true
  end

	def tick_mark(i)
		if @@drawing.class != NumberLine
			puts "call view_numberline to initialize the number line"
		elsif i < 0 || i >= @@drawing.npoints
			puts "tick_mark: 0 <= i < #{@@drawing.npoints}"
		else
		  x0, y0, x1, y1 = @@drawing.line.coords
			tx = (i.to_f / @@drawing.npoints) * (x1-x0)
			ty = y0 - @@drawing.options[:tickHeight]
			Canvas.line(tx, y0, tx, ty, :width => @@drawing.options[:tickWidth], :fill => @@drawing.options[:tickColor])
			sleep(@@delay)
		end
		return true
	end
	
  def view_histogram(*args)
    begin
      if args[0].class == Array
        userOptions = args.length > 1 ? args[1] : { }
        raise "usage:  view_histogram(keys, options)" unless userOptions.class == Hash
        keys = args[0]
        nbins = max = keys.length
      else
        userOptions = args.length > 2 ? args[2] : { }
        raise "usage:  view_histogram(nbins, max, options)" unless userOptions.class == Hash
        nbins = args[0]
        max = args.length > 1 ? args[1] : nbins
        keys = nil
      end
    rescue Exception => e
      puts e
      return false
    end
    
    Canvas.init(500, 300, "RandomLab::Histogram")
    counts = Hash.new(0)
    options = @@histogramOptions.merge(userOptions)
    bins = []
    binHeight = 3
    binBorder = 2
    binWidth = (500/(nbins+1))
    binTop = 280
    nbins.times do |i| 
      x = i * binWidth + binWidth/2
      bins << Canvas.rectangle( x + binBorder, binTop, x + binWidth - binBorder, binTop + binHeight, :outline => options[:binColor], :fill => options[:binColor] ) 
    end

    @@drawing = Histogram.new(bins, max.to_f, keys, counts, binTop, options)

    return true  
  end
	

  def update_bin(x)
    if @@drawing.class != Histogram
      puts "call view_histogram to initialize a histogram"
      return nil
    end
    if @@drawing.keys
      i = @@drawing.keys.index(x)
      if i.nil?
        puts "unknown bin: #{x}"
        return nil
      end
    else
      xmax = @@drawing.max
      nb = @@drawing.bins.length
      if x < 0 || x >= xmax
        puts "x must be between 0 and #{xmax-1}"
        return nil
      end
      i = ((x / xmax) * nb).to_i
    end
    @@drawing.counts[i] += 1
    rect = @@drawing.bins[i]
    x1, y1, x2, y2 = rect.coords
    y1 = y1 - @@drawing.options[:boxIncrement]
    rect.coords = [x1, y1, x2, y2]
    if y1 < @@drawing.options[:rescaleTrigger]
      base = @@drawing.base
      @@drawing.bins.each do |rect|
        x1, y1, x2, y2 = rect.coords
        y1 =  base - ((base - y1) / 2)
        rect.coords = [x1, y1, x2, y2]
      end
      @@drawing.options[:boxIncrement] /= 2
    end
    return true
  end

  def get_counts
    if @@drawing.class == Histogram
      return @@drawing.counts
    else
      puts "current drawing is not a histogram"
      return nil
    end
  end

  def view_dotplot(npoints, userOptions = {})
    Canvas.init(500, 500, "RandomLab::DotPlot")
    options = @@dotPlotOptions.merge(userOptions)
    @@drawing = DotPlot.new(npoints, options)
    return true
  end

  def plot_point(x,y)
    if @@drawing.class != DotPlot
      puts "call view_dotplot to initialize a dot plot"
		elsif x < 0 || x >= @@drawing.max || y < 0 || y >= @@drawing.max
			puts "plot_point: 0 <= x, y < #{@@drawing.max}"
		else
      px = (x.to_f / @@drawing.max) * Canvas.width
      py = (y.to_f / @@drawing.max) * Canvas.height
      r = @@drawing.options[:dotRadius]
      color = @@drawing.options[:dotColor]
      Canvas.circle( px, py, r, :outline => color, :fill => color ) 
		end
		return nil
  end
  
  NumberLine = Struct.new(:line, :npoints, :options)
  Histogram = Struct.new(:bins, :max, :keys, :counts, :base, :options)
  DotPlot = Struct.new(:max, :options)

  @@numberLineOptions = { 
    :lineThickness => 3, 
    :lineColor => '#777777', 
    :tickHeight => 20, 
    :tickWidth => 1,
    :tickColor => '#0000FF',
  }
  
  @@histogramOptions = {
    :binColor => '#000080',
    :boxIncrement => 8.0,
    :rescaleTrigger => 50,
  }
  
  @@dotPlotOptions = {
    :dotColor => '#000080',
    :dotRadius => 1.0,
  }
  
  @@drawing = nil
  @@delay = 0.1
  
end # RandomLab

end # RubyLabs