/***************************************************************************
 *   Copyright (C) 2005-2009 by Rajko Albrecht  ral@alwins-world.de        *
 *   http://kdesvn.alwins-world.de/                                        *
 *                                                                         *
 *   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.         *
 ***************************************************************************/
/* This file was part of KCachegrind.
   Copyright (C) 2002, 2003 Josef Weidendorfer <Josef.Weidendorfer@gmx.de>
   Adapted for the needs of kdesvn  by Rajko Albrecht <ral@alwins-world.de>
*/
/*
 * A Widget for visualizing hierarchical metrics as areas.
 * The API is similar to QListView.
 */

#include "drawparams.h"

#include <math.h>

#include <qpainter.h>
#include <qtooltip.h>
#include <qregexp.h>
#include <qstyle.h>
#include <QPixmap>

#include <klocale.h>
#include <kconfig.h>
#include <kdebug.h>
#include <kglobalsettings.h>



// set this to 1 to enable debug output
#define DEBUG_DRAWING 0
#define MAX_FIELD 12


//
// StoredDrawParams
//
StoredDrawParams::StoredDrawParams()
{
  _selected = false;
  _current = false;
  _shaded = true;
  _rotated = false;

  _backColor = Qt::white;

  // field array has size 0
}

StoredDrawParams::StoredDrawParams(QColor c,
                                   bool selected, bool current)
{
  _backColor = c;

  _selected = selected;
  _current = current;
  _shaded = true;
  _rotated = false;
  _drawFrame = true;

  // field array has size 0
}

QString StoredDrawParams::text(int f) const
{
  if ((f<0) || (f >= (int)_field.size()))
    return QString();

  return _field[f].text;
}

QPixmap StoredDrawParams::pixmap(int f) const
{
  if ((f<0) || (f >= (int)_field.size()))
    return QPixmap();

  return _field[f].pix;
}

DrawParams::Position StoredDrawParams::position(int f) const
{
  if ((f<0) || (f >= (int)_field.size()))
    return Default;

  return _field[f].pos;
}

int StoredDrawParams::maxLines(int f) const
{
  if ((f<0) || (f >= (int)_field.size()))
    return 0;

  return _field[f].maxLines;
}

QFont StoredDrawParams::font() const
{
  return KGlobalSettings::fixedFont();
}

void StoredDrawParams::ensureField(int f)
{
  static Field* def = 0;
  if (!def) {
    def = new Field();
    def->pos = Default;
    def->maxLines = 0;
  }

  if (f<0 || f>=MAX_FIELD) return;

  while(_field.size()<f+1) {
      _field.append(*def);
  }
}


void StoredDrawParams::setField(int f, QString t, QPixmap pm,
                           Position p, int maxLines)
{
  if (f<0 || f>=MAX_FIELD) return;
  ensureField(f);

  _field[f].text = t;
  _field[f].pix  = pm;
  _field[f].pos  = p;
  _field[f].maxLines = maxLines;
}

void StoredDrawParams::setText(int f, QString t)
{
  if (f<0 || f>=MAX_FIELD) return;
  ensureField(f);

  _field[f].text = t;
}

void StoredDrawParams::setPixmap(int f, QPixmap pm)
{
  if (f<0 || f>=MAX_FIELD) return;
  ensureField(f);

  _field[f].pix = pm;
}

void StoredDrawParams::setPosition(int f, Position p)
{
  if (f<0 || f>=MAX_FIELD) return;
  ensureField(f);

  _field[f].pos = p;
}

void StoredDrawParams::setMaxLines(int f, int m)
{
  if (f<0 || f>=MAX_FIELD) return;
  ensureField(f);

  _field[f].maxLines = m;
}



//
// RectDrawing
//

RectDrawing::RectDrawing(QRect r)
{
  _fm = 0;
  _dp = 0;
  setRect(r);
}


RectDrawing::~RectDrawing()
{
  delete _fm;
  delete _dp;
}

DrawParams* RectDrawing::drawParams()
{
  if (!_dp)
    _dp = new StoredDrawParams();

  return _dp;
}


void RectDrawing::setDrawParams(DrawParams* dp)
{
  if (_dp) delete _dp;
  _dp = dp;
}

