/*
 *
 *    soniK digital audio editor
 *    Copyright (C) 2003-2006  Robert Walker <rob@tenfoot.org.uk>
 *
 *    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, write to the Free Software
 *    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
#include "normaldisplayplugin.h"

#include "normaldisplaysettings.h"
#include "normaldisplayconfigdlg.h"

#include "wavewidget.h"
#include "data.h"
#include "sonik_util.h"
#include "sonik_sigproc.h"

#include <kcolorbutton.h>
#include <kapplication.h>
#include <kconfig.h>
#include <klocale.h>
#include <kgenericfactory.h>
#include <kdebug.h>

#include <qpainter.h>
#include <qlabel.h>

using Sonik::NormalDisplayPlugin;
using Sonik::WaveWidget;

namespace
{
  static const uint32_t kBorder = 10;
  static const uint32_t kCacheThreshold = 1024;
}

NormalDisplayPlugin::NormalDisplayPlugin(QObject* parent, const char* name,
                                         const QStringList& args)
  : Sonik::Display("normal", i18n("Normal"), parent, name, args),
    mConfigDlg(0)
{
  applyConfig();
}

NormalDisplayPlugin::~NormalDisplayPlugin()
{
}

Sonik::WaveWidget* NormalDisplayPlugin::makeWidget(const Data& data,
                                                   uint8_t channel,
                                                   QWidget* parent,
                                                   const char* name) const
{
  return new NormalDisplayPlugin::Widget(*this, data, channel, parent, name);
}

uint32_t NormalDisplayPlugin::verticalBorder() const
{
  return kBorder;
}

QWidget* NormalDisplayPlugin::makeConfigPage(QWidget* parent)
{
  mConfigDlg = new NormalDisplayConfigDlg(parent, "normal_display_config");
  mConfigDlg->backgroundColour->setColor(mBackgroundColour);
  mConfigDlg->sampleColour->setColor(mSampleColour);
  mConfigDlg->selectBackgroundColour->setColor(mSelectBackgroundColour);
  mConfigDlg->selectColour->setColor(mSelectColour);
  mConfigDlg->waveColour->setColor(mWavePen.color());
  mConfigDlg->gridColour->setColor(mGridPen.color());

  return mConfigDlg;
}

void NormalDisplayPlugin::applyConfigPage()
{
  NormalDisplaySettings::setBackgroundColour(mConfigDlg->backgroundColour->color());
  NormalDisplaySettings::setSampleColour(mConfigDlg->sampleColour->color());
  NormalDisplaySettings::setSelectBackgroundColour(mConfigDlg->selectBackgroundColour->color());
  NormalDisplaySettings::setSelectColour(mConfigDlg->selectColour->color());
  NormalDisplaySettings::setWaveColour(mConfigDlg->waveColour->color());
  NormalDisplaySettings::setGridColour(mConfigDlg->gridColour->color());
  NormalDisplaySettings::writeConfig();

  applyConfig();
}

void NormalDisplayPlugin::applyConfig()
{
  mBackgroundColour       = NormalDisplaySettings::backgroundColour();
  mSampleColour           = NormalDisplaySettings::sampleColour();
  mSelectBackgroundColour = NormalDisplaySettings::selectBackgroundColour();
  mSelectColour           = NormalDisplaySettings::selectColour();
  mWavePen                = QPen(NormalDisplaySettings::waveColour(), 1, Qt::SolidLine);
  mGridPen                = QPen(NormalDisplaySettings::gridColour(), 1, Qt::DotLine);
}

NormalDisplayPlugin::Widget::Widget(const NormalDisplayPlugin& display,
                                    const Data& data, uint8_t channel,
                                    QWidget* parent, const char* name)
  : Sonik::WaveWidget(data, channel, parent, name),
    mDisplay(display)
{
}

NormalDisplayPlugin::Widget::~Widget()
{
}

void NormalDisplayPlugin::Widget::render(QPainter& p, const QRect& r)
{
  // Y dimensions
  uint maxDisp = (rect().height() >> 1) - kBorder;
  uint vCentre = rect().top() + kBorder + maxDisp;

  // selection state
  bool selection = selLength() > 0;
  int selLeft, selRight;
  if (selection)
  {
    uint selEnd = selStart() + selLength() - 1;
    selLeft = Sonik::timeToScreenL(selStart(), scrollPos(), zoom());
    selRight = Sonik::timeToScreenU(selEnd, scrollPos(), zoom());
  }
  else
  {
    // cursor pos
    selLeft = Sonik::timeToScreenM(selStart(), scrollPos(), zoom());
    selRight = selLeft;
  }

  drawBackground(p, r, selection, selLeft, selRight);

  drawAxes(p, r, vCentre, maxDisp);

  drawSamples(p, r, vCentre, maxDisp, selection, selLeft, selRight);
}

void NormalDisplayPlugin::Widget::clearCache(CacheClearOp op, off_t start, size_t length)
{
  for (Cache::iterator cacheEntry = mCache.begin();
       cacheEntry != mCache.end();
       ++cacheEntry)
  {
    CacheKey k = cacheEntry.key();
    CacheData& c = cacheEntry.data();
    if (op == WaveWidget::LengthChanged)
    {
      c.resize((length / k) + 1);
    }
    else if (op == WaveWidget::DataChanged)
    {
      size_t i = start/k;
      size_t e = i + length / k + 2;
      for ( ; i < c.size() && i < e; ++i)
        c[i].valid = false;
    }
  }
}

// Clear background & draw selection
void NormalDisplayPlugin::Widget::drawBackground(QPainter& p, const QRect& r,
                                                 bool selection, int selLeft, int selRight)
{
  p.setPen(Qt::NoPen);

  if (selection && selLeft <= r.right() && selRight >= r.left())
  {
    // area before selection
    if (selLeft > r.left())
    {
      p.fillRect(r.left(), r.top(),
                 selLeft - r.left(), r.height(),
                 mDisplay.backgroundColour());
    }

    // selection
    int sl = selLeft < r.left() ? r.left() : selLeft;
    int sr = selRight > r.right() ? r.right() : selRight;
    p.fillRect(sl, r.top(),
               sr - sl + 1, r.height(),
               mDisplay.selectBackgroundColour());

    // area after selection
    if (selRight < r.right())
    {
      p.fillRect(selRight + 1, r.top(),
                 r.right() - selRight, r.height(),
                 mDisplay.backgroundColour());
    }
  }
  else
  {
    // no selection or selection not visible

    p.fillRect(r, mDisplay.backgroundColour());

    // selLeft is cursor pos
    if (!selection &&
        selLeft >= r.left() && selLeft <= r.right())
    {
      p.setPen(mDisplay.selectBackgroundColour());
      p.drawLine(selLeft, r.top(), selLeft, r.bottom());
    }
  }
}


void NormalDisplayPlugin::Widget::drawAxes(QPainter& p, const QRect& r,
                                           int vCentre, int maxDisp)
{
  int xStart = r.left() - (r.left() % 6);
  p.setPen(mDisplay.gridPen());
  if (vCentre >= r.top() && vCentre <= r.bottom())
    p.drawLine(xStart, vCentre, r.right(), vCentre);
  if ((vCentre-maxDisp) >= r.top() && (vCentre-maxDisp) <= r.bottom())
    p.drawLine(xStart, (vCentre-maxDisp), r.right(), (vCentre-maxDisp));
  if ((vCentre+maxDisp) >= r.top() && (vCentre+maxDisp) <= r.bottom())
    p.drawLine(xStart, (vCentre+maxDisp), r.right(), (vCentre+maxDisp));
}


void NormalDisplayPlugin::Widget::drawSamples(QPainter& p, const QRect& r,
                                              int vCentre, int maxDisp,
                                              bool selection, int selLeft, int selRight)
{
  // calculate sample numbers
  uint start = scrollPos() + (int)(r.left() / zoom());
  uint end = scrollPos() + (int)(r.right() / zoom());

  if (start > data().length() - 1)
    start = data().length() - 1;

  if (end > data().length() - 1)
    end = data().length() - 1;

  uint length = end - start + 1;

  uint samplesPerPixel = (int)(1.0 / zoom());

  uint pre = (zoom() < 1.0) ? samplesPerPixel : 1;
  uint post = (zoom() < 1.0) ? samplesPerPixel : 1;

  uint validLength = pre + length + post;

  if (zoom() < 1.0)
    // several samples per pixel
    drawSamplesZoomedOut(p, r,
                         vCentre, maxDisp,
                         selection, selLeft, selRight,
                         start, end, length,
                         pre, post, validLength);
  else
    // several pixels per sample
    drawSamplesZoomedIn(p, r,
                        vCentre, maxDisp,
                        selection, selLeft, selRight,
                        start, end, length,
                        pre, post, validLength);
}


void Sonik::NormalDisplayPlugin::Widget::drawSamplesZoomedOut(QPainter& p, const QRect& r,
                                                              int vCentre, int maxDisp,
                                                              bool selection, int selLeft, int selRight,
                                                              uint start, uint /*end*/, uint /*length*/,
                                                              uint pre, uint post, uint validLength)
{
  uint samplesPerPixel = (int)(1.0 / zoom());

  uint pixelsLeft = r.width() + (pre + post) / samplesPerPixel;
  if (pixelsLeft * samplesPerPixel > validLength)
  {
    pixelsLeft = (validLength + samplesPerPixel - 1) / samplesPerPixel;
  }

  if (pixelsLeft == 0)
    return;

  Sonik::SampleBuffer min(pixelsLeft), max(pixelsLeft);
  getMinMaxData(start - pre, validLength, samplesPerPixel, min, max);

  Sample* iMin = min.data();
  Sample* iMax = max.data();
  Sample oMin = *iMin++, oMax = *iMax++;

  if (selection && r.left() >= selLeft && r.left() < selRight ||
      !selection && r.left() == selLeft)
    p.setPen(mDisplay.selectColour());
  else
    p.setPen(mDisplay.wavePen());

  for (int xPos = r.left();
       iMin != min.end() && iMax != max.end() && xPos <= r.right();
       xPos++, iMin++, iMax++)
  {
    int yTop = vCentre - (int)(maxDisp * QMAX(*iMax, oMin));
    int yBottom = vCentre - (int)(maxDisp * QMIN(*iMin, oMax));

    if (selection && xPos == selRight ||
        !selection && xPos == selLeft+1)
      p.setPen(mDisplay.wavePen());
    else if (xPos == selLeft)
      p.setPen(mDisplay.selectColour());

    if (yTop != yBottom)
      p.drawLine(xPos, yTop, xPos, yBottom);
    else
      p.drawPoint(xPos, yTop);

    oMin = *iMin;
    oMax = *iMax;
  }
}


