/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2019-2021 Baldur Karlsson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/

#include "TimelineBar.h"
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QScrollBar>
#include <QStylePainter>
#include <QWheelEvent>
#include "Code/QRDUtils.h"
#include "Code/Resources.h"

QPointF aliasAlign(QPointF pt)
{
  pt.setX(int(pt.x()) + 0.5);
  pt.setY(int(pt.y()) + 0.5);
  return pt;
}

QMarginsF uniformMargins(qreal m)
{
  return QMarginsF(m, m, m, m);
}

QMargins uniformMargins(int m)
{
  return QMargins(m, m, m, m);
}

class PipRanges
{
public:
  void push(qreal pos, const int triRadius)
  {
    if(ranges.isEmpty())
    {
      ranges.push_back({pos, pos});
      return;
    }

    QPair<qreal, qreal> &range = ranges.back();

    if(range.second + triRadius >= pos)
      range.second = pos;
    else
      ranges.push_back({pos, pos});
  }

  QPainterPath makePath(const int triRadius, const int triHeight, qreal y)
  {
    QPainterPath path;

    for(const QPair<qreal, qreal> &range : ranges)
    {
      if(range.first == range.second)
      {
        QPointF pos = aliasAlign(QPointF(range.first, y));

        QPainterPath triangle;
        triangle.addPolygon(
            QPolygonF({pos + QPoint(0, triHeight), pos + QPoint(triRadius * 2, triHeight),
                       pos + QPoint(triRadius, 0)}));
        triangle.closeSubpath();

        path = path.united(triangle);
      }
      else
      {
        QPointF left = aliasAlign(QPointF(range.first, y));
        QPointF right = aliasAlign(QPointF(range.second, y));

        QPainterPath trapezoid;
        trapezoid.addPolygon(
            QPolygonF({left + QPoint(0, triHeight), right + QPoint(triRadius * 2, triHeight),
                       right + QPoint(triRadius, 0), left + QPoint(triRadius, 0)}));
        trapezoid.closeSubpath();

        path = path.united(trapezoid);
      }
    }

    return path;
  }

private:
  QVector<QPair<qreal, qreal>> ranges;
};

TimelineBar::TimelineBar(ICaptureContext &ctx, QWidget *parent)
    : QAbstractScrollArea(parent), m_Ctx(ctx)
{
  m_Ctx.AddCaptureViewer(this);

  setMouseTracking(true);

  setFrameShape(NoFrame);

  setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);

  QObject::connect(horizontalScrollBar(), &QScrollBar::valueChanged,
                   [this](int value) { m_pan = -value; });

  setWindowTitle(tr("Timeline"));
}

TimelineBar::~TimelineBar()
{
  m_Ctx.BuiltinWindowClosed(this);

  m_Ctx.RemoveCaptureViewer(this);
}

void TimelineBar::HighlightResourceUsage(ResourceId id)
{
  m_ID = id;
  m_UsageEvents.clear();
  m_UsageTarget = m_Ctx.GetResourceName(id);

  m_Ctx.Replay().AsyncInvoke([this, id](IReplayController *r) {
    rdcarray<EventUsage> usage = r->GetUsage(id);

    GUIInvoke::call(this, [this, usage]() {
      for(const EventUsage &u : usage)
        m_UsageEvents << u;
      std::sort(m_UsageEvents.begin(), m_UsageEvents.end());
      viewport()->update();
    });
  });

  viewport()->update();
}

void TimelineBar::HighlightHistory(ResourceId id, const rdcarray<PixelModification> &history)
{
  m_ID = id;
  m_HistoryTarget = QString();
  m_HistoryEvents.clear();

  if(id != ResourceId())
  {
    m_HistoryTarget = m_Ctx.GetResourceName(id);

    for(const PixelModification &mod : history)
      m_HistoryEvents << mod;
    std::sort(m_HistoryEvents.begin(), m_HistoryEvents.end());
  }

  viewport()->update();
}

void TimelineBar::OnCaptureClosed()
{
  setWindowTitle(tr("Timeline"));

  m_ID = ResourceId();
  m_HistoryTarget = m_UsageTarget = QString();
  m_HistoryEvents.clear();
  m_UsageEvents.clear();

  m_Draws.clear();
  m_RootDraws.clear();
  m_RootMarkers.clear();

  layout();
}