void RectDrawing::setRect(QRect r)
{
  _rect = r;

  _usedTopLeft = 0;
  _usedTopCenter = 0;
  _usedTopRight = 0;
  _usedBottomLeft = 0;
  _usedBottomCenter = 0;
  _usedBottomRight = 0;

  _fontHeight = 0;
}

QRect RectDrawing::remainingRect(DrawParams* dp)
{
  if (!dp) dp = drawParams();

  if ((_usedTopLeft >0) ||
      (_usedTopCenter >0) ||
      (_usedTopRight >0)) {
    if (dp->rotated())
      _rect.setLeft(_rect.left() + _fontHeight);
    else
      _rect.setTop(_rect.top() + _fontHeight);
  }

  if ((_usedBottomLeft >0) ||
      (_usedBottomCenter >0) ||
      (_usedBottomRight >0)) {
    if (dp->rotated())
      _rect.setRight(_rect.right() - _fontHeight);
    else
      _rect.setBottom(_rect.bottom() - _fontHeight);
  }
  return _rect;
}


void RectDrawing::drawBack(QPainter* p, DrawParams* dp)
{
    if (!dp) dp = drawParams();
    if (_rect.width()<=0 || _rect.height()<=0) return;

    QRect r = _rect;
    QColor normal = dp->backColor();
    if (dp->selected()) normal = normal.light();
    bool isCurrent = dp->current();

    if (dp->drawFrame() || isCurrent) {
        // 3D raised/sunken frame effect...
        QColor high = normal.light();
        QColor low = normal.dark();
        p->setPen( isCurrent ? low:high);
        p->drawLine(r.left(), r.top(), r.right(), r.top());
        p->drawLine(r.left(), r.top(), r.left(), r.bottom());
        p->setPen( isCurrent ? high:low);
        p->drawLine(r.right(), r.top(), r.right(), r.bottom());
        p->drawLine(r.left(), r.bottom(), r.right(), r.bottom());
        r.setRect(r.x()+1, r.y()+1, r.width()-2, r.height()-2);
    }
    if (r.width()<=0 || r.height()<=0) return;
    if (dp->shaded() && (r.width()>0 && r.height()>0)) {
        // adjustment for drawRect semantic in Qt4: decrement height/width
        r.setRect(r.x(), r.y(), r.width()-1, r.height()-1);

        // some shading
        bool goDark = qGray(normal.rgb())>128;
        int rBase, gBase, bBase;
        normal.getRgb(&rBase, &gBase, &bBase);
        p->setBrush(Qt::NoBrush);

        // shade parameters:
        int d = 7;
        float factor = 0.1, forth=0.7, back1 =0.9, toBack2 = .7, back2 = 0.97;

        // coefficient corrections because of rectangle size
        int s = r.width();
        if (s > r.height()) s = r.height();
        if (s<100) {
            forth -= .3  * (100-s)/100;
            back1 -= .2  * (100-s)/100;
            back2 -= .02 * (100-s)/100;
        }


        // maximal color difference
        int rDiff = goDark ? -rBase/d : (255-rBase)/d;
        int gDiff = goDark ? -gBase/d : (255-gBase)/d;
        int bDiff = goDark ? -bBase/d : (255-bBase)/d;

        QColor shadeColor;
        while (factor<.95 && (r.width()>=0 && r.height()>=0)) {
            shadeColor.setRgb((int)(rBase+factor*rDiff+.5),
                               (int)(gBase+factor*gDiff+.5),
                               (int)(bBase+factor*bDiff+.5));
                               p->setPen(shadeColor);
                               p->drawRect(r);
                               r.setRect(r.x()+1, r.y()+1, r.width()-2, r.height()-2);
                               factor = 1.0 - ((1.0 - factor) * forth);
        }

        // and back (1st half)
        while (factor>toBack2 && (r.width()>=0 && r.height()>=0)) {
            shadeColor.setRgb((int)(rBase+factor*rDiff+.5),
                               (int)(gBase+factor*gDiff+.5),
                               (int)(bBase+factor*bDiff+.5));
                               p->setPen(shadeColor);
                               p->drawRect(r);
                               r.setRect(r.x()+1, r.y()+1, r.width()-2, r.height()-2);
                               factor = 1.0 - ((1.0 - factor) / back1);
        }

        // and back (2nd half)
        while (factor>.01 && (r.width()>=0 && r.height()>=0)) {
            shadeColor.setRgb((int)(rBase+factor*rDiff+.5),
                               (int)(gBase+factor*gDiff+.5),
                               (int)(bBase+factor*bDiff+.5));
                               p->setPen(shadeColor);
                               p->drawRect(r);
                               r.setRect(r.x()+1, r.y()+1, r.width()-2, r.height()-2);
                               factor = factor * back2;
        }

        normal = shadeColor;
        // for filling, width and height has to be incremented again
        r.setRect(r.x(), r.y(), r.width()+1, r.height()+1);
    }

    // fill inside
    p->fillRect(r, normal);
}

