text: /******************************************************************************
   * The MIT License (MIT)
   *
   * Copyright (c) 2019-2020 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 "ConstantBufferPreviewer.h"
  #include <QFontDatabase>
  #include <QTextStream>
  #include "toolwindowmanager/ToolWindowManager.h"
  #include "ui_ConstantBufferPreviewer.h"
  
  QList<ConstantBufferPreviewer *> ConstantBufferPreviewer::m_Previews;
  
  ConstantBufferPreviewer::ConstantBufferPreviewer(ICaptureContext &ctx, const ShaderStage stage,
                                                   uint32_t slot, uint32_t idx, QWidget *parent)
      : QFrame(parent), ui(new Ui::ConstantBufferPreviewer), m_Ctx(ctx)
  {
    ui->setupUi(this);
  
    m_stage = stage;
    m_slot = slot;
    m_arrayIdx = idx;
  
    QObject::connect(ui->formatSpecifier, &BufferFormatSpecifier::processFormat, this,
                     &ConstantBufferPreviewer::processFormat);
  
    ui->formatSpecifier->setContext(&m_Ctx);
  
    ui->splitter->setCollapsible(1, true);
    ui->splitter->setSizes({1, 0});
    ui->splitter->handle(1)->setEnabled(false);
  
    ui->variables->setColumns({tr("Name"), tr("Value"), tr("Type")});
    {
      ui->variables->header()->setSectionResizeMode(0, QHeaderView::Interactive);
      ui->variables->header()->setSectionResizeMode(1, QHeaderView::Interactive);
      ui->variables->header()->setSectionResizeMode(2, QHeaderView::Interactive);
    }
  
    ui->variables->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
  
    m_Previews.push_back(this);
    m_Ctx.AddCaptureViewer(this);
  }
  
  ConstantBufferPreviewer::~ConstantBufferPreviewer()
  {
    m_Ctx.RemoveCaptureViewer(this);
    m_Previews.removeOne(this);
    delete ui;
  }
  
  ConstantBufferPreviewer *ConstantBufferPreviewer::has(ShaderStage stage, uint32_t slot, uint32_t idx)
  {
    for(ConstantBufferPreviewer *c : m_Previews)
    {
      if(c->m_stage == stage && c->m_slot == slot && c->m_arrayIdx == idx)
        return c;
    }
  
    return NULL;
  }
  
  void ConstantBufferPreviewer::OnCaptureLoaded()
  {
    Reset();
  }
  
  void ConstantBufferPreviewer::OnCaptureClosed()
  {
    Reset();
  
    ToolWindowManager::closeToolWindow(this);
  }
  
  void ConstantBufferPreviewer::Reset()
  {
    ui->variables->clear();
    ui->variables->clearInternalExpansions();
  
    ui->saveCSV->setEnabled(false);
  }
  
  void ConstantBufferPreviewer::OnEventChanged(uint32_t eventId)
  {
    BoundCBuffer cb = m_Ctx.CurPipelineState().GetConstantBuffer(m_stage, m_slot, m_arrayIdx);
    m_cbuffer = cb.resourceId;
    uint64_t offset = cb.byteOffset;
    uint64_t size = cb.byteSize;
  
    ResourceId prevShader = m_shader;
  
    m_pipe = m_stage == ShaderStage::Compute ? m_Ctx.CurPipelineState().GetComputePipelineObject()
                                             : m_Ctx.CurPipelineState().GetGraphicsPipelineObject();
    m_shader = m_Ctx.CurPipelineState().GetShader(m_stage);
    rdcstr entryPoint = m_Ctx.CurPipelineState().GetShaderEntryPoint(m_stage);
    const ShaderReflection *reflection = m_Ctx.CurPipelineState().GetShaderReflection(m_stage);
  
    bool wasEmpty = ui->variables->topLevelItemCount() == 0;
  
    updateLabels();
  
    if(reflection == NULL || m_slot >= reflection->constantBlocks.size())
    {
      // save expansion before clearing
      if(m_formatOverride.type.members.empty())
      {
        RDTreeViewExpansionState &prevShaderExpansionState =
            ui->variables->getInternalExpansion(qHash(ToQStr(prevShader)));
        ui->variables->saveExpansion(prevShaderExpansionState, 0);
      }
  
      setVariables({});
      return;
    }
  
    if(!m_formatOverride.type.members.empty())
    {
      m_Ctx.Replay().AsyncInvoke([this, offset, size, wasEmpty](IReplayController *r) {
        bytebuf data;
        if(size > 0)
          data = r->GetBufferData(m_cbuffer, offset, size);
        rdcarray<ShaderVariable> vars = applyFormatOverride(data);
        GUIInvoke::call(this, [this, vars, wasEmpty] {
          RDTreeViewExpansionState state;
          ui->variables->saveExpansion(state, 0);
          setVariables(vars);
          if(wasEmpty)
          {
            // Expand before resizing so that collapsed data will already be visible when expanded
            ui->variables->expandAll();
            for(int i = 0; i < 3; i++)
              ui->variables->resizeColumnToContents(i);
            ui->variables->collapseAll();
          }
          ui->variables->applyExpansion(state, 0);
        });
      });
    }
    else
    {
      m_Ctx.Replay().AsyncInvoke(
          [this, prevShader, entryPoint, offset, size, wasEmpty](IReplayController *r) {
            rdcarray<ShaderVariable> vars = r->GetCBufferVariableContents(
                m_pipe, m_shader, entryPoint, m_slot, m_cbuffer, offset, size);
            GUIInvoke::call(this, [this, prevShader, vars, wasEmpty] {
  
              RDTreeViewExpansionState &prevShaderExpansionState =
                  ui->variables->getInternalExpansion(qHash(ToQStr(prevShader)));
  
              // stage, slot, and array index are all invariant across a given ConstantBufferPreviewer
              // instance. We only need to use the actual bound shader as a key.
              ui->variables->saveExpansion(prevShaderExpansionState, 0);
  
              setVariables(vars);
              if(wasEmpty)
              {
                // Expand before resizing so that collapsed data will already be visible when expanded
                ui->variables->expandAll();
                for(int i = 0; i < 3; i++)
                  ui->variables->resizeColumnToContents(i);
                ui->variables->collapseAll();
              }
  
              // if we have saved expansion state for the new shader, apply it, otherwise apply the
              // previous one to get any overlap (e.g. two different shaders with very similar or
              // identical constants)
              if(ui->variables->hasInternalExpansion(qHash(ToQStr(m_shader))))
                ui->variables->applyExpansion(
                    ui->variables->getInternalExpansion(qHash(ToQStr(m_shader))), 0);
              else
                ui->variables->applyExpansion(prevShaderExpansionState, 0);
            });
          });
    }
  }
  
  void ConstantBufferPreviewer::on_setFormat_toggled(bool checked)
  {
    if(!checked)
    {
      ui->splitter->setCollapsible(1, true);
      ui->splitter->setSizes({1, 0});
      ui->splitter->handle(1)->setEnabled(false);
  
      processFormat(QString());
      return;
    }
  
    ui->splitter->setCollapsible(1, false);
    ui->splitter->setSizes({1, 1});
    ui->splitter->handle(1)->setEnabled(true);
  }
  
  void ConstantBufferPreviewer::on_resourceDetails_clicked()
  {
    if(!m_Ctx.HasResourceInspector())
      m_Ctx.ShowResourceInspector();
  
    m_Ctx.GetResourceInspector()->Inspect(m_cbuffer);
  
    ToolWindowManager::raiseToolWindow(m_Ctx.GetResourceInspector()->Widget());
  }
  
  void ConstantBufferPreviewer::on_saveCSV_clicked()
  {
    QString filename = RDDialog::getSaveFileName(this, tr("Export buffer data as CSV"), QString(),
                                                 tr("CSV Files (*.csv)"));
  
    if(!filename.isEmpty())
    {
      QDir dirinfo = QFileInfo(filename).dir();
      if(dirinfo.exists())
      {
        QFile f(filename, this);
        if(f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
        {
          QTextStream ts(&f);
  
          ts << tr("Name,Value,Type\n");
  
          for(int i = 0; i < ui->variables->topLevelItemCount(); i++)
            exportCSV(ts, QString(), ui->variables->topLevelItem(i));
  
          return;
        }
  
        RDDialog::critical(
            this, tr("Error exporting buffer data"),
            tr("Couldn't open path %1 for write.\n%2").arg(filename).arg(f.errorString()));
      }
      else
      {
        RDDialog::critical(this, tr("Invalid directory"),
                           tr("Cannot find target directory to save to"));
      }
    }
  }
  
  void ConstantBufferPreviewer::exportCSV(QTextStream &ts, const QString &prefix, RDTreeWidgetItem *item)
  {
    if(item->childCount() == 0)
    {
      ts << QFormatStr("%1,\"%2\",%3\n").arg(item->text(0)).arg(item->text(1)).arg(item->text(2));
    }
    else
    {
      ts << QFormatStr("%1,,%2\n").arg(item->text(0)).arg(item->text(2));
      for(int i = 0; i < item->childCount(); i++)
        exportCSV(ts, item->text(0) + lit("."), item->child(i));
    }
  }
  
  void ConstantBufferPreviewer::processFormat(const QString &format)
  {
    if(format.isEmpty())
    {
      m_formatOverride = ShaderConstant();
      ui->formatSpecifier->setErrors(QString());
    }
    else
    {
      QString errors;
  
      m_formatOverride = BufferFormatter::ParseFormatString(format, ~0ULL, false, errors);
      ui->formatSpecifier->setErrors(errors);
    }
  
    OnEventChanged(m_Ctx.CurEvent());
  }
  
  void ConstantBufferPreviewer::addVariables(RDTreeWidgetItem *root,
                                             const rdcarray<ShaderVariable> &vars)
  {
    for(const ShaderVariable &v : vars)
    {
      RDTreeWidgetItem *n = new RDTreeWidgetItem({v.name, VarString(v), TypeString(v)});
  
      root->addChild(n);
  
      if(v.rows > 1)
      {
        for(uint32_t i = 0; i < v.rows; i++)
          n->addChild(new RDTreeWidgetItem(
              {QFormatStr("%1.row%2").arg(v.name).arg(i), RowString(v, i), RowTypeString(v)}));
      }
  
      if(!v.members.isEmpty())
        addVariables(n, v.members);
    }
  }
  
  void ConstantBufferPreviewer::setVariables(const rdcarray<ShaderVariable> &vars)
  {
    ui->variables->beginUpdate();
  
    ui->variables->clear();
  
    ui->saveCSV->setEnabled(false);
  
    if(!vars.isEmpty())
    {
      addVariables(ui->variables->invisibleRootItem(), vars);
      ui->saveCSV->setEnabled(true);
    }
  
    ui->variables->endUpdate();
  }
  
  void ConstantBufferPreviewer::updateLabels()
  {
    QString bufName = m_Ctx.GetResourceName(m_cbuffer);
  
    const ShaderReflection *reflection = m_Ctx.CurPipelineState().GetShaderReflection(m_stage);
  
    if(reflection != NULL)
    {
      if(m_Ctx.IsAutogeneratedName(m_cbuffer) && m_slot < reflection->constantBlocks.size() &&
         !reflection->constantBlocks[m_slot].name.isEmpty())
        bufName = QFormatStr("<%1>").arg(reflection->constantBlocks[m_slot].name);
    }
  
    ui->nameLabel->setText(bufName);
  
    GraphicsAPI pipeType = m_Ctx.APIProps().pipelineType;
  
    QString title = QFormatStr("%1 %2 %3")
                        .arg(ToQStr(m_stage, pipeType))
                        .arg(IsD3D(pipeType) ? lit("CB") : lit("UBO"))
                        .arg(m_slot);
  
    if(m_Ctx.CurPipelineState().SupportsResourceArrays())
      title += QFormatStr(" [%1]").arg(m_arrayIdx);
  
    ui->slotLabel->setText(title);
    setWindowTitle(title);
  }
  
  rdcarray<ShaderVariable> ConstantBufferPreviewer::applyFormatOverride(const bytebuf &bytes)
  {
    ShaderVariable var = InterpretShaderVar(m_formatOverride, bytes.begin(), bytes.end());
  
    return var.members;
  }
  