require 'fox'
require 'opengl'

module Fox

  HANDLE_SIZE = 4.0

  #
  # OpenGL point object
  #
  class FXGLPoint < FXGLObject
  
    # Point position, in model coordinates (a 3-element array)
    attr_accessor :pos
    
    #
    # Returns an initialized FXGLPoint instance.
    # If no arguments are passed to #new, the initial point position is
    # (0.0, 0.0, 0.0). You can specify a different initial position by
    # passing in the x, y and z coordinates individually:
    #
    #     aPoint = FXGLPoint.new(x, y, z)
    #
    # or as a 3-element array:
    #
    #     aPoint = FXGLPoint.new([x, y, z])
    #
    def initialize(*args)
      if args.length == 0
	@pos = [0.0, 0.0, 0.0]
      elsif args.length == 3
	@pos = [args[0], args[1], args[2]]
      else
	@pos = args[0]
      end
    end
    
    #
    # Return the bounding box (an FXRange instance) for this point.
    #
    def bounds
      FXRange.new(@pos[0], @pos[0], @pos[1], @pos[1], @pos[2], @pos[2])
    end
    
    #
    # Draw this point into _viewer_ (an FXGLViewer instance).
    #
    def draw(viewer)
      GL::Color(0.0, 0.0, 1.0)
      GL::PointSize(HANDLE_SIZE)
      GL::Begin(GL::POINTS)
      GL::Vertex(@pos)
      GL::End()
    end
    
    #
    # Perform hit test for this point in _viewer_ (an FXGLViewer instance).
    #
    def hit(viewer)
      GL::Begin(GL::POINTS)
      GL::Vertex(@pos)
      GL::End()
    end
  end

  #
  # OpenGL line object
  #
  class FXGLLine < FXGLObject
  
    # Starting point for line [FXGLPoint]
    attr_accessor :fm
    
    # End point for line [FXGLPoint]
    attr_accessor :to
    
    #
    # Return an initialized FXGLLine instance.
    #
    # If no arguments are passed to #new, the initial starting and ending
    # points for the line are (-0.5, 0.0, 0.0) and (0.5, 0.0, 0.0), respectively.
    # You can specify different initial start and end points by passing in
    # another FXGLLine instance from which to copy the start and end point
    # values, e.g.
    #
    #     aLine = FXGLLine.new(anotherLine)
    #
    # or by passing in the x, y and z coordinates individually:
    #
    #     aLine = FXGLLine.new(x0, y0, z0, x1, y1, z1)
    #
    def initialize(*args)
      if args.length == 0
	@fm = FXGLPoint.new(-0.5, 0.0, 0.0)
	@to = FXGLPoint.new( 0.5, 0.0, 0.0)
      elsif args.length == 1
	@fm = args[0].fm
	@to = args[0].to
      else
	@fm = FXGLPoint.new(args[0], args[1], args[2])
	@to = FXGLPoint.new(args[3], args[4], args[5])
      end
    end
    
    #
    # Return the bounding box (an FXRange instance) for this line.
    #
    def bounds
      FXRange.new(min(@fm.pos[0], @to.pos[0]),
                  max(@fm.pos[0], @to.pos[0]),
                  min(@fm.pos[1], @to.pos[1]),
                  max(@fm.pos[1], @to.pos[1]),
                  min(@fm.pos[2], @to.pos[2]),
                  max(@fm.pos[2], @to.pos[2]))
    end
    
    #
    # Draw this line into _viewer_ (an FXGLViewer instance).
    #
    def draw(viewer)
      GL::Color(1.0, 0.0, 0.0)
      GL::PointSize(HANDLE_SIZE)
      GL::Begin(GL::LINES)
      GL::Vertex(@fm.pos)
      GL::Vertex(@to.pos)
      GL::End()
    end
    
    #
    # Perform hit-test for this line in _viewer_ (an FXGLViewer instance).
    #
    def hit(viewer)
      GL::Begin(GL::LINES)
      GL::Vertex(@fm.pos)
      GL::Vertex(@to.pos)
      GL::End()
    end
  end

