
// Define a namespace for the DI3Dwebview module
var DI3Dwebview = DI3Dwebview || {};

// Dynamically include the required submodules of O3D
o3djs.require("o3djs.util");
o3djs.require("o3djs.rendergraph");
o3djs.require("o3djs.camera");
o3djs.require("o3djs.math");
o3djs.require("o3djs.pack");
o3djs.require("o3djs.scene");
o3djs.require("o3djs.arcball");
o3djs.require("o3djs.effect");
o3djs.require("o3djs.canvas");
o3djs.require("o3djs.loader");
o3djs.require("o3djs.error");



/////////////////////////////////////////////////////////////////////////////////////////////
/// Simple function call wrapper for converting O3D errors into exceptions
/////////////////////////////////////////////////////////////////////////////////////////////
var O3D_SAFE_CALL = function()
{
    // initialise the error list
    var errors = [];
    // setup the error callbacks
    for (var i = 0; i < O3D_SAFE_CALL.clients.length; i++) {
        O3D_SAFE_CALL.clients[i].setErrorCallback(function(msg) { errors.push(msg); });
    }

    // return a function that will check for errors
    return function(funcRetVal)
    {
        // clear the error callbacks
        for (var i = 0; i < O3D_SAFE_CALL.clients.length; i++) {
            O3D_SAFE_CALL.clients[i].clearErrorCallback();
        }
        // check for errors
        if (errors.length > 0)
            throw errors;
        else
            return funcRetVal;
    }
}
O3D_SAFE_CALL.clients = [];



/////////////////////////////////////////////////////////////////////////////////////////////
/// Constructor for the DI3Dwebview viewer object
///
/// @param id               The ID of the HTML <div> element for the viewer
/// @param onCreated        Function to be called when the viewer object has been created. Is
///                         called with a value of true if created successfully and false if not
/// @param onPluginFailed   Function to be called if the O3D plugin failed to initialise
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.DI3Dwebview = function(id, onCreated, onPluginFailed)
{
    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Load a model into the viewer
    ///
    /// @param url          The URL of the model to load
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.Load = function(url)
    {
        // unload any existing model
        if (m_model != null) {
            m_model.Destroy();
            m_model = null;
            m_modelShaderDiffuse = null;
            m_modelShaderTexture = null;
            m_modelShaderDiffuseTexture = null;
        }

        // create a new model loader - this starts loading on construction
        m_splash.ClearText();
        m_modelLoader = new DI3Dwebview.ModelLoader(m_o3dClient, url, onModelLoaded, onModelLoadProgress);
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Destroys the viewer
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.Destroy = function()
    {
        m_o3dClient.clearRenderCallback();
        m_o3dClient.cleanup();
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Enumeration for the rendering mode
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.RenderMode = {
        Texture : "Texture",
        Diffuse : "Shading",
        DiffuseTexture : "DiffuseTexture"
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Set the render mode for the viewer
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.SetRenderMode = function(renderMode)
    {
        m_renderMode = renderMode;
        if (m_model != null) {
            if (m_renderMode == this.RenderMode.DiffuseTexture)
                m_model.ApplyShader(m_modelShaderDiffuseTexture);
            else if (m_renderMode == this.RenderMode.Diffuse)
                m_model.ApplyShader(m_modelShaderDiffuse);
            else if (m_renderMode == this.RenderMode.Texture)
                m_model.ApplyShader(m_modelShaderTexture);
            else {
                alert("Invalid render mode");
                m_model.ApplyShader(m_modelShaderTexture);
            }
        }
    };
    
    //
    // private members - note that in javascript, the constructor arguments are stored as private members
    // these members are accessible from the private functions nested within this constructor
    //
    var m_inst = this;                          ///< copy of "this" for use inside private functions
    var m_htmlElement = null;                   ///< the encapsulating HTML element for this viewer
    var m_o3dClient = null;                     ///< the O3D client object for this viewer

    var m_viewPack = null;                      ///< O3D pack to manage resources for the rendergraph view
    var m_view = null;                          ///< O3D view object for this viewer 
    var m_camera = null;                        ///< DI3Dwebview.Camera for this viewer
    var m_ambLight = null;                      ///< The ambient lighting in the scene
    var m_ptLight = null;                       ///< The point light in the scene
    var m_renderMode = this.RenderMode.Texture; ///< The current render mode for the viewer

    var m_hudView = null;                       ///< The HUD view object for 2D drawing
    var m_splash = null;                        ///< The 2D text box used as a splash screen
    var m_hud = null;                           ///< The HUD object containing all UI widgets

    var m_shaderDiffuse = null;                 ///< Shader object for showing lighting
    var m_shaderTexture = null;                 ///< Shader object for showing textures
    var m_shaderDiffTexture = null;             ///< Shader object for showing textures and lighting

    var m_model = null;                         ///< The current loaded model
    var m_modelLoader = null;                   ///< The object currently loading a model
    var m_modelShaderDiffuse = null;            ///< Current model shader for lighting
    var m_modelShaderTexture = null;            ///< Current model shader for textures
    var m_modelShaderDiffuseTexture = null;     ///< Current model shader for textures and lighting

    var m_currentCtrler = null;                 ///< the current active controller for mouse actions
    var m_rotateCtrler = null;                  ///< mouse controller to handle camera rotation
    var m_panCtrler = null;                     ///< mouse controller to handle camera panning
    var m_zoomCtrler = null;                    ///< mouse controller to handle camera zooming


    // define a private callback function to be called when creation is complete
    function makeClientsDone(o3dElements)
    {
        try {
           // get the created O3D element
            var elem = o3dElements[0];

            // get some objects from the O3D element
            m_htmlElement = elem.parentNode;
            m_o3dClient = elem.client;
            // add this client to the global safe call handler
            O3D_SAFE_CALL.clients.push(m_o3dClient);

            // create a pack to manage viewer resources
            m_viewPack = m_o3dClient.createPack();

            // create the render graph
            m_view = o3djs.rendergraph.createBasicView(m_viewPack, m_o3dClient.root,
                                                       m_o3dClient.renderGraphRoot,
                                                       [0.235, 0.235, 0.235, 1.0]);

            // create the 2D HUD view and the splash screen
            m_hudView = new DI3Dwebview.HUDview(m_o3dClient, m_view);
            m_splash = new DI3Dwebview.HUDTextBox(m_hudView, 0, 0, 0, m_o3dClient.width, m_o3dClient.height);
            m_splash.SetText("Loading... Please Wait...");

            // create the lights and camera
            m_camera = new DI3Dwebview.Camera(m_view, m_o3dClient.width, m_o3dClient.height);
            m_ambLight = new DI3Dwebview.AmbientLight();
            m_ptLight = new DI3Dwebview.PointLight();

            // create controllers for the viewer
            m_rotateCtrler = new DI3Dwebview.RotateController(m_camera);
            m_panCtrler = new DI3Dwebview.PanController(m_camera);
            m_zoomCtrler = new DI3Dwebview.ZoomController(m_camera);

            // create shaders for rendering
            m_shaderDiffuse = new DI3Dwebview.ShaderDiffuse(m_viewPack);
            m_shaderTexture = new DI3Dwebview.ShaderTexture(m_viewPack);
            m_shaderDiffTexture = new DI3Dwebview.ShaderDiffuseTexture(m_viewPack);

            // finally, create the HUD - finish remaining initialisation when the HUD has
            // been created
            m_hud = new DI3Dwebview.HUD(m_hudView, onHUDCreated, onChangeRenderMode);
        }
        catch (exception) {
            onCreated(false);
        }
    }

    // callback function called when the HUD has been created
    function onHUDCreated(success)
    {
        if (success) {
            // initialise event handlers for the viewer
            m_o3dClient.setEventCallback("mousedown", onMouseDown);
            m_o3dClient.setEventCallback("mousemove", onMouseMove);
            m_o3dClient.setEventCallback("mouseup", onMouseUp);
            m_o3dClient.setEventCallback("wheel", onMouseWheel);
            m_o3dClient.setRenderCallback(onRender);
            m_splash.ClearText();
        }

        // call the callback to send notification that the viewer has been created, successfully or not
        onCreated(success);
    }

    // callback function called when a model has been loaded
    function onModelLoaded(modelLoader, exception) {
        // a different ModelLoader finished - just destroy it
        if (modelLoader != m_modelLoader) {
            modelLoader.Destroy();
            return;
        }
        
        // handle the exception
        if (exception) {
            // display a message
            m_splash.SetText("There was a problem downloading this model");
            m_modelLoader = null;
            return;
        }

        // create the model and shaders for it
        m_model = new DI3Dwebview.Model(modelLoader, m_o3dClient.root, m_view);
        m_modelShaderDiffuse = m_shaderDiffuse.CreateInstance(m_ambLight, m_ptLight);
        m_modelShaderTexture = m_shaderTexture.CreateInstance(m_model.textureSampler);
        m_modelShaderDiffuseTexture = m_shaderDiffTexture.CreateInstance(m_ambLight, m_ptLight, m_model.textureSampler);
        // setting the render mode to its current value will apply the current shader to the model
        m_inst.SetRenderMode(m_renderMode);
        // finished with the loader
        m_modelLoader = null;

        // reset the camera and make sure the current loaded model is in fov 
        m_camera.Reset();
        // get bounding box of the scene
        var bBox = o3djs.util.getBoundingBoxOfTree(m_o3dClient.root);
        // set camera target as centre of bounding box
        m_camera.target = o3djs.math.lerpVector(bBox.minExtent, bBox.maxExtent, 0.5);
        // calculate radius of bounding sphere
        var radius = o3djs.math.distance(bBox.minExtent, bBox.maxExtent) * 0.5;
        // calculate the focal length of the camera so that the bounding sphere fits the frustum
        m_camera.focalLength = radius / Math.sin(m_camera.fov * 0.5);
        m_camera.nearZ = 0.001;
        m_camera.farZ = (m_camera.focalLength + radius) * 10.0;
        m_camera.Update();
    }

    // callback function called during rendering
    function onRender() {
        // update loading progress
        if (m_modelLoader != null) {
            m_modelLoader.Update();
        }

        // update lighting
        m_ptLight.position = m_camera.GetPosition();

        // update the material parameters in the current model shader
        if (m_model != null) {
            m_model.UpdateShaderMaterials();
        }
    }

    // callback to handle mouse down events
    function onMouseDown(event)
    {
        m_currentCtrler = null;

        // pass to the HUD first to handle UI interaction
        if (m_hud.OnMouseDown(event))
            return;

        // no HUD interaction - 3D camera control
        if (event.button == o3djs.base.o3d.Event.BUTTON_LEFT) {
            m_currentCtrler = m_rotateCtrler;
        }
        else if (event.button == o3djs.base.o3d.Event.BUTTON_RIGHT) {
            m_currentCtrler = m_panCtrler;
        }
        if (m_currentCtrler != null) {
            m_currentCtrler.OnMouseDown(event);
        }
    }

    // callback to handle mouse move events
    function onMouseMove(event) {
        if (m_currentCtrler != null) {
            m_currentCtrler.OnMouseMove(event);
        }
        else {
            // no current controller is active - pass the mouse moves to the HUD
            m_hud.OnMouseMoveInactive(event);
        }
    }

    // callback to handle mouse up events
    function onMouseUp(event) {
        if (m_currentCtrler != null) {
            m_currentCtrler.OnMouseUp(event);
            m_currentCtrler = null;
        }
    }

    // callback to handle mouse wheel events
    function onMouseWheel(event) {
        m_zoomCtrler.OnMouseWheel(event);
    }

    // callback function for loading progress
    function onModelLoadProgress(progInfo)
    {
        if (progInfo != null)
            m_splash.SetText("Loading... " + progInfo.percent + "%");
        else
            m_splash.ClearText();
    }

    // callback function for changing render mode
    function onChangeRenderMode()
    {
        var mode = m_hud.GetRenderModeButtonState();
        if (mode == 0) {
            m_inst.SetRenderMode(m_inst.RenderMode.Texture);
        }
        else if (mode == 1) {
            m_inst.SetRenderMode(m_inst.RenderMode.Diffuse);
        }
        else if (mode == 2) {
            m_inst.SetRenderMode(m_inst.RenderMode.DiffuseTexture);
        }
    }

    // create the O3D element for this HTML id
    o3djs.util.makeClients(makeClientsDone, "LargeGeometry", undefined, onPluginFailed, id);
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Class for loading models
///
/// @param o3dClient    The O3D client loading the model
/// @param url          The URL of the model to load
/// @param onLoaded     Callback function to be called when loading has finished. Has the
///                     ModelLoader instance and an exception (which can be null) as
///                     arguments.
/// @param onProgress   Callback function to be called when loading progress is updated.
///                     Has the progress info object returned from
///                     o3djs.io.LoadInfo.getKnownProgressInfoSoFar() as argument or null
///                     to signal loading has finished.
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.ModelLoader = function(o3dClient, url, onLoaded, onProgress)
{
    // public members
    this.pack = o3dClient.createPack();                 ///< O3D pack to manage the resources
    this.root = this.pack.createObject("Transform");    ///< O3D transform to act as the model root

    // private members
    var m_inst = this;
    var m_loadPercent = -1;
    var m_loadInfo = o3djs.scene.loadScene(o3dClient, this.pack, this.root, url, loadFinished);

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Update the loading progress
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.Update = function()
    {
        var progInfo = m_loadInfo.getKnownProgressInfoSoFar();
        if (progInfo.percent != m_loadPercent)
        {
            m_loadPercent = progInfo.percent;
            onProgress(progInfo);
        }
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Destroy the loader. This will discard the loaded model resources and, so, should
    /// only be called when this is desired.
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.Destroy = function()
    {
        this.root = null;
        this.pack.destroy();
        this.pack = null;
    };

    // callback to handle the end of load event 
    function loadFinished(pack, parent, exception)
    {
        if (exception) {
            m_inst.Destroy();
        }
        onProgress(null);
        onLoaded(m_inst, exception);
    };
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Class constructor for a loaded model
///
/// @param modelLoader  The ModelLoader object used to load the model resources
/// @param parent       The transform node that is to be the parent of the model
/// @param view         The view that the model will be rendered in. Required for the
///                     call to o3djs.pack.preparePack()
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.Model = function(modelLoader, parent, view)
{
    // public members
    this.textureSampler = null;         ///< The single texture sampler for this model

    // private members
    var m_pack = modelLoader.pack;      ///< The pack that manages the model's resources
    var m_root = modelLoader.root;      ///< The root transform node of the model
    var m_currShader = null;            ///< The current ModelShader used for rendering

    // prepare the pack for display
    o3djs.pack.preparePack(m_pack, view);

    // attach model to root of hierarchy
    m_root.parent = parent;

    // create a texture sampler for the first texture
    var textures = m_pack.getObjectsByClassName("o3d.Texture");
    if (textures.length > 0) {
        this.textureSampler = m_pack.createObject("Sampler");
        this.textureSampler.texture = textures[0];
        // disable mip-mapping
        this.textureSampler.mipFilter = o3djs.base.o3d.Sampler.NONE;
    }

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Destroy the model
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.Destroy = function()
    {
        // disconnect the model root from the client root
        m_root.parent = null;
        // clear root reference, so the pack has the only reference left
        m_root = null;

        // clean up texture sampler
        if (this.textureSampler != null) {
            m_pack.removeObject(this.textureSampler);
            this.textureSampler = null;
        }

        // destroy the pack
        m_pack.destroy();
        m_pack = null;
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Apply the given shader to all materials in the model
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.ApplyShader = function(shader)
    {
        var mats = m_pack.getObjectsByClassName("o3d.Material");
        for (var i=0 ; i<mats.length ; i++) {
            shader.Apply(mats[i]);
        }
        m_currShader = shader;
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Update all materials in the model with the current values of the shader parameters
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.UpdateShaderMaterials = function()
    {
        if (m_currShader != null) {
            var mats = m_pack.getObjectsByClassName("o3d.Material");
            for (var i=0 ; i<mats.length ; i++) {
                m_currShader.Update(mats[i]);
            }
        }
    };
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Class constructor for information about the viewer camera
///
/// @param view     The O3D view that the camera is for
/// @param width    The width of the view in pixels
/// @param height   The height of the view in pixels
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.Camera = function(view, width, height)
{
    this.view = view;       ///< The O3D view the camera is for
    this.width = width;     ///< The pixel width of the view
    this.height = height;   ///< The pixel height of the view

    // define and initialise members for calculating the view matrix
    this.target = [0, 0, 0];    ///< The world space point the camera looks at
    this.focalLength = 10;      ///< The distance between the camera and target point
    this.rotationMatrix = o3djs.math.matrix4.identity();    ///< The rotation component of the world transformation matrix 
    this.viewMatrix = o3djs.math.matrix4.identity();        ///< The world-to-view transformation matrix

    // define and initialise members for calculating the projection matrix
    this.nearZ = 0.1;                   ///< The Z depth of the camera's near clipping plane 
    this.farZ = 100;                    ///< The Z depth of the camera's far clipping plane
    this.fov = o3djs.math.degToRad(45); ///< The camera's field of view 
    this.projMatrix = o3djs.math.matrix4.perspective(this.fov,
                                                     this.width/this.height,
                                                     this.nearZ, this.farZ);    ///< The camera projection transform
    this.Update();
};

/////////////////////////////////////////////////////////////////////////////////////////////
/// Reset all camera parameters to their defaults
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.Camera.prototype.Reset = function()
{
    this.target = [0, 0, 0];
    this.focalLength = 10;
    this.rotationMatrix = o3djs.math.matrix4.identity();
    this.nearZ = 0.1;
    this.farZ = 100;
    this.fov = o3djs.math.degToRad(45);
    this.Update();
};

/////////////////////////////////////////////////////////////////////////////////////////////
/// Update the camera view and projection matrices from the current parameters
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.Camera.prototype.Update = function()
{
    // camera looks along negative z-axis - create matrix to move it back by the
    // focal length
    var focalMatrix = o3djs.math.matrix4.translation([0, 0, this.focalLength]);
    // apply the current camera rotation
    var tmpMatrix = o3djs.math.mulMatrixMatrix4(focalMatrix, this.rotationMatrix);
    // create matrix to translate the camera so it looks at the target
    var targetMatrix = o3djs.math.matrix4.translation(this.target);
    // create the world transformation matrix for the camera
    var worldMatrix = o3djs.math.mulMatrixMatrix4(tmpMatrix, targetMatrix);
    // now invert the world matrix to get the view matrix
    this.viewMatrix = o3djs.math.inverse4(worldMatrix);

    // generate the projection matrix
    this.projMatrix = o3djs.math.matrix4.perspective(this.fov,
                                                     this.width/this.height,
                                                     this.nearZ, this.farZ);
    // update the drawing context
    this.view.drawContext.view = this.viewMatrix;
    this.view.drawContext.projection = this.projMatrix;
};

/////////////////////////////////////////////////////////////////////////////////////////////
/// Get the current world space position of the camera
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.Camera.prototype.GetPosition = function()
{
    var worldMatrix = o3djs.math.inverse4(this.viewMatrix);
    return o3djs.math.matrix4.transformPoint(worldMatrix, [0, 0, 0]);
};

/////////////////////////////////////////////////////////////////////////////////////////////
/// Get the current world space direction that the camera is pointing
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.Camera.prototype.GetDirection = function()
{
    var worldMatrix = o3djs.math.inverse4(this.viewMatrix);
    return o3djs.math.matrix4.transformDirection(worldMatrix, [0, 0, -1]);
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Some simple classes defining light parameters
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.PointLight = function()
{
    this.position = [0, 0, 0];          ///< The world space position of the light
    this.colour = [0.7, 0.7, 0.7, 1];   ///< The colour of the light
    this.intensity = [1, 1, 1, 1];      ///< The intensity of the light
};

DI3Dwebview.AmbientLight = function()
{
    this.colour = [1, 1, 1, 1];             ///< The colour of the light
    this.intensity = [0.2, 0.2, 0.2, 1];    ///< The intensity of the light
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Controller class constructor to handle rotation of the camera by the mouse
///
/// @param camera   The camera object to control
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.RotateController = function(camera)
{
    this.camera = camera;
    this.arcball = o3djs.arcball.create(camera.width, camera.height);
    this.initialMatrix = o3djs.math.matrix4.identity();
};

DI3Dwebview.RotateController.prototype.OnMouseDown = function(event)
{
    // initialise the arcball with the coordinates of the mouse click
    this.arcball.click([event.x, event.y]);
    // store the initial rotation matrix for the camera
    this.initialMatrix = this.camera.rotationMatrix;
};

DI3Dwebview.RotateController.prototype.OnMouseMove = function(event)
{
    // get the arcball quaternion corresponding to the new mouse point and convert to matrix
    var arcballQuat = this.arcball.drag([event.x, event.y]);
    var arcballMatrix = o3djs.quaternions.quaternionToRotation(arcballQuat);
    // need to invert the matrix
    var invArcballMatrix = o3djs.math.matrix4.inverse(arcballMatrix);
    // calculate the new camera rotation matrix and update
    this.camera.rotationMatrix = o3djs.math.mulMatrixMatrix4(invArcballMatrix, this.initialMatrix);
    this.camera.Update();
};

DI3Dwebview.RotateController.prototype.OnMouseUp = function(event)
{
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Controller class constructor to handle panninng of the camera by the mouse
///
/// @param camera   The camera object to control
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.PanController = function(camera)
{
    this.camera = camera;
    this.prevX = 0;
    this.prevY = 0;
};

DI3Dwebview.PanController.prototype.OnMouseDown = function(event)
{
    this.prevX = event.x;
    this.prevY = event.y;
};

DI3Dwebview.PanController.prototype.OnMouseMove = function(event)
{
    // calculate change in mouse position
    var dX = this.prevX - event.x;
    var dY = this.prevY - event.y;
    this.prevX = event.x;
    this.prevY = event.y;

    // calculate change in position in 3D camera space in the focal plane
    var tanFov = Math.tan(this.camera.fov / 2);
    var deltaX = dX * this.camera.focalLength * tanFov / (this.camera.width / 2);
    var deltaY = -dY * this.camera.focalLength * tanFov / (this.camera.height / 2);

    // calculate the camera x & y-axes in world space
    var xAxis = o3djs.math.matrix4.transformDirection(this.camera.rotationMatrix, [1, 0, 0]);
    var yAxis = o3djs.math.matrix4.transformDirection(this.camera.rotationMatrix, [0, 1, 0]);

    // scale the world space x & y-axes to get the movement vectors
    var xAxisScaled = o3djs.math.mulScalarVector(deltaX, xAxis);
    var yAxisScaled = o3djs.math.mulScalarVector(deltaY, yAxis);
    var vec = o3djs.math.addVector(this.camera.target, xAxisScaled);
    this.camera.target = o3djs.math.addVector(vec, yAxisScaled);
    this.camera.Update();
};

DI3Dwebview.PanController.prototype.OnMouseUp = function(event)
{
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Controller class constructor to handle zooming of the camera by the mouse
///
/// @param camera   The camera object to control
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.ZoomController = function(camera)
{
    this.camera = camera;
};

DI3Dwebview.ZoomController.prototype.OnMouseWheel = function(event)
{
    var wheel = Math.log(this.camera.focalLength);
    if (event.deltaY < 0) {
        wheel += 0.1;
    }
    else {
        wheel -= 0.1;
    }
    this.camera.focalLength = Math.exp(wheel);
    this.camera.Update();
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Helper functions for shaders
/////////////////////////////////////////////////////////////////////////////////////////////
function LoadShader(pack, name)
{
    var shader = pack.createObject("Effect");
    O3D_SAFE_CALL()(o3djs.effect.loadEffect(shader, "../shaders/" + name + ".shader"));
    return shader;
}

function SetParam(material, name, value)
{
    var param = material.getParam(name);
    param.value = value;
}



/////////////////////////////////////////////////////////////////////////////////////////////
/// Class constructor for a shader that is to be applied to a model
///
/// @param  shader  The O3D effect for the shader
/// @param  params  Object defining the parameters for this shader
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.ModelShader = function(shader, params)
{
    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Apply this shader to the given material
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.Apply = function(material) {
        shader.createUniformParameters(material);
        material.effect = shader;
        params.Update(material);
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Update the material parameters for the given material
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.Update = function(material) {
        params.Update(material);
    };
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Shader definition:
///     - diffuse point light and ambient light
///     - no texture
///     - no material or vertex colour
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.ShaderDiffuse = function(pack)
{
    var m_shader = LoadShader(pack ,"diffuse");

    this.CreateInstance = function(ambLight, ptLight)
    {
        return new DI3Dwebview.ModelShader(m_shader, new Params(ambLight, ptLight));
    };

    function Params(ambLight, ptLight)
    {
        this.Update = function(material)
        {
            SetParam(material, "ambientIntensity", ambLight.intensity);
            SetParam(material, "ambient", ambLight.colour);
            SetParam(material, "lightIntensity", ptLight.intensity);
            SetParam(material, "diffuse", ptLight.colour);
            SetParam(material, "lightWorldPos", ptLight.position);
        };
    }
};

/////////////////////////////////////////////////////////////////////////////////////////////
/// Shader definition:
///     - 1 texture
///     - no lighting
///     - no material or vertex colour
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.ShaderTexture = function(pack)
{
    var m_shader = LoadShader(pack ,"texture");

    this.CreateInstance = function(texSampler)
    {
        return new DI3Dwebview.ModelShader(m_shader, new Params(texSampler));
    };

    function Params(texSampler)
    {
        this.Update = function(material)
        {
            SetParam(material, "texSampler0", texSampler);
        };
    }
};

/////////////////////////////////////////////////////////////////////////////////////////////
/// Shader definition:
///     - diffuse point light and ambient light
///     - 1 texture
///     - no material or vertex colour
///     - lighting and texture are multiplied
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.ShaderDiffuseTexture = function(pack)
{
    var m_shader = LoadShader(pack ,"diffuse-texture");

    this.CreateInstance = function(ambLight, ptLight, texSampler)
    {
        return new DI3Dwebview.ModelShader(m_shader, new Params(ambLight, ptLight, texSampler));
    };

    function Params(ambLight, ptLight, texSampler)
    {
        this.Update = function(material)
        {
            SetParam(material, "ambientIntensity", ambLight.intensity);
            SetParam(material, "ambient", ambLight.colour);
            SetParam(material, "lightIntensity", ptLight.intensity);
            SetParam(material, "diffuse", ptLight.colour);
            SetParam(material, "lightWorldPos", ptLight.position);
            SetParam(material, "texSampler0", texSampler);
        };
    }
};




/////////////////////////////////////////////////////////////////////////////////////////////
/// Class constructor for the HUD view. This initialises the orthographic view required
/// for rendering the HUD along with the camera matrices for rendering.
///
/// @param o3dClient            The O3D client object for the viewer
/// @param view3D               The O3D view object for the main 3D view
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.HUDview = function(o3dClient, view3D)
{
    // create a pack to manage the HUD
    this.pack = o3dClient.createPack();
    // create a root node for the HUD
    this.root = this.pack.createObject("Transform");

    // create a separate view for the HUD
    this.view = o3djs.rendergraph.createBasicView(this.pack, this.root, o3dClient.renderGraphRoot);
    // set the priority of the HUD view to be drawn after the 3D scene
    this.view.root.priority = view3D.root.priority + 1;
    // set the view to **not** clear the back buffer or it will erase the 3D scene, however,
    // let it clear the depth and stencil so it does not interact with anything in the 3D scene
    this.view.clearBuffer.clearColorFlag = false;
    // disable culling and writing to the depth buffer
    this.view.zOrderedState.getStateParam("CullMode").value = o3djs.base.o3d.State.CULL_NONE;
    this.view.zOrderedState.getStateParam("ZWriteEnable").value = false;

    // create an orthographic projection matrix for the view - same size as client view in x & y
    // but with near and far clipping at distances of 0.5 & 1.5.
    this.view.drawContext.projection = o3djs.math.matrix4.orthographic(0+0.5, o3dClient.width+0.5, o3dClient.height+0.5, 0+0.5, 0.5, 1.5);
    // the view matrix is for a camera at [0,0,1] looking at the origin (i.e. along the -ve z-axis)
    // this means that the Z range of our scene should be in the range (-0.5, 0.5)
    this.view.drawContext.view = o3djs.math.matrix4.lookAt([0, 0, 1], [0, 0, 0], [0, 1, 0]);
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Simple class for a HUD based text box object.
///
/// @param hudView  The HUDview object to render this text box
/// @param x        The x-coordinate of the top left corner of the box
/// @param y        The y-coordinate of the top left corner of the box
/// @param z        The z-coordinate of the top left corner of the box
/// @param width    The width of the box
/// @param height   The height of the box
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.HUDTextBox = function(hudView, x, y, z, width, height)
{
    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Set the text in the text box. This overwrites any existing text.
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.SetText = function(text)
    {
        m_textQuad.canvas.clear([0, 0, 0, 0]);
        m_textQuad.canvas.drawText(text, width/2, height/2, m_canvasPaint);
        m_textQuad.updateTexture();
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Clear the text.
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.ClearText = function()
    {
        m_textQuad.canvas.clear([0, 0, 0, 0]);
        m_textQuad.updateTexture();
    };

    // create the canvas objects for rendering text
    var m_canvasPaint = hudView.pack.createObject("CanvasPaint");
    m_canvasPaint.color = [0.6, 0.6, 0.6, 1];
    m_canvasPaint.textSize = 15;
    m_canvasPaint.textTypeface = "Arial";//, Helvetica, sans-serif";
    m_canvasPaint.textAlign = o3djs.base.o3d.CanvasPaint.CENTER;
    m_canvasPaint.shader = null;

    var m_canvasLib = o3djs.canvas.create(hudView.pack, hudView.root, hudView.view);
    var m_textQuad = m_canvasLib.createXYQuad(x, y, z, width, height, true);
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Class constructor for the viewer's HUD. This creates all UI elements in the HUD
///
/// @param hudView              The HUDview object to render the HUD
/// @param onCreated            Function to be called when the HUD has been created. The argument
///                             will be true if successful, false if not.
/// @param onChangeRenderMode   Function to be called when the state of the render mode
///                             radio button changes
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.HUD = function(hudView, onCreated, onChangeRenderMode)
{
    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Get the current rendermode button state.
    ///
    /// @return 0 - texture only
    ///         1 - shading only
    ///         2 - texture and shading
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.GetRenderModeButtonState = function()
    {
        return m_renderModeButton.value;
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Mouse down event handler for the HUD elements. This returns true if the HUD has handled
    /// the event, false if not.
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.OnMouseDown = function(event)
    {
        // check against buttons
        if (m_renderModeButton.OnMouseDown(event))
            return true;
        return false;
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Mouse move event handler for the HUD elements. This is called when "Inactive" mouse move
    /// events occur i.e. when no mouse button is clicked.
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.OnMouseMoveInactive = function(event)
    {
        // update the hovering status of each button
        for (var i = 0; i < m_buttons.length; i++) {
            m_buttons[i].SetHover( m_buttons[i].HitTest(event.x, event.y) );
        }
    };

    // create shader for use by billboards
    var m_shader = new DI3Dwebview.ShaderTexture(hudView.pack);

    // define the members for the buttons, but don't create until images have loaded
    var m_textures = new Array();
    var m_buttons = new Array();
    var m_renderModeButton = null;

    // finally, load the texture images and create the buttons in the onFinished handler
    var m_loader = o3djs.loader.createLoader(onLoaded);
    loadTexture("../images/ViewerControls/texture_off.jpg", 0);
    loadTexture("../images/ViewerControls/texture_on.jpg", 1);
    loadTexture("../images/ViewerControls/shading_off.jpg", 2);
    loadTexture("../images/ViewerControls/shading_on.jpg", 3);
    loadTexture("../images/ViewerControls/textureshading_off.jpg", 4);
    loadTexture("../images/ViewerControls/textureshading_on.jpg", 5);
    loadTexture("../images/ViewerControls/hover.png", 6);
    m_loader.finish();

    // private function to asynchronously load a texture
    function loadTexture(uri, index)
    {
        m_loader.loadTexture(hudView.pack, uri,
            function(texture, exception) {
                m_textures[index] = exception ? null : texture;
            });
    }

    // callback function called when all textures have finished loading (successfully or not)
    function onLoaded()
    {
        // check that all textures loaded
        for (var i = 0; i < m_textures.length; i++) {
            if (m_textures[i] == null) {
                onCreated(false);
                return;
            }
        }

        // create the billboards
        var billboards = new Array();
        for (var i = 0; i < m_textures.length; i++) {
            billboards[i] = new DI3Dwebview.Billboard(hudView.pack, m_textures[i], hudView.view, m_shader);
        }
        // create rendermode buttons
        m_buttons[0] = new DI3Dwebview.Button(hudView.pack, hudView.root, billboards[0], billboards[1], billboards[6]);
        m_buttons[1] = new DI3Dwebview.Button(hudView.pack, hudView.root, billboards[2], billboards[3], billboards[6]);
        m_buttons[2] = new DI3Dwebview.Button(hudView.pack, hudView.root, billboards[4], billboards[5], billboards[6]);
        m_buttons[0].SetPosition(1000, 10, 0);
        m_buttons[1].SetPosition(1000, 40, 0);
        m_buttons[2].SetPosition(1000, 70, 0);
        m_renderModeButton = new DI3Dwebview.RadioButton(m_buttons, 0, onChangeRenderMode);

        // the HUD has now been created
        onCreated(true);
    }
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Class constructor for a simple billboard i.e. a rectangular piece of geometry with
/// a texture
///
/// @param pack     O3D pack used to manage the texture resources
/// @param texture  The texture for the billboard
/// @param view     The O3D view in which the billboard is rendered in
/// @param shader   Shader used to create ModelShader instance for the billboard material
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.Billboard = function(pack, texture, view, shader)
{
    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Create a new instance of the billboard
    ///
    /// @param parent   The parent transform of the instance to create
    /// @return An O3D transform that has the billboard added as a shape
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.CreateInstance = function(parent)
    {
        var xform = pack.createObject("Transform");
        xform.parent = parent;
        xform.addShape(m_geometry);
        return xform;
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Perform a hit test against the billboard. It is assumed that the billboard is rendered
    /// with an orthographic camera such that the mouse position (x,y) is equivalent to the
    /// 3D world position in the XY plane i.e. no transformation of the mouse point is required.
    ///
    /// @param x    The x position of the mouse
    /// @param y    The y position of the mouse
    /// @param inst The transform for the instance of the billboard
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.HitTest = function(x, y, inst)
    {
        var pos = o3djs.math.matrix4.transformPoint(inst.worldMatrix, [0, 0, 0]);
        return (x >= pos[0] && x < pos[0] + texture.width &&
                y >= pos[1] && y < pos[1] + texture.height);
    };

    // create the texture sampler
    var m_textureSampler = pack.createObject("Sampler");
    m_textureSampler.texture = texture;
    m_textureSampler.addressModeU = o3djs.base.o3d.Sampler.CLAMP;
    m_textureSampler.addressModeV = o3djs.base.o3d.Sampler.CLAMP;
    m_textureSampler.mipFilter = o3djs.base.o3d.Sampler.NONE;

    // create the material and set its draw list so it can be alpha blended
    var m_material = pack.createObject("Material");
    m_material.drawList = view.zOrderedDrawList;

    // create an instance of the shader
    var m_shader = shader.CreateInstance(m_textureSampler);
    m_shader.Apply(m_material);

    // create the geometry - create in XZ plane by default, so add a rotation to make
    // it in the XY plane. Also, it is created with its origin at the centre of the
    // plane, so add a translation to put the origin at the top left
    var posXform = o3djs.math.matrix4.translation([texture.width/2, 0, texture.height/2]);
    var rotXform = o3djs.math.matrix4.rotationX(-Math.PI * 0.5);
    var xform = o3djs.math.mulMatrixMatrix4(posXform, rotXform);
    var m_geometry = o3djs.primitives.createPlane(pack, m_material,
                                                  texture.width, texture.height, 1, 1, xform);
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Class constructor for a simple button with on/off state
///
/// @param pack     O3D pack used to manage resources
/// @param parent   The parent O3D transform for this button
/// @param bbOff    The billboard representing the "off" state of the button
/// @param bbOn     The billboard representing the "on" state of the button
/// @param bbHover  The billboard representing the "hover" state of the button
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.Button = function(pack, parent, bbOff, bbOn, bbHover)
{
    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Set the position of the button
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.SetPosition = function(x, y, z)
    {
        m_root.identity();
        m_root.translate(x, y, z);
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Perform a hit test against the button. Returns true if hit, false if not.
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.HitTest = function(x, y)
    {
        if (m_bbInstOff.visible) {
            return bbOff.HitTest(x, y, m_bbInstOff);
        }
        else {
            return bbOn.HitTest(x, y, m_bbInstOn);
        }
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Set the button to be "on" or "off"
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.SetOn = function(on)
    {
        m_bbInstOff.visible = !on;
        m_bbInstOn.visible = on;
    };

    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Set the button to be in its hover state (or not)
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.SetHover = function(hover)
    {
        m_bbInstHover.visible = hover;
    };

    // create the root transform for the button
    var m_root = pack.createObject("Transform");
    m_root.parent = parent;

    // create instances of the billboards
    var m_bbInstOff = bbOff.CreateInstance(m_root);
    var m_bbInstOn = bbOn.CreateInstance(m_root);
    var m_bbInstHover = bbHover.CreateInstance(m_root);
    m_bbInstHover.translate(0, 0, 0.1);

    // initialise the button state to "off"
    this.SetOn(false);
    this.SetHover(false);
};



/////////////////////////////////////////////////////////////////////////////////////////////
/// Class constructor for a radio button.
///
/// @param buttons      List of individual buttons in this radio button
/// @param initialValue The index of the initially selected button
/// @param onChange     Function to be called when the selected value of the button changes
/////////////////////////////////////////////////////////////////////////////////////////////
DI3Dwebview.RadioButton = function(buttons, initialValue, onChange)
{
    /////////////////////////////////////////////////////////////////////////////////////////////
    /// Mouse down event handler for the radio button. Returns true if the event was handled
    /// and false if not. The event is handled if any of the individual buttons are hit. The
    /// onChange callback specified in the constructor is called if the event causes the
    /// value of this.value to change.
    /////////////////////////////////////////////////////////////////////////////////////////////
    this.OnMouseDown = function(event)
    {
        // check for a hit against each button
        for (var i=0 ; i<buttons.length ; i++) {
            if (buttons[i].HitTest(event.x, event.y)) {
                // this button was hit
                if (i != this.value) {
                    // change the state of the button
                    buttons[this.value].SetOn(false);
                    buttons[i].SetOn(true);
                    this.value = i;
                    onChange();
                }
                return true;
            }
        }
        // no buttons were hit
        return false;
    }

    // set the initial value
    this.value = initialValue;

    // initialise the buttons to the initial value if selected
    for (var i=0 ; i<buttons.length ; i++) {
        buttons[i].SetOn(false);
    }
    buttons[this.value].SetOn(true);
};







