/* A demonstration of Tessellation Shaders in OpenScenegraph.
 *
 * Instructions:
 *   Press plus to increase tesselation and minus to decrease it.
 *   Press right arrow to increase inner tesselation and left arrow to decrease it.
 *   Press up arrow to increase outer tesselation and down arrow to decrease it.
 *
 * Original code by Philip Rideout
 * Adapted to OpenScenegraph by John Kaniarz
 * Additional work by Michael Mc Donnell
 */

#include <osg/Program>
#include <osg/PatchParameter>
#include <osg/ShapeDrawable>
#include <osgViewer/Viewer>
#include <osgViewer/ViewerEventHandlers>
#include <osgGA/TrackballManipulator>

static const char* vertSource = {
"#version 400\n"
"in vec4 osg_Vertex;\n"
"out vec3 vPosition;\n"
"void main(){\n"
"    vPosition = osg_Vertex.xyz;\n"
"}\n"
};
static const char* tessControlSource = {
"#version 400\n"
"layout(vertices = 3) out;\n"
"in vec3 vPosition[];\n"
"out vec3 tcPosition[];\n"
"uniform float TessLevelInner;\n"
"uniform float TessLevelOuter;\n"
"#define ID gl_InvocationID\n"
"void main(){\n"
"    tcPosition[ID] = vPosition[ID];\n"
"    if (ID == 0) {\n"
"        gl_TessLevelInner[0] = TessLevelInner;\n"
"        gl_TessLevelOuter[0] = TessLevelOuter;\n"
"        gl_TessLevelOuter[1] = TessLevelOuter;\n"
"        gl_TessLevelOuter[2] = TessLevelOuter;\n"
"    }\n"
"}\n"
};
static const char* tessEvalSource = {
"#version 400\n"
"layout(triangles, equal_spacing, cw) in;\n"
"in vec3 tcPosition[];\n"
"out vec3 tePosition;\n"
"out vec3 tePatchDistance;\n"
"uniform mat4 osg_ProjectionMatrix;\n"
"uniform mat4 osg_ModelViewMatrix;\n"
"void main(){\n"
"    vec3 p0 = gl_TessCoord.x * tcPosition[0];\n"
"    vec3 p1 = gl_TessCoord.y * tcPosition[1];\n"
"    vec3 p2 = gl_TessCoord.z * tcPosition[2];\n"
"    tePatchDistance = gl_TessCoord;\n"
"    tePosition = normalize(p0 + p1 + p2);\n"
"    gl_Position = osg_ProjectionMatrix * osg_ModelViewMatrix * vec4(tePosition, 1);\n"
"}\n"
};
static const char* geomSource = {
"#version 400\n"
"uniform mat4 osg_ModelViewMatrix;\n"
"uniform mat3 osg_NormalMatrix;\n"
"layout(triangles) in;\n"
"layout(triangle_strip, max_vertices = 3) out;\n"
"in vec3 tePosition[3];\n"
"in vec3 tePatchDistance[3];\n"
"out vec3 gFacetNormal;\n"
"out vec3 gPatchDistance;\n"
"out vec3 gTriDistance;\n"
"out vec4 gColor;\n"
"void main(){\n"
"    vec3 A = tePosition[2] - tePosition[0];\n"
"    vec3 B = tePosition[1] - tePosition[0];\n"
"    gFacetNormal = osg_NormalMatrix * normalize(cross(A, B));\n"
"    gPatchDistance = tePatchDistance[0];\n"
"    gTriDistance = vec3(1, 0, 0);\n"
"    gColor = osg_ModelViewMatrix[0];\n"
"    gl_Position = gl_in[0].gl_Position; EmitVertex();\n"
"    gPatchDistance = tePatchDistance[1];\n"
"    gTriDistance = vec3(0, 1, 0);\n"
"    gColor = osg_ModelViewMatrix[1];\n"
"    gl_Position = gl_in[1].gl_Position; EmitVertex();\n"
"    gPatchDistance = tePatchDistance[2];\n"
"    gTriDistance = vec3(0, 0, 1);\n"
"    gColor = osg_ModelViewMatrix[2];\n"
"    gl_Position = gl_in[2].gl_Position; EmitVertex();\n"
"    EndPrimitive();\n"
"}\n"
};
static const char* fragSource = {
"#version 400\n"
"out vec4 FragColor;\n"
"in vec3 gFacetNormal;\n"
"in vec3 gTriDistance;\n"
"in vec3 gPatchDistance;\n"
"in vec4 gColor;\n"
"in float gPrimitive;\n"
"uniform vec3 LightPosition;\n"
"uniform vec3 DiffuseMaterial;\n"
"uniform vec3 AmbientMaterial;\n"
"float amplify(float d, float scale, float offset){\n"
"    d = scale * d + offset;\n"
"    d = clamp(d, 0, 1);\n"
"    d = 1 - exp2(-2*d*d);\n"
"    return d;\n"
"}\n"
"void main(){\n"
"    vec3 N = normalize(gFacetNormal);\n"
"    vec3 L = LightPosition;\n"
"    float df = abs(dot(N, L));\n"
"    vec3 color = AmbientMaterial + df * DiffuseMaterial;\n"
"    float d1 = min(min(gTriDistance.x, gTriDistance.y), gTriDistance.z);\n"
"    float d2 = min(min(gPatchDistance.x, gPatchDistance.y), gPatchDistance.z);\n"
"    color = amplify(d1, 40, -0.5) * amplify(d2, 60, -0.5) * color;\n"
"    FragColor = vec4(color, 1.0);\n"
"}\n"
};

