/******************************************************************************
 * 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 "PerformanceCounterSelection.h"
#include <QMenu>
#include <QSet>
#include "Code/Interface/QRDInterface.h"
#include "Code/Resources.h"
#include "ui_PerformanceCounterSelection.h"

#include <unordered_map>

#define JSON_ID "rdocPerformanceCounterSettings"
#define JSON_VER 1

// we can't specialise the template, but creating an overload works. This lets us use
// QSet<GPUCounter>
inline uint qHash(const GPUCounter &t)
{
  return qHash(uint32_t(t));
}

namespace
{
enum class CounterFamily
{
  Unknown,
  Generic,
  AMD,
  Intel,
  NVIDIA,
  VulkanExtended,
  ARM,
};

CounterFamily GetCounterFamily(GPUCounter counter)
{
  if(IsAMDCounter(counter))
  {
    return CounterFamily::AMD;
  }
  else if(IsIntelCounter(counter))
  {
    return CounterFamily::Intel;
  }
  else if(IsNvidiaCounter(counter))
  {
    return CounterFamily::NVIDIA;
  }
  else if(IsVulkanExtendedCounter(counter))
  {
    return CounterFamily::VulkanExtended;
  }
  else if(IsARMCounter(counter))
  {
    return CounterFamily::ARM;
  }

  return CounterFamily::Generic;
}

QString ToString(CounterFamily family)
{
  switch(family)
  {
    case CounterFamily::AMD: return lit("AMD");
    case CounterFamily::Generic: return lit("Generic");
    case CounterFamily::Intel: return lit("Intel");
    case CounterFamily::NVIDIA: return lit("NVIDIA");
    case CounterFamily::VulkanExtended: return lit("Vulkan Extended");
    case CounterFamily::ARM: return lit("ARM");
    case CounterFamily::Unknown: return lit("Unknown");
  }

  return QString();
}
}    // namespace

const int PerformanceCounterSelection::CounterDescriptionRole = Qt::UserRole + 1;
const int PerformanceCounterSelection::CounterIdRole = Qt::UserRole + 2;
const int PerformanceCounterSelection::PreviousCheckStateRole = Qt::UserRole + 3;

void PerformanceCounterSelection::uncheckAllChildren(RDTreeWidgetItem *item)
{
  for(int i = 0; i < item->childCount(); i++)
  {
    uncheckAllChildren(item->child(i));

    item->child(i)->setCheckState(0, Qt::Unchecked);
    item->child(i)->setData(0, PreviousCheckStateRole, Qt::Unchecked);
  }
}

void PerformanceCounterSelection::checkAllChildren(RDTreeWidgetItem *item)
{
  for(int i = 0; i < item->childCount(); i++)
  {
    checkAllChildren(item->child(i));

    item->child(i)->setCheckState(0, Qt::Checked);
    item->child(i)->setData(0, PreviousCheckStateRole, Qt::Checked);
  }
}

void PerformanceCounterSelection::updateParentCheckState(RDTreeWidgetItem *item)
{
  if(!item)
    return;

  int numChecked = 0;
  int numPartial = 0;

  for(int i = 0; i < item->childCount(); i++)
  {
    Qt::CheckState state = item->child(i)->checkState(0);
    if(state == Qt::PartiallyChecked)
      numPartial++;
    else if(state == Qt::Checked)
      numChecked++;
  }

  if(numChecked == item->childCount())
    item->setCheckState(0, Qt::Checked);
  else if(numChecked > 0 || numPartial > 0)
    item->setCheckState(0, Qt::PartiallyChecked);
  else
    item->setCheckState(0, Qt::Unchecked);

  item->setData(0, PreviousCheckStateRole, item->checkState(0));

  updateParentCheckState(item->parent());
}

void PerformanceCounterSelection::expandToNode(RDTreeWidgetItem *node)
{
  RDTreeWidgetItem *n = node;
  while(node != NULL)
  {
    ui->counterTree->expandItem(node);
    node = node->parent();
  }

  if(n)
    ui->counterTree->scrollToItem(n);
}

PerformanceCounterSelection::PerformanceCounterSelection(ICaptureContext &ctx,
                                                         const QList<GPUCounter> &selectedCounters,
                                                         QWidget *parent)
    : QDialog(parent), ui(new Ui::PerformanceCounterSelection), m_Ctx(ctx)
{
  ui->setupUi(this);

  setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);

  ui->counterTree->setColumns({QString()});
  ui->counterTree->setHeaderHidden(true);

  connect(ui->counterTree, &RDTreeWidget::currentItemChanged,
          [this](RDTreeWidgetItem *item, RDTreeWidgetItem *) -> void {
            const QVariant d = item->data(0, CounterDescriptionRole);

            if(d.isValid())
            {
              ui->counterDescription->setText(
                  QString(lit("<b>%1</b><hr>%2")).arg(item->text(0)).arg(d.toString()));
            }
          });

  connect(ui->save, &QPushButton::clicked, this, &PerformanceCounterSelection::Save);
  connect(ui->load, &QPushButton::clicked, this, &PerformanceCounterSelection::Load);
  connect(ui->sampleCounters, &QPushButton::clicked, this, &PerformanceCounterSelection::accept);
  connect(ui->cancel, &QPushButton::clicked, this, &PerformanceCounterSelection::reject);

  connect(ui->counterTree, &RDTreeWidget::itemChanged, [this](RDTreeWidgetItem *item, int) -> void {
    const QVariant d = item->data(0, CounterIdRole);

    static bool recurse = false;

    if(d.isValid())
    {
      if(item->checkState(0) == Qt::Checked)
      {
        // Add
        if(!m_SelectedCounters.contains((GPUCounter)d.toUInt()))
        {
          QListWidgetItem *listItem = new QListWidgetItem(ui->enabledCounters);
          listItem->setText(item->text(0));
          listItem->setData(CounterIdRole, d);
          m_SelectedCounters.insert((GPUCounter)d.toUInt(), listItem);
        }
      }
      else
      {
        // Remove
        QListWidgetItem *listItem = m_SelectedCounters.take((GPUCounter)d.toUInt());
        delete listItem;
      }

      if(!recurse)
      {
        recurse = true;
        updateParentCheckState(item->parent());
        recurse = false;
      }
    }
    else if(!recurse)
    {
      Qt::CheckState prev = item->data(0, PreviousCheckStateRole).value<Qt::CheckState>();

      if(item->checkState(0) != prev)
      {
        recurse = true;

        if(item->checkState(0) == Qt::Checked)
        {
          checkAllChildren(item);
        }
        else
        {
          uncheckAllChildren(item);
        }

        item->setData(0, PreviousCheckStateRole, item->checkState(0));

        updateParentCheckState(item);

        recurse = false;
      }
    }
  });

  ui->counterTree->setMouseTracking(true);

  ctx.Replay().AsyncInvoke([this, selectedCounters](IReplayController *controller) {
    QVector<CounterDescription> counterDescriptions;
    for(const GPUCounter counter : controller->EnumerateCounters())
    {
      counterDescriptions.append(controller->DescribeCounter(counter));
    }

    GUIInvoke::call(this, [counterDescriptions, selectedCounters, this]() {
      SetCounters(counterDescriptions);
      SetSelectedCounters(selectedCounters);
    });
  });
}

PerformanceCounterSelection::~PerformanceCounterSelection()
{
  delete ui;
}

void PerformanceCounterSelection::SetCounters(const QVector<CounterDescription> &descriptions)
{
  ui->counterTree->clear();
  ui->enabledCounters->clear();

  RDTreeWidgetItem *currentRoot = NULL;
  CounterFamily currentFamily = CounterFamily::Unknown;

  QMap<rdcstr, RDTreeWidgetItem *> categories;

  for(const CounterDescription &desc : descriptions)
  {
    m_CounterToUuid[desc.counter] = desc.uuid;
    m_UuidToCounter[desc.uuid] = desc.counter;

    const CounterFamily family = GetCounterFamily(desc.counter);
    if(family != currentFamily)
    {
      currentRoot = new RDTreeWidgetItem();
      currentRoot->setText(0, ToString(family));
      currentRoot->setCheckState(0, Qt::Unchecked);
      currentRoot->setData(0, PreviousCheckStateRole, Qt::Unchecked);
      ui->counterTree->addTopLevelItem(currentRoot);

      categories.clear();

      currentFamily = family;
    }

    RDTreeWidgetItem *categoryItem = NULL;

    const rdcstr category = desc.category;
    auto categoryIterator = categories.find(category);

    if(categoryIterator == categories.end())
    {
      RDTreeWidgetItem *item = new RDTreeWidgetItem();
      item->setText(0, desc.category);
      item->setCheckState(0, Qt::Unchecked);
      item->setData(0, PreviousCheckStateRole, Qt::Unchecked);
      currentRoot->addChild(item);

      categories[category] = item;
      categoryItem = item;
    }
    else
    {
      categoryItem = categoryIterator.value();
    }

    RDTreeWidgetItem *counterItem = new RDTreeWidgetItem();
    counterItem->setText(0, desc.name);
    counterItem->setData(0, CounterDescriptionRole, desc.description);
    counterItem->setData(0, CounterIdRole, (uint32_t)desc.counter);
    counterItem->setCheckState(0, Qt::Unchecked);
    counterItem->setData(0, PreviousCheckStateRole, Qt::Unchecked);
    categoryItem->addChild(counterItem);

    m_CounterToTreeItem[desc.counter] = counterItem;
  }
}

QList<GPUCounter> PerformanceCounterSelection::GetSelectedCounters() const
{
  return m_SelectedCounters.keys();
}

void PerformanceCounterSelection::SetSelectedCounters(const QList<GPUCounter> &counters)
{
  // We we walk over the complete tree, and toggle everything so it
  // matches the settings
  RDTreeWidgetItemIterator it(ui->counterTree);
  while(*it)
  {
    const QVariant id = (*it)->data(0, CounterIdRole);
    if(id.isValid())
    {
      const GPUCounter counter = (GPUCounter)id.toUInt();

      (*it)->setCheckState(0, counters.contains(counter) ? Qt::Checked : Qt::Unchecked);
    }

    // The loop above will uncheck all unknown counters, and not crash if some counter is no
    // longer present, or unknown

    ++it;
  }
}

void PerformanceCounterSelection::Save()
{
  QString filename = RDDialog::getSaveFileName(this, tr("Save File"), QDir::homePath(),
                                               tr("Performance Counter Settings (*.json)"));

  if(filename.isEmpty())
    return;

  QVariantList counterIds;
  for(const GPUCounter v : m_SelectedCounters.keys())
  {
    const Uuid uuid = m_CounterToUuid[v];
    QVariantList e;

    for(const uint32_t b : uuid.words)
    {
      e.append(b);
    }

    counterIds.append(QVariant(e));
  }

  QVariantMap doc;
  doc[lit("counters")] = counterIds;

  QFile f(filename);
  if(f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
  {
    SaveToJSON(doc, f, JSON_ID, JSON_VER);
  }
  else
  {
    RDDialog::critical(this, tr("Error saving config"),
                       tr("Couldn't open path %1 for write.").arg(filename));
  }
}

void PerformanceCounterSelection::Load()
{
  QString filename = RDDialog::getOpenFileName(this, tr("Load file"), QDir::homePath(),
                                               tr("Performance Counter Settings (*.json)"));

  if(filename.isEmpty())
    return;

  QVariantMap doc;
  QFile f(filename);
  if(f.open(QIODevice::ReadOnly | QIODevice::Text))
  {
    bool success = LoadFromJSON(doc, f, JSON_ID, JSON_VER);

    if(success)
    {
      QSet<GPUCounter> selectedCounters;

      QVariantList counters = doc[lit("counters")].toList();

      for(const QVariant &counter : counters)
      {
        QVariantList bytes = counter.toList();
        Uuid uuid;

        if(bytes.size() != 4)
        {
          qWarning() << "Counter ID doesn't count 4 words";
          continue;
        }

        for(int i = 0; i < 4; ++i)
        {
          uuid.words[i] = bytes[i].toUInt();
        }

        if(!m_UuidToCounter.contains(uuid))
          continue;

        selectedCounters.insert(m_UuidToCounter[uuid]);
      }

      SetSelectedCounters(selectedCounters.toList());
    }
    else
    {
      RDDialog::critical(this, tr("Error loading config"),
                         tr("Couldn't interpret settings in %1.").arg(filename));
    }
  }
  else
  {
    RDDialog::critical(this, tr("Error loading config"),
                       tr("Couldn't open path %1 for reading.").arg(filename));
  }
}

void PerformanceCounterSelection::on_enabledCounters_activated(const QModelIndex &index)
{
  QListWidgetItem *item = ui->enabledCounters->item(index.row());

  if(!item)
    return;

  QVariant counterID = item->data(CounterIdRole);

  // locate the item in the description tree
  auto it = m_CounterToTreeItem.find((GPUCounter)counterID.toUInt());

  if(it != m_CounterToTreeItem.end())
  {
    ui->counterTree->setCurrentItem(it.value());
    ui->counterTree->setSelectedItem(it.value());

    expandToNode(it.value());
  }
}