void TimelineBar::OnCaptureLoaded()
{
  uint32_t frameNumber = m_Ctx.FrameInfo().frameNumber;

  if(frameNumber != ~0U)
    setWindowTitle(tr("Timeline - Frame #%1").arg(frameNumber));
  else
    setWindowTitle(tr("Timeline - Capture"));

  processDraws(m_RootMarkers, m_RootDraws, m_Ctx.CurDrawcalls());

  m_zoom = 1.0;
  m_pan = 0.0;
  m_lastPos = QPointF();

  layout();
}

void TimelineBar::OnEventChanged(uint32_t eventId)
{
  if(!m_HistoryTarget.isEmpty())
    m_HistoryTarget = m_Ctx.GetResourceName(m_ID);
  if(!m_UsageTarget.isEmpty())
    m_UsageTarget = m_Ctx.GetResourceName(m_ID);

  viewport()->update();
}

QSize TimelineBar::minimumSizeHint() const
{
  return QSize(margin * 4 + borderWidth * 2 + 100,
               margin * 4 + borderWidth * 2 + m_eidAxisRect.height() * 2 +
                   m_highlightingRect.height() + horizontalScrollBar()->sizeHint().height());
}

void TimelineBar::resizeEvent(QResizeEvent *e)
{
  layout();
}

void TimelineBar::layout()
{
  QFontMetrics fm(Formatter::PreferredFont());

  // the area of everything
  m_area = QRectF(viewport()->rect()).marginsRemoved(uniformMargins(borderWidth + margin));

  m_titleWidth = fm.boundingRect(eidAxisTitle).width() + fm.height();

  m_dataArea = m_area;
  m_dataArea.setLeft(m_dataArea.left() + m_titleWidth);

  m_eidAxisRect = m_dataArea.marginsRemoved(uniformMargins(margin));
  m_eidAxisRect.setHeight(qMax(fm.height(), dataBarHeight));

  m_markerRect = m_dataArea.marginsRemoved(uniformMargins(margin));
  m_markerRect.setTop(m_eidAxisRect.bottom() + margin);

  m_highlightingRect = m_area;
  m_highlightingRect.setHeight(qMax(fm.height(), dataBarHeight) * 2 + highlightingMargin);
  m_highlightingRect.moveTop(m_markerRect.bottom() - m_highlightingRect.height());

  m_markerRect.setBottom(m_highlightingRect.top());

  uint32_t maxEID = m_Draws.isEmpty() ? 0 : m_Draws.back();

  int stepSize = 1;
  int stepMagnitude = 1;

  m_eidAxisLabelTextWidth = fm.boundingRect(QString::number(maxEID)).width();
  m_eidAxisLabelWidth = m_eidAxisLabelTextWidth + fm.height();
  m_eidAxisLabelStep = stepSize * stepMagnitude;

  qreal virtualSize = m_dataArea.width() * m_zoom;

  while(virtualSize > 0 &&
        (qreal(maxEID) / qreal(m_eidAxisLabelStep)) * m_eidAxisLabelWidth > virtualSize)
  {
    // increment 1, 2, 5, 10, 20, 50, 100, ...
    if(stepSize == 1)
    {
      stepSize = 2;
    }
    else if(stepSize == 2)
    {
      stepSize = 5;
    }
    else if(stepSize == 5)
    {
      stepSize = 1;
      stepMagnitude *= 10;
    }

    m_eidAxisLabelStep = stepSize * stepMagnitude;
  }

  int numLabels = maxEID / m_eidAxisLabelStep + 1;

  m_eidAxisLabelWidth = virtualSize / numLabels;

  m_eidWidth = virtualSize / (maxEID + 1);

  int savedPan = m_pan;

  horizontalScrollBar()->setRange(0, virtualSize - m_dataArea.width());
  horizontalScrollBar()->setSingleStep(m_eidAxisLabelWidth);
  horizontalScrollBar()->setPageStep(m_dataArea.width());
  horizontalScrollBar()->setValue(-savedPan);

  viewport()->update();
}

