// Copyright (c) 2008 Sundog Software, LLC. All rights reserved worldwide.

#include "SkyDrawable.h"
#include "SilverLining.h"
#include "AtmosphereReference.h"

#include <GL/gl.h>
#include <GL/glu.h>
#include <assert.h>

using namespace SilverLining;

SkyDrawable::SkyDrawable(osgViewer::Viewer* view)
        : osg::Drawable()
        , _view(view)
		, _skyboxSize(0)
        , _cloudShadowTexgen(0)
        , _cloudShadowTextureWhiteSubstitute(0)
        , _cloudShadowTextureStage(7)
        , _cloudShadowTexgenStage(7)
{
    setDataVariance(osg::Object::DYNAMIC);
    setUseVertexBufferObjects(false);
    setUseDisplayList(false);

    { // Create white fake texture for use if no cloud layer casting shadows is present
       osg::Image * image = new osg::Image;
       image->allocateImage( 1, 1, 1, GL_LUMINANCE, GL_UNSIGNED_BYTE );
       image->data()[0] = 0xFF;
        
       _cloudShadowTextureWhiteSubstitute = new osg::Texture2D( image );
       _cloudShadowTextureWhiteSubstitute->setWrap(osg::Texture2D::WRAP_S,osg::Texture2D::REPEAT);
       _cloudShadowTextureWhiteSubstitute->setWrap(osg::Texture2D::WRAP_T,osg::Texture2D::REPEAT);
       _cloudShadowTextureWhiteSubstitute->setFilter(osg::Texture2D::MIN_FILTER,osg::Texture2D::NEAREST);
       _cloudShadowTextureWhiteSubstitute->setFilter(osg::Texture2D::MAG_FILTER,osg::Texture2D::NEAREST);
    }

    { // Create cloud shadow casting texgen
       _cloudShadowTexgen = new osg::TexGen();
       _cloudShadowTexgen->setMode( osg::TexGen::EYE_LINEAR ); 
    }

    _cloudShadowCoordMatrixUniform = new osg::Uniform( "cloudShadowCoordMatrix", osg::Matrix() );
    _view->getSceneData()->getOrCreateStateSet()->addUniform( _cloudShadowCoordMatrixUniform );

    _view->getSceneData()->getOrCreateStateSet()->addUniform
                                                        ( new osg::Uniform( "cloudShadowTexture", 7 ) );

}

void SkyDrawable::setLighting(SilverLining::Atmosphere *atmosphere) const
{
    osg::Light *light = _view->getLight();
    osg::Vec4 ambient, diffuse;
    osg::Vec3 direction;

    if (atmosphere && light)
    {
        float ra, ga, ba, rd, gd, bd, x, y, z;
        atmosphere->GetAmbientColor(&ra, &ga, &ba);
        atmosphere->GetSunOrMoonColor(&rd, &gd, &bd);
        atmosphere->GetSunOrMoonPosition(&x, &y, &z);

        direction = osg::Vec3(x, y, z);
        // For shadow purpose use fraction of the ambient  otherwise shadows are hard to notice
        ambient = osg::Vec4(0.1 * ra, 0.1 * ga, 0.1 * ba, 1.0);
        diffuse = osg::Vec4(rd, gd, bd, 1.0);

        direction.normalize();

        light->setAmbient(ambient);
        light->setDiffuse(diffuse);
        light->setSpecular(osg::Vec4(0,0,0,1));
        light->setPosition(osg::Vec4(direction.x(), direction.y(), direction.z(), 0));
    }
}

void SkyDrawable::setShadow(SilverLining::Atmosphere *atmosphere, osg::RenderInfo & renderInfo ) const
{    
    if (!atmosphere || !renderInfo.getState()) return;

    osg::State & state = *renderInfo.getState(); 

    state.setActiveTextureUnit(_cloudShadowTextureStage);
    _cloudShadowTextureWhiteSubstitute->apply( state );

    osg::Matrix cloudShadowProjection;
    cloudShadowProjection.makeIdentity();

    // Tell state about our changes
    state.haveAppliedTextureAttribute(_cloudShadowTextureStage, _cloudShadowTextureWhiteSubstitute);

    // Find cloud layer casting shadows and bind its texture
    SL_MAP(int, CloudLayer*) &cloudLayers = atmosphere->GetConditions()->GetCloudLayers();
    for( SL_MAP(int, CloudLayer*)::iterator it = cloudLayers.begin();
         it != cloudLayers.end(); 
         ++it )
    {
        CloudLayer * cloudLayer = it->second;
        double m[4][4];

        if( !cloudLayer->SupportsShadowMaps() ||
            !cloudLayer->BindShadowMap( _cloudShadowTextureStage, &(m[0][0]) ) )
                continue;

        // Lets transpose the data to store in row major OSG matrix 
        cloudShadowProjection.set( m[0][0], m[1][0], m[2][0], m[3][0],
                                   m[0][1], m[1][1], m[2][1], m[3][1],
                                   m[0][2], m[1][2], m[2][2], m[3][2],
                                   m[0][3], m[1][3], m[2][3], m[3][3] );

        cloudShadowProjection = renderInfo.getCurrentCamera()->getInverseViewMatrix() *
                                cloudShadowProjection *
                                osg::Matrix::translate(1.0,1.0,1.0) *
                                osg::Matrix::scale(0.5f,0.5f,0.5f);
        break;
    }

    _cloudShadowCoordMatrixUniform->set( cloudShadowProjection );       
    _cloudShadowTexgen->setPlanesFromMatrix( cloudShadowProjection );

    // Now goes tricky part of the code !!!
    // We need to interact with OSG during render phase in delicate way and
    // not go out of sync with OpenGL states recorded by osg::State. 

    // Change texture stage to proper one
    state.setActiveTextureUnit(_cloudShadowTexgenStage);
    
    // We set texgen with identity on OpenGL modelview matrix            
    // since our TexGen was already premultiplied with inverse view.
    // This method minimizes precision errors as we used OSG double matrices.
    // If we had not premultiplied the texgen then we would need to set
    // view on OpenGL modelview matrix which would cause internal OpenGL
    // premutiplication of TexGen by inverse view using float matrices.
    // Such float computation may cause shadow texture jittering between 
    // frames if scenes/terrains spanning large coordinate ranges are used.
    state.applyModelViewMatrix( NULL );

    // Now apply our TexGen
    _cloudShadowTexgen->apply( state );

    state.applyTextureMode(_cloudShadowTexgenStage,GL_TEXTURE_GEN_S,osg::StateAttribute::ON);
    state.applyTextureMode(_cloudShadowTexgenStage,GL_TEXTURE_GEN_T,osg::StateAttribute::ON);
    state.applyTextureMode(_cloudShadowTexgenStage,GL_TEXTURE_GEN_R,osg::StateAttribute::ON);
    state.applyTextureMode(_cloudShadowTexgenStage,GL_TEXTURE_GEN_Q,osg::StateAttribute::ON);

    // Tell state about our changes
    state.haveAppliedTextureAttribute(_cloudShadowTexgenStage, _cloudShadowTexgen);

    // Set this TexGen as a global default 
    state.setGlobalDefaultTextureAttribute(_cloudShadowTexgenStage, _cloudShadowTexgen);
}