=begin

  # Shape drawing options
  SURFACE_SINGLESIDED   = 0              // Single-sided [both sides same]
  SURFACE_DUALSIDED     = 0x00000001     // Dual-sided surface
  SHADING_NONE          = 0              // No light source
  SHADING_SMOOTH        = 0x00000002     // Smooth shaded
  SHADING_FLAT          = 0x00000004     // Flag shaded
  FACECULLING_OFF       = 0              // No face culling
  FACECULLING_ON        = 0x00000008     // Cull backward facing surfaces
  STYLE_SURFACE         = 0x00000010     // Draw filled surfaces
  STYLE_WIREFRAME       = 0x00000020     // Draw wire frame
  STYLE_POINTS          = 0x00000040     // Draw as points
  STYLE_BOUNDBOX        = 0x00000080     // Draw bounding box
  
  class FXGLShape < FXGLObject
    def initialize(x, y, z, opts, front=nil, back=nil)
      @position = [x, y, z]
      @material = [nil, nil]
      if front.nil?
        @material[0] = FXMaterial.new
        material[0].ambient = FXHVec.new(0.2, 0.2, 0.2, 1.0)
        material[0].diffuse = FXHVec.new(0.8, 0.8, 0.8, 1.0)
        material[0].specular = FXHVec.new(1.0, 1.0, 1.0, 1.0)
        material[0].emission = FXHVec.new(0.0, 0.0, 0.0, 1.0)
        material[0].shininess = 30.0
      else
        @material[0] = front
      end
      if back.nil?
        @material[1] = FXMaterial.new
        material[1].ambient = FXHVec.new(0.2, 0.2, 0.2, 1.0)
        material[1].diffuse = FXHVec.new(0.8, 0.8, 0.8, 1.0)
        material[1].specular = FXHVec.new(1.0, 1.0, 1.0, 1.0)
        material[1].emission = FXHVec.new(0.0, 0.0, 0.0, 1.0)
        material[1].shininess = 30.0;
      else
        @material[1] = back
      end
      @range = FXRange.new(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0)
      @options = opts
    end
    
    def canDrag?
      true
    end
    
    def canDelete?
      true
    end

    # Handle drag-and-drop drop
    def onDNDDrop(sender, sel, ptr)
      FXushort *clr; FXuint len; FXHVec color;
      clr = sender.getDNDData(FROM_DRAGNDROP, FXWindow::colorType)
      if clr
	color[0]=clr[0]/65535.0f;
	color[1]=clr[1]/65535.0f;
	color[2]=clr[2]/65535.0f;
	color[3]=clr[3]/65535.0f;
	# FXFREE(&clr);
	material[0].ambient=color;
	material[0].diffuse=color;
	material[1].ambient=color;
	material[1].diffuse=color;
	return 1
      else
        return 0
      end
    end

    # Cursor got dragged over here.
    def onDNDMotion(sender, sel, ptr)
      return 1
    end

    # We were asked about tip text
    def onQueryTip(sender, sel, ptr)
      sender.handle(self, MKUINT(FXWindow::ID_SETSTRINGVALUE, SEL_COMMAND), @tip)
      return 1
    end

    # Shading off
    def onCmdShadeOff(sender, sel, ptr)
      @options &= ~(SHADING_SMOOTH|SHADING_FLAT)
      return 1
    end

    # Shading on [flat]
    def onCmdShadeOn(sender, sel, ptr)
      @options &= ~SHADING_SMOOTH
      @options |=  SHADING_FLAT
      return 1
    end

    # Shading on [smoooooooooth]
    def onCmdShadeSmooth(sender, sel, ptr)
      @options &= ~SHADING_FLAT
      @options |=  SHADING_SMOOTH
      return 1
    end

    # Update shading off button
    def onUpdShadeOff(sender, sel, ptr)
      msg = (@options & (SHADING_FLAT|SHADING_SMOOTH)) != 0 ? FXWindow::ID_UNCHECK : FXWindow::ID_CHECK
      sender.handle(self, MKUINT(FXWindow::ID_ENABLE, SEL_COMMAND), nil)
      sender.handle(self, MKUINT(msg, SEL_COMMAND), nil)
      return 1
      }
    
    # Update shading on button
    def onUpdShadeOn(sender, sel, ptr)
      msg = (@options & SHADING_FLAT) != 0 ? FXWindow::ID_CHECK : FXWindow::ID_UNCHECK
      sender.handle(self, MKUINT(FXWindow::ID_ENABLE, SEL_COMMAND), nil)
      sender.handle(self, MKUINT(msg, SEL_COMMAND), nil)
      return 1
    end
    
    # Update shading smooth button
    def onUpdShadeSmooth(sender, sel, ptr)
      msg = (@options & SHADING_SMOOTH) != 0 ? FXWindow::ID_CHECK : FXWindow::ID_UNCHECK
      sender.handle(self, MKUINT(FXWindow::ID_ENABLE, SEL_COMMAND), nil)
      sender.handle(self, MKUINT(msg, SEL_COMMAND), nil)
      return 1
    end

    # Drag shape around
    def drag(viewer, fx, fy, tx, ty)
      zz = viewer.worldToEyeZ(position)
      wf = viewer.eyeToWorld(viewer.screenToEye(fx, fy, zz))
      wt = viewer.eyeToWorld(viewer.screenToEye(tx, ty, zz))
      @position += wt - wf
      true
    end

    # Draw for hit
    def hit(viewer)
      draw(viewer)
    end

    # Get bounding box
    def bounds
      FXRange.new(@position[0] + @range[0][0], @position[0] + @range[0][1],
                  @position[1] + @range[1][0], @position[1] + @range[1][1],
                  @position[2] + @range[2][0], @position[2] + @range[2][1])

    end
  end