void TimelineBar::mousePressEvent(QMouseEvent *e)
{
  m_lastPos = e->localPos();

  qreal x = e->localPos().x();

  if((e->modifiers() & Qt::AltModifier) == 0)
  {
    Marker *marker = findMarker(m_RootMarkers, m_markerRect, m_lastPos);
    if(marker)
    {
      marker->expanded = !marker->expanded;
      m_lastPos = QPointF();
      viewport()->update();
      return;
    }

    if(m_highlightingRect.contains(m_lastPos))
    {
      uint32_t eid = eventAt(x);

      m_lastPos = QPointF();

      // history events get first crack at any selection, if they exist
      if(!m_HistoryEvents.isEmpty())
      {
        PixelModification search = {eid};
        auto it = std::lower_bound(m_HistoryEvents.begin(), m_HistoryEvents.end(), search);

        if(it != m_HistoryEvents.end())
        {
          // lower_bound will have returned the next highest hit. Check if there is one below, and
          // if it's closer.
          if(it != m_HistoryEvents.begin() && (eid - (it - 1)->eventId) < (it->eventId - eid))
            it--;

          m_Ctx.SetEventID({}, it->eventId, it->eventId);
        }

        return;
      }

      if(!m_UsageEvents.isEmpty())
      {
        EventUsage search(eid, ResourceUsage::Unused);
        auto it = std::lower_bound(m_UsageEvents.begin(), m_UsageEvents.end(), search);

        if(it != m_UsageEvents.end())
        {
          // lower_bound will have returned the next highest hit. Check if there is one below, and
          // if it's closer.
          if(it != m_UsageEvents.begin() && (eid - (it - 1)->eventId) < (it->eventId - eid))
            it--;

          m_Ctx.SetEventID({}, it->eventId, it->eventId);
        }
      }

      return;
    }

    if(!m_Draws.isEmpty() && m_dataArea.contains(m_lastPos))
    {
      uint32_t eid = eventAt(x);
      auto it = std::find_if(m_Draws.begin(), m_Draws.end(), [eid](uint32_t d) {
        if(d >= eid)
          return true;

        return false;
      });

      if(it == m_Draws.end())
        m_Ctx.SetEventID({}, m_Draws.back(), m_Draws.back());
      else
        m_Ctx.SetEventID({}, *it, *it);
    }
  }
}

void TimelineBar::mouseReleaseEvent(QMouseEvent *e)
{
}

void TimelineBar::mouseMoveEvent(QMouseEvent *e)
{
  if(e->buttons() == Qt::LeftButton && m_lastPos != QPointF())
  {
    qreal x = e->localPos().x();

    if(e->modifiers() & Qt::AltModifier)
    {
      qreal delta = x - m_lastPos.x();
      m_pan += delta;

      m_pan = qBound(-m_eidAxisRect.width() * (m_zoom - 1.0), m_pan, 0.0);

      layout();
    }
    else if(!m_Draws.isEmpty() && m_dataArea.contains(e->localPos()) &&
            !m_highlightingRect.contains(e->localPos()))
    {
      uint32_t eid = eventAt(x);
      auto it = std::find_if(m_Draws.begin(), m_Draws.end(), [eid](uint32_t d) {
        if(d >= eid)
          return true;

        return false;
      });

      if(it != m_Draws.end())
        m_Ctx.SetEventID({}, *it, *it);
    }
  }
  else
  {
    viewport()->update();
  }

  m_lastPos = e->localPos();

  Marker *marker = findMarker(m_RootMarkers, m_markerRect, m_lastPos);
  if(marker)
    setCursor(Qt::PointingHandCursor);
  else
    unsetCursor();
}

void TimelineBar::wheelEvent(QWheelEvent *e)
{
  float mod = (1.0 + e->delta() / 2500.0f);

  qreal prevZoom = m_zoom;

  m_zoom = qMax(1.0, m_zoom * mod);

  qreal zoomDelta = (m_zoom / prevZoom);

  // adjust the pan so that it's still in bounds, and so the zoom acts centred on the mouse
  qreal newPan = m_pan;

  newPan -= (e->x() - m_eidAxisRect.left());
  newPan = newPan * zoomDelta;
  newPan += (e->x() - m_eidAxisRect.left());

  m_pan = qBound(-m_dataArea.width() * (m_zoom - 1.0), newPan, 0.0);

  e->accept();

  layout();
}

void TimelineBar::leaveEvent(QEvent *e)
{
  unsetCursor();
  viewport()->update();
}

