/*
 * This file is part of the KFTPGrabber project
 *
 * Copyright (C) 2003-2004 by the KFTPGrabber developers
 * Copyright (C) 2003-2004 Jernej Kos <kostko@jweb-network.net>
 *
 * 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
 * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
 * NON-INFRINGEMENT.  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 Steet, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations
 * including the two.
 *
 * You must obey the GNU General Public License in all respects
 * for all of the code used other than OpenSSL.  If you modify
 * file(s) with this exception, you may extend this exception to your
 * version of the file(s), but you are not obligated to do so.  If you
 * do not wish to do so, delete this exception statement from your
 * version.  If you delete this exception statement from all source
 * files in the program, then also delete it here.
 */

#include <math.h>
#include <string.h>

#include <qpainter.h>
#include <qpixmap.h>

#include <klocale.h>

#include "trafficgraph.h"

namespace KFTPWidgets {

static inline int min(int a, int b)
{
  return (a < b ? a : b);
}

TrafficGraph::TrafficGraph(QWidget *parent, const char *name)
 : QWidget(parent, name)
{
  // Auto deletion does not work for pointer to arrays.
  m_beamData.setAutoDelete(false);

  setBackgroundMode(NoBackground);

  m_samples = 0;
  m_minValue = m_maxValue = 0.0;
  m_useAutoRange = true;

  m_graphStyle = GRAPH_POLYGON;

  // Anything smaller than this does not make sense.
  setMinimumSize(16, 100);
  setSizePolicy(QSizePolicy(QSizePolicy::Expanding,
                            QSizePolicy::Expanding, false));

  m_showVerticalLines = true;
  m_verticalLinesColor = QColor(0x04FB1D);
  m_verticalLinesDistance = 30;
  m_verticalLinesScroll = true;
  m_verticalLinesOffset = 0;
  m_horizontalScale = 1;

  m_showHorizontalLines = true;
  m_horizontalLinesColor = QColor(0x04FB1D);
  m_horizontalLinesCount = 5;

  m_showLabels = true;
  m_showTopBar = false;
  m_fontSize = 8;

  m_backgroundColor = QColor(0x313031);
}


TrafficGraph::~TrafficGraph()
{
  for (double* p = m_beamData.first(); p; p = m_beamData.next())
    delete [] p;
}

bool TrafficGraph::addBeam(const QColor &color)
{
  double* d = new double[m_samples];
  memset(d, 0, sizeof(double) * m_samples);
  m_beamData.append(d);
  m_beamColor.append(color);

  return true;
}

void TrafficGraph::addSample(const QValueList<double>& sampleBuf)
{
  if (m_beamData.count() != sampleBuf.count())
    return;

  double* d;
  if (m_useAutoRange) {
    double sum = 0;
    for (d = m_beamData.first(); d; d = m_beamData.next()) {
      sum += d[0];
      if (sum < m_minValue)
        m_minValue = sum;
      if (sum > m_maxValue)
        m_maxValue = sum;
    }
  }

  /* If the vertical lines are scrolling, increment the offset
   * so they move with the data. The vOffset / hScale confusion
   * is because v refers to Vertical Lines, and h to the horizontal
   * distance between the vertical lines. */
  if (m_verticalLinesScroll) {
    m_verticalLinesOffset = (m_verticalLinesOffset + m_horizontalScale)
                           % m_verticalLinesDistance;
  }

  // Shift data buffers one sample down and insert new samples.
  QValueList<double>::ConstIterator s;
  for (d = m_beamData.first(), s = sampleBuf.begin(); d; d = m_beamData.next(), ++s) {
    memmove(d, d + 1, (m_samples - 1) * sizeof(double));
    d[m_samples - 1] = *s;
  }

  update();
}

void TrafficGraph::changeRange(int beam, double min, double max)
{
  // Only the first beam affects range calculation.
  if (beam > 1)
    return;

  m_minValue = min;
  m_maxValue = max;
}

QValueList<QColor> &TrafficGraph::beamColors()
{
  return m_beamColor;
}

void TrafficGraph::removeBeam(uint pos)
{
  m_beamColor.remove(m_beamColor.at(pos));
  m_beamData.remove(pos);
}

void TrafficGraph::setUseAutoRange(bool value)
{
  m_useAutoRange = value;
}

bool TrafficGraph::useAutoRange() const
{
  return m_useAutoRange;
}

void TrafficGraph::setMinValue(double min)
{
  m_minValue = min;
}

double TrafficGraph::minValue() const
{
  return (m_useAutoRange ? 0 : m_minValue);
}

void TrafficGraph::setMaxValue(double max)
{
  m_maxValue = max;
}

double TrafficGraph::maxValue() const
{
  return (m_useAutoRange ? 0 : m_maxValue);
}

void TrafficGraph::setGraphStyle(uint style)
{
  m_graphStyle = style;
}

uint TrafficGraph::graphStyle() const
{
  return m_graphStyle;
}

void TrafficGraph::setHorizontalScale(uint scale)
{
  if (scale == m_horizontalScale)
     return;

  m_horizontalScale = scale;
  if (isVisible())
    updateDataBuffers();
}

uint TrafficGraph::horizontalScale() const
{
  return m_horizontalScale;
}

void TrafficGraph::setShowVerticalLines(bool value)
{
  m_showVerticalLines = value;
}

bool TrafficGraph::showVerticalLines() const
{
  return m_showVerticalLines;
}

void TrafficGraph::setVerticalLinesColor(const QColor &color)
{
  m_verticalLinesColor = color;
}

QColor TrafficGraph::verticalLinesColor() const
{
  return m_verticalLinesColor;
}

void TrafficGraph::setVerticalLinesDistance(int distance)
{
  m_verticalLinesDistance = distance;
}

int TrafficGraph::verticalLinesDistance() const
{
  return m_verticalLinesDistance;
}

void TrafficGraph::setVerticalLinesScroll(bool value)
{
  m_verticalLinesScroll = value;
}

bool TrafficGraph::verticalLinesScroll() const
{
  return m_verticalLinesScroll;
}

void TrafficGraph::setShowHorizontalLines(bool value)
{
  m_showHorizontalLines = value;
}

bool TrafficGraph::showHorizontalLines() const
{
  return m_showHorizontalLines;
}

void TrafficGraph::setHorizontalLinesColor(const QColor &color)
{
  m_horizontalLinesColor = color;
}

QColor TrafficGraph::horizontalLinesColor() const
{
  return m_horizontalLinesColor;
}

void TrafficGraph::setHorizontalLinesCount(int count)
{
  m_horizontalLinesCount = count;
}

int TrafficGraph::horizontalLinesCount() const
{
  return m_horizontalLinesCount;
}

void TrafficGraph::setShowLabels(bool value)
{
  m_showLabels = value;
}

bool TrafficGraph::showLabels() const
{
  return m_showLabels;
}

void TrafficGraph::setShowTopBar(bool value)
{
  m_showTopBar = value;
}

bool TrafficGraph::showTopBar() const
{
  return m_showTopBar;
}

void TrafficGraph::setFontSize(int size)
{
  m_fontSize = size;
}

int TrafficGraph::fontSize() const
{
  return m_fontSize;
}

void TrafficGraph::setBackgroundColor(const QColor &color)
{
  m_backgroundColor = color;
}

QColor TrafficGraph::backgroundColor() const
{
  return m_backgroundColor;
}

void TrafficGraph::resizeEvent(QResizeEvent*)
{
  updateDataBuffers();
}

void TrafficGraph::updateDataBuffers()
{
  /* Since the data buffers for the beams are equal in size to the
   * width of the widget minus 2 we have to enlarge or shrink the
   * buffers accordingly when a resize occures. To have a nicer
   * display we try to keep as much data as possible. Data that is
   * lost due to shrinking the buffers cannot be recovered on
   * enlarging though. */

  /* Determine new number of samples first.
   *  +0.5 to ensure rounding up
   *  +2 for extra data points so there is
   *     1) no wasted space and
   *     2) no loss of precision when drawing the first data point. */
  uint newSampleNum = static_cast<uint>(((width() - 2 ) / m_horizontalScale) + 2.5);

  // overlap between the old and the new buffers.
  int overlap = min(m_samples, newSampleNum);

  for (uint i = 0; i < m_beamData.count(); ++i) {
    double* nd = new double[newSampleNum];

    // initialize new part of the new buffer
    if (newSampleNum > (uint) overlap)
      memset(nd, 0, sizeof(double) * (newSampleNum - overlap));

    // copy overlap from old buffer to new buffer
    memcpy(nd + (newSampleNum - overlap), m_beamData.at(i) + (m_samples - overlap), overlap * sizeof(double));

    m_beamData.remove(i);
    m_beamData.insert(i, nd);
  }

  m_samples = newSampleNum;
}

void TrafficGraph::paintEvent(QPaintEvent*)
{
  uint w = width();
  uint h = height();

  /* Do not do repaints when the widget is not yet setup properly. */
  if (w <= 2)
    return;

  QPixmap pm(w, h);
  QPainter p;
  p.begin(&pm, this);

  pm.fill(m_backgroundColor);
  /* Draw white line along the bottom and the right side of the
   * widget to create a 3D like look. */
  p.setPen(QColor(colorGroup().light()));
  p.drawLine(0, h - 1, w - 1, h - 1);
  p.drawLine(w - 1, 0, w - 1, h - 1);

  p.setClipRect(1, 1, w - 2, h - 2);
  double range = m_maxValue - m_minValue;

  /* If the range is too small we will force it to 1.0 since it
   * looks a lot nicer. */
  if (range < 0.000001)
    range = 1.0;

  double minValue = m_minValue;
  if (m_useAutoRange) {
    if (m_minValue != 0.0) {
      double dim = pow(10, floor(log10(fabs(m_minValue )))) / 2;
      if (m_minValue < 0.0)
        minValue = dim * floor(m_minValue / dim);
      else
        minValue = dim * ceil(m_minValue / dim);
      range = m_maxValue - minValue;
      if (range < 0.000001)
        range = 1.0;
    }
    
    // Massage the range so that the grid shows some nice values.
    double step = range / m_horizontalLinesCount;
    double dim = pow(10, floor(log10(step))) / 2;
    range = dim * ceil(step / dim) * m_horizontalLinesCount;
  }
  
  double maxValue = minValue + range;

  int top = 0;
  if (m_showTopBar && h > (m_fontSize + 2 + m_horizontalLinesCount * 10)) {
    /* Draw horizontal bar with current sensor values at top of display. */
    p.setPen(m_horizontalLinesColor);
    int x0 = w / 2;
    p.setFont(QFont(p.font().family(), m_fontSize));
    top = p.fontMetrics().height();
    h -= top;
    int h0 = top - 2;
    p.drawText(0, 0, x0, top - 2, Qt::AlignCenter, i18n("Bandwidth usage"));

    p.drawLine(x0 - 1, 1, x0 - 1, h0);
    p.drawLine(0, top - 1, w - 2, top - 1);

    double bias = -minValue;
    double scaleFac = ( w - x0 - 2 ) / range;
    QValueList<QColor>::Iterator col;
    col = m_beamColor.begin();
    
    for (double *d = m_beamData.first(); d; d = m_beamData.next(), ++col) {
      int start = x0 + (int) (bias * scaleFac);
      int end = x0 + (int) ((bias += d[ w - 3 ]) * scaleFac);
      
      /* If the rect is wider than 2 pixels we draw only the last
       * pixels with the bright color. The rest is painted with
       * a 50% darker color. */
      if (end - start > 1) {
        p.setPen((*col).dark(150));
        p.setBrush((*col).dark(150));
        p.drawRect(start, 1, end - start, h0);
        p.setPen(*col);
        p.drawLine(end, 1, end, h0);
      } else if (start - end > 1) {
        p.setPen((*col).dark(150));
        p.setBrush((*col).dark(150));
        p.drawRect(end, 1, start - end, h0);
        p.setPen(*col);
        p.drawLine(end, 1, end, h0);
      } else {
        p.setPen(*col);
        p.drawLine(start, 1, start, h0);
      }
    }
  }

  /* Draw scope-like grid vertical lines */
  if (m_showVerticalLines && w > 60) {
    p.setPen(m_verticalLinesColor);
    for (uint x = m_verticalLinesOffset; x < (w - 2); x += m_verticalLinesDistance)
      p.drawLine(w - x, top, w - x, h + top - 2);
  }

  /* In autoRange mode we determine the range and plot the values in
   * one go. This is more efficiently than running through the
   * buffers twice but we do react on recently discarded samples as
   * well as new samples one plot too late. So the range is not
   * correct if the recently discarded samples are larger or smaller
   * than the current extreme values. But we can probably live with
   * this. */
  if (m_useAutoRange)
    m_minValue = m_maxValue = 0.0;

  /* Plot stacked values */
  double scaleFac = (h - 2) / range;
  if (m_graphStyle == GRAPH_ORIGINAL) {
    int xPos = 0;
    
    for (int i = 0; i < m_samples; i++, xPos += m_horizontalScale) {
      double bias = -minValue;
      QValueList<QColor>::Iterator col;
      col = m_beamColor.begin();
      double sum = 0.0;
      
      for (double *d = m_beamData.first(); d; d = m_beamData.next(), ++col) {
        if (m_useAutoRange) {
          sum += d[i];
          if (sum < m_minValue)
            m_minValue = sum;
          if (sum > m_maxValue)
            m_maxValue = sum;
        }
        
        int start = top + h - 2 - (int) (bias * scaleFac);
        int end = top + h - 2 - (int) ((bias + d[ i ] ) * scaleFac);
        bias += d[i];
        
        /* If the line is longer than 2 pixels we draw only the last
         * 2 pixels with the bright color. The rest is painted with
         * a 50% darker color. */
        if (end - start > 2) {
          p.fillRect(xPos, start, m_horizontalScale, end - start - 1, (*col).dark(150));
          p.fillRect(xPos, end - 1, m_horizontalScale, 2, *col);
        } else if (start - end > 2) {
          p.fillRect(xPos, start, m_horizontalScale, end - start + 1, (*col).dark(150));
          p.fillRect(xPos, end + 1, m_horizontalScale, 2, *col);
        } else
          p.fillRect(xPos, start, m_horizontalScale, end - start, *col);

      }
    }
  } else if (m_graphStyle == GRAPH_POLYGON) {
    int *prevVals = new int[m_beamData.count()];
    int hack[4];
    int x1 = w - ((m_samples + 1) * m_horizontalScale);

    for (int i = 0; i < m_samples; i++) {
      QValueList<QColor>::Iterator col;
      col = m_beamColor.begin();
      double sum = 0.0;
      int y = top + h - 2;
      int oldY = top + h;
      int oldPrevY = oldY;
      int height = 0;
      int j = 0;
      int jMax = m_beamData.count() - 1;
      x1 += m_horizontalScale;
      int x2 = x1 + m_horizontalScale;

      for (double *d = m_beamData.first(); d; d = m_beamData.next(), ++col, j++) {
        if (m_useAutoRange) {
          sum += d[i];
          
          if ( sum < m_minValue )
            m_minValue = sum;
          if ( sum > m_maxValue )
            m_maxValue = sum;
        }
        
        height = (int) ((d[i] - minValue) * scaleFac);
        y -= height;

        /* If the line is longer than 2 pixels we draw only the last
         * 2 pixels with the bright color. The rest is painted with
         * a 50% darker color. */
        QPen lastPen = QPen(p.pen());
        p.setPen((*col).dark(150));
        p.setBrush((*col).dark(150));
        QPointArray pa(4);
        int prevY = (i == 0) ? y : prevVals[j];
        pa.putPoints(0, 1, x1, prevY);
        pa.putPoints(1, 1, x2, y);
        pa.putPoints(2, 1, x2, oldY);
        pa.putPoints(3, 1, x1, oldPrevY);
        p.drawPolygon(pa);
        p.setPen(lastPen);
        if (jMax == 0) {
          // draw as normal, no deferred drawing req'd.
          p.setPen(*col);
          p.drawLine(x1, prevY, x2, y);
        } else if (j == jMax) {
          // draw previous values and current values
          p.drawLine(hack[0], hack[1], hack[2], hack[3]);
          p.setPen(*col);
          p.drawLine(x1, prevY, x2, y);
        } else if (j == 0) {
          // save values only
          hack[0] = x1;
          hack[1] = prevY;
          hack[2] = x2;
          hack[3] = y;
          p.setPen(*col);
        } else {
          p.drawLine(hack[0], hack[1], hack[2], hack[3]);
          hack[0] = x1;
          hack[1] = prevY;
          hack[2] = x2;
          hack[3] = y;
          p.setPen(*col);
        }

        prevVals[j] = y;
        oldY = y;
        oldPrevY = prevY;
      }
    }

    delete[] prevVals;
  }

  /* Draw horizontal lines and values. Lines are drawn when the
   * height is greater than 10 times hCount + 1, values are shown
   * when width is greater than 60 */
  if (m_showHorizontalLines && h > (10 * (m_horizontalLinesCount + 1))) {
    p.setPen(m_horizontalLinesColor);
    p.setFont(QFont(p.font().family(), m_fontSize));
    QString val;
    
    for (uint y = 1; y < m_horizontalLinesCount; y++) {
      p.drawLine(0, top + y * (h / m_horizontalLinesCount), w - 2, top + y * (h / m_horizontalLinesCount));
      
      if (m_showLabels && h > (m_fontSize + 1) * (m_horizontalLinesCount + 1) && w > 60 ) {
        val = QString("%1").arg(maxValue - y * (range / m_horizontalLinesCount));
        p.drawText(6, top + y * (h / m_horizontalLinesCount) - 1, val);
      }
    }

    if (m_showLabels && h > (m_fontSize + 1) * (m_horizontalLinesCount + 1) && w > 60) {
      val = QString("%1").arg(minValue);
      p.drawText(6, top + h - 2, val);
    }
  }

  p.end();
  bitBlt(this, 0, 0, &pm);
}

}

#include "trafficgraph.moc"
