// --------------------------------------------------------------------
// IpeCanvas
// --------------------------------------------------------------------
/*

    This file is part of the extensible drawing editor Ipe.
    Copyright (C) 1993-2004  Otfried Cheong

    Ipe 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.

    As a special exception, you have permission to link Ipe with the
    CGAL library and distribute executables, as long as you follow the
    requirements of the Gnu General Public License in regard to all of
    the software in the executable aside from CGAL.

    Ipe 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 Ipe; if not, you can find it at
    "http://www.gnu.org/copyleft/gpl.html", or write to the Free
    Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

*/

#include "ipepage.h"
#include "ipestyle.h"

#include "ipecanvas.h"
#include "ipeoverlay.h"
#include "ipefonts.h"

#include <qpainter.h>
#include <qstatusbar.h>

inline QPoint QPt(const IpeVector &v)
{
  return QPoint(int(v.iX), int(v.iY));
}

inline IpeString IpeQ(const QString &str)
{
  return IpeString(str.utf8());
}

inline QString QIpe(const IpeString &str)
{
  return QString::fromUtf8(str.CString());
}

// --------------------------------------------------------------------

/*! \class IpeCanvas
  \brief A widget that displays an Ipe document page.
*/

//! Construct a new canvas.
IpeCanvas::IpeCanvas(IpeCanvasServices *services,
		     QStatusBar *statusBar,
		     QWidget* parent, WFlags f)
  : QWidget(parent, "canvas", f), iServices(services), iStatusBar(statusBar)
{
  assert(iServices);
  setBackgroundMode(NoBackground);
  iPage = 0;
  iPan = IpeVector::Zero;
  iZoom = 1.0;
  iDimmed = false;
  iDoubleBuffer = false;
  iOffscreen.resize(size());
  iRepaintObjects = false;
  iOverlay = 0;
  iFonts = 0;
  setMouseTracking(true);
  iAutoSnap = false;
  iFifiVisible = false;
  setFocusPolicy(NoFocus);
}

//! IpeCanvas destructor.
IpeCanvas::~IpeCanvas()
{
  delete iOverlay;
  delete iFonts;
}

// --------------------------------------------------------------------

void IpeCanvas::SetFontPool(bool antiAlias, const IpeFontPool *fontPool)
{
  delete iFonts;
  iFonts = IpeCanvasFonts::New(antiAlias, fontPool, iServices);
}

// --------------------------------------------------------------------
//! Set the page to be displayed.
/*! Doesn't take ownership of any argument. */
void IpeCanvas::SetPage(const IpePage *page, int view,
			const IpeStyleSheet *sheet,
			const IpeRect &media, IpeAttribute pageColor)
{
  iPage = page;
  iView = view;
  iStyleSheet = sheet;
  iMediaBox = media;
  iPageColor = pageColor;
  delete iOverlay;
  iOverlay = 0;
  Update();
}

//! Set current pan position.
/*! The pan position is the user coordinate that is displayed at
  the very center of the canvas. */
void IpeCanvas::SetPan(const IpeVector &v)
{
  iPan = v;
  Update();
}

//! Set current zoom factor.
/*! The zoom factor maps user coordinates to screen pixel coordinates. */
void IpeCanvas::SetZoom(IpeScalar zoom)
{
  iZoom = zoom;
  Update();
}

//! Set the snapping information.
void IpeCanvas::SetSnap(const IpeSnapData &s)
{
  iSnapData = s;
}

//! Dim whole canvas, except for the overlay.
/*! This mode will be reset when the overlay finishes. */
void IpeCanvas::SetDimmed(bool dimmed)
{
  iDimmed = dimmed;
  Update();
}

//! Set double buffering for overlay.
/*! This mode will be reset when the overlay finishes. */
void IpeCanvas::SetDoubleBuffer(bool db)
{
  iDoubleBuffer = db;
}

//! Enable automatic angular snapping with this origin.
/*! Snapping is disabled automatically on FinishOverlay(). */
void IpeCanvas::SetAutoOrigin(const IpeVector &v)
{
  iAutoOrigin = v;
  iAutoSnap = true;
}

//! Mark for update with redrawing of objects.
void IpeCanvas::Update()
{
  iRepaintObjects = true;
  update();
}

//! Mark for update with redrawing of overlay only.
void IpeCanvas::UpdateOverlay()
{
  update();
}

//! Clear status bar.
void IpeCanvas::ClearMessage()
{
  iStatusBar->clear();
}

//! Display a message in the status bar.
void IpeCanvas::Message(QString str)
{
  iStatusBar->message(str);
}

IpeVector IpeCanvas::DevToUser(const IpeVector &arg) const
{
  IpeVector v = arg - Center();
  v.iX /= iZoom;
  v.iY /= -iZoom;
  v += iPan;
  return v;
}

