/* This file is part of Clementine.
   Copyright 2004, Enrico Ros <eros.kde@email.it>
   Copyright 2009, David Sansome <davidsansome@gmail.com>
   Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
   Copyright 2014, John Maguire <john.maguire@gmail.com>

   Clementine 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 3 of the License, or
   (at your option) any later version.

   Clementine 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 Clementine.  If not, see <http://www.gnu.org/licenses/>.
*/

/* Original Author:  Enrico Ros  <eros.kde@email.it>  2004
 */

#include <config.h>

#ifdef HAVE_QGLWIDGET

#include <kdebug.h>
#include <kstandarddirs.h>
#include <qimage.h>
#include <sys/time.h>

#include <cmath>
#include <cstdlib>

#include "glanalyzer3.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}) {}
  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:
  explicit 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(locate("data", "amarok/data/ball.png"), ballTexture);
  loadTexture(locate("data", "amarok/data/grid.png"), gridTexture);

  balls.setAutoDelete(true);
  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;
  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 = static_cast<float>(w) / static_cast<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, nullptr);
  show.timeStamp = static_cast<double>(tv.tv_sec) +
                   static_cast<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, nullptr);
  double currentTime = static_cast<double>(tv.tv_sec) +
                       static_cast<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 / static_cast<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
