/**
 * FORGE.BackgroundRenderer
 * BackgroundRenderer class.
 *
 * @constructor FORGE.BackgroundRenderer
 * @param {FORGE.Viewer} viewer - viewer reference
 * @param {THREE.WebGLRenderTarget} target - render target
 * @param {SceneMediaOptionsConfig} options - the options for the cubemap
 * @param {string=} type - The type of the object as long as many other object inherits from this one.
 * @extends {FORGE.BaseObject}
 */
FORGE.BackgroundRenderer = function(viewer, target, options, type)
{
    /**
     * @name FORGE.BackgroundRenderer#_viewer
     * @type {FORGE.Viewer}
     * @private
     */
    this._viewer = viewer;

    /**
     * @name FORGE.BackgroundRenderer#_canvas
     * @type {FORGE.Canvas}
     * @private
     */
    this._canvas = null;

    /**
     * The mesh (cube) the video is on.
     * @type {THREE.Mesh}
     * @private
     */
    this._mesh = null;

    /**
     * @name FORGE.BackgroundRenderer#_scene
     * @type {THREE.Scene}
     * @private
     */
    this._scene = null;

    /**
     * @name FORGE.BackgroundRenderer#_camera
     * @type {THREE.Camera}
     * @private
     */
    this._camera = null;

    /**
     * @name FORGE.BackgroundRenderer#_frustum
     * @type {THREE.Frustum}
     * @private
     */
    this._frustum = null;

    /**
     * Media format (cubemap, equi...)
     * @type {string}
     * @private
     */
    this._mediaFormat = options.mediaFormat || FORGE.MediaFormat.CUBE;

    /**
     * @name FORGE.BackgroundRenderer#_renderTarget
     * @type {THREE.WebGLRenderTarget}
     * @private
     */
    this._renderTarget = target || null;

    FORGE.BaseObject.call(this, type || "BackgroundRenderer");

    this._boot();
};

FORGE.BackgroundRenderer.prototype = Object.create(FORGE.BaseObject.prototype);
FORGE.BackgroundRenderer.prototype.constructor = FORGE.BackgroundRenderer;

/**
 * Init routine.
 * @method FORGE.BackgroundRenderer#_boot
 * @private
 */
FORGE.BackgroundRenderer.prototype._boot = function()
{
    this._scene = new THREE.Scene();
    this._scene.name = "Background scene";

    if (this._renderTarget === null)
    {
        var width = this._viewer.renderer.canvasResolution.width;
        var height = this._viewer.renderer.canvasResolution.height;

        var rtParams =
        {
            minFilter: THREE.LinearFilter,
            magFilter: THREE.LinearFilter,
            format: THREE.RGBAFormat,
            stencilBuffer: false
        };

        this._renderTarget = new THREE.WebGLRenderTarget(width, height, rtParams);
    }

    this._frustum = new THREE.Frustum();
};

/**
 * Abstract method that should be implemented by subclass.
 * @method FORGE.BackgroundRenderer#_setDisplayObject
 * @param {FORGE.DisplayObject} displayObject - The display object to set.
 * @private
 */
FORGE.BackgroundRenderer.prototype._setDisplayObject = function(displayObject)
{
    this.log(displayObject); //@closure
    throw "Please implement " + this._className + "::_setDisplayObject";
};

/**
 * Abstract method that should be implemented by subclass.
 * @method FORGE.BackgroundRenderer#_clear
 * @private
 */
FORGE.BackgroundRenderer.prototype._clear = function()
{
    throw "Please implement " + this._className + "::_clear";
};

/**
 * Update texture if needed (video only).
 * @method FORGE.BackgroundRenderer#_updateTexture
 * @private
 */
FORGE.BackgroundRenderer.prototype._updateTexture = function()
{
    // doesn't refresh when there is no texture or texture container and when a video as WebGL texture is paused
    if (this._texture === null || this._textureCanvas === null ||
        this._textureContext === null || typeof this._displayObject === "undefined" || this._displayObject === null ||
        this._displayObject.element === null || (FORGE.Utils.isTypeOf(this._displayObject, ["VideoHTML5", "VideoDash"]) === true && this._displayObject.playing === false))
    {
        return;
    }

    var video = this._displayObject.element;
    if (video instanceof HTMLVideoElement && video.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA)
    {
        if (this._textureContext)
        {
            this._textureContext.drawImage(video,
                0, 0, video.videoWidth, video.videoHeight,
                0, 0, this._textureCanvas.width, this._textureCanvas.height);
            this._texture.needsUpdate = true;
            this.log("texture update done");
        }
    }
};

/**
 * Update after view change
 * @todo change name of this method to be more generic (used by init and )
 * Should be overriden by subclass
 * @method FORGE.BackgroundRenderer#updateAfterViewChange
 */
FORGE.BackgroundRenderer.prototype.updateAfterViewChange = function()
{
    throw new Error(this._className + "::updateAfterViewChange not implemented");
};

/**
 * Update size (resolution)
 * @method FORGE.BackgroundRenderer#setSize
 * @param {FORGE.Size} size - size [px]
 */
FORGE.BackgroundRenderer.prototype.setSize = function(size)
{
    if (this.renderTarget !== null)
    {
        this.renderTarget.setSize(size.width, size.height);
    }
};