void TimelineBar::paintEvent(QPaintEvent *e)
{
  QStylePainter p(viewport());

  p.setFont(font());
  p.setRenderHint(QPainter::TextAntialiasing);

  // draw boundaries and background
  {
    QRectF r = viewport()->rect();

    p.fillRect(r, palette().brush(QPalette::Window));

    QRectF borderRect = r.marginsRemoved(uniformMargins(margin));
    r = borderRect.marginsRemoved(uniformMargins(borderWidth));

    p.fillRect(r, palette().brush(QPalette::Base));

    QStyleOptionFrame frameOpt;
    frameOpt.initFrom(viewport());
    frameOpt.rect = borderRect.toRect();
    frameOpt.frameShape = QFrame::Box;
    frameOpt.lineWidth = 1;
    p.drawControl(QStyle::CE_ShapedFrame, frameOpt);
  }

  QTextOption to;

  to.setWrapMode(QTextOption::NoWrap);
  to.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);

  {
    QRectF titleRect = m_eidAxisRect;
    titleRect.setLeft(titleRect.left() - m_titleWidth);
    titleRect.setWidth(m_titleWidth);

    p.setPen(QPen(palette().brush(QPalette::Text), 1.0));

    // add an extra margin for the text
    p.drawText(titleRect.marginsRemoved(QMarginsF(margin, 0, 0, 0)), eidAxisTitle, to);

    titleRect.setLeft(titleRect.left() - margin);
    titleRect.setTop(titleRect.top() - margin);

    drawLine(p, titleRect.bottomLeft(), titleRect.bottomRight());
    drawLine(p, titleRect.topRight(), titleRect.bottomRight());
  }

  QRectF eidAxisRect = m_eidAxisRect;

  drawLine(p, eidAxisRect.bottomLeft(), eidAxisRect.bottomRight() + QPointF(margin, 0));

  drawLine(p, m_highlightingRect.topLeft(), m_highlightingRect.topRight());

  if(m_Draws.isEmpty())
    return;

  eidAxisRect.setLeft(m_eidAxisRect.left() + m_pan);

  uint32_t maxEID = m_Draws.isEmpty() ? 0 : m_Draws.back();

  to.setAlignment(Qt::AlignCenter | Qt::AlignVCenter);

  p.setFont(Formatter::PreferredFont());

  QFontMetrics fm = p.fontMetrics();

  QRectF hoverRect = eidAxisRect;

  // clip labels to the visible section
  p.setClipRect(m_eidAxisRect.marginsAdded(QMargins(0, margin, margin, 0)));

  // draw where we're hovering
  {
    QPoint pos = viewport()->mapFromGlobal(QCursor::pos());

    if(m_dataArea.contains(pos))
    {
      uint32_t hoverEID = eventAt(pos.x());

      hoverRect.setLeft(offsetOf(hoverEID));
      hoverRect.setWidth(m_eidAxisLabelWidth);

      // recentre
      hoverRect.moveLeft(hoverRect.left() - m_eidAxisLabelWidth / 2 + m_eidWidth / 2);

      QColor backCol = palette().color(QPalette::Base);

      if(getLuminance(backCol) < 0.2f)
        backCol = backCol.lighter(120);
      else
        backCol = backCol.darker(120);

      QRectF backRect = hoverRect.marginsAdded(QMargins(0, margin - borderWidth, 0, 0));

      backRect.setLeft(qMax(backRect.left(), m_eidAxisRect.left() + 1));

      p.fillRect(backRect, backCol);

      p.drawText(hoverRect, QString::number(hoverEID), to);

      // re-add the top margin so the lines match up with the border around the EID axis
      hoverRect = hoverRect.marginsAdded(QMargins(0, margin, 0, 0));

      if(hoverRect.left() >= m_eidAxisRect.left())
        drawLine(p, hoverRect.topLeft(), hoverRect.bottomLeft());
      drawLine(p, hoverRect.topRight(), hoverRect.bottomRight());

      // shrink the rect a bit for clipping against labels below
      hoverRect.setX(qRound(hoverRect.x() + 0.5));
      hoverRect.setWidth(int(hoverRect.width()));
    }
    else
    {
      hoverRect = QRectF();
    }
  }

  QRectF labelRect = eidAxisRect;
  labelRect.setWidth(m_eidAxisLabelWidth);

  // iterate through the EIDs from 0, starting from possible a negative offset if the user has
  // panned to the right.
  for(uint32_t i = 0; i <= maxEID; i += m_eidAxisLabelStep)
  {
    labelRect.moveLeft(offsetOf(i) - labelRect.width() / 2 + m_eidWidth / 2);

    // check if this label is visible at all, but don't draw labels that intersect with the hovered
    // number
    if(labelRect.right() >= 0 && !labelRect.intersects(hoverRect))
      p.drawText(labelRect, QString::number(i), to);

    // check if labelRect is off the edge of the screen
    if(labelRect.left() >= m_eidAxisRect.right())
      break;
  }

  // stop clipping
  p.setClipRect(viewport()->rect());

  // clip the markers
  p.setClipRect(m_markerRect);

  {
    QPen pen = p.pen();
    paintMarkers(p, m_RootMarkers, m_RootDraws, m_markerRect);
    p.setPen(pen);
  }

  // stop clipping
  p.setClipRect(viewport()->rect());

  QRectF currentRect = eidAxisRect;

  // draw the current label and line
  {
    uint32_t curEID = m_Ctx.CurEvent();

    currentRect.setLeft(offsetOf(curEID));
    currentRect.setWidth(
        qMax(m_eidAxisLabelWidth, m_eidAxisLabelTextWidth + dataBarHeight + margin * 2));

    // recentre
    currentRect.moveLeft(currentRect.left() - currentRect.width() / 2 + m_eidWidth / 2);

    // remember where the middle would have been, without clamping
    qreal realMiddle = currentRect.center().x();

    // clamp the position from the left or right side
    if(currentRect.left() < m_eidAxisRect.left())
      currentRect.moveLeft(m_eidAxisRect.left());
    else if(currentRect.right() > m_eidAxisRect.right())
      currentRect.moveRight(m_eidAxisRect.right());

    // re-add the top margin so the lines match up with the border around the EID axis
    QRectF currentBackRect = currentRect.marginsAdded(QMargins(0, margin, 0, 0));

    p.fillRect(currentBackRect, palette().brush(QPalette::Base));
    drawLine(p, currentBackRect.topLeft(), currentBackRect.bottomLeft());
    drawLine(p, currentBackRect.topRight(), currentBackRect.bottomRight());

    // draw the 'current marker' pixmap
    const QPixmap &px = Pixmaps::flag_green(devicePixelRatio());
    p.drawPixmap(currentRect.topLeft() + QPointF(margin, 1), px, px.rect());

    // move to where the text should be and draw it
    currentRect.setLeft(currentRect.left() + margin * 2 + dataBarHeight);
    p.drawText(currentRect, QString::number(curEID), to);

    // draw a line from the bottom of the shadow downwards
    QPointF currentTop = currentRect.center();
    currentTop.setX(int(qBound(m_eidAxisRect.left(), realMiddle, m_eidAxisRect.right() - 2.0)) + 0.5);
    currentTop.setY(currentRect.bottom());

    QPointF currentBottom = currentTop;
    currentBottom.setY(m_markerRect.bottom());

    drawLine(p, currentTop, currentBottom);
  }

  to.setAlignment(Qt::AlignLeft | Qt::AlignTop);

  if(!m_UsageTarget.isEmpty() || !m_HistoryTarget.isEmpty())
  {
    p.setRenderHint(QPainter::Antialiasing);

    QRectF highlightLabel = m_highlightingRect.marginsRemoved(uniformMargins(margin));

    highlightLabel.setX(highlightLabel.x() + margin);

    QString text;

    if(!m_HistoryTarget.isEmpty())
      text = tr("Pixel history for %1").arg(m_HistoryTarget);
    else
      text = tr("Usage for %1: ").arg(m_UsageTarget);

    p.drawText(highlightLabel, text, to);

    const int triRadius = fm.averageCharWidth();
    const int triHeight = fm.ascent();

    QPainterPath triangle;
    triangle.addPolygon(
        QPolygonF({QPoint(0, triHeight), QPoint(triRadius * 2, triHeight), QPoint(triRadius, 0)}));
    triangle.closeSubpath();

    enum
    {
      ReadUsage,
      WriteUsage,
      ReadWriteUsage,
      ClearUsage,
      BarrierUsage,

      HistoryPassed,
      HistoryFailed,

      UsageCount,
    };

    // colors taken from http://mkweb.bcgsc.ca/colorblind/ to be distinct for people with color
    // blindness

    const QColor colors[UsageCount] = {
        // read
        QColor::fromRgb(230, 159, 0),
        // write
        QColor::fromRgb(86, 180, 233),
        // read/write
        QColor::fromRgb(240, 228, 66),
        // clear
        QColor::fromRgb(0, 0, 0),
        // barrier
        QColor::fromRgb(204, 121, 167),

        // pass
        QColor::fromRgb(0, 158, 115),
        // fail
        QColor::fromRgb(213, 94, 0),
    };

    // draw the key
    if(m_HistoryTarget.isEmpty())
    {
      // advance past the first text to draw the key
      highlightLabel.setLeft(highlightLabel.left() + fm.boundingRect(text).width());

      text = lit(" Reads ( ");
      p.drawText(highlightLabel, text, to);
      highlightLabel.setLeft(highlightLabel.left() + fm.boundingRect(text).width());

      QPainterPath path = triangle.translated(aliasAlign(highlightLabel.topLeft()));
      p.fillPath(path, colors[ReadUsage]);
      p.drawPath(path);
      highlightLabel.setLeft(highlightLabel.left() + triRadius * 2);

      text = lit(" ), Writes ( ");
      p.drawText(highlightLabel, text, to);
      highlightLabel.setLeft(highlightLabel.left() + fm.boundingRect(text).width());

      path = triangle.translated(aliasAlign(highlightLabel.topLeft()));
      p.fillPath(path, colors[WriteUsage]);
      p.drawPath(path);
      highlightLabel.setLeft(highlightLabel.left() + triRadius * 2);

      text = lit(" ), Read/Write ( ");
      p.drawText(highlightLabel, text, to);
      highlightLabel.setLeft(highlightLabel.left() + fm.boundingRect(text).width());

      path = triangle.translated(aliasAlign(highlightLabel.topLeft()));
      p.fillPath(path, colors[ReadWriteUsage]);
      p.drawPath(path);
      highlightLabel.setLeft(highlightLabel.left() + triRadius * 2);

      if(m_Ctx.CurPipelineState().SupportsBarriers())
      {
        text = lit(" ) Barriers ( ");
        p.drawText(highlightLabel, text, to);
        highlightLabel.setLeft(highlightLabel.left() + fm.boundingRect(text).width());

        path = triangle.translated(aliasAlign(highlightLabel.topLeft()));
        p.fillPath(path, colors[BarrierUsage]);
        p.drawPath(path);
        highlightLabel.setLeft(highlightLabel.left() + triRadius * 2);
      }

      text = lit(" ), and Clears ( ");
      p.drawText(highlightLabel, text, to);
      highlightLabel.setLeft(highlightLabel.left() + fm.boundingRect(text).width());

      path = triangle.translated(aliasAlign(highlightLabel.topLeft()));
      p.fillPath(path, colors[ClearUsage]);
      p.drawPath(path);
      highlightLabel.setLeft(highlightLabel.left() + triRadius * 2);

      text = lit(" )");
      p.drawText(highlightLabel, text, to);
    }

    PipRanges pipranges[UsageCount];

    QRectF pipsRect = m_highlightingRect.marginsRemoved(uniformMargins(margin));

    pipsRect.setX(pipsRect.x() + margin + m_titleWidth);
    pipsRect.setHeight(triHeight + margin);
    pipsRect.moveBottom(m_highlightingRect.bottom());

    p.setClipRect(pipsRect);

    qreal leftClip = -triRadius * 2.0;
    qreal rightClip = pipsRect.width() + triRadius * 10.0;

    if(!m_HistoryEvents.isEmpty())
    {
      for(const PixelModification &mod : m_HistoryEvents)
      {
        qreal pos = offsetOf(mod.eventId) + m_eidWidth / 2 - triRadius;

        if(pos < leftClip || pos > rightClip)
          continue;

        if(mod.Passed())
          pipranges[HistoryPassed].push(pos, triRadius);
        else
          pipranges[HistoryFailed].push(pos, triRadius);
      }
    }
    else
    {
      for(const EventUsage &use : m_UsageEvents)
      {
        qreal pos = offsetOf(use.eventId) + m_eidWidth / 2 - triRadius;

        if(pos < leftClip || pos > rightClip)
          continue;

        if(((int)use.usage >= (int)ResourceUsage::VS_RWResource &&
            (int)use.usage <= (int)ResourceUsage::All_RWResource) ||
           use.usage == ResourceUsage::GenMips || use.usage == ResourceUsage::Copy ||
           use.usage == ResourceUsage::Resolve)
        {
          pipranges[ReadWriteUsage].push(pos, triRadius);
        }
        else if(use.usage == ResourceUsage::StreamOut || use.usage == ResourceUsage::ResolveDst ||
                use.usage == ResourceUsage::ColorTarget ||
                use.usage == ResourceUsage::DepthStencilTarget ||
                use.usage == ResourceUsage::CopyDst || use.usage == ResourceUsage::CPUWrite)
        {
          pipranges[WriteUsage].push(pos, triRadius);
        }
        else if(use.usage == ResourceUsage::Clear || use.usage == ResourceUsage::Discard)
        {
          pipranges[ClearUsage].push(pos, triRadius);
        }
        else if(use.usage == ResourceUsage::Barrier)
        {
          pipranges[BarrierUsage].push(pos, triRadius);
        }
        else
        {
          pipranges[ReadUsage].push(pos, triRadius);
        }
      }
    }

    for(int i = 0; i < UsageCount; i++)
    {
      QPainterPath path = pipranges[i].makePath(triRadius, triHeight, pipsRect.y());

      if(!path.isEmpty())
      {
        p.drawPath(path);
        p.fillPath(path, colors[i]);
      }
    }
  }
  else
  {
    QRectF highlightLabel = m_highlightingRect;
    highlightLabel = highlightLabel.marginsRemoved(uniformMargins(margin));

    highlightLabel.setX(highlightLabel.x() + margin);

    p.drawText(highlightLabel, tr("No resource selected for highlighting."), to);
  }
}

