require 'strscan'

class LSystem
  attr_reader :output

  def initialize( in_axiom, in_rules, in_iterations = 0 )
    @axiom = in_axiom
    @output = in_axiom
    @rules = in_rules
    
    in_iterations.times do iterate end

    return @output
  end
  
  def iterate
    temp_string = ""
    @output.scan( /./ ) do |letter|
      rule_hit = false
      @rules.each do |rule|
        if( letter[ rule[0] ] )
          rule_hit = true
          temp_string << rule[1]
        end
      end
      if( not rule_hit )
        temp_string << letter
      end
    end
    @output = temp_string
  end
end

the_rules = [
  [ /F/, "" ],
  [ /Y/, "+FX--FY+" ],
  [ /X/, "-FX++FY-" ]
]

the_system = LSystem.new( "FX", the_rules, 10 )

class Pen
  attr_accessor :position_stack
  attr_reader :position
  attr_reader :heading
  attr_accessor :theta
  attr_accessor :speed #coefficent

  DEG2RAD = Math::PI/180

  def initialize( in_position, in_shoes )
    @shoes = in_shoes
    @position = in_position
    @up = false #starts out down
    @position_stack = []
    @heading = 90
    @theta = 60
    @unit_move = 10
    @speed = 1
    @reverse_coef = 1
  end
  
  def is_up?
    return @up
  end

  def pen_down
    @up = false
  end

  def pen_up
    @up = true
  end

  def position=(new_position)
    #p new_position
    old_position = @position
    @position = new_position
    @shoes.append { @shoes.line old_position[0], old_position[1], new_position[0], new_position[1] }
  end

  def heading=(in_value)
    in_value = in_value.to_f
    while in_value < 0
      in_value += 360
    end

    @heading = (in_value % 360)
  end

  def forward( in_amount )
    if( in_amount )
      in_amount = in_amount.to_f
      dx = in_amount * Math.sin(@heading*DEG2RAD)
      dy = in_amount * Math.cos(@heading*DEG2RAD)
      self.position = [@position[0]+dx, @position[1]+dy]
    else
      pen_down
      dx = @unit_move * @speed * Math.sin(@heading*DEG2RAD)
      dy = @unit_move * @speed * Math.cos(@heading*DEG2RAD)
      self.position = [@position[0]+dx, @position[1]+dy]
    end
  end

  def back( in_amount )
    if( in_amount )
      in_amount = in_amount.to_f
      dx = in_amount * Math.sin(@heading*DEG2RAD)
      dy = in_amount * Math.cos(@heading*DEG2RAD)
      self.position = [@position[0]-dx, @position[1]-dy]
    else
      pen_down
      dx = @unit_move * @speed * Math.sin(@heading*DEG2RAD)
      dy = @unit_move * @speed * Math.cos(@heading*DEG2RAD)
      self.position = [@position[0]-dx, @position[1]-dy]
    end
  end

  def go
    pen_up
    dx = @unit_move * @speed * Math.sin(@heading*DEG2RAD)
    dy = @unit_move * @speed * Math.cos(@heading*DEG2RAD)
    self.position = [@position[0]+dx, @position[1]+dy]
  end

  def left
    @heading += @theta * @reverse_coef
  end

  def right
    @heading -= @theta * @reverse_coef
  end

  def about_face
    if( @heading >= 180 )
      @heading -= 180
    else
      @heading += 180
    end
  end

  def up_theta( in_amount )
    @theta += in_amount.to_f * @reverse_coef
  end

  def down_theta( in_amount )
    @theta -= in_amount.to_f * @reverse_coef
  end

  def push_position
    @position_stack.push( [@position, @heading] )
  end

  def pop_position
    pen_up
    self.position, @heading = @position_stack.pop
    pen_down
  end

  def go_reverse
    @reverse_coef = -@reverse_coef
  end

  def set_speed( in_inverse, in_sqrt, in_value )
    @speed = in_value.to_f
    @speed = 1 / @speed if in_inverse
    @speed = Math.sqrt( @speed ) if in_sqrt
  end

  def set_color( in_r, in_g, in_b )
    stroke in_r, in_g, in_b
  end

  def follow_instructions( in_instructions )
    scanner = StringScanner.new( in_instructions )
      while inst = scanner.getch do
        case inst
          ###
          # Fractint compatibility
          when "F", "D"
            self.forward( scanner.scan( /[+-]?\d+\.?\d*/ ) )
          when "G", "M"
            self.go
          when "+"
            self.left
          when "-"
            self.right
          when "|"
            self.about_face
          when "\\"
            the_value = scanner..scan( /[+-]?\d+\.?\d*/ )
            self.up_theta( the_value )
          when "/"
            the_value = scanner..scan( /[+-]?\d+\.?\d*/ )
            self.down_theta( the_value )
          when "["
            self.push_position
          when "]"
            self.pop_position
          when "!"
            self.go_reverse
          when "@"
            the_value = scanner.scan( /I?Q?[+-]?\d+\.?\d*/ )
            self.set_speed( the_value[/I/], the_value[/Q/], the_value[/[+-]?\d+\.?\d*/] )
          
          ###
          # Turtle compatibility
          when "^"
            self.pen_up
          when "#"
            self.pen_down
          when "S"
            case scanner.getch
              when "P"
                x = scanner..scan( /[+-]?\d+\.?\d*/ )
                scanner.getch
                y = scanner..scan( /[+-]?\d+\.?\d*/ )
                x = x.to_i + 250
                y = -y.to_i + 250
                self.position = [ x, y ]
              when "H"
                theta = scanner..scan( /[+-]?\d+\.?\d*/ )
                self.heading = theta
              when "T"
                self.theta = scanner..scan( /[+-]?\d+\.?\d*/ ).to_f
            end
          when "B"
            self.back( scanner..scan( /[+-]?\d+\.?\d*/ ) )
          when "T"
            case scanner.getch
              when "R"
                theta = scanner..scan( /[+-]?\d+\.?\d*/ )
                theta = theta.to_f
                self.heading -= theta
              when "L"
                theta = scanner..scan( /[+-]?\d+\.?\d*/ )
                theta = theta.to_f
                self.heading += theta
            end
          when "C"
            r = scanner..scan( /[+-]?\d+\.?\d*/ )
            scanner.getch
            g = scanner..scan( /[+-]?\d+\.?\d*/ )
            scanner.getch
            b = scanner..scan( /[+-]?\d+\.?\d*/ )
          self.set_color( r.to_f, g.to_f, b.to_f )            
        end
      end
  end
end

Shoes.app :width=>800, :height=>450 do
  strokewidth 2

  button "Night Night" do
    the_pen = Pen.new([400,225], self)
    the_pen.down_theta( 15 )
    the_pen.follow_instructions( the_system.output )
  end
end