#!/usr/bin/env ruby

require 'fox'
begin
  require 'opengl'
rescue LoadError
  require 'fox/missingdep'
  MSG = <<EOM
  Sorry, this example depends on the OpenGL extension. Please
  check the Ruby Application Archives for an appropriate
  download site.
EOM
  missingDependency(MSG)
end


include Fox

class GLTestWindow < FXMainWindow

  # How often our timer will fire (in milliseconds)
  TIMER_INTERVAL = 100

  # Rotate the boxes when a timer message is received
  def onTimeout(sender, sel, ptr)
    @angle += 2.0
    if @angle > 360.0
      @angle -= 360.0
    end
    drawScene()
    @timer = getApp().addTimeout(TIMER_INTERVAL, method(:onTimeout))
  end

  # Rotate the boxes when a chore message is received
  def onChore(sender, sel, ptr)
    @angle += 2.0
    if @angle > 360.0
      @angle -= 360.0
    end
    drawScene()
    @chore = getApp().addChore(method(:onChore))
  end

  # Draws a simple box using the given corners
  def drawBox(xmin, ymin, zmin, xmax, ymax, zmax)
    GL.Begin(GL::TRIANGLE_STRIP)
      GL.Normal(0.0, 0.0, -1.0)
      GL.Vertex(xmin, ymin, zmin)
      GL.Vertex(xmin, ymax, zmin)
      GL.Vertex(xmax, ymin, zmin)
      GL.Vertex(xmax, ymax, zmin)
    GL.End()
  
    GL.Begin(GL::TRIANGLE_STRIP)
      GL.Normal(1.0, 0.0, 0.0)
      GL.Vertex(xmax, ymin, zmin)
      GL.Vertex(xmax, ymax, zmin)
      GL.Vertex(xmax, ymin, zmax)
      GL.Vertex(xmax, ymax, zmax)
    GL.End()
  
    GL.Begin(GL::TRIANGLE_STRIP)
      GL.Normal(0.0, 0.0, 1.0)
      GL.Vertex(xmax, ymin, zmax)
      GL.Vertex(xmax, ymax, zmax)
      GL.Vertex(xmin, ymin, zmax)
      GL.Vertex(xmin, ymax, zmax)
    GL.End()
  
    GL.Begin(GL::TRIANGLE_STRIP)
      GL.Normal(-1.0, 0.0, 0.0)
      GL.Vertex(xmin, ymin, zmax)
      GL.Vertex(xmin, ymax, zmax)
      GL.Vertex(xmin, ymin, zmin)
      GL.Vertex(xmin, ymax, zmin)
    GL.End()
  
    GL.Begin(GL::TRIANGLE_STRIP)
      GL.Normal(0.0, 1.0, 0.0)
      GL.Vertex(xmin, ymax, zmin)
      GL.Vertex(xmin, ymax, zmax)
      GL.Vertex(xmax, ymax, zmin)
      GL.Vertex(xmax, ymax, zmax)
    GL.End()
  
    GL.Begin(GL::TRIANGLE_STRIP)
      GL.Normal(0.0, -1.0, 0.0)
      GL.Vertex(xmax, ymin, zmax)
      GL.Vertex(xmax, ymin, zmin)
      GL.Vertex(xmin, ymin, zmax)
      GL.Vertex(xmin, ymin, zmin)
    GL.End()
  end

  # Draw the GL scene
  def drawScene
    lightPosition = [15.0, 10.0, 5.0, 1.0]
    lightAmbient  = [ 0.1,  0.1, 0.1, 1.0]
    lightDiffuse  = [ 0.9,  0.9, 0.9, 1.0]
    redMaterial   = [ 1.0,  0.0, 0.0, 1.0]
    blueMaterial  = [ 0.0,  0.0, 1.0, 1.0]
  
    width = @glcanvas.width.to_f
    height = @glcanvas.height.to_f
    aspect = width/height
  
    # Make context current
    @glcanvas.makeCurrent()
    
    GL.Viewport(0, 0, @glcanvas.width, @glcanvas.height)
  
    GL.ClearColor(1.0, 1.0, 1.0, 1.0)
    GL.Clear(GL::COLOR_BUFFER_BIT|GL::DEPTH_BUFFER_BIT)
    GL.Enable(GL::DEPTH_TEST)
    
    GL.Disable(GL::DITHER)
  
    GL.MatrixMode(GL::PROJECTION)
    GL.LoadIdentity()
    GLU.Perspective(30.0, aspect, 1.0, 100.0)
  
    GL.MatrixMode(GL::MODELVIEW)
    GL.LoadIdentity()
    GLU.LookAt(5.0, 10.0, 15.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)
  
    GL.ShadeModel(GL::SMOOTH)
    GL.Light(GL::LIGHT0, GL::POSITION, lightPosition)
    GL.Light(GL::LIGHT0, GL::AMBIENT, lightAmbient)
    GL.Light(GL::LIGHT0, GL::DIFFUSE, lightDiffuse)
    GL.Enable(GL::LIGHT0)
    GL.Enable(GL::LIGHTING)
  
    GL.Material(GL::FRONT, GL::AMBIENT, blueMaterial)
    GL.Material(GL::FRONT, GL::DIFFUSE, blueMaterial)
  
    GL.PushMatrix()
    GL.Rotated(@angle, 0.0, 1.0, 0.0)
    drawBox(-1, -1, -1, 1, 1, 1)
  
    GL.Material(GL::FRONT, GL::AMBIENT, redMaterial)
    GL.Material(GL::FRONT, GL::DIFFUSE, redMaterial)
  
    GL.PushMatrix()
    GL.Translated(0.0, 1.75, 0.0)
    GL.Rotated(@angle, 0.0, 1.0, 0.0)
    drawBox(-0.5, -0.5, -0.5, 0.5, 0.5, 0.5)
    GL.PopMatrix()
  
    GL.PushMatrix()
    GL.Translated(0.0, -1.75, 0.0)
    GL.Rotated(@angle, 0.0, 1.0, 0.0)
    drawBox(-0.5, -0.5, -0.5, 0.5, 0.5, 0.5)
    GL.PopMatrix()
  
    GL.PushMatrix()
    GL.Rotated(90.0, 1.0, 0.0, 0.0)
    GL.Translated(0.0, 1.75, 0.0)
    GL.Rotated(@angle, 0.0, 1.0, 0.0)
    drawBox(-0.5,-0.5,-0.5,0.5,0.5,0.5)
    GL.PopMatrix()
  
    GL.PushMatrix()
    GL.Rotated(90.0, -1.0, 0.0, 0.0)
    GL.Translated(0.0,1.75,0.0)
    GL.Rotated(@angle, 0.0, 1.0, 0.0)
    drawBox(-0.5,-0.5,-0.5,0.5,0.5,0.5)
    GL.PopMatrix()
  
    GL.PushMatrix()
    GL.Rotated(90.0, 0.0, 0.0, 1.0)
    GL.Translated(0.0,1.75,0.0)
    GL.Rotated(@angle, 0.0, 1.0, 0.0)
    drawBox(-0.5,-0.5,-0.5,0.5,0.5,0.5)
    GL.PopMatrix()
  
    GL.PushMatrix()
    GL.Rotated(90.0, 0.0, 0.0, -1.0)
    GL.Translated(0.0,1.75,0.0)
    GL.Rotated(@angle, 0.0, 1.0, 0.0)
    drawBox(-0.5,-0.5,-0.5,0.5,0.5,0.5)
    GL.PopMatrix()
  
    GL.PopMatrix()
  
    # Swap if it is double-buffered
    if @glvisual.isDoubleBuffer
      @glcanvas.swapBuffers
    end
    
    # Make context non-current
    @glcanvas.makeNonCurrent
  end

  def initialize(app)
    # Invoke the base class initializer
    super(app, "OpenGL Test Application", nil, nil, DECOR_ALL, 0, 0, 800, 600)

    # Construct the main window elements
    frame = FXHorizontalFrame.new(self, LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y)
    frame.padLeft, frame.padRight = 0, 0
    frame.padTop, frame.padBottom = 0, 0

    # Left pane to contain the glcanvas
    glcanvasFrame = FXVerticalFrame.new(frame,
      LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT)
    glcanvasFrame.padLeft, glcanvasFrame.padRight = 10, 10
    glcanvasFrame.padTop, glcanvasFrame.padBottom = 10, 10
  
    # Label above the glcanvas
    FXLabel.new(glcanvasFrame, "OpenGL Canvas Frame", nil,
      JUSTIFY_CENTER_X|LAYOUT_FILL_X)
  
    # Horizontal divider line
    FXHorizontalSeparator.new(glcanvasFrame, SEPARATOR_GROOVE|LAYOUT_FILL_X)
  
    # Drawing glcanvas
    glpanel = FXVerticalFrame.new(glcanvasFrame, (FRAME_SUNKEN|FRAME_THICK|
      LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT))
    glpanel.padLeft, glpanel.padRight = 0, 0
    glpanel.padTop, glpanel.padBottom = 0, 0
      
    # A visual to draw OpenGL
    @glvisual = FXGLVisual.new(getApp(), VISUAL_DOUBLEBUFFER)
  
    # Drawing glcanvas
    @glcanvas = FXGLCanvas.new(glpanel, @glvisual, nil, 0,
      LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT)
    @glcanvas.connect(SEL_PAINT) {
      drawScene
    }
    @glcanvas.connect(SEL_CONFIGURE) {
      if @glcanvas.makeCurrent
        GL.Viewport(0, 0, @glcanvas.width, @glcanvas.height)
        @glcanvas.makeNonCurrent
      end
    }
  
    # Right pane for the buttons
    buttonFrame = FXVerticalFrame.new(frame, LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT)
    buttonFrame.padLeft, buttonFrame.padRight = 10, 10
    buttonFrame.padTop, buttonFrame.padBottom = 10, 10
      
    # Label above the buttons
    FXLabel.new(buttonFrame, "Button Frame", nil,
      JUSTIFY_CENTER_X|LAYOUT_FILL_X)
  
    # Horizontal divider line
    FXHorizontalSeparator.new(buttonFrame, SEPARATOR_RIDGE|LAYOUT_FILL_X)
  
    # Spin according to timer
    spinTimerBtn = FXButton.new(buttonFrame, "Spin &Timer\tSpin using interval timers\nNote the app blocks until the interal has elapsed...",nil,nil,0,FRAME_THICK|FRAME_RAISED|LAYOUT_FILL_X|LAYOUT_TOP|LAYOUT_LEFT)
    spinTimerBtn.padLeft, spinTimerBtn.padRight = 10, 10
    spinTimerBtn.padTop, spinTimerBtn.padBottom = 5, 5
    spinTimerBtn.connect(SEL_COMMAND) {
      @spinning = true
      @timer = getApp().addTimeout(TIMER_INTERVAL, method(:onTimeout))
    }
    spinTimerBtn.connect(SEL_UPDATE) { |sender, sel, ptr|
      @spinning ? sender.disable : sender.enable
    }

    # Spin according to chore
    spinChoreBtn = FXButton.new(buttonFrame,
      "Spin &Chore\tSpin as fast as possible using chores\nNote even though the
      app is very responsive, it never blocks;\nthere is always something to
      do...", nil,
      nil, 0, FRAME_THICK|FRAME_RAISED|LAYOUT_FILL_X|LAYOUT_TOP|LAYOUT_LEFT)
    spinChoreBtn.padLeft, spinChoreBtn.padRight = 10, 10
    spinChoreBtn.padTop, spinChoreBtn.padBottom = 5, 5
    spinChoreBtn.connect(SEL_COMMAND) {
      @spinning = true
      @chore = getApp().addChore(method(:onChore))
    }
    spinChoreBtn.connect(SEL_UPDATE) { |sender, sel, ptr|
      @spinning ? sender.disable : sender.enable
    }
  
    # Stop spinning
    stopBtn = FXButton.new(buttonFrame,
      "&Stop Spin\tStop this mad spinning, I'm getting dizzy", nil,
      nil, 0, FRAME_THICK|FRAME_RAISED|LAYOUT_FILL_X|LAYOUT_TOP|LAYOUT_LEFT)
    stopBtn.padLeft, stopBtn.padRight = 10, 10
    stopBtn.padTop, stopBtn.padBottom = 5, 5
    stopBtn.connect(SEL_COMMAND) {
      @spinning = false
      if @timer
        getApp().removeTimeout(@timer)
        @timer = nil
      end
      if @chore
        getApp().removeChore(@chore)
        @chore = nil
      end
    }
    stopBtn.connect(SEL_UPDATE) { |sender, sel, ptr|
      @spinning ? sender.enable : sender.disable
    }
  
    # Exit button
    exitBtn = FXButton.new(buttonFrame, "&Exit\tExit the application", nil,
      getApp(), FXApp::ID_QUIT,
      FRAME_THICK|FRAME_RAISED|LAYOUT_FILL_X|LAYOUT_TOP|LAYOUT_LEFT)
    exitBtn.padLeft, exitBtn.padRight = 10, 10
    exitBtn.padTop, exitBtn.padBottom = 5, 5
      
    # Make a tooltip
    FXTooltip.new(getApp())
  
    # Initialize private variables
    @spinning = false
    @chore = nil
    @timer = nil
    @angle = 0.0
  end

  # Create and initialize
  def create
    super
    show(PLACEMENT_SCREEN)
  end
end

if __FILE__ == $0
  # Construct the application
  application = FXApp.new("GLTest", "FoxTest")
  
  # To ensure that the chores-based spin will run as fast as possible,
  # we can disable the chore in FXRuby's event loop that tries to schedule
  # other threads. This is OK for this program because there aren't any
  # other Ruby threads running.
  
  application.disableThreads

  # Construct the main window
  GLTestWindow.new(application)

  # Create the app's windows
  application.create

  # Run the application
  application.run
end
