/****************************************************************************************
 * Copyright (c) 2004 Enrico Ros <eros.kde@email.it>                                    *
 *                                                                                      *
 * This program is free software; you can redistribute it and/or modify it under        *
 * the terms of the GNU General Public License as published by the Free Software        *
 * Foundation; either version 2 of the License, or (at your option) any later           *
 * version.                                                                             *
 *                                                                                      *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
 *                                                                                      *
 * You should have received a copy of the GNU General Public License along with         *
 * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
 ****************************************************************************************/

#include "glanalyzer3.h"

#include <config-amarok.h>  

#ifdef HAVE_QGLWIDGET

#include <cmath>
#include <cstdlib>
#include <kdebug.h>
#include <kstandarddirs.h>
#include <QImage>
#include <sys/time.h>

#ifndef HAVE_FABSF
inline float fabsf(float f)
{
    return f < 0.f ? -f : f;
}
#endif


class Ball
{
    public:
    Ball() : x( drand48() - drand48() ), y( 1 - 2.0 * drand48() ),
        z( drand48() ), vx( 0.0 ), vy( 0.0 ), vz( 0.0 ),
        mass( 0.01 + drand48()/10.0 )
        //,color( (float[3]) { 0.0, drand48()*0.5, 0.7 + drand48() * 0.3 } )
    {
        //this is because GCC < 3.3 can't compile the above line, we aren't sure why though
        color[0] = 0.0; color[1] = drand48()*0.5; color[2] = 0.7 + drand48() * 0.3;
    };

    float x, y, z, vx, vy, vz, mass;
    float color[3];

    void updatePhysics( float dT )
    {
        x += vx * dT;                // position
        y += vy * dT;                // position
        z += vz * dT;                // position
        if ( y < -0.8 ) vy = fabsf( vy );
        if ( y > 0.8 ) vy = -fabsf( vy );
        if ( z < 0.1 ) vz = fabsf( vz );
        if ( z > 0.9 ) vz = -fabsf( vz );
        vx += (( x > 0 ) ? 4.94 : -4.94) * dT;    // G-force
        vx *= (1 - 2.9 * dT);            // air friction
        vy *= (1 - 2.9 * dT);            // air friction
        vz *= (1 - 2.9 * dT);            // air friction
    }
};

class Paddle
{
    public:
    Paddle( float xPos ) : onLeft( xPos < 0 ), mass( 1.0 ),
        X( xPos ), x( xPos ), vx( 0.0 ) {};

    void updatePhysics( float dT )
    {
        x += vx * dT;                // posision
        vx += (1300 * (X - x) / mass) * dT;        // elasticity
        vx *= (1 - 4.0 * dT);            // air friction
    }

    void renderGL()
    {
        glBegin( GL_TRIANGLE_STRIP );
         glColor3f( 0.0f, 0.1f, 0.3f );
         glVertex3f( x, -1.0f, 0.0 );
         glVertex3f( x, 1.0f, 0.0 );
         glColor3f( 0.1f, 0.2f, 0.6f );
         glVertex3f( x, -1.0f, 1.0 );
         glVertex3f( x, 1.0f, 1.0 );
        glEnd();
    }

    void bounce( Ball * ball )
    {
        if ( onLeft && ball->x < x )
        {
        ball->vx = vx * mass / (mass + ball->mass) + fabsf( ball->vx );
        ball->vy = (drand48() - drand48()) * 1.8;
        ball->vz = (drand48() - drand48()) * 0.9;
        ball->x = x;
        }
        else if ( !onLeft && ball->x > x )
        {
        ball->vx = vx * mass / (mass + ball->mass) - fabsf( ball->vx );
        ball->vy = (drand48() - drand48()) * 1.8;
        ball->vz = (drand48() - drand48()) * 0.9;
        ball->x = x;
        }
    }

    void impulse( float strength )
    {
        if ( (onLeft && strength > vx) || (!onLeft && strength < vx) )
        vx += strength;
    }

    private:
    bool onLeft;
    float mass, X, x, vx;
};


GLAnalyzer3::GLAnalyzer3( QWidget *parent ):
Analyzer::Base3D(parent, 15)
{
    //initialize openGL context before managing GL calls
    makeCurrent();
    loadTexture( KStandardDirs::locate("data","amarok/data/ball.png"), ballTexture );
    loadTexture( KStandardDirs::locate("data","amarok/data/grid.png"), gridTexture );

    leftPaddle = new Paddle( -1.0 );
    rightPaddle = new Paddle( 1.0 );
    for ( int i = 0; i < NUMBER_OF_BALLS; i++ )
    balls.append( new Ball() );

    show.colorK = 0.0;
    show.gridScrollK = 0.0;
    show.gridEnergyK = 0.0;
    show.camRot = 0.0;
    show.camRoll = 0.0;
    show.peakEnergy = 1.0;
    frame.silence = true;
    frame.energy = 0.0;
    frame.dEnergy = 0.0;
}

GLAnalyzer3::~GLAnalyzer3()
{
    freeTexture( ballTexture );
    freeTexture( gridTexture );
    delete leftPaddle;
    delete rightPaddle;
    qDeleteAll( balls );
    balls.clear();
}

void GLAnalyzer3::initializeGL()
{
    // Set a smooth shade model
    glShadeModel(GL_SMOOTH);

    // Disable depth test (all is drawn 'z-sorted')
    glDisable( GL_DEPTH_TEST );

    // Set blending function (Alpha addition)
    glBlendFunc( GL_SRC_ALPHA, GL_ONE );

    // Clear frame with a black background
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}

void GLAnalyzer3::resizeGL( int w, int h )
{
    // Setup screen. We're going to manually do the perspective projection
    glViewport( 0, 0, (GLint)w, (GLint)h );
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    glFrustum( -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 4.5f );

    // Get the aspect ratio of the screen to draw 'circular' particles
    float ratio = (float)w / (float)h;
    if ( ratio >= 1.0 ) {
    unitX = 0.34 / ratio;
    unitY = 0.34;
    } else {
    unitX = 0.34;
    unitY = 0.34 * ratio;
    }

    // Get current timestamp.
    timeval tv;
    gettimeofday( &tv, NULL );
    show.timeStamp = (double)tv.tv_sec + (double)tv.tv_usec/1000000.0;
}

void GLAnalyzer3::paused()
{
    analyze( Scope() );
}

void GLAnalyzer3::analyze( const Scope &s )
{
    // compute the dTime since the last call
    timeval tv;
    gettimeofday( &tv, NULL );
    double currentTime = (double)tv.tv_sec + (double)tv.tv_usec/1000000.0;
    show.dT = currentTime - show.timeStamp;
    show.timeStamp = currentTime;

    // compute energy integrating frame's spectrum
    if ( !s.empty() )
    {
    int bands = s.size();
    float currentEnergy = 0,
          maxValue = 0;
    // integrate spectrum -> energy
    for ( int i = 0; i < bands; i++ )
    {
        float value = s[i];
        currentEnergy += value;
        if ( value > maxValue )
        maxValue = value;
    }
    currentEnergy *= 100.0 / (float)bands;
    // emulate a peak detector: currentEnergy -> peakEnergy (3tau = 30 seconds)
    show.peakEnergy = 1.0 + ( show.peakEnergy - 1.0 ) * exp( - show.dT / 10.0 );
    if ( currentEnergy > show.peakEnergy )
        show.peakEnergy = currentEnergy;
    // check for silence
    frame.silence = currentEnergy < 0.001;
    // normalize frame energy against peak energy and compute frame stats
    currentEnergy /= show.peakEnergy;
    frame.dEnergy = currentEnergy - frame.energy;
    frame.energy = currentEnergy;
    } else
    frame.silence = true;

    // update the frame
    updateGL();
}

void GLAnalyzer3::paintGL()
{
    // limit max dT to 0.05 and update color and scroll constants
    if ( show.dT > 0.05 )
    show.dT = 0.05;
    show.colorK += show.dT * 0.4;
    if ( show.colorK > 3.0 )
    show.colorK -= 3.0;
    show.gridScrollK += 0.2 * show.peakEnergy * show.dT;

    // Switch to MODEL matrix and clear screen
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    glClear( GL_COLOR_BUFFER_BIT );

    // Draw scrolling grid
    if ( (show.gridEnergyK > 0.05) || (!frame.silence && frame.dEnergy < -0.3) )
    {
    show.gridEnergyK *= exp( -show.dT / 0.1 );
    if ( -frame.dEnergy > show.gridEnergyK )
        show.gridEnergyK = -frame.dEnergy*2.0;
    float gridColor[4] = { 0.0, 1.0, 0.6, show.gridEnergyK };
    drawScrollGrid( show.gridScrollK, gridColor );
    }

    // Roll camera up/down handling the beat
    show.camRot += show.camRoll * show.dT;        // posision
    show.camRoll -= 400 * show.camRot * show.dT;    // elasticity
    show.camRoll *= (1 - 2.0 * show.dT);        // friction
    if ( !frame.silence && frame.dEnergy > 0.4 )
    show.camRoll += show.peakEnergy*2.0;
    glRotatef( show.camRoll / 2.0, 1,0,0 );

    // Translate the drawing plane
    glTranslatef( 0.0f, 0.0f, -1.8f );

    // Draw upper/lower planes and paddles
    drawHFace( -1.0 );
    drawHFace( 1.0 );
    leftPaddle->renderGL();
    rightPaddle->renderGL();

    // Draw Balls
    if ( ballTexture ) {
    glEnable( GL_TEXTURE_2D );
    glBindTexture( GL_TEXTURE_2D, ballTexture );
    } else
    glDisable( GL_TEXTURE_2D );
    glEnable( GL_BLEND );
    Ball * ball = balls.first();
    for ( ; ball; ball = balls.next() )
    {
    float color[3],
          angle = show.colorK;
    // Rotate the color based on 'angle' value [0,3)
    if ( angle < 1.0 )
    {
        color[ 0 ] = ball->color[ 0 ] * (1 - angle) + ball->color[ 1 ] * angle;
        color[ 1 ] = ball->color[ 1 ] * (1 - angle) + ball->color[ 2 ] * angle;
        color[ 2 ] = ball->color[ 2 ] * (1 - angle) + ball->color[ 0 ] * angle;
    }
    else if ( angle < 2.0 )
    {
        angle -= 1.0;
        color[ 0 ] = ball->color[ 1 ] * (1 - angle) + ball->color[ 2 ] * angle;
        color[ 1 ] = ball->color[ 2 ] * (1 - angle) + ball->color[ 0 ] * angle;
        color[ 2 ] = ball->color[ 0 ] * (1 - angle) + ball->color[ 1 ] * angle;
    }
    else
    {
        angle -= 2.0;
        color[ 0 ] = ball->color[ 2 ] * (1 - angle) + ball->color[ 0 ] * angle;
        color[ 1 ] = ball->color[ 0 ] * (1 - angle) + ball->color[ 1 ] * angle;
        color[ 2 ] = ball->color[ 1 ] * (1 - angle) + ball->color[ 2 ] * angle;
    }
    // Draw the dot and update its physics also checking at bounces
    glColor3fv( color );
    drawDot3s( ball->x, ball->y, ball->z, 1.0 );
    ball->updatePhysics( show.dT );
    if ( ball->x < 0 )
        leftPaddle->bounce( ball );
    else
        rightPaddle->bounce( ball );
    }
    glDisable( GL_BLEND );
    glDisable( GL_TEXTURE_2D );

    // Update physics of paddles
    leftPaddle->updatePhysics( show.dT );
    rightPaddle->updatePhysics( show.dT );
    if ( !frame.silence )
    {
    leftPaddle->impulse( frame.energy*3.0 + frame.dEnergy*6.0 );
    rightPaddle->impulse( -frame.energy*3.0 - frame.dEnergy*6.0 );
    }
}

void GLAnalyzer3::drawDot3s( float x, float y, float z, float size )
{
    // Circular XY dot drawing functions
    float sizeX = size * unitX,
      sizeY = size * unitY,
      pXm = x - sizeX,
      pXM = x + sizeX,
      pYm = y - sizeY,
      pYM = y + sizeY;
    // Draw the Dot
    glBegin( GL_QUADS );
    glTexCoord2f( 0, 0 );    // Bottom Left
    glVertex3f( pXm, pYm, z );
    glTexCoord2f( 0, 1 );    // Top Left
    glVertex3f( pXm, pYM, z );
    glTexCoord2f( 1, 1 );    // Top Right
    glVertex3f( pXM, pYM, z );
    glTexCoord2f( 1, 0 );    // Bottom Right
    glVertex3f( pXM, pYm, z );
    glEnd();

    // Shadow XZ drawing functions
    float sizeZ = size / 10.0,
      pZm = z - sizeZ,
      pZM = z + sizeZ,
      currentColor[4];
    glGetFloatv( GL_CURRENT_COLOR, currentColor );
    float alpha = currentColor[3],
      topSide = (y + 1) / 4,
      bottomSide = (1 - y) / 4;
    // Draw the top shadow
    currentColor[3] = topSide * topSide * alpha;
    glColor4fv( currentColor );
    glBegin( GL_QUADS );
    glTexCoord2f( 0, 0 );    // Bottom Left
    glVertex3f( pXm, 1, pZm );
    glTexCoord2f( 0, 1 );    // Top Left
    glVertex3f( pXm, 1, pZM );
    glTexCoord2f( 1, 1 );    // Top Right
    glVertex3f( pXM, 1, pZM );
    glTexCoord2f( 1, 0 );    // Bottom Right
    glVertex3f( pXM, 1, pZm );
    glEnd();
    // Draw the bottom shadow
    currentColor[3] = bottomSide * bottomSide * alpha;
    glColor4fv( currentColor );
    glBegin( GL_QUADS );
    glTexCoord2f( 0, 0 );    // Bottom Left
    glVertex3f( pXm, -1, pZm );
    glTexCoord2f( 0, 1 );    // Top Left
    glVertex3f( pXm, -1, pZM );
    glTexCoord2f( 1, 1 );    // Top Right
    glVertex3f( pXM, -1, pZM );
    glTexCoord2f( 1, 0 );    // Bottom Right
    glVertex3f( pXM, -1, pZm );
    glEnd();
}