osg::ref_ptr<osg::Geode> CreateIcosahedron(osg::Program *program)
{
    osg::Geode *geode=new osg::Geode();
    osg::Geometry *geometry = new osg::Geometry();
    const unsigned int Faces[] = {
        2, 1, 0,
        3, 2, 0,
        4, 3, 0,
        5, 4, 0,
        1, 5, 0,

        11, 6,  7,
        11, 7,  8,
        11, 8,  9,
        11, 9,  10,
        11, 10, 6,

        1, 2, 6,
        2, 3, 7,
        3, 4, 8,
        4, 5, 9,
        5, 1, 10,

        2,  7, 6,
        3,  8, 7,
        4,  9, 8,
        5, 10, 9,
        1, 6, 10 };
    int IndexCount = sizeof(Faces) / sizeof(Faces[0]);
    const float Verts[] = {
         0.000f,  0.000f,  1.000f,
         0.894f,  0.000f,  0.447f,
         0.276f,  0.851f,  0.447f,
        -0.724f,  0.526f,  0.447f,
        -0.724f, -0.526f,  0.447f,
         0.276f, -0.851f,  0.447f,
         0.724f,  0.526f, -0.447f,
        -0.276f,  0.851f, -0.447f,
        -0.894f,  0.000f, -0.447f,
        -0.276f, -0.851f, -0.447f,
         0.724f, -0.526f, -0.447f,
         0.000f,  0.000f, -1.000f };

    int VertexCount = sizeof(Verts)/sizeof(float);
    osg::Vec3Array* vertices = new osg::Vec3Array();
    for(int i=0;i<VertexCount;i+=3){
        vertices->push_back(osg::Vec3(Verts[i],Verts[i+1],Verts[i+2]));
    }
    geometry->setVertexArray(vertices);
    geometry->addPrimitiveSet(new osg::DrawElementsUInt(osg::PrimitiveSet::PATCHES,IndexCount,Faces));

    // Expand the bounding box, otherwise the geometry is clipped in front when tessellating.
    osg::BoundingBox bbox(osg::Vec3(-1.0f, -1.9f, -1.0f), osg::Vec3(1.0f, 1.0f, 1.0f));
    geometry->setInitialBound(bbox);

    geode->addDrawable(geometry);
    return geode;
}

osg::ref_ptr<osg::Program> createProgram()
{
    osg::Program *program = new osg::Program();
    program->addShader(new osg::Shader(osg::Shader::VERTEX,vertSource));
    program->addShader(new osg::Shader(osg::Shader::TESSCONTROL,tessControlSource));
    program->addShader(new osg::Shader(osg::Shader::TESSEVALUATION,tessEvalSource));
    program->addShader(new osg::Shader(osg::Shader::GEOMETRY,geomSource));
    program->addShader(new osg::Shader(osg::Shader::FRAGMENT,fragSource));
    program->setParameter(GL_GEOMETRY_VERTICES_OUT_EXT, 3);
    program->setParameter(GL_GEOMETRY_INPUT_TYPE_EXT, GL_TRIANGLES);
    program->setParameter(GL_GEOMETRY_OUTPUT_TYPE_EXT, GL_TRIANGLE_STRIP);
    return program;
}