TimelineBar::Marker *TimelineBar::findMarker(QVector<Marker> &markers, QRectF markerRect, QPointF pos)
{
  QFontMetrics fm(Formatter::PreferredFont());

  for(Marker &m : markers)
  {
    QRectF r = markerRect;
    r.setLeft(qMax(m_markerRect.left() + borderWidth * 2, offsetOf(m.eidStart)));
    r.setRight(qMin(m_markerRect.right() - borderWidth, offsetOf(m.eidEnd + 1)));
    r.setHeight(fm.height() + borderWidth * 2);

    if(r.width() <= borderWidth * 2)
      continue;

    if(r.contains(pos))
    {
      return &m;
    }

    if(!m.children.isEmpty() && m.expanded)
    {
      QRectF childRect = r;
      childRect.setTop(r.bottom() + borderWidth * 2);
      childRect.setBottom(markerRect.bottom());

      Marker *res = findMarker(m.children, childRect, pos);

      if(res)
        return res;
    }
  }

  return NULL;
}

void TimelineBar::drawLine(QStylePainter &p, QPointF start, QPointF end)
{
  if(start.x() == end.x() || start.y() == end.y())
  {
    QStyleOptionFrame opt;
    opt.initFrom(viewport());
    opt.rect = QRectF(start, end).toRect();
    opt.frameShape = (start.y() == end.y() ? QFrame::HLine : QFrame::VLine);
    p.drawControl(QStyle::CE_ShapedFrame, opt);
  }
  else
  {
    p.drawLine(start, end);
  }
}

