/*global defineSuite*/
defineSuite([
        'Scene/Material',
        'Core/Cartesian3',
        'Core/Color',
        'Core/Ellipsoid',
        'Core/GeometryInstance',
        'Core/Math',
        'Core/PolygonGeometry',
        'Renderer/ClearCommand',
        'Scene/EllipsoidSurfaceAppearance',
        'Scene/PolylineCollection',
        'Scene/Primitive',
        'Specs/createCamera',
        'Specs/createContext',
        'Specs/createFrameState',
        'Specs/pollToPromise',
        'Specs/render'
    ], function(
        Material,
        Cartesian3,
        Color,
        Ellipsoid,
        GeometryInstance,
        CesiumMath,
        PolygonGeometry,
        ClearCommand,
        EllipsoidSurfaceAppearance,
        PolylineCollection,
        Primitive,
        createCamera,
        createContext,
        createFrameState,
        pollToPromise,
        render) {
    "use strict";

    var context;
    var frameState;
    var polygon;
    var polylines;
    var polyline;
    var us;

    beforeAll(function() {
        context = createContext();
    });

    afterAll(function() {
        context.destroyForSpecs();
    });

    beforeEach(function() {
        frameState = createFrameState(context, createCamera({
            offset : new Cartesian3(1.02, 0.0, 0.0)
        }));

        us = context.uniformState;
        us.update(frameState);

        var ellipsoid = Ellipsoid.UNIT_SPHERE;

        polygon = new Primitive({
            geometryInstances: new GeometryInstance({
                geometry: PolygonGeometry.fromPositions({
                    positions: Cartesian3.fromDegreesArray([
                        -50.0, -50.0,
                        50.0, -50.0,
                        50.0, 50.0,
                        -50.0, 50.0
                    ], ellipsoid),
                    vertexFormat: EllipsoidSurfaceAppearance.VERTEX_FORMAT,
                    ellipsoid: ellipsoid,
                    granularity: CesiumMath.toRadians(20.0)
                })
            }),
            appearance: new EllipsoidSurfaceAppearance({
                aboveGround: false
            }),
            asynchronous: false
        });

        polylines = new PolylineCollection();
        polyline = polylines.add({
            positions : Cartesian3.fromDegreesArray([
                -50.0, 0.0,
                50.0, 0.0
            ], ellipsoid),
            width : 5.0
        });
    });

    afterEach(function() {
        polygon = polygon && polygon.destroy();
        polylines = polylines && polylines.destroy();
        us = undefined;
    });

    function renderMaterial(material) {
        polygon.appearance.material = material;

        ClearCommand.ALL.execute(context);
        expect(context.readPixels()).toEqual([0, 0, 0, 0]);

        render(frameState, polygon);
        return context.readPixels();
    }

    function renderPolylineMaterial(material) {
        polyline.material = material;

        ClearCommand.ALL.execute(context);
        expect(context.readPixels()).toEqual([0, 0, 0, 0]);

        render(frameState, polylines);
        return context.readPixels();
    }

    function verifyMaterial(type) {
        var material = new Material({
            strict : true,
            fabric : {
                type : type
            }
        });
        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    }

    function verifyPolylineMaterial(type) {
        var material = new Material({
            strict : true,
            fabric : {
                type : type
            }
        });
        var pixel = renderPolylineMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    }

    it('draws Color built-in material', function() {
        verifyMaterial('Color');
    });

    it('draws Image built-in material', function() {
        verifyMaterial('Image');
    });

    it('draws DiffuseMap built-in material', function() {
        verifyMaterial('DiffuseMap');
    });

    it('draws AlphaMap built-in material', function() {
        verifyMaterial('AlphaMap');
    });

    it('draws SpecularMap built-in material', function() {
        verifyMaterial('SpecularMap');
    });

    it('draws EmissionMap built-in material', function() {
        verifyMaterial('EmissionMap');
    });

    it('draws BumpMap built-in material', function() {
        verifyMaterial('BumpMap');
    });

    it('draws NormalMap built-in material', function() {
        verifyMaterial('NormalMap');
    });

    it('draws Grid built-in material', function() {
        verifyMaterial('Grid');
    });

    it('draws Stripe built-in material', function() {
        verifyMaterial('Stripe');
    });

    it('draws Checkerboard built-in material', function() {
        verifyMaterial('Checkerboard');
    });

    it('draws Dot built-in material', function() {
        verifyMaterial('Dot');
    });

    it('draws Water built-in material', function() {
        verifyMaterial('Water');
    });

    it('draws RimLighting built-in material', function() {
        verifyMaterial('RimLighting');
    });

    it('draws Fade built-in material', function() {
        verifyMaterial('Fade');
    });

    it('draws PolylineArrow built-in material', function() {
        verifyPolylineMaterial('PolylineArrow');
    });

    it('draws PolylineGlow built-in material', function() {
        verifyPolylineMaterial('PolylineGlow');
    });

    it('draws PolylineOutline built-in material', function() {
        verifyPolylineMaterial('PolylineOutline');
    });

    it('gets the material type', function() {
        var material = new Material({
            strict : true,
            fabric : {
                type : 'Color'
            }
        });
        expect(material.type).toEqual('Color');
    });

    it('creates opaque/translucent materials', function() {
        var material = new Material({
            translucent : true,
            strict : true,
            fabric : {
                type : 'Color'
            }
        });
        expect(material.isTranslucent()).toEqual(true);

        material = new Material({
            translucent : false,
            strict : true,
            fabric : {
                type : 'Color'
            }
        });
        expect(material.isTranslucent()).toEqual(false);
    });

    it('creates a new material type and builds off of it', function() {
        var material1 = new Material({
            strict : true,
            fabric : {
                type : 'New',
                components : {
                    diffuse : 'vec3(0.0, 0.0, 0.0)'
                }
            }
        });

        var material2 = new Material({
            strict : true,
            fabric : {
                materials : {
                    first : {
                        type : 'New'
                    }
                },
                components : {
                    diffuse : 'first.diffuse'
                }
            }
        });

        var pixel1 = renderMaterial(material1);
        expect(pixel1).not.toEqual([0, 0, 0, 0]);
        var pixel2 = renderMaterial(material2);
        expect(pixel2).not.toEqual([0, 0, 0, 0]);
    });

    it('accesses material properties after construction', function() {
        var material = new Material({
            strict : true,
            fabric : {
                materials : {
                    first : {
                        type : 'DiffuseMap'
                    }
                },
                uniforms : {
                    value : {
                        x : 0.0,
                        y : 0.0,
                        z : 0.0
                    }
                },
                components : {
                    diffuse : 'value + first.diffuse'
                }
            }
        });
        material.uniforms.value.x = 1.0;
        material.materials.first.uniforms.repeat.x = 2.0;

        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('creates a material inside a material inside a material', function () {
        var material = new Material({
            strict : true,
            fabric : {
                materials : {
                    first : {
                        materials : {
                            second : {
                                components : {
                                    diffuse : 'vec3(0.0, 0.0, 0.0)'
                                }
                            }
                        },
                        components : {
                            diffuse : 'second.diffuse'
                        }
                    }
                },
                components : {
                    diffuse : 'first.diffuse'
                }
            }
        });
        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('creates a material with an image uniform', function () {
        var material = new Material({
            strict : true,
            fabric : {
                type : 'DiffuseMap',
                uniforms : {
                    image :  './Data/Images/Blue.png'
                }
            }
        });
        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('creates a material with an image canvas uniform', function() {
        var canvas = document.createElement('canvas');
        var context2D = canvas.getContext('2d');
        context2D.width = 1;
        context2D.height = 1;
        context2D.fillStyle = 'rgb(0,0,255)';
        context2D.fillRect(0, 0, 1, 1);

        var material = new Material({
            strict : true,
            fabric : {
                type : 'DiffuseMap',
                uniforms : {
                    image : canvas
                }
            }
        });

        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('creates a material with a cube map uniform', function() {
        var material = new Material({
            strict : true,
            fabric : {
                uniforms : {
                    cubeMap : {
                        positiveX : './Data/Images/Blue.png',
                        negativeX : './Data/Images/Blue.png',
                        positiveY : './Data/Images/Blue.png',
                        negativeY : './Data/Images/Blue.png',
                        positiveZ : './Data/Images/Blue.png',
                        negativeZ : './Data/Images/Blue.png'
                    }
                },
                source : 'uniform samplerCube cubeMap;\n' +
                    'czm_material czm_getMaterial(czm_materialInput materialInput)\n' +
                    '{\n' +
                    '    czm_material material = czm_getDefaultMaterial(materialInput);\n' +
                    '    material.diffuse = textureCube(cubeMap, vec3(1.0)).xyz;\n' +
                    '    return material;\n' +
                    '}\n'
            }
        });
        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('does not crash if source uniform is formatted differently', function() {
        var material = new Material({
            strict : true,
            fabric : {
                uniforms : {
                    cubeMap : {
                        positiveX : './Data/Images/Blue.png',
                        negativeX : './Data/Images/Blue.png',
                        positiveY : './Data/Images/Blue.png',
                        negativeY : './Data/Images/Blue.png',
                        positiveZ : './Data/Images/Blue.png',
                        negativeZ : './Data/Images/Blue.png'
                    }
                },
                source : 'uniform   samplerCube   cubeMap  ;\r\n' +
                    'czm_material czm_getMaterial(czm_materialInput materialInput)\r\n' +
                    '{\r\n' +
                    '    czm_material material = czm_getDefaultMaterial(materialInput);\r\n' +
                    '    material.diffuse = textureCube(cubeMap, vec3(1.0)).xyz;\r\n' +
                    '    return material;\r\n' +
                    '}'
            }
        });
        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('creates a material with a boolean uniform', function () {
        var material = new Material({
            strict : true,
            fabric : {
                uniforms : {
                    value : true
                },
                components : {
                    diffuse : 'float(value) * vec3(1.0)'
                }
            }
        });
        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('create a material with a matrix uniform', function () {
        var material1 = new Material({
            strict : true,
            fabric : {
                uniforms : {
                    value : [0.5, 0.5, 0.5, 0.5]
                },
                components : {
                    diffuse : 'vec3(value[0][0], value[0][1], value[1][0])',
                    alpha : 'value[1][1]'
                }
            }
        });
        var pixel = renderMaterial(material1);
        expect(pixel).not.toEqual([0, 0, 0, 0]);

        var material2 = new Material({
            strict : true,
            fabric : {
                uniforms : {
                    value : [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
                },
                components : {
                    diffuse : 'vec3(value[0][0], value[0][1], value[1][0])',
                    alpha : 'value[2][2]'
                }
            }
        });
        pixel = renderMaterial(material2);
        expect(pixel).not.toEqual([0, 0, 0, 0]);

        var material3 = new Material({
            strict : true,
            fabric : {
                uniforms : {
                    value : [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
                },
                components : {
                    diffuse : 'vec3(value[0][0], value[0][1], value[1][0])',
                    alpha : 'value[3][3]'
                }
            }
        });
        pixel = renderMaterial(material3);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('creates a material using unusual uniform and material names', function () {
        var material = new Material({
            strict : true,
            fabric : {
                uniforms : {
                    i : 0.5
                },
                materials : {
                    d : {
                        type : 'Color'
                    },
                    diffuse : {
                        type : 'Color'
                    }
                },
                components : {
                    diffuse : '(d.diffuse + diffuse.diffuse)*i',
                    specular : 'i'
                }
            }
        });
        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('create a material using fromType', function () {
        var material = Material.fromType('Color');
        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('create material using fromType and overide default uniforms', function() {
        var material1 = Material.fromType('Color', {
            color : new Color(0.0, 1.0, 0.0, 1.0)
        });

        var pixel = renderMaterial(material1);
        expect(pixel).toEqual([0, 255, 0, 255]);
    });

    it('create multiple materials from the same type', function() {
        var material1 = Material.fromType('Color', {
            color : new Color(0.0, 1.0, 0.0, 1.0)
        });

        var material2 = Material.fromType('Color', {
            color : new Color(0.0, 0.0, 1.0, 1.0)
        });

        expect(material1.shaderSource).toEqual(material2.shaderSource);

        var pixel = renderMaterial(material2);
        expect(pixel).toEqual([0, 0, 255, 255]);

        pixel = renderMaterial(material1);
        expect(pixel).toEqual([0, 255, 0, 255]);
    });

    it('create material with sub-materials of the same type', function() {
        var material = new Material({
            fabric : {
                materials : {
                    color1 : {
                        type : 'Color',
                        uniforms : {
                            color : new Color(0.0, 1.0, 0.0, 1.0)
                        }
                    },
                    color2 : {
                        type : 'Color',
                        uniforms : {
                            color : new Color(0.0, 0.0, 1.0, 1.0)
                        }
                    }
                },
                components : {
                    diffuse : 'color1.diffuse + color2.diffuse'
                }
            }
        });

        var pixel = renderMaterial(material);
        expect(pixel).toEqual([0, 255, 255, 255]);
    });

    it('throws with source and components in same template', function () {
        expect(function() {
            return new Material({
                strict : true,
                fabric : {
                    components : {
                        diffuse : 'vec3(0.0, 0.0, 0.0)'
                    },
                    source : 'czm_material czm_getMaterial(czm_materialInput materialInput)\n{\n' +
                             'czm_material material = czm_getDefaultMaterial(materialInput);\n' +
                             'return material;\n}\n'
                }
            });
        }).toThrowDeveloperError();

        expect(function() {
            return new Material({
                strict : true,
                fabric : {
                    type : 'DiffuseMap',
                    components : {
                        diffuse : 'vec3(0.0, 0.0, 0.0)'
                    }
                }
            });
        }).toThrowDeveloperError();
    });

    it('throws with duplicate names in materials and uniforms', function () {
        expect(function() {
            return new Material({
                strict : false,
                fabric : {
                    uniforms : {
                        first : 0.0,
                        second : 0.0
                    },
                    materials : {
                        second : {}
                    }
                }
            });
        }).toThrowDeveloperError();
    });

    it('throws with invalid template type', function() {
        expect(function() {
            return new Material({
                strict : true,
                fabric : {
                    invalid : 3.0
                }
            });
        }).toThrowDeveloperError();
    });

    it('throws with invalid component type', function () {
        expect(function() {
            return new Material({
                strict : true,
                fabric : {
                    components : {
                        difuse : 'vec3(0.0, 0.0, 0.0)'
                    }
                }
            });
        }).toThrowDeveloperError();
    });

    it('throws with invalid uniform type', function() {
        expect(function() {
            return new Material({
                strict : true,
                fabric : {
                    uniforms : {
                        value : {
                            x : 0.0,
                            y : 0.0,
                            z : 0.0,
                            w : 0.0,
                            t : 0.0
                        }
                    }
                }
            });
        }).toThrowDeveloperError();

        expect(function() {
            return new Material({
                strict : true,
                fabric : {
                    uniforms : {
                        value : [0.0, 0.0, 0.0, 0.0, 0.0]
                    }
                }
            });
        }).toThrowDeveloperError();
    });

    it('throws with unused channels', function() {
        expect(function() {
            return new Material({
                strict : true,
                fabric : {
                    uniforms : {
                        nonexistant : 'rgb'
                    }
                }
            });
        }).toThrowDeveloperError();

        // If strict is false, unused uniform strings are ignored.
        var material = new Material({
            strict : false,
            fabric : {
                uniforms : {
                    nonexistant : 'rgb'
                }
            }
        });
        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('throws with unused uniform', function() {
        expect(function() {
            return new Material({
                strict : true,
                fabric : {
                    uniforms : {
                        first : {
                            x : 0.0,
                            y : 0.0,
                            z : 0.0
                        }
                    }
                }
            });
        }).toThrowDeveloperError();

        // If strict is false, unused uniforms are ignored.
        var material = new Material({
            strict : false,
            fabric : {
                uniforms : {
                    first : {
                        x : 0.0,
                        y : 0.0,
                        z : 0.0
                    }
                }
            }
        });
        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('throws with unused material', function() {
        expect(function() {
            return new Material({
                strict : true,
                fabric : {
                    materials : {
                        first : {
                            type : 'DiffuseMap'
                        }
                    }
                }
            });
        }).toThrowDeveloperError();

        // If strict is false, unused materials are ignored.
        var material = new Material({
            strict : false,
            fabric : {
                materials : {
                    first : {
                        type : 'DiffuseMap'
                    }
                }
            }
        });
        var pixel = renderMaterial(material);
        expect(pixel).not.toEqual([0, 0, 0, 0]);
    });

    it('throws with invalid type sent to fromType', function() {
        expect(function() {
            return Material.fromType('Nothing');
        }).toThrowDeveloperError();
    });

    it('destroys material with texture', function() {
        var material = Material.fromType(Material.DiffuseMapType);
        material.uniforms.image = './Data/Images/Green.png';

        pollToPromise(function() {
            return material._loadedImages.length !== 0;
        }).then(function() {
            var pixel = renderMaterial(material);
            expect(pixel).not.toEqual([0, 0, 0, 0]);
            material.destroy();
            expect(material.isDestroyed()).toEqual(true);
        });
    });

    it('destroys sub-materials', function() {
        var material = new Material({
            strict : true,
            fabric : {
                materials : {
                    diffuseMap : {
                        type : 'DiffuseMap'
                    }
                },
                uniforms : {
                    value : {
                        x : 0.0,
                        y : 0.0,
                        z : 0.0
                    }
                },
                components : {
                    diffuse : 'value + diffuseMap.diffuse'
                }
            }
        });
        material.materials.diffuseMap.uniforms.image = './Data/Images/Green.png';

        pollToPromise(function() {
            return material.materials.diffuseMap._loadedImages.length !== 0;
        }).then(function() {
            var pixel = renderMaterial(material);
            expect(pixel).not.toEqual([0, 0, 0, 0]);

            var diffuseMap = material.materials.diffuseMap;
            material.destroy();
            expect(material.isDestroyed()).toEqual(true);
            expect(diffuseMap.isDestroyed()).toEqual(true);
        });
    });

    it('does not destroy default material', function() {
        var material = Material.fromType(Material.DiffuseMapType);
        renderMaterial(material);
        material.destroy();
    });

}, 'WebGL');