class KeyboardEventHandler : public osgGA::GUIEventHandler
{
public:
    KeyboardEventHandler(osg::ref_ptr<osg::Uniform> tessInnerU, osg::ref_ptr<osg::Uniform> tessOuterU):
        _tessInnerU(tessInnerU),
        _tessOuterU(tessOuterU)
    {
        tessInnerU->get(_tessInner);
        tessOuterU->get(_tessOuter);
    }

    virtual bool handle(const osgGA::GUIEventAdapter& ea,osgGA::GUIActionAdapter& gaa)
    {
        if(ea.getEventType()==osgGA::GUIEventAdapter::KEYDOWN){
            switch (ea.getKey()){
                case osgGA::GUIEventAdapter::KEY_Up:
                    increaseOuterTesselation();
                    return true;
                case osgGA::GUIEventAdapter::KEY_Down:
                    decreaseOuterTesselation();
                    return true;
                case osgGA::GUIEventAdapter::KEY_Left:
                    decreaseInnerTesselation();
                    return true;
                case osgGA::GUIEventAdapter::KEY_Right:
                    increaseInnerTesselation();
                    return true;
                case osgGA::GUIEventAdapter::KEY_Plus:
                case osgGA::GUIEventAdapter::KEY_KP_Add:
                    increaseInnerTesselation();
                    increaseOuterTesselation();
                    return true;
                case osgGA::GUIEventAdapter::KEY_Minus:
                case osgGA::GUIEventAdapter::KEY_KP_Subtract:
                    decreaseInnerTesselation();
                    decreaseOuterTesselation();
                    return true;
            }
        }
        return osgGA::GUIEventHandler::handle(ea, gaa);
    }

private:
    osg::ref_ptr<osg::Uniform> _tessInnerU;
    osg::ref_ptr<osg::Uniform> _tessOuterU;
    float _tessInner;
    float _tessOuter;

    void increaseInnerTesselation()
    {
        _tessInnerU->set(++_tessInner);
    }

    void decreaseInnerTesselation()
    {
        _tessInner = std::max(1.0f, _tessInner-1.0f);
        _tessInnerU->set(_tessInner);
    }

    void increaseOuterTesselation()
    {
        _tessOuterU->set(++_tessOuter);
    }

    void decreaseOuterTesselation()
    {
        _tessOuter = std::max(1.0f, _tessOuter-1.0f);
        _tessOuterU->set(_tessOuter);
    }
};

int main(int argc, char* argv[])
{
    osgViewer::Viewer viewer;
    viewer.setUpViewInWindow(100,100,800,600);
    osg::ref_ptr<osg::Program> program = createProgram();
    osg::ref_ptr<osg::Geode> geode = CreateIcosahedron(program.get());
    osg::ref_ptr<osg::Uniform> tessInnerU = new osg::Uniform("TessLevelInner", 1.0f);
    osg::ref_ptr<osg::Uniform> tessOuterU = new osg::Uniform("TessLevelOuter", 1.0f);

    osg::StateSet *state;
    state = geode->getOrCreateStateSet();
    state->addUniform(new osg::Uniform("AmbientMaterial",osg::Vec3(0.04f, 0.04f, 0.04f)));
    state->addUniform(new osg::Uniform("DiffuseMaterial",osg::Vec3(0.0f, 0.75f, 0.75f)));
    state->addUniform(new osg::Uniform("LightPosition",osg::Vec3(0.25f, 0.25f, 1.0f)));
    state->addUniform(tessInnerU.get());
    state->addUniform(tessOuterU.get());
    state->setAttribute(new osg::PatchParameter(3));
    state->setAttribute(program.get());

    // switch on the uniforms that track the modelview and projection matrices
    osgViewer::Viewer::Windows windows;
    viewer.getWindows(windows);
    for(osgViewer::Viewer::Windows::iterator itr = windows.begin();
        itr != windows.end();
        ++itr)
    {
        osg::State *s=(*itr)->getState();
        s->setUseModelViewAndProjectionUniforms(true);
        s->setUseVertexAttributeAliasing(true);
    }

    viewer.addEventHandler(new KeyboardEventHandler(tessInnerU, tessOuterU));
    viewer.addEventHandler(new osgViewer::StatsHandler);
    viewer.setSceneData(geode.get());
    return viewer.run();
}