/* Helper for drawField
* Find a line break position in a string, given a font and maximum width
*
* Returns the actually used width, and sets <breakPos>
*/
static
int findBreak(int& breakPos, QString text, QFontMetrics* fm, int maxWidth)
{
    int usedWidth;

    // does full text fit?
    breakPos = text.length();
    usedWidth = fm->width(text);
    if (usedWidth < maxWidth)
        return usedWidth;

    // now lower breakPos until best position is found.
    // first by binary search, resulting in a position a little bit too large
    int bottomPos = 0;
    while(qAbs(maxWidth - usedWidth) > 3 * fm->maxWidth()) {
        int halfPos = (bottomPos + breakPos)/2;
        int halfWidth = fm->width(text, halfPos);
        if (halfWidth < maxWidth)
            bottomPos = halfPos;
        else {
            breakPos = halfPos;
            usedWidth = halfWidth;
        }
    }

    // final position by taking break boundaries into account.
    // possible break boundaries are changing char categories but not middle of "Aa"
    QChar::Category lastCat, cat;
    int pos = breakPos;
    lastCat = text[pos-1].category();
    // at minimum 2 chars before break
    while (pos > 2) {
        pos--;
        cat = text[pos-1].category();
        if (cat == lastCat) continue;

        // "Aa" has not a possible break inbetween
        if ((cat == QChar::Letter_Uppercase) &&
            (lastCat == QChar::Letter_Lowercase)) {
            lastCat = cat;
        continue;
        }
        lastCat = cat;

        breakPos = pos;
        usedWidth = fm->width(text, breakPos);
        if (usedWidth < maxWidth) break;
    }
    return usedWidth;
}


/* Helper for drawField
* Find last line break position in a string from backwards,
* given a font and maximum width
*
* Returns the actually used width, and sets <breakPos>
*/
static
int findBreakBackwards(int& breakPos, QString text, QFontMetrics* fm, int maxWidth)
{
    int usedWidth;

    // does full text fit?
    breakPos = 0;
    usedWidth = fm->width(text);
    if (usedWidth < maxWidth)
        return usedWidth;

    // now raise breakPos until best position is found.
    // first by binary search, resulting in a position a little bit too small
    int topPos = text.length();
    while(qAbs(maxWidth - usedWidth) > 3 * fm->maxWidth()) {
        int halfPos = (breakPos + topPos)/2;
        int halfWidth = fm->width(text.mid(halfPos));
        if (halfWidth < maxWidth) {
            breakPos = halfPos;
            usedWidth = halfWidth;
        }
        else
            topPos = halfPos;
    }

    // final position by taking break boundaries into account.
    // possible break boundaries are changing char categories but not middle of "Aa"
    QChar::Category lastCat, cat;
    int pos = breakPos;
    lastCat = text[pos].category();
    // at minimum 2 chars before break
    while (pos < text.length()-2) {
        pos++;
        cat = text[pos].category();
        if (cat == lastCat) continue;

        // "Aa" has not a possible break inbetween
        if ((lastCat == QChar::Letter_Uppercase) &&
            (cat == QChar::Letter_Lowercase)) {
            lastCat = cat;
        continue;
        }
        lastCat = cat;

        breakPos = pos;
        usedWidth = fm->width(text.mid(breakPos));
        if (usedWidth < maxWidth) break;
    }
    return usedWidth;
}

