/*global define*/
define(['Core/DeveloperError', 'Core/destroyObject', 'Renderer/PixelFormat'], function(
        DeveloperError,
        destroyObject,
        PixelFormat) {
    "use strict";

    /**
     * A framebuffer is a target for draw and clear calls.  It can contain color, depth, and stencil attachments
     * that are written to in response to these calls.  If the attachments are textures, they can be read in
     * later rendering passes.
     *
     * @alias Framebuffer
     *
     * @see Context#createFramebuffer
     *
     * @internalConstructor
     */
    var Framebuffer = function(gl, description) {
        this._gl = gl;
        this._framebuffer = gl.createFramebuffer();

        this._colorTexture = undefined;
        this._colorRenderbuffer = undefined;
        this._depthTexture = undefined;
        this._depthRenderbuffer = undefined;
        this._stencilRenderbuffer = undefined;
        this._depthStencilTexture = undefined;
        this._depthStencilRenderbuffer = undefined;

        /**
         * When true, the framebuffer owns its attachments so they will be destroyed when
         * {@link Framebuffer#destroy} is called or when a new attachment is assigned
         * to an attachment point.
         *
         * @type {Boolean}
         * @default true
         *
         * @see Framebuffer#destroy
         */
        this.destroyAttachments = true;

        if (description) {
            // Throw if a texture and renderbuffer are attached to the same point.  This won't
            // cause a WebGL error (because only one will be attached), but is likely a developer error.

            if (description.colorTexture && description.colorRenderbuffer) {
                throw new DeveloperError('Cannot have both a color texture and color renderbuffer attachment.');
            }

            if (description.depthTexture && description.depthRenderbuffer) {
                throw new DeveloperError('Cannot have both a depth texture and depth renderbuffer attachment.');
            }

            if (description.depthStencilTexture && description.depthStencilRenderbuffer) {
                throw new DeveloperError('Cannot have both a depth-stencil texture and depth-stencil renderbuffer attachment.');
            }

            // Avoid errors defined in Section 6.5 of the WebGL spec
            var depthAttachment = (description.depthTexture || description.depthRenderbuffer);
            var depthStencilAttachment = (description.depthStencilTexture || description.depthStencilRenderbuffer);

            if (depthAttachment && depthStencilAttachment) {
                throw new DeveloperError('Cannot have both a depth and depth-stencil attachment.');
            }

            if (description.stencilRenderbuffer && depthStencilAttachment) {
                throw new DeveloperError('Cannot have both a stencil and depth-stencil attachment.');
            }

            if (depthAttachment && description.stencilRenderbuffer) {
                throw new DeveloperError('Cannot have both a depth and stencil attachment.');
            }

            ///////////////////////////////////////////////////////////////////

            if (description.colorTexture) {
                this.setColorTexture(description.colorTexture);
            }

            if (description.colorRenderbuffer) {
                this.setColorRenderbuffer(description.colorRenderbuffer);
            }

            if (description.depthTexture) {
                this.setDepthTexture(description.depthTexture);
            }

            if (description.depthRenderbuffer) {
                this.setDepthRenderbuffer(description.depthRenderbuffer);
            }

            if (description.stencilRenderbuffer) {
                this.setStencilRenderbuffer(description.stencilRenderbuffer);
            }

            if (description.depthStencilTexture) {
                this.setDepthStencilTexture(description.depthStencilTexture);
            }

            if (description.depthStencilRenderbuffer) {
                this.setDepthStencilRenderbuffer(description.depthStencilRenderbuffer);
            }
        }
    };

    Framebuffer.prototype._bind = function() {
        var gl = this._gl;
        gl.bindFramebuffer(gl.FRAMEBUFFER, this._framebuffer);
    };

    Framebuffer.prototype._unBind = function() {
        var gl = this._gl;
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    };

    function attachTexture(framebuffer, attachment, texture) {
        framebuffer._bind();
        var gl = framebuffer._gl;

        if (texture) {
            gl.framebufferTexture2D(gl.FRAMEBUFFER, attachment, texture._getTarget(), texture._getTexture(), 0);
        } else {
            gl.framebufferTexture2D(gl.FRAMEBUFFER, attachment, gl.TEXTURE_2D, null, 0);
        }
        framebuffer._unBind();
    }

    function attachRenderbuffer(framebuffer, attachment, renderbuffer) {
        framebuffer._bind();
        var gl = framebuffer._gl;

        if (renderbuffer) {
            gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachment, gl.RENDERBUFFER, renderbuffer._getRenderbuffer());
        } else {
            gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachment, gl.RENDERBUFFER, null);
        }
        framebuffer._unBind();
    }

    function destroyAttachment(framebuffer, attachment) {
        if (framebuffer.destroyAttachments && attachment && attachment.destroy) {
            attachment.destroy();
        }
    }

    /**
     * Attaches a texture to the color attachment point.  When this framebuffer is passed to a draw
     * or clear call, the texture is the target of color output, e.g., <code>gl_FragColor</code>.
     *
     * @memberof Framebuffer
     *
     * @param {Texture} The texture to attach.  <code>undefined</code> dettaches the current texture.
     *
     * @exception {DeveloperError} The color-texture pixel-format must be a color format.
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.setColorTexture = function(texture) {
        if (texture && !PixelFormat.isColorFormat(texture.getPixelFormat())) {
            throw new DeveloperError('The color-texture pixel-format must be a color format.');
        }

        attachTexture(this, this._gl.COLOR_ATTACHMENT0, texture);
        destroyAttachment(this, this._colorTexture);
        this._colorTexture = texture;
    };

    /**
     * Returns the color texture attached to this framebuffer.
     *
     * @memberof Framebuffer
     *
     * @returns {Texture} The color texture attached to this framebuffer.
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.getColorTexture = function() {
        return this._colorTexture;
    };

    /**
     * Prefer {@link Framebuffer#setColorTexture}.
     *
     * @memberof Framebuffer
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.setColorRenderbuffer = function(renderbuffer) {
        attachRenderbuffer(this, this._gl.COLOR_ATTACHMENT0, renderbuffer);
        destroyAttachment(this, this._colorRenderbuffer);
        this._colorRenderbuffer = renderbuffer;
    };

    /**
     * Returns the color renderbuffer attached to this framebuffer.
     *
     * @memberof Framebuffer
     *
     * @returns {Texture} The color renderbuffer attached to this framebuffer.
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.getColorRenderbuffer = function() {
        return this._colorRenderbuffer;
    };

    /**
     * Attaches a texture to the depth attachment point.  When this framebuffer is passed to a draw
     * or clear call, the texture is the target of depth output.
     *
     * @memberof Framebuffer
     *
     * @param {Texture} The texture to attach.  <code>undefined</code> dettaches the current texture.
     *
     * @exception {DeveloperError} The depth-texture pixel-format must be DEPTH_COMPONENT.
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.setDepthTexture = function(texture) {
        if (texture && (texture.getPixelFormat() !== PixelFormat.DEPTH_COMPONENT)) {
            throw new DeveloperError('The depth-texture pixel-format must be DEPTH_COMPONENT.');
        }

        attachTexture(this, this._gl.DEPTH_ATTACHMENT, texture);
        destroyAttachment(this, this._depthTexture);
        this._depthTexture = texture;
    };

    /**
     * Returns the depth texture attached to this framebuffer.
     *
     * @memberof Framebuffer
     *
     * @returns {Texture} The depth texture attached to this framebuffer.
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.getDepthTexture = function() {
        return this._depthTexture;
    };

    /**
     * Prefer {@link Framebuffer#setDepthTexture}.
     *
     * @memberof Framebuffer
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.setDepthRenderbuffer = function(renderbuffer) {
        attachRenderbuffer(this, this._gl.DEPTH_ATTACHMENT, renderbuffer);
        destroyAttachment(this, this._depthRenderbuffer);
        this._depthRenderbuffer = renderbuffer;
    };

    /**
     * Returns the depth renderbuffer attached to this framebuffer.
     *
     * @memberof Framebuffer
     *
     * @returns {Texture} The depth renderbuffer attached to this framebuffer.
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.getDepthRenderbuffer = function() {
        return this._depthRenderbuffer;
    };

    /**
     * Prefer {@link Framebuffer#setDepthStencilTexture}.
     *
     * @memberof Framebuffer
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.setStencilRenderbuffer = function(renderbuffer) {
        attachRenderbuffer(this, this._gl.STENCIL_ATTACHMENT, renderbuffer);
        destroyAttachment(this, this._stencilRenderbuffer);
        this._stencilRenderbuffer = renderbuffer;
    };

    /**
     * Returns the stencil renderbuffer attached to this framebuffer.
     *
     * @memberof Framebuffer
     *
     * @returns {Texture} The stencil renderbuffer attached to this framebuffer.
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.getStencilRenderbuffer = function() {
        return this._stencilRenderbuffer;
    };

    /**
     * Attaches a texture to the depth-stencil attachment point.  When this framebuffer is passed to a draw
     * or clear call, the texture is the target of depth and stencil output.
     *
     * @memberof Framebuffer
     *
     * @param {Texture} The texture to attach.  <code>undefined</code> dettaches the current texture.
     *
     * @exception {DeveloperError} The depth-stencil-texture pixel-format must be DEPTH_STENCIL.
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.setDepthStencilTexture = function(texture) {
        if (texture && (texture.getPixelFormat() !== PixelFormat.DEPTH_STENCIL)) {
            throw new DeveloperError('The depth-stencil pixel-format must be DEPTH_STENCIL.');
        }

        attachTexture(this, this._gl.DEPTH_STENCIL_ATTACHMENT, texture);
        destroyAttachment(this, this._depthStencilTexture);
        this._depthStencilTexture = texture;
    };

    /**
     * Returns the depth-stencil texture attached to this framebuffer.
     *
     * @memberof Framebuffer
     *
     * @returns {Texture} The depth-stencil texture attached to this framebuffer.
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.getDepthStencilTexture = function() {
        return this._depthStencilTexture;
    };

    /**
     * Prefer {@link Framebuffer#setDepthStencilTexture}.
     *
     * @memberof Framebuffer
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.setDepthStencilRenderbuffer = function(renderbuffer) {
        attachRenderbuffer(this, this._gl.DEPTH_STENCIL_ATTACHMENT, renderbuffer);
        destroyAttachment(this, this._depthStencilRenderbuffer);
        this._depthStencilRenderbuffer = renderbuffer;
    };

    /**
     * Returns the depth-stencil renderbuffer attached to this framebuffer.
     *
     * @memberof Framebuffer
     *
     * @returns {Texture} The depth-stencil renderbuffer attached to this framebuffer.
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.getDepthStencilRenderbuffer = function() {
        return this._depthStencilRenderbuffer;
    };

    /**
     * Returns true if the framebuffer has a depth attachment.  Depth attachments include
     * depth and depth-stencil textures, and depth and depth-stencil renderbuffers.  When
     * rendering to a framebuffer, a depth attachment is required for the depth test to have effect.
     *
     * @memberof Framebuffer
     *
     * @returns {Boolean} Returns true if the framebuffer has a depth attachment; otherwise, false.
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     */
    Framebuffer.prototype.hasDepthAttachment = function() {
        return !!(this.getDepthTexture() || this.getDepthRenderbuffer() || this.getDepthStencilTexture() || this.getDepthStencilRenderbuffer());
    };

    /**
     * Returns true if this object was destroyed; otherwise, false.
     * <br /><br />
     * If this object was destroyed, it should not be used; calling any function other than
     * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
     *
     * @memberof Framebuffer
     *
     * @returns {Boolean} True if this object was destroyed; otherwise, false.
     *
     * @see Framebuffer#destroy
     */
    Framebuffer.prototype.isDestroyed = function() {
        return false;
    };

    /**
     * Destroys the WebGL resources held by this object.  Destroying an object allows for deterministic
     * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
     * <br /><br />
     * Framebuffer attachments are only destoryed if the framebuffer owns them, i.e., {@link destroyAttachments}
     * is true.
     * <br /><br />
     * Once an object is destroyed, it should not be used; calling any function other than
     * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.  Therefore,
     * assign the return value (<code>undefined</code>) to the object as done in the example.
     *
     * @memberof Framebuffer
     *
     * @returns {undefined}
     *
     * @exception {DeveloperError} This framebuffer was destroyed, i.e., destroy() was called.
     *
     * @see Framebuffer#isDestroyed
     * @see Framebuffer#destroyAttachments
     * @see <a href='http://www.khronos.org/opengles/sdk/2.0/docs/man/glDeleteFramebuffers.xml'>glDeleteFramebuffers</a>
     * @see <a href='http://www.khronos.org/opengles/sdk/2.0/docs/man/glDeleteTextures.xml'>glDeleteTextures</a>
     * @see <a href='http://www.khronos.org/opengles/sdk/2.0/docs/man/glDeleteRenderbuffers.xml'>glDeleteRenderbuffers</a>
     *
     * @example
     * var texture = context.createTexture2D({ width : 1, height : 1 });
     * framebuffer = context.createFramebuffer({ colorTexture : texture });
     * // ...
     * framebuffer = framebuffer.destroy();
     * // texture is also destroyed.
     */
    Framebuffer.prototype.destroy = function() {
        if (this.destroyAttachments) {
            // If the color texture is a cube map face, it is owned by the cube map, and will not be destroyed.
            this._colorTexture = this._colorTexture && this._colorTexture.destroy && this._colorTexture.destroy();
            this._colorRenderbuffer = this._colorRenderbuffer && this._colorRenderbuffer.destroy();
            this._depthTexture = this._depthTexture && this._depthTexture.destroy();
            this._depthRenderbuffer = this._depthRenderbuffer && this._depthRenderbuffer.destroy();
            this._stencilRenderbuffer = this._stencilRenderbuffer && this._stencilRenderbuffer.destroy();
            this._depthStencilTexture = this._depthStencilTexture && this._depthStencilTexture.destroy();
            this._depthStencilRenderbuffer = this._depthStencilRenderbuffer && this._depthStencilRenderbuffer.destroy();
        }

        this._gl.deleteFramebuffer(this._framebuffer);
        return destroyObject(this);
    };

    return Framebuffer;
});