# PPM Ruby library for use in 15-110 at CMU, Fall 2011
# by Jeffery von Ronne
# This library is, hereby, released into the public doamin.


def plot(matrix,mag=1)
  height = matrix.length
  width = matrix.first.length 
  Canvas.init(mag*width+2,mag*height+2, "Plot")
  Canvas::Rectangle.new(1,1,mag*(width+1),mag*(height+1),:width=>0, :fill=>"black")
  color = "white"
  for y in 0..height-1 do
    for x in 0..width-1 do
      rgb_tuple = matrix[y][x]
      color = "#%02X%02X%02X" % [rgb_tuple[0], rgb_tuple[1], rgb_tuple[2]]
      Canvas::Rectangle.new(1+mag*x,1+mag*y,1+mag*(x+1),1+mag*(y+1), :width=>0, :fill=>color)
    end
  end                                         
  return nil
end

def save_ppm(matrix, filename)
  file = File.new(filename,"w")
  height = matrix.length
  width = matrix.first.length
  maxval = 255
  file.print "P3 #{width} #{height} #{maxval}\n"
  for row in 0..height-1 do
    for col in 0..width-1 do
      pixel = matrix[row][col]
      red = pixel[0]
      green = pixel[1]
      blue = pixel[2]

      check_range("red", red, row, col)
      check_range("green", green, row, col)
      check_range("blue", blue, row, col)

      file.print "#{red} #{green} #{blue}\n"
    end
  end
  file.close()
end

def load_ppm(filename)
  file = File.new(filename)
  bytes = file.bytes.to_a

  if p3_magic?(bytes) then
    # ASCII (aka "plain") PPM file
    format = :plain
  elsif p6_magic?(bytes)
    # binary (aka "raw") PPM file
    format = :raw
  else
    raise "file doesn't start with P3/P6" 
  end

  bytes = bytes[2..-1]
  skip_whitespace(bytes)
  width = parse_integer(bytes)
  skip_whitespace(bytes)
  height = parse_integer(bytes)
  skip_whitespace(bytes)
  maxval = parse_integer(bytes)
  if format == :plain then
    skip_whitespace(bytes)
    matrix = read_ascii_data(bytes,width,height,maxval)
  else # :raw
    raise "whitespace expected" if ! space?(bytes.shift)
    matrix = read_binary_data(bytes,width,height,maxval)
  end
end


# helper functions

def check_range(color, val, row, col)
      id = "#{color} value (#{val}) at #{row},#{col}"
      raise "#{id} is not a smallish integer" if ! val.kind_of?(Fixnum)
      raise "#{id} is out of range" if val < 0 || val > 255
end

def p6_magic?(bytes)
  return bytes[0].chr == "P" && bytes[1].chr == "6"
end

def p3_magic?(bytes)
  return bytes[0].chr == "P" && bytes[1].chr == "3"
end

def read_binary_data(bytes,width,height,maxval)
  raise "16-bit pximaps not supported" if maxval > 255

  matrix = []
  for row in 0..(height-1) do
    matrix[row] = []
    for col in 0..(width-1) do
      base_index = 3*(row*width+col)
      red = bytes[base_index] * 256 / (maxval+1)
      green = bytes[base_index+1] * 256 / (maxval+1)
      blue = bytes[base_index+2] * 256 / (maxval+1)
      matrix[row][col] = [red, green, blue]
    end
  end
  return matrix
end

def read_ascii_data(bytes,width,height,maxval)
  raise "16-bit pximaps not supported" if maxval > 255

  matrix = []
  for row in 0..(height-1) do
    matrix[row] = []
    for col in 0..(width-1) do
      red = parse_integer(bytes) * 256 / (maxval+1)
      skip_whitespace(bytes)
      green = parse_integer(bytes) * 256 / (maxval+1)
      skip_whitespace(bytes)
      blue = parse_integer(bytes) * 256 / (maxval+1)
      skip_whitespace(bytes)
      matrix[row][col] = [red, green, blue]
    end
  end
  return matrix
end

def parse_integer(byte_array)
  raise "number expected" if ! digit?(byte_array.first)
  num = 0
  while byte_array.length > 1 && digit?(byte_array.first) do
    num = num * 10 + (byte_array.shift - "0"[0])
  end
  return num
end

def digit?(ascii_code)
  return true if (ascii_code.chr =~ /\d/) != nil
end

def skip_whitespace(bytes)
  skip_comment(bytes) if bytes.first.chr == "#"
  raise "white space expected" if ! space?(bytes[0])
  while bytes.length > 1 && space?(bytes[0]) do
    bytes.shift
    skip_comment(bytes) if bytes.first.chr == "#"
  end
end

def skip_comment(bytes)
  raise "comment doesn't start with #" if bytes.first.chr != "#"
  while bytes.first.chr != "\n" do
    bytes.shift
  end
  bytes.shift
end
  
def space?(ascii_code)
  return true if ascii_code.chr == "\n"
  return true if (ascii_code.chr =~ /\s/) != nil
end