=end

  #
  # OpenGL cube object
  #
  class FXGLCube < FXGLShape

    # Cube width [Float]
    attr_accessor :width
    
    # Cube height [Float]
    attr_accessor :height
    
    # Cube depth [Float]
    attr_accessor :depth

    #
    # Return an initialized FXGLCube instance.
    #
    # One option is to initialize the cube with a specified origin,
    # width, height and depth:
    #
    #     aCube = FXGLCube.new(x, y, z, w, h, d)
    #
    # If left unspecified, the width (_w_), height (_h_) and depth (_d_)
    # default to 1.0.
    #
    # Another option is to initialize the cube with a specified origin,
    # width, height, depth and material:
    #
    #     aCube = FXGLCube.new(x, y, z, w, h, d, material)
    #
    # where the _material_ is an FXMaterial instance.
    #
    def initialize(*args)
      if args.length == 7
	super(args[0], args[1], args[2], SHADING_SMOOTH|STYLE_SURFACE,
              args[6], args[6])
      else
	super(args[0], args[1], args[2], SHADING_SMOOTH|STYLE_SURFACE)
      end
      @width = args[3] ? args[3] : 1.0
      @height = args[4] ? args[4] : 1.0
      @depth = args[5] ? args[5] : 1.0
      setRange(FXRange.new(-0.5*@width, 0.5*@width,
                           -0.5*@height, 0.5*@height,
                           -0.5*@depth, 0.5*@depth))
    end

    #
    # Draws this cube into _viewer_ (an FXGLViewer instance).
    #
    def drawshape(viewer)
      xmin, xmax = -0.5*@width, 0.5*@width
      ymin, ymax = -0.5*@height, 0.5*@height
      zmin, zmax = -0.5*@depth, 0.5*@depth

      # Draw low face
      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()

      # Draw east face
      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()

      # Draw high face
      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()

      # Draw west face
      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()

      # Draw north face
      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()

      # Draw south face
      GL::Begin(GL::TRIANGLE_STRIP)
	GL::Normal(0.0, -1.0, 0.0)
	GL::Vertex(xmin, ymin, zmax)
	GL::Vertex(xmin, ymin, zmin)
	GL::Vertex(xmax, ymin, zmax)
	GL::Vertex(xmax, ymin, zmin)
      GL::End()
    end
  end

  #
  # OpenGL cone object
  #
  class FXGLCone < FXGLShape
    # Cone fidelity
    SLICES_NUMBER = 20
    STACKS_NUMBER = 20
    LOOPS = 4

    # Cone height [Float]
    attr_accessor :height
    
    # Cone base radius [Float]
    attr_accessor :radius
    
    # Number of slices (default is 20) [Integer]
    attr_accessor :slices
    
    # Number of stacks (default is 20) [Integer]
    attr_accessor :stacks
    
    # Number of loops (default is 4) [Integer]
    attr_accessor :loops

    #
    # Returns an initialized FXGLCone instance.
    #
    # One option is to initialize the cone with a specified origin,
    # height and radius:
    #
    #     aCone = FXGLCone.new(x, y, z, h, r)
    #
    # If left unspecified, the height (_h_) and radius (_r_) default to 1.0.
    #
    # Another option is to initialize the cone with a specified origin,
    # height, radius and material:
    #
    #     aCone = FXGLCone.new(x, y, z, h, r, material)
    #
    # where the _material_ is an FXMaterial instance.
    #
    def initialize(*args)
      if args.length == 5
	super(args[0], args[1], args[2], SHADING_SMOOTH|STYLE_SURFACE)
      elsif args.length == 6
	super(args[0], args[1], args[2], SHADING_SMOOTH|STYLE_SURFACE,
              args[5], args[5])
      end
      @height = args[3] ? args[3] : 1.0
      @radius = args[4] ? args[4] : 1.0
      @slices = SLICES_NUMBER
      @stacks = STACKS_NUMBER
      @loops  = LOOPS
      setRange(FXRange.new(-@radius, @radius, 0, @height, -@radius, @radius))
    end
    
    #
    # Draw this cone into _viewer_ (an FXGLViewer instance).
    #
    def drawshape(viewer)
      quad = GLU::NewQuadric()
      GLU::QuadricDrawStyle(quad, GLU::FILL)
      GL::PushMatrix()
      GL::Rotate(-90, 1, 0, 0)
      GLU::Cylinder(quad, @radius, 0, @height, @slices, @stacks)
      GLU::QuadricOrientation(quad, GLU::INSIDE)
      GLU::Disk(quad, 0, @radius, @slices, @loops)
      GLU::DeleteQuadric(quad)
      GL::PopMatrix()
    end
  end

  #
  # OpenGL cylinder object
  #
  class FXGLCylinder < FXGLShape
    # Cylinder fidelity
    SLICES_NUMBER = 20
    STACKS_NUMBER = 20
    LOOPS = 4

    # Cylinder height [Float]
    attr_accessor :height
    
    # Cylinder radius [Float]
    attr_accessor :radius
    
    # Number of slices (default is 20) [Integer]
    attr_accessor :slices
    
    # Number of stacks (default is 20) [Integer]
    attr_accessor :stacks
    
    # Number of loops (default is 4) [Integer]
    attr_accessor :loops

    #
    # Returns an initialized FXGLCylinder instance.
    #
    # One option is to initialize the cylinder with a specified origin,
    # height and radius:
    #
    #     aCylinder = FXGLCylinder.new(x, y, z, h, r)
    #
    # If left unspecified, the height (_h_) and radius (_r_) default to 1.0.
    #
    # Another option is to initialize the cylinder with a specified origin,
    # height, radius and material:
    #
    #     aCylinder = FXGLCylinder.new(x, y, z, h, r, material)
    #
    # where the _material_ is an FXMaterial instance.
    #
    def initialize(*args)
      if args.length == 5
	super(args[0], args[1], args[2], SHADING_SMOOTH|STYLE_SURFACE)
      else
	super(args[0], args[1], args[2], SHADING_SMOOTH|STYLE_SURFACE,
              args[5], args[5])
      end
      @height = args[3] ? args[3] : 1.0
      @radius = args[4] ? args[4] : 1.0
      @slices = SLICES_NUMBER
      @stacks = STACKS_NUMBER
      @loops  = LOOPS
      setRange(FXRange.new(-@radius, @radius, 0, @height, -@radius, @radius))
    end
    
    #
    # Draw this cylinder into _viewer_ (an FXGLViewer instance).
    #
    def drawshape(viewer)
      quad = GLU::NewQuadric()
      GLU::QuadricDrawStyle(quad, GLU::FILL)
      GL::PushMatrix()
      GL::Rotate(-90, 1, 0, 0)
      GLU::Cylinder(quad, @radius, @radius, @height, @slices, @stacks)
      GLU::QuadricOrientation(quad, GLU::INSIDE)
      GLU::Disk(quad, 0, @radius, @slices, @loops)
      GL::Translate(0, 0, @height)
      GLU::QuadricOrientation(quad, GLU::OUTSIDE)
      GLU::Disk(quad, 0, @radius, @slices, @loops)
      GL::PopMatrix()
      GLU::DeleteQuadric(quad)
    end
  end

  #
  # OpenGL sphere object
  #
  class FXGLSphere < FXGLShape
    # Sphere fidelity
    SLICES_NUMBER = 20
    STACKS_NUMBER = 20

    # Sphere radius [Float]
    attr_accessor :radius
    
    # Number of slices (default is 20) [Integer]
    attr_accessor :slices
    
    # Number of stacks (default is 20) [Integer]
    attr_accessor :stacks

    #
    # Returns an initialized FXGLSphere instance.
    #
    # One option is to initialize the sphere with a specified origin and
    # radius:
    #
    #     aSphere = FXGLSphere.new(x, y, z, r)
    #
    # If left unspecified, the radius (_r_) defaults to 1.0.
    #
    # Another option is to initialize the sphere with a specified origin,
    # radius and material:
    #
    #     aSphere = FXGLSphere.new(x, y, z, r, material)
    #
    # where the _material_ is an FXMaterial instance.
    #
    def initialize(*args)
      if args.length == 4
	super(args[0], args[1], args[2], SHADING_SMOOTH|STYLE_SURFACE)
      else
	super(args[0], args[1], args[2], SHADING_SMOOTH|STYLE_SURFACE,
              args[4], args[4])
      end
      @radius = args[3] ? args[3] : 1.0
      @slices = SLICES_NUMBER
      @stacks = STACKS_NUMBER
      setRange(FXRange.new(-@radius, @radius, -@radius, @radius, -@radius, @radius))
    end
    
    #
    # Draw this sphere into _viewer_ (an FXGLViewer instance).
    #
    def drawshape(viewer)
      quad = GLU::NewQuadric()
      GLU::QuadricDrawStyle(quad, GLU::FILL)
      GLU::Sphere(quad, @radius, @slices, @stacks)
      GLU::DeleteQuadric(quad)
    end
  end
end