void TimelineBar::paintMarkers(QPainter &p, const QVector<Marker> &markers,
                               const QVector<uint32_t> &draws, QRectF markerRect)
{
  if(markers.isEmpty() && draws.isEmpty())
    return;

  QTextOption to;

  to.setWrapMode(QTextOption::NoWrap);
  to.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);

  QFontMetrics fm(Formatter::PreferredFont());

  // store a reference of what a completely elided string looks like
  QString tooshort = fm.elidedText(lit("asd"), Qt::ElideRight, fm.height());

  for(const Marker &m : markers)
  {
    QRectF r = markerRect;
    r.setLeft(qMax(m_dataArea.left() + borderWidth * 3, offsetOf(m.eidStart)));
    r.setRight(qMin(m_dataArea.right() - borderWidth, offsetOf(m.eidEnd + 1)));
    r.setHeight(fm.height() + borderWidth * 2);

    if(r.width() <= borderWidth * 2)
      continue;

    QColor backColor = m.color;
    if(r.contains(m_lastPos))
      backColor.setAlpha(150);

    p.setPen(QPen(palette().brush(QPalette::Text), 1.0));
    p.fillRect(r, QBrush(backColor));
    p.drawRect(r);

    p.setPen(QPen(QBrush(contrastingColor(backColor, palette().color(QPalette::Text))), 1.0));

    r.setLeft(r.left() + margin);

    int plusWidth = fm.boundingRect(QLatin1Char('+')).width();
    if(r.width() > plusWidth)
    {
      QRectF plusRect = r;
      plusRect.setWidth(plusWidth);

      QTextOption plusOption = to;
      plusOption.setAlignment(Qt::AlignCenter | Qt::AlignVCenter);

      p.drawText(plusRect, m.expanded ? lit("-") : lit("+"), plusOption);

      r.setLeft(r.left() + plusWidth + margin);
    }

    QString elided = fm.elidedText(m.name, Qt::ElideRight, r.width());

    // if everything was elided, just omit the title entirely
    if(elided == tooshort)
      elided.clear();

    r.setLeft(qRound(r.left() + margin));

    p.drawText(r, elided, to);

    if(m.expanded)
    {
      QRectF childRect = r;
      childRect.setTop(r.bottom() + borderWidth * 2);
      childRect.setBottom(markerRect.bottom());

      paintMarkers(p, m.children, m.draws, childRect);
    }
  }

  p.setRenderHint(QPainter::Antialiasing);

  for(uint32_t d : draws)
  {
    QRectF r = markerRect;
    r.setLeft(qMax(m_dataArea.left() + borderWidth * 3, offsetOf(d)));
    r.setRight(qMin(m_dataArea.right() - borderWidth, offsetOf(d + 1)));
    r.setHeight(fm.height() + borderWidth * 2);

    QPainterPath path;
    path.addRoundedRect(r, 5, 5);

    p.setPen(QPen(palette().brush(QPalette::Text), 1.0));
    p.fillPath(path, d == m_Ctx.CurEvent() ? Qt::green : Qt::blue);
    p.drawPath(path);
  }

  p.setRenderHint(QPainter::Antialiasing, false);
}

uint32_t TimelineBar::eventAt(qreal x)
{
  if(m_Draws.isEmpty())
    return 0;

  // clamp to the visible viewport
  x = qBound(m_eidAxisRect.left(), x, m_eidAxisRect.right());

  // do the reverse of offsetOf() - first make the x relative to the root
  x -= m_pan + m_eidAxisRect.left();

  // multiply up to get a floating point 'steps'
  qreal steps = x / m_eidAxisLabelWidth;

  // finally convert to EID and clamp
  uint32_t maxEID = m_Draws.back();
  return qMin(maxEID, uint32_t(steps * m_eidAxisLabelStep));
}

qreal TimelineBar::offsetOf(uint32_t eid)
{
  int steps = eid / m_eidAxisLabelStep;

  qreal fractionalPart = qreal(eid % m_eidAxisLabelStep) / qreal(m_eidAxisLabelStep);

  return m_eidAxisRect.left() + m_pan + steps * m_eidAxisLabelWidth +
         fractionalPart * m_eidAxisLabelWidth;
}

uint32_t TimelineBar::processDraws(QVector<Marker> &markers, QVector<uint32_t> &draws,
                                   const rdcarray<DrawcallDescription> &curDraws)
{
  uint32_t maxEID = 0;

  for(const DrawcallDescription &d : curDraws)
  {
    if(!d.children.isEmpty())
    {
      markers.push_back(Marker());
      Marker &m = markers.back();

      m.name = d.name;
      m.eidStart = d.eventId;
      m.eidEnd = processDraws(m.children, m.draws, d.children);

      maxEID = qMax(maxEID, m.eidEnd);

      if(d.markerColor.w > 0.0f)
      {
        m.color = QColor::fromRgb(
            qRgb(d.markerColor.x * 255.0f, d.markerColor.y * 255.0f, d.markerColor.z * 255.0f));
      }
      else
      {
        m.color = QColor(Qt::gray);
      }
    }
    else
    {
      if(!(d.flags & DrawFlags::SetMarker))
      {
        m_Draws.push_back(d.eventId);
        draws.push_back(d.eventId);
      }
    }

    maxEID = qMax(maxEID, d.eventId);
  }

  return maxEID;
}