bool RectDrawing::drawField(QPainter* p, int f, DrawParams* dp)
{
    if (!dp) dp = drawParams();

    if (!_fm) {
        _fm = new QFontMetrics(dp->font());
        _fontHeight = _fm->height();
    }

    QRect r = _rect;

    int h = _fontHeight;
    bool rotate = dp->rotated();
    int width   = (rotate ? r.height() : r.width()) -4;
    int height  = (rotate ? r.width() : r.height());
    int lines   = height / h;

    // stop if there is no space available
    if (lines<1) return false;

    // calculate free space in first line (<unused>)
    int pos = dp->position(f);
    if (pos == DrawParams::Default) {
        switch(f%4) {
            case 0: pos = DrawParams::TopLeft; break;
            case 1: pos = DrawParams::TopRight; break;
            case 2: pos = DrawParams::BottomRight; break;
            case 3: pos = DrawParams::BottomLeft; break;
        }
    }

    int unused = 0;
    bool isBottom = false;
    bool isCenter = false;
    bool isRight = false;
    int* used = 0;
    switch(pos) {
        case DrawParams::TopLeft:
            used = &_usedTopLeft;
            if (_usedTopLeft == 0) {
                if (_usedTopCenter)
                    unused = (width - _usedTopCenter)/2;
                else
                    unused = width - _usedTopRight;
            }
            break;

        case DrawParams::TopCenter:
            isCenter = true;
            used = &_usedTopCenter;
            if (_usedTopCenter == 0) {
                if (_usedTopLeft > _usedTopRight)
                    unused = width - 2 * _usedTopLeft;
                else
                    unused = width - 2 * _usedTopRight;
            }
            break;

        case DrawParams::TopRight:
            isRight = true;
            used = &_usedTopRight;
            if (_usedTopRight == 0) {
                if (_usedTopCenter)
                    unused = (width - _usedTopCenter)/2;
                else
                    unused = width - _usedTopLeft;
            }
            break;

        case DrawParams::BottomLeft:
            isBottom = true;
            used = &_usedBottomLeft;
            if (_usedBottomLeft == 0) {
                if (_usedBottomCenter)
                    unused = (width - _usedBottomCenter)/2;
                else
                    unused = width - _usedBottomRight;
            }
            break;

        case DrawParams::BottomCenter:
            isCenter = true;
            isBottom = true;
            used = &_usedBottomCenter;
            if (_usedBottomCenter == 0) {
                if (_usedBottomLeft > _usedBottomRight)
                    unused = width - 2 * _usedBottomLeft;
                else
                    unused = width - 2 * _usedBottomRight;
            }
            break;

        case DrawParams::BottomRight:
            isRight = true;
            isBottom = true;
            used = &_usedBottomRight;
            if (_usedBottomRight == 0) {
                if (_usedBottomCenter)
                    unused = (width - _usedBottomCenter)/2;
                else
                    unused = width - _usedBottomLeft;
            }
            break;
    }
    if (isBottom) {
        if ((_usedTopLeft >0) ||
            (_usedTopCenter >0) ||
            (_usedTopRight >0))
            lines--;
    }
    else if (!isBottom) {
        if ((_usedBottomLeft >0) ||
            (_usedBottomCenter >0) ||
            (_usedBottomRight >0))
            lines--;
    }
    if (lines<1) return false;

    int y = isBottom ? height - h : 0;

    if (unused < 0) unused = 0;
    if (unused == 0) {
        // no space available in last line at this position
        y = isBottom ? (y-h) : (y+h);
        lines--;

        if (lines<1) return false;

        // new line: reset used space
        if (isBottom)
            _usedBottomLeft = _usedBottomCenter = _usedBottomRight = 0;
        else
            _usedTopLeft = _usedTopCenter = _usedTopRight = 0;

        unused = width;
    }

    // stop as soon as possible when there is no space for "..."
    static int dotW = 0;
    if (!dotW) dotW = _fm->width("...");
    if (width < dotW) return false;

    // get text and pixmap now, only if we need to, because it is possible
    // that they are calculated on demand (and this can take some time)
    QString name = dp->text(f);
    if (name.isEmpty()) return 0;
    QPixmap pix = dp->pixmap(f);

    // check if pixmap can be drawn
    int pixW = pix.width();
    int pixH = pix.height();
    int pixY = 0;
    bool pixDrawn = true;
    if (pixW>0) {
        pixW += 2; // X distance from pix
        if ((width < pixW + dotW) || (height < pixH)) {
            // do not draw
            pixW = 0;
        } else {
            pixDrawn = false;
        }
    }

    // width of text and pixmap to be drawn
    int w = pixW + _fm->width(name);

    // if we have limited space at 1st line:
    // use it only if whole name does fit in last line...
    if ((unused < width) && (w > unused)) {
        y = isBottom ? (y-h) : (y+h);
        lines--;

        if (lines<1) return false;

        // new line: reset used space
        if (isBottom)
            _usedBottomLeft = _usedBottomCenter = _usedBottomRight = 0;
        else
            _usedTopLeft = _usedTopCenter = _usedTopRight = 0;
    }

    p->save();
    p->setPen( (qGray(dp->backColor().rgb())>100) ? Qt::black : Qt::white);
    p->setFont(dp->font());
    if (rotate) {
        //p->translate(r.x()+2, r.y()+r.height());
        p->translate(r.x(), r.y()+r.height()-2);
        p->rotate(270);
    } else {
        p->translate(r.x()+2, r.y());
    }
    // adjust available lines according to maxLines
    int max = dp->maxLines(f);
    if ((max > 0) && (lines>max)) lines = max;
    /* loop over name parts to break up string depending on available width.
     * every char category change is supposed a possible break,
     * with the exception Uppercase=>Lowercase.
     * It is good enough for numbers, Symbols...
     *
     * If the text is to be written at the bottom, we start with the
     * end of the string (so everything is reverted)
    */
    QString remaining;
    int origLines = lines;
    while (lines>0) {

        // more than one line: search for line break
        if (w>width && lines>1) {
            int breakPos;

            if (!isBottom) {
                w = pixW + findBreak(breakPos, name, _fm, width - pixW);

                remaining = name.mid(breakPos);
                // remove space on break point
                if (name[breakPos-1].category() == QChar::Separator_Space)
                    name = name.left(breakPos-1);
                else
                    name = name.left(breakPos);
            } else { // bottom
                w = pixW + findBreakBackwards(breakPos, name, _fm, width - pixW);

                remaining = name.left(breakPos);
                // remove space on break point
                if (name[breakPos].category() == QChar::Separator_Space)
                    name = name.mid(breakPos+1);
                else
                    name = name.mid(breakPos);
            }
        } else {
            remaining.clear();
        }
        /* truncate and add ... if needed */
        if (w > width) {
            name = _fm->elidedText(name, Qt::ElideRight, width - pixW);
            w = _fm->width(name) + pixW;
        }

        int x = 0;
        if (isCenter)
            x = (width - w)/2;
        else if (isRight)
            x = width - w;
        if (!pixDrawn) {
            pixY = y+(h-pixH)/2; // default: center vertically
            if (pixH > h) pixY = isBottom ? y-(pixH-h) : y;

            p->drawPixmap( x, pixY, pix);

            // for distance to next text
            pixY = isBottom ? (pixY - h - 2) : (pixY + pixH + 2);
            pixDrawn = true;
        }
        p->drawText( x+pixW, y,
                    width - pixW, h,
                    Qt::AlignLeft, name);
                    y = isBottom ? (y-h) : (y+h);
                    lines--;

                    if (remaining.isEmpty()) break;
                    name = remaining;
                    w = pixW + _fm->width(name);
    }

    // make sure the pix stays visible
    if (pixDrawn && (pixY>0)) {
        if (isBottom && (pixY<y)) y = pixY;
        if (!isBottom && (pixY>y)) y = pixY;
    }

    if (origLines > lines) {
        // if only 1 line written, do not reset _used* vars
        if (lines - origLines >1) {
            if (isBottom)
                _usedBottomLeft = _usedBottomCenter = _usedBottomRight = 0;
            else
                _usedTopLeft = _usedTopCenter = _usedTopRight = 0;
        }

        // take back one line
        y = isBottom ? (y+h) : (y-h);
        if (used) *used = w;
    }

    // update free space
    if (!isBottom) {
        if (rotate)
            _rect.setRect(r.x()+y, r.y(), r.width()-y, r.height());
        else
            _rect.setRect(r.x(), r.y()+y, r.width(), r.height()-y);
    }
    else {
        if (rotate)
            _rect.setRect(r.x(), r.y(), y+h, r.height());
        else
            _rect.setRect(r.x(), r.y(), r.width(), y+h);
    }

    p->restore();

    return true;
}