/**
 * Render routine.
 * @method FORGE.BackgroundRenderer#render
 * @param {THREE.PerspectiveCamera} camera - perspective camera with mesh rendering, N/A with shader rendering (null)
 */
FORGE.BackgroundRenderer.prototype.render = function(camera)
{
    if (this._viewer.renderer === null || this._renderTarget === null)
    {
        return;
    }

    this._updateTexture();

    var renderCamera = (camera !== null) ? camera : this._camera;
    this._frustum.setFromMatrix( new THREE.Matrix4().multiplyMatrices( renderCamera.projectionMatrix, renderCamera.matrixWorldInverse ) );
    this._viewer.renderer.webGLRenderer.render ( this._scene, renderCamera, this._renderTarget, true );
};

/**
 * Check if some 3D object is interesecting the rendering frustum.
 * @method FORGE.BackgroundRenderer#isObjectInFrustum
 * @param {THREE.Object3D} object - 3D object
 */
FORGE.BackgroundRenderer.prototype.isObjectInFrustum = function(object)
{
    return this._frustum.intersectsObject(object);
};

/**
 * Check if some 3D object is in the scene
 * @method FORGE.BackgroundRenderer#isObjectInScene
 * @param {THREE.Object3D} object - 3D object
 */
FORGE.BackgroundRenderer.prototype.isObjectInScene = function(object)
{
    return this._scene.getObjectByName(object.name) !== undefined;
};

/**
 * Update routine.
 * @method FORGE.BackgroundRenderer#update
 */
FORGE.BackgroundRenderer.prototype.update = function()
{
    if (this._mesh === null || !(this._mesh.material instanceof THREE.ShaderMaterial))
    {
        this._viewer.renderer.view.current.updateUniforms();
        return;
    }

    var resolution = this._viewer.renderer.displayResolution;

    if (this._mesh.material.uniforms.hasOwnProperty("tViewportResolution"))
    {
        this._mesh.material.uniforms.tViewportResolution.value = new THREE.Vector2(resolution.width, resolution.height);
    }

    if (this._mesh.material.uniforms.hasOwnProperty("tViewportResolutionRatio"))
    {
        this._mesh.material.uniforms.tViewportResolutionRatio.value = resolution.ratio;
    }

    if (this._mesh.material.uniforms.hasOwnProperty("tModelViewMatrixInverse"))
    {
        this._mesh.material.uniforms.tModelViewMatrixInverse.value = this._viewer.renderer.camera.modelViewInverse;
    }

    this._viewer.renderer.view.current.updateUniforms(this._mesh.material.uniforms);
};

/**
 * Destroy sequence.
 * @method FORGE.BackgroundRenderer#destroy
 */
FORGE.BackgroundRenderer.prototype.destroy = function()
{
    this._camera = null;
    this._frustum = null;

    if (this._renderTarget !== null)
    {
        this._renderTarget.dispose();
        this._renderTarget = null;
    }

    while (this._scene.children.length > 0)
    {
        var mesh = this._scene.children.pop();

        if (mesh.geometry !== null)
        {
            mesh.geometry.dispose();
            mesh.geometry = null;
        }

        this._scene.remove(mesh);
    }

    this._scene = null;
    this._viewer = null;

    FORGE.BaseObject.prototype.destroy.call(this);
};

/**
 * Get texture size.
 * @name FORGE.BackgroundRenderer#textureSize
 * @type {FORGE.Size}
 */
Object.defineProperty(FORGE.BackgroundRenderer.prototype, "textureSize",
{
    /** @this {FORGE.BackgroundRenderer} */
    get: function()
    {
        if (this._texture === null || typeof this._texture.image === "undefined")
        {
            return null;
        }

        return new FORGE.Size(this._texture.image.width, this._texture.image.height);
    }
});

/**
 * Get background render target.
 * @name FORGE.BackgroundRenderer#renderTarget
 * @type {THREE.WebGLRenderTarget}
 */
Object.defineProperty(FORGE.BackgroundRenderer.prototype, "renderTarget",
{
    /** @this {FORGE.BackgroundRenderer} */
    get: function()
    {
        return this._renderTarget;
    }
});

/**
 * Get/Set background renderer displayObject.
 * @name FORGE.BackgroundRenderer#displayObject
 * @type {string}
 */
Object.defineProperty(FORGE.BackgroundRenderer.prototype, "displayObject",
{
    /** @this {FORGE.BackgroundRenderer} */
    get: function()
    {
        return this._displayObject;
    },
    /** @this {FORGE.BackgroundRenderer} */
    set: function(value)
    {
        if (value === null)
        {
            this._clear();
        }
        else
        {
            this._setDisplayObject(value);
        }
    }
});

/**
 * Get background scene.
 * @name FORGE.BackgroundRenderer#scene
 * @type {THREE.Scene}
 */
Object.defineProperty(FORGE.BackgroundRenderer.prototype, "scene",
{
    /** @this {FORGE.BackgroundRenderer} */
    get: function()
    {
        return this._scene;
    }
});

/**
 * Get camera frustum.
 * @name FORGE.BackgroundRenderer#frustum
 * @type {THREE.Frustum}
 */
Object.defineProperty(FORGE.BackgroundRenderer.prototype, "frustum",
{
    /** @this {FORGE.BackgroundRenderer} */
    get: function()
    {
        return this._frustum;
    }
});