/************************************************************************
*
* Copyright 2010-2012 Jakob Leben (jakob.leben@gmail.com)
*
* This file is part of Qt GUI for SuperCollider.
*
* 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 "QcScopeShm.h"
#include "scope_shm_interface.hpp"
#include "../QcWidgetFactory.h"
#include "../debug.h"

#include <QPainter>
#include <QTimer>
#include <QResizeEvent>
#include <QWindow>
#include <QBackingStore>

QC_DECLARE_QWIDGET_FACTORY(QcScopeShm);

QcScopeShm::QcScopeShm() :
  _srvPort(-1),
  _scopeIndex(-1),
  _shm(new ScopeShm(this)),
  _running(false),
  _data(0),
  _availableFrames(0),
  xOffset( 0.f ),
  yOffset( 0.f ),
  xZoom( 1.f ),
  yZoom( 1.f ),
  _style( 0 ),
  _bkg( QColor(0,0,0) ),
  _fill( true )
{
  setAttribute( Qt::WA_OpaquePaintEvent, true );
  setAutoFillBackground(false);

  setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
  
  timer = new QTimer( this );
  timer->setInterval( 50 );
  connect( timer, SIGNAL( timeout() ), this, SLOT( updateScope() ) );
}

QcScopeShm::~QcScopeShm()
{
  stop();
}

void QcScopeShm::setServerPort( int port )
{
  if( _running ) {
    qcWarningMsg( "QScope: Can not change server port while running!" );
    return;
  }

  _srvPort = port;
}

void QcScopeShm::setBufferNumber( int n )
{
  if( _running ) {
    // TODO: release used reader?
    initScopeReader( _shm, n );
  }
  _scopeIndex = n;
}

void QcScopeShm::setWaveColors( const QVariantList & newColors )
{
  colors.clear();
  Q_FOREACH( const QVariant & var, newColors ) {
    QColor color = var.value<QColor>();
    if( !color.isValid() )
      colors.append( QColor( 0,0,0 ) );
    else
      colors.append( color );
  }
}

int QcScopeShm::updateInterval() const {
  return timer->interval();
}

void QcScopeShm::setUpdateInterval( int interval ) {
  timer->setInterval( qMax(0, interval) );
}

void QcScopeShm::start()
{
  if( _running ) return;
  if( _srvPort < 0 || _scopeIndex < 0 ) return;

  connectSharedMemory( _srvPort );
  if( !_shm->client ) {
    stop();
    return;
  }

  initScopeReader( _shm, _scopeIndex );

  timer->start();

  _running = true;
}

void QcScopeShm::stop()
{
  // TODO: release used reader?

  delete _shm->client;
  _shm->client = 0;

  timer->stop();

  _running = false;
}

void QcScopeShm::updateScope()
{
  bool valid = _shm->reader.valid();
  //qcDebugMsg(1, tr("valid = %1").arg(valid));
  if(!valid) return;

  bool ok = _shm->reader.pull( _availableFrames );
  //qcDebugMsg(1, tr("Got %1 frames").arg(_availableFrames) );
  if(ok) {
    _data = _shm->reader.data();
    update();
  }
}

void QcScopeShm::resizeEvent ( QResizeEvent * ev )
{
  qreal ratio = backingStore()->window()->devicePixelRatio();
  _pixmap = QPixmap(ev->size() * ratio);
  _pixmap.setDevicePixelRatio(ratio);
}

void QcScopeShm::paintEvent ( QPaintEvent * event )
{
  Q_UNUSED( event );

  QPainter p;

  _pixmap.fill( _bkg );

  if( _running && _availableFrames ) {

    int chanCount = _shm->reader.channels();
    int maxFrames = _shm->reader.max_frames();
    QRect area (_pixmap.rect());
    area.setSize(area.size() / _pixmap.devicePixelRatio());
    
    p.begin(&_pixmap);

    switch (_style) {
      case 0:
        paint1D( false, chanCount, maxFrames, _availableFrames, area, p );
        break;
      case 1:
        paint1D( true, chanCount, maxFrames, _availableFrames, area, p );
        break;
      case 2:
        paint2D( chanCount, maxFrames, _availableFrames, area, p );
        break;
    }

    p.end();
  }

  p.begin(this);
  p.drawPixmap(0, 0, _pixmap);
}

void QcScopeShm::paint1D( bool overlapped, int chanCount, int maxFrames, int frameCount,
                          const QRect &area, QPainter & painter )
{
  //qcDebugMsg( 0, tr("Drawing: data %1 / channels %2 / max-size %3").arg(_data!=0).arg(chanCount).arg(maxFrames) );

  if( frameCount < 2 || area.width() < 1 || area.height() < 1 ) return;

  float yRatio = - yZoom * area.height() * 0.5;
  if( !overlapped ) yRatio /= chanCount;
  float yHeight = area.height();
  if( !overlapped ) yHeight /= chanCount;
  QPen pen;
  pen.setWidth(0);  // width==0 means width 1 regardless of transformations
  pen.setCapStyle( Qt::FlatCap );

  if( frameCount < area.width() )
  {
    float xRatio = xZoom * area.width() / (frameCount-1);

    for( int ch = 0; ch < chanCount; ch++ ) {
      float *frameData = _data + (ch * maxFrames); //frame vector
      float yOrigin = yHeight * (overlapped ? 0.5 : ch + 0.5);
      QColor strokeColor = ch < colors.count() ? colors[ch] : QColor(255,255,255);
      QColor fillColor(strokeColor); fillColor.setAlpha(0.65 * 255);
      pen.setColor( strokeColor );

      painter.save();
      painter.translate( area.x(), area.y() + yOrigin );
      painter.scale( xRatio, yRatio );
      painter.setPen(pen);

      QPainterPath path;
      path.moveTo( xOffset, frameData[0] );
      for( int f = 1; f < frameCount; ++f )
        path.lineTo( xOffset + f, frameData[f] );

      if (_fill) {
        path.lineTo(xOffset + frameCount, 0);
        path.lineTo(0, 0);
        path.lineTo(xOffset, frameData[0]);
        
        painter.fillPath(path, QBrush(fillColor));
      }
      
      painter.drawPath(path);

      painter.restore();
    }
  }
  else
  {
    int w = area.width();
    float fpp = frameCount / (float) w; // frames per x pixel

    for( int ch = 0; ch < chanCount; ch++ )
    {
      float *frameData = _data + (ch * maxFrames); //frame vector
      float yOrigin = yHeight * (overlapped ? 0.5 : ch + 0.5);
      QColor strokeColor = ch < colors.count() ? colors[ch] : QColor(255,255,255);
      QColor fillColor(strokeColor); fillColor.setAlpha(0.65 * 255);

      painter.save();
      painter.translate( area.x(), area.y() + yOrigin );
      painter.setPen(pen);
      
      qreal ratio = 1.0 / _pixmap.devicePixelRatio();

      QPainterPath pathLine;
      QPainterPath pathFill;

      int frame = 1;
      float min, max;
      min = max = frameData[0];

      for( qreal pixel = 0; pixel < w; pixel += ratio )
      {
        for( ; frame < fpp * pixel; ++frame )
        {
          float const d = frameData[frame];
          if( d < min ) min = d;
          if( d > max ) max = d;
        }

        // Make sure min is always lesser; otherwise if no frames are read
        // this produces odd-looking artifacts as min and max are accidentally
        // swapped twice and a line-pixel-line sequence is drawn.
        // TODO: interpolate in this case somehow?
        if (min > max)
          std::swap( min, max );

        float y = max * yRatio;
        pathLine.moveTo( pixel, y );
        y = qMax( min * yRatio, y + 1 );
        pathLine.lineTo( pixel, y );
        
        if (_fill) {
          pathFill.moveTo( pixel, y );
          pathFill.lineTo( pixel, 0 );
        }
        
        // flip min/max to ensure continuity
        std::swap( min, max );
      }
      
      pen.setColor(strokeColor);
      painter.strokePath(pathLine, pen);
      
      if (_fill) {
        pen.setColor(fillColor);
        painter.strokePath(pathFill, pen);
      }

      painter.restore();
    }
  }
}

void QcScopeShm::paint2D( int chanCount, int maxFrames, int frameCount,
                          const QRect &area, QPainter & painter )
{
  int minSize = qMin( area.width(), area.height() );
  // NOTE: use yZoom for both axis, since both represent value, as opposed to index
  float xRatio = yZoom * minSize * 0.5;
  float yRatio = -yZoom * minSize * 0.5;
  QPoint center = area.center();
  QPen pen;
  pen.setWidth(0);  // width==0 means width 1 regardless of transformations
  pen.setCapStyle( Qt::FlatCap );
  pen.setColor(colors.count() ? colors[0] : QColor(255,255,255));
    
  painter.setPen(pen);
  painter.translate( center.x(), center.y() );
  painter.scale( xRatio, yRatio );

  QPainterPath path;

  if( chanCount >= 2 )
  {
    float *data1 = _data;
    float *data2 = _data + maxFrames;

    path.moveTo( data1[0], data2[0] );
    for( int f = 1; f < frameCount; ++f )
      path.lineTo( data1[f], data2[f] );
  }
  else
  {
    float *data1 = _data;
    path.moveTo( data1[0], 0.f );
    for( int f = 1; f < frameCount; ++f )
      path.lineTo( data1[f], 0.f );
  }

  painter.drawPath(path);
}

void QcScopeShm::connectSharedMemory( int port )
{
  try {
      server_shared_memory_client * client = new server_shared_memory_client(port);
      _shm->client = client;
      qcDebugMsg(1,"Shared memory connected");
  } catch (std::exception & e) {
      _shm->client = 0;
      qcErrorMsg(QStringLiteral("Cannot connect to shared memory: %1").arg(e.what()) );
  }
}

void QcScopeShm::initScopeReader( ScopeShm *shm, int index )
{
  shm->reader = shm->client->get_scope_buffer_reader( index );
  qcDebugMsg(1,QStringLiteral("Initialized scope buffer reader for index %1.").arg(index));
}