IpeVector IpeCanvas::UserToDev(const IpeVector &arg) const
{
  IpeVector v = arg - iPan;
  v.iX *= iZoom;
  v.iY *= -iZoom;
  v += Center();
  return v;
}

//! Adjust painter for canvas coordinate system.
IpeMatrix IpeCanvas::CanvasTfm() const
{
  return IpeMatrix(Center()) *
    IpeLinear(iZoom, 0, 0, -iZoom) *
    IpeMatrix(-iPan);
}

// --------------------------------------------------------------------

void IpeCanvas::SetOverlay(IpeOverlay *overlay)
{
  assert(overlay);
  iOverlay = overlay;
  iServices->CvSvcSetDrawingMode(true);
  UpdateOverlay();
}

//! Current overlay has done its job.
/*! Overlay is deleted, canvas fully updated, and cursor reset. */
void IpeCanvas::FinishOverlay()
{
  delete iOverlay;
  iOverlay = 0;
  iDimmed = false;
  iAutoSnap = false;
  iDoubleBuffer = false;
  Update();
  unsetCursor();
  iServices->CvSvcSetDrawingMode(false);
  ClearMessage();
}

/*! Stores the mouse position in iUnsnappedMousePos, computes Fifi if
  snapping is enabled, and stores snapped position in iMousePos. */
void IpeCanvas::ComputeFifi(QMouseEvent *ev)
{
  iUnsnappedMousePos = DevToUser(IpeVector(ev->x(), ev->y()));
  iMousePos = iUnsnappedMousePos;

  int mask = iAutoSnap ? 0 : IpeSnapData::ESnapAuto;
  if (iSnapData.iSnap & ~mask) {
    iSnapData.Snap(iMousePos, iPage, iSnapData.iSnapDistance / iZoom,
		   (iAutoSnap ? &iAutoOrigin : 0));

    // convert fifi coordinates back into device space
    QPoint qfifi = QPt(UserToDev(iMousePos));
    if (iFifi != qfifi) {
      // update old and new fifi position
      update(iFifi.x() - 8, iFifi.y() - 8, 17, 17);
      iFifi = qfifi;
      update(iFifi.x() - 8, iFifi.y() - 8, 17, 17);
    }
  } else if (iFifiVisible) {
    iFifiVisible = false;
    update(iFifi.x() - 8, iFifi.y() - 8, 17, 17);
  }
  iServices->CvSvcMousePosition(iMousePos);
}

// --------------------------------------------------------------------

void IpeCanvas::DrawAxes(QPainter &qPainter)
{
  IpeMatrix m = CanvasTfm() * IpeMatrix(iSnapData.iOrigin);

  qPainter.setPen(Qt::green);
  int x = int(m.Translation().iX);
  int y = int(m.Translation().iY);

  double alpha = 0.0;
  IpeVector ep((width() + height()) / iZoom, 0);

  while (alpha < IpeTwoPi) {
    double beta = iSnapData.iDir + alpha;
    qPainter.moveTo(x, y);
    qPainter.lineTo(QPt(m * IpeLinear(beta) * ep));
    alpha += iSnapData.iAngleSize;
  }

  if (iSnapData.iAngleSize > (0.9 * IpePi))
    qPainter.drawEllipse(x - 2, y - 2, 4, 4);
}

void IpeCanvas::DrawGrid(QPainter &qPainter)
{
  double step = iSnapData.iGridSize;
  double pixstep = step * iZoom;
  if (pixstep < 2.0)
    return;

  int vfactor = 8;
  if (pixstep > 4.0)
    vfactor = 4;
  if (pixstep > 8.0)
    vfactor = 2;
  if (pixstep > 16.0)
    vfactor = 1;

  double vstep = step * vfactor;

  IpeVector ll = iMediaBox.Min();
  IpeVector ur = iMediaBox.Max();

  // grid should remain inside the paper
  ll.iX = step * int(ll.iX / step);
  ll.iY = step * int(ll.iY / step);
  if (ll.iX < iMediaBox.Min().iX - 1.0)
    ll.iX += step;
  if (ll.iY < iMediaBox.Min().iY - 1.0)
    ll.iY += step;

  IpeMatrix m = CanvasTfm();

  IpeVector v;
  qPainter.setPen(Qt::black);
  for (v.iX = ll.iX; v.iX < ur.iX; v.iX += vstep)
    for (v.iY = ll.iY; v.iY < ur.iY; v.iY += step)
      qPainter.drawPoint(QPt(m * v));
  if (vfactor != 1) {
    for (v.iY = ll.iY; v.iY < ur.iY; v.iY += vstep)
      for (v.iX = ll.iX; v.iX < ur.iX; v.iX += step)
	qPainter.drawPoint(QPt(m * v));
  }
}

void IpeCanvas::RepaintObjects()
{
  iOffscreen.fill(Qt::lightGray);
  iRepaintObjects = false;
  if (!iPage)
    return;

  QPainter qPainter;
  qPainter.begin(&iOffscreen);

  IpeCanvasPainter painter(iStyleSheet, iFonts, &iOffscreen,
			   &qPainter, iZoom, false, 200);
  painter.Transform(CanvasTfm());
  painter.Push();
  painter.SetStroke(IpeAttribute::Void());
  painter.SetFill(iPageColor);
  painter.NewPath();
  painter.Rect(iMediaBox);
  painter.DrawPath();
  painter.Pop();
  painter.SetDimmed(iDimmed);

  if (iSnapData.iGridVisible)
    DrawGrid(qPainter);
  if (iSnapData.iWithAxes)
    DrawAxes(qPainter);

  painter.Push();
  std::vector<bool> layerVisible;
  iPage->MakeLayerTable(layerVisible, iView, false);

  for (IpePage::const_iterator it = iPage->begin(); it != iPage->end(); ++it) {
    if (layerVisible[it->Layer()]) {
      painter.SetDimmed(iDimmed || iPage->Layer(it->Layer()).IsDimmed());
      it->Object()->Draw(painter);
    }
  }
  painter.Pop();
  qPainter.end();
}

// --------------------------------------------------------------------

void IpeCanvas::paintEvent(QPaintEvent *ev)
{
  if (iRepaintObjects)
    RepaintObjects();

  QPixmap buffer;
  QPainter qPainter;

  if (iDoubleBuffer) {
    buffer.resize(size());
    bitBlt(&buffer, 0, 0, &iOffscreen,
	   0, 0, iOffscreen.width(), iOffscreen.height());
    qPainter.begin(&buffer);
  } else {
#if 0
    bitBlt(this, 0, 0, &iOffscreen,
	   0, 0, iOffscreen.width(), iOffscreen.height());
    qPainter.begin(this);
#else
    // bitBlt cannot be used, because the qPainter is clipped
    // to the area needing to
    qPainter.begin(this);
    QRect r = ev->rect();
    qPainter.drawPixmap(r.left(), r.top(), iOffscreen,
			r.left(), r.top(), r.width(), r.height());
#endif
  }

  if (iOverlay) {
    iOverlay->Draw(ev, &qPainter);
  } else {
    IpeOverlayPainter painter(iStyleSheet, &qPainter);
    painter.Transform(CanvasTfm());

    QPen primaryPen;
    QPen secondaryPen;
    primaryPen.setColor(Qt::red);
    primaryPen.setWidth(2);
    secondaryPen.setColor(QColor("purple"));

    painter.Push();
    for (IpePage::const_iterator it = iPage->begin();
	 it != iPage->end(); ++it) {
      if (it->Select() == IpePgObject::EPrimary) {
	qPainter.setPen(primaryPen);
	it->Object()->Draw(painter);
      } else if (it->Select() == IpePgObject::ESecondary) {
	qPainter.setPen(secondaryPen);
	it->Object()->Draw(painter);
      }
    }
    painter.Pop();
  }

  if (iSnapData.iSnap) {
    qPainter.setPen(Qt::red);
    qPainter.drawLine(iFifi.x() - 8, iFifi.y(), iFifi.x() + 8, iFifi.y());
    qPainter.drawLine(iFifi.x(), iFifi.y() - 8, iFifi.x(), iFifi.y() + 8);
    iFifiVisible = true;
  }
  qPainter.end();
  if (iDoubleBuffer)
    bitBlt(this, 0, 0, &buffer, 0, 0, buffer.width(), buffer.height());
}

void IpeCanvas::resizeEvent(QResizeEvent *)
{
  iOffscreen.resize(size());
  iRepaintObjects = true;
}

void IpeCanvas::wheelEvent(QWheelEvent *ev)
{
  iServices->CvSvcWheelZoom(ev->delta());
  ev->accept();
}

void IpeCanvas::mousePressEvent(QMouseEvent *ev)
{
  ClearMessage();
  ComputeFifi(ev);
  if (iOverlay) {
    iOverlay->MousePress(ev);
  } else {
    // iServices will call SetOverlay
    iServices->CvSvcRequestOverlay(ev);
  }
}

void IpeCanvas::mouseReleaseEvent(QMouseEvent *ev)
{
  ComputeFifi(ev);
  if (iOverlay)
    iOverlay->MouseRelease(ev);
}

void IpeCanvas::mouseMoveEvent(QMouseEvent *ev)
{
  ComputeFifi(ev);
  if (iOverlay)
    iOverlay->MouseMove(ev);
}

QSize IpeCanvas::sizeHint() const
{
  return QSize(640, 480);
}

// --------------------------------------------------------------------