void Sonik::NormalDisplayPlugin::Widget::drawSamplesZoomedIn(QPainter& p, const QRect& r,
                                                             int vCentre, int maxDisp,
                                                             bool selection, int selLeft, int selRight,
                                                             uint start, uint end, uint length,
                                                             uint pre, uint /*post*/, uint validLength)
{
  // Get data
  // TODO: chunk data
  Sonik::SampleBuffer buf(validLength);
  data().data(channel(), start - pre, validLength, buf);

  const Sonik::Sample* pData = buf.data();

  int xStep = (int)zoom();

  int xPos = r.left() - (r.left() % xStep); // round to previous sample
  int yPos;

  int lastXPos = xPos - xStep;
  int lastYPos = vCentre - (int)(maxDisp * *pData++);

  p.moveTo(lastXPos, lastYPos);
  if (selection && lastXPos >= selLeft && lastXPos < selRight)
    p.setPen(mDisplay.selectColour());
  else
    p.setPen(mDisplay.wavePen());


  for (size_t i = 0; i < length; i++)
  {
    yPos = vCentre - (int)(maxDisp * *pData++);

    if (selection)
    {
      if (lastXPos < selLeft && xPos >= selLeft)
      {
        // draw as 2 lines - outside and inside sel
        if (xStep > 1)
          p.lineTo(lastXPos + xStep / 2, (lastYPos + yPos) / 2);
        p.setPen(mDisplay.selectColour());
      }
      else if (lastXPos <= selRight && xPos > selRight)
      {
        // draw as 2 lines - inside and outide sel
        if (xStep > 1)
          p.lineTo(lastXPos + xStep / 2, (lastYPos + yPos) / 2);
        p.setPen(mDisplay.wavePen());
      }
    }

    p.lineTo(xPos, yPos);

    lastXPos = xPos;
    lastYPos = yPos;

    xPos += xStep;
  }

  // tail sample
  if (xPos <= r.right() + xStep && end < data().length()-1)
  {
    yPos = vCentre - (int)(maxDisp * *pData);

    if (selection)
    {
      if (lastXPos < selLeft && xPos > selLeft)
      {
        // draw as 2 lines - outside and inside sel
        p.lineTo(lastXPos + xStep / 2, (lastYPos + yPos) / 2);
        p.setPen(mDisplay.selectColour());
      }
      else if (lastXPos < selRight && xPos > selRight)
      {
        // draw as 2 lines - inside and outide sel
        p.lineTo(lastXPos + xStep / 2, (lastYPos + yPos) / 2);
        p.setPen(mDisplay.wavePen());
      }
    }

    p.lineTo(xPos, yPos);
  }

  // dots at high zoom
  if (xStep >= 8)
  {
    pData = buf.data() + 1;
    xPos = r.left() - (r.left() % xStep);
    for (size_t i = 0; i < length; i++)
    {
      yPos = vCentre - (int)(maxDisp * *pData++);

      QColor c = mDisplay.sampleColour();
      if ((!selection && xPos == selLeft) ||
          (selection && xPos >= selLeft && xPos <= selRight))
        c = mDisplay.selectColour();

      p.fillRect(xPos-1, yPos-1, 3, 3, c);

      xPos += xStep;
    }
  }
}