void SkyDrawable::initializeSilverLining(AtmosphereReference *ar) const
{
    if (ar && !ar->atmosphereInitialized)
    {
        ar->atmosphereInitialized = true; // only try once.
		SilverLining::Atmosphere *atmosphere = ar->atmosphere;

		if (atmosphere)
        {
			srand(1234); // constant random seed to ensure consistent clouds across windows

            // Update the path below to where you installed SilverLining's resources folder.
            const char *slPath = getenv("SILVERLINING_PATH");
            if (!slPath)
            {
                printf("Can't find SilverLining; set the SILVERLINING_PATH environment variable ");
                printf("to point to the directory containing the SDK.\n");
                exit(0);
            }
            std::string resPath(slPath);
            resPath += "\\Resources\\";

            atmosphere->SetConfigOption( "render-offscreen", "yes" );

            int ret = atmosphere->Initialize(SilverLining::Atmosphere::OPENGL, resPath.c_str(),
                                             true, 0);
            if (ret != SilverLining::Atmosphere::E_NOERROR)
            {
                printf("SilverLining failed to initialize; error code %d.\n", ret);
                printf("Check that the path to the SilverLining installation directory is set properly ");
                printf("in SkyDrawable.cpp (in SkyDrawable::initializeSilverLining)\n");
                exit(0);
            }

            // Let SilverLining know which way is up. OSG usually has Z going up.
            atmosphere->SetUpVector(0, 0, 1);
            atmosphere->SetRightVector(1, 0, 0);

            //atmosphere->ShowFramerate(true);

            // Set our location (change this to your own latitude and longitude)
            SilverLining::Location loc;
            loc.SetAltitude(0);
            loc.SetLatitude(45);
            loc.SetLongitude(-122);
            atmosphere->GetConditions()->SetLocation(loc);

            // Set the time to noon in PST
            SilverLining::LocalTime t;
            t.SetFromSystemTime();
			t.SetHour(12);
			t.SetTimeZone(PST);
            atmosphere->GetConditions()->SetTime(t);

            // Center the clouds around the camera's initial position
            osg::Vec3d pos = _view->getCameraManipulator()->getMatrix().getTrans();

            // This config option must set to get shadow clouds
            // if not set shadow clouds are black and hence overall shadow gets black too
            atmosphere->SetConfigOption( "cumulus-lighting-quick-and-dirty", "no" );

            SilverLining::CloudLayer *cumulusCongestusLayer;
            cumulusCongestusLayer = SilverLining::CloudLayerFactory::Create(CUMULUS_CONGESTUS);
            cumulusCongestusLayer->SetIsInfinite(false);
            cumulusCongestusLayer->SetBaseAltitude(4000);
            cumulusCongestusLayer->SetThickness(500);
            cumulusCongestusLayer->SetBaseLength(40000);
            cumulusCongestusLayer->SetBaseWidth(40000);
            cumulusCongestusLayer->SetDensity(0.3);

            // Note, we pass in X and -Y since this accepts "east" and "south" coordinates.
            cumulusCongestusLayer->SetLayerPosition(pos.x(), -pos.y());
            cumulusCongestusLayer->SeedClouds(*atmosphere);

            // Make sure shadow textures are generated 
            cumulusCongestusLayer->GenerateShadowMaps(true);

            atmosphere->GetConditions()->AddCloudLayer(cumulusCongestusLayer);
            atmosphere->GetConditions()->ClearWindVolumes();
        }
    }
}

void SkyDrawable::drawImplementation(osg::RenderInfo& renderInfo) const
{
	SilverLining::Atmosphere *atmosphere = 0;

	AtmosphereReference *ar = dynamic_cast<AtmosphereReference *>(renderInfo.getCurrentCamera()->getUserData());
	if (ar) atmosphere = ar->atmosphere;

	renderInfo.getState()->disableAllVertexArrays();

    if (atmosphere)
    {
        initializeSilverLining(ar);
        atmosphere->DrawSky(true, false, _skyboxSize);
        setLighting(atmosphere);
        setShadow(atmosphere, renderInfo);
    }

	renderInfo.getState()->dirtyAllVertexArrays();
}
