require 'json'

class Fuzzer
  def initialize(n, freqs = {})
    sum = freqs.inject(0.0) { |s, x| s + x.last }
    freqs.each_key { |x| freqs[x] /= sum }
    s = 0.0
    freqs.each_key do |x|
      freqs[x] = s .. (s + t = freqs[x])
      s += t
    end
    @freqs = freqs
    @n = n
    @alpha = (0..0xff).to_a
  end

  def random_string
    s = ''
    30.times { s << @alpha[rand(@alpha.size)] }
    s
  end

  def pick
    r = rand
    found = @freqs.find { |k, f| f.include? rand }
    found && found.first
  end

  def make_pick
    k = pick
    case
    when k == Hash, k == Array
      k.new
    when k == true, k == false, k == nil
      k
    when k == String
      random_string
    when k == Fixnum
      rand(2 ** 30) - 2 ** 29
    when k == Bignum
      rand(2 ** 70) - 2 ** 69
    end
  end

  def fuzz(current = nil)
    if @n > 0
      case current
      when nil
        @n -= 1
        current = fuzz [ Hash, Array ][rand(2)].new
      when Array
        while @n > 0
          @n -= 1
          current << case p = make_pick
          when Array, Hash
            fuzz(p)
          else
            p
          end
        end
      when Hash
        while @n > 0
          @n -= 1
          current[random_string] = case p = make_pick
          when Array, Hash
            fuzz(p)
          else
            p
          end
        end
      end
    end
    current
  end
end

class MyState < JSON.state
  WS = " \r\t\n"

  def initialize
    super(
          :indent       => make_spaces,
          :space        => make_spaces,
          :space_before => make_spaces,
          :object_nl    => make_spaces,
          :array_nl     => make_spaces,
          :max_nesting  => false
         )
  end

  def make_spaces
    s = ''
    rand(1).times { s << WS[rand(WS.size)] }
    s
  end
end

n = (ARGV.shift || 500).to_i
loop do
  fuzzer = Fuzzer.new(n,
                      Hash => 25,
                      Array => 25,
                      String => 10,
                      Fixnum => 10,
                      Bignum => 10,
                      nil => 5,
                      true => 5,
                      false => 5
                     )
  o1 = fuzzer.fuzz
  json = JSON.generate o1, MyState.new
  if $DEBUG
    puts "-" * 80
    puts json, json.size
  else
    puts json.size
  end
  begin
    o2 = JSON.parse(json, :max_nesting => false)
  rescue JSON::ParserError => e
    puts "Caught #{e.class}: #{e.message}\n#{e.backtrace * "\n"}"
    puts "o1 = #{o1.inspect}", "json = #{json}", "json_str = #{json.inspect}"
    puts "locals = #{local_variables.inspect}"
    exit
  end
  if o1 != o2
    puts "mismatch", "o1 = #{o1.inspect}", "o2 = #{o2.inspect}",
      "json = #{json}", "json_str = #{json.inspect}"
    puts "locals = #{local_variables.inspect}"
  end
end