void NormalDisplayPlugin::Widget::getMinMaxData(off_t pos, size_t length,
                                                size_t samplesPerPixel,
                                                SampleBuffer& min, SampleBuffer& max)
{
  const size_t kDefaultBlockSize = 16384;

  Sample* iMin = min.data();
  Sample* iMax = max.data();

  // ensure pos >= 0, so cache access is valid
  while (pos < 0)
  {
    *iMin++ = 0.0f;
    *iMax++ = 0.0f;
    pos += samplesPerPixel;
  }

  // check if cache should be used & if so get the appropriate buffer
  bool useCache = (samplesPerPixel >= kCacheThreshold);

  CacheData::iterator c;
  if (useCache)
  {
    Cache::iterator cacheEntry = mCache.find(samplesPerPixel);
    if (cacheEntry == mCache.end())
    {
      // initialise cache array for this zoom level
      cacheEntry = mCache.insert(samplesPerPixel, CacheData(data().length() / samplesPerPixel + 1));
    }
    c = (*cacheEntry).begin() + (pos / samplesPerPixel);
  }

  Sonik::SampleBuffer buf(QMAX(kDefaultBlockSize, samplesPerPixel));
  buf.resize(0);
  off_t bufPos = -1;

  // process data in chunks
  while (length--)
  {
    size_t chunkSize = QMIN(length, buf.capacity());

    length -= chunkSize; // update now as chunkSize changes below

    // process chunk of data one pixel at a time
    //   assumes l % samplesPerPixel == 0 || at end of data
    while (chunkSize)
    {
      size_t cnt = QMIN(chunkSize, samplesPerPixel);

      if (useCache && (*c).valid)
      {
        *iMin = (*c).min;
        *iMax = (*c).max;
        c++;
      }
      else
      {
        // not in cache

        // get data block if current block does not cover the current pos
        if (pos >= static_cast<off_t>(bufPos + buf.size()))
        {
          data().data(channel(), pos, cnt, buf);
          bufPos = pos;
        }

        // calc the min / max
        size_t minPos, maxPos;
        Sonik::minmax(buf.data() + (pos - bufPos), cnt,
                      minPos, *iMin, maxPos, *iMax);

        // fill cache
        if (useCache)
        {
          (*c).valid = true;
          (*c).min = *iMin;
          (*c).max = *iMax;
          c++;
        }
      }

      iMin++;
      iMax++;
      chunkSize -= cnt;
      pos += cnt;
    }
  }
}


K_EXPORT_COMPONENT_FACTORY(libsonik_displaynormal,
                           KGenericFactory<NormalDisplayPlugin>(
                             "sonik-display-normal")
                           );