void GLAnalyzer3::drawHFace( float y )
{
    glBegin( GL_TRIANGLE_STRIP );
    glColor3f( 0.0f, 0.1f, 0.2f );
    glVertex3f( -1.0f, y, 0.0 );
    glVertex3f( 1.0f, y, 0.0 );
    glColor3f( 0.1f, 0.6f, 0.5f );
    glVertex3f( -1.0f, y, 2.0 );
    glVertex3f( 1.0f, y, 2.0 );
    glEnd();
}

void GLAnalyzer3::drawScrollGrid( float scroll, float color[4] )
{
    if ( !gridTexture )
    return;
    glMatrixMode( GL_TEXTURE );
    glLoadIdentity();
    glTranslatef( 0.0, -scroll, 0.0 );
    glMatrixMode( GL_MODELVIEW );
    float backColor[4] = { 1.0, 1.0, 1.0, 0.0 };
    for ( int i = 0; i < 3; i++ )
    backColor[ i ] = color[ i ];
    glEnable( GL_TEXTURE_2D );
    glBindTexture( GL_TEXTURE_2D, gridTexture );
    glEnable( GL_BLEND );
    glBegin( GL_TRIANGLE_STRIP );
     glColor4fv( color );    // top face
    glTexCoord2f( 0.0f, 1.0f );
    glVertex3f( -1.0f, 1.0f, -1.0f );
    glTexCoord2f( 1.0f, 1.0f );
    glVertex3f( 1.0f, 1.0f, -1.0f );
     glColor4fv( backColor );    // central points
    glTexCoord2f( 0.0f, 0.0f );
    glVertex3f( -1.0f, 0.0f, -3.0f );
    glTexCoord2f( 1.0f, 0.0f );
    glVertex3f( 1.0f, 0.0f, -3.0f );
     glColor4fv( color );    // bottom face
    glTexCoord2f( 0.0f, 1.0f );
    glVertex3f( -1.0f, -1.0f, -1.0f );
    glTexCoord2f( 1.0f, 1.0f );
    glVertex3f( 1.0f, -1.0f, -1.0f );
    glEnd();
    glDisable( GL_BLEND );
    glDisable( GL_TEXTURE_2D );
    glMatrixMode( GL_TEXTURE );
    glLoadIdentity();
    glMatrixMode( GL_MODELVIEW );
}

bool GLAnalyzer3::loadTexture( QString fileName, GLuint& textureID )
{
    //reset texture ID to the default EMPTY value
    textureID = 0;

    //load image
    QImage tmp;
    if ( !tmp.load( fileName ) )
    return false;

    //convert it to suitable format (flipped RGBA)
    QImage texture = QGLWidget::convertToGLFormat( tmp );
    if ( texture.isNull() )
    return false;

    //get texture number and bind loaded image to that texture
    glGenTextures( 1, &textureID );
    glBindTexture( GL_TEXTURE_2D, textureID );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexImage2D( GL_TEXTURE_2D, 0, 4, texture.width(), texture.height(),
    0, GL_RGBA, GL_UNSIGNED_BYTE, texture.bits() );
    return true;
}

void GLAnalyzer3::freeTexture( GLuint& textureID )
{
    if ( textureID > 0 )
    glDeleteTextures( 1, &textureID );
    textureID = 0;
}

#endif

