// Copyright 2017 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "DolphinQt/FIFO/FIFOPlayerWindow.h"

#include <QCheckBox>
#include <QDialogButtonBox>
#include <QEvent>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QIcon>
#include <QKeyEvent>
#include <QKeySequence>
#include <QLabel>
#include <QPushButton>
#include <QSpinBox>
#include <QTabWidget>
#include <QVBoxLayout>

#include <algorithm>

#include "Core/Core.h"
#include "Core/FifoPlayer/FifoDataFile.h"
#include "Core/FifoPlayer/FifoPlayer.h"
#include "Core/FifoPlayer/FifoRecorder.h"

#include "DolphinQt/Config/ToolTipControls/ToolTipCheckBox.h"
#include "DolphinQt/FIFO/FIFOAnalyzer.h"
#include "DolphinQt/QtUtils/DolphinFileDialog.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
#include "DolphinQt/QtUtils/QueueOnObject.h"
#include "DolphinQt/Resources.h"
#include "DolphinQt/Settings.h"

FIFOPlayerWindow::FIFOPlayerWindow(QWidget* parent) : QWidget(parent)
{
  setWindowTitle(tr("FIFO Player"));
  setWindowIcon(Resources::GetAppIcon());

  CreateWidgets();
  LoadSettings();
  ConnectWidgets();
  AddDescriptions();

  UpdateInfo();

  UpdateControls();

  FifoPlayer::GetInstance().SetFileLoadedCallback(
      [this] { QueueOnObject(this, &FIFOPlayerWindow::OnFIFOLoaded); });
  FifoPlayer::GetInstance().SetFrameWrittenCallback([this] {
    QueueOnObject(this, [this] {
      UpdateInfo();
      UpdateControls();
    });
  });

  connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, [this](Core::State state) {
    if (state == Core::State::Running && m_emu_state != Core::State::Paused)
      OnEmulationStarted();
    else if (state == Core::State::Uninitialized)
      OnEmulationStopped();
    m_emu_state = state;
  });

  installEventFilter(this);
}

FIFOPlayerWindow::~FIFOPlayerWindow()
{
  FifoPlayer::GetInstance().SetFileLoadedCallback({});
  FifoPlayer::GetInstance().SetFrameWrittenCallback({});
}

void FIFOPlayerWindow::CreateWidgets()
{
  auto* layout = new QVBoxLayout;

  // Info
  auto* info_group = new QGroupBox(tr("File Info"));
  auto* info_layout = new QHBoxLayout;

  m_info_label = new QLabel;
  info_layout->addWidget(m_info_label);
  info_group->setLayout(info_layout);

  m_info_label->setFixedHeight(QFontMetrics(font()).lineSpacing() * 3);

  // Object Range
  auto* object_range_group = new QGroupBox(tr("Object Range"));
  auto* object_range_layout = new QHBoxLayout;

  m_object_range_from = new QSpinBox;
  m_object_range_from_label = new QLabel(tr("From:"));
  m_object_range_to = new QSpinBox;
  m_object_range_to_label = new QLabel(tr("To:"));

  object_range_layout->addWidget(m_object_range_from_label);
  object_range_layout->addWidget(m_object_range_from);
  object_range_layout->addWidget(m_object_range_to_label);
  object_range_layout->addWidget(m_object_range_to);
  object_range_group->setLayout(object_range_layout);

  // Frame Range
  auto* frame_range_group = new QGroupBox(tr("Frame Range"));
  auto* frame_range_layout = new QHBoxLayout;

  m_frame_range_from = new QSpinBox;
  m_frame_range_from_label = new QLabel(tr("From:"));
  m_frame_range_to = new QSpinBox;
  m_frame_range_to_label = new QLabel(tr("To:"));

  frame_range_layout->addWidget(m_frame_range_from_label);
  frame_range_layout->addWidget(m_frame_range_from);
  frame_range_layout->addWidget(m_frame_range_to_label);
  frame_range_layout->addWidget(m_frame_range_to);
  frame_range_group->setLayout(frame_range_layout);

  // Playback Options
  auto* playback_group = new QGroupBox(tr("Playback Options"));
  auto* playback_layout = new QGridLayout;
  m_early_memory_updates = new ToolTipCheckBox(tr("Early Memory Updates"));
  m_loop = new ToolTipCheckBox(tr("Loop"));

  playback_layout->addWidget(object_range_group, 0, 0);
  playback_layout->addWidget(frame_range_group, 0, 1);
  playback_layout->addWidget(m_early_memory_updates, 1, 0);
  playback_layout->addWidget(m_loop, 1, 1);
  playback_group->setLayout(playback_layout);

  // Recording Options
  auto* recording_group = new QGroupBox(tr("Recording Options"));
  auto* recording_layout = new QHBoxLayout;
  m_frame_record_count = new QSpinBox;
  m_frame_record_count_label = new QLabel(tr("Frames to Record:"));

  m_frame_record_count->setMinimum(1);
  m_frame_record_count->setMaximum(3600);
  m_frame_record_count->setValue(3);

  recording_layout->addWidget(m_frame_record_count_label);
  recording_layout->addWidget(m_frame_record_count);
  recording_group->setLayout(recording_layout);

  m_button_box = new QDialogButtonBox(QDialogButtonBox::Close);

  // Action Buttons
  m_load = m_button_box->addButton(tr("Load..."), QDialogButtonBox::ActionRole);
  m_save = m_button_box->addButton(tr("Save..."), QDialogButtonBox::ActionRole);
  m_record = m_button_box->addButton(tr("Record"), QDialogButtonBox::ActionRole);
  m_stop = m_button_box->addButton(tr("Stop"), QDialogButtonBox::ActionRole);

  layout->addWidget(info_group);
  layout->addWidget(playback_group);
  layout->addWidget(recording_group);
  layout->addWidget(m_button_box);

  m_main_widget = new QWidget(this);
  m_main_widget->setLayout(layout);

  m_tab_widget = new QTabWidget(this);

  m_analyzer = new FIFOAnalyzer;

  m_tab_widget->addTab(m_main_widget, tr("Play / Record"));
  m_tab_widget->addTab(m_analyzer, tr("Analyze"));

  auto* tab_layout = new QVBoxLayout;
  tab_layout->addWidget(m_tab_widget);

  setLayout(tab_layout);
}

void FIFOPlayerWindow::LoadSettings()
{
  m_early_memory_updates->setChecked(Config::Get(Config::MAIN_FIFOPLAYER_EARLY_MEMORY_UPDATES));
  m_loop->setChecked(Config::Get(Config::MAIN_FIFOPLAYER_LOOP_REPLAY));
}

void FIFOPlayerWindow::ConnectWidgets()
{
  connect(m_load, &QPushButton::clicked, this, &FIFOPlayerWindow::LoadRecording);
  connect(m_save, &QPushButton::clicked, this, &FIFOPlayerWindow::SaveRecording);
  connect(m_record, &QPushButton::clicked, this, &FIFOPlayerWindow::StartRecording);
  connect(m_stop, &QPushButton::clicked, this, &FIFOPlayerWindow::StopRecording);
  connect(m_button_box, &QDialogButtonBox::rejected, this, &FIFOPlayerWindow::hide);
  connect(m_early_memory_updates, &QCheckBox::toggled, this, &FIFOPlayerWindow::OnConfigChanged);
  connect(m_loop, &QCheckBox::toggled, this, &FIFOPlayerWindow::OnConfigChanged);

  connect(m_frame_range_from, qOverload<int>(&QSpinBox::valueChanged), this,
          &FIFOPlayerWindow::OnLimitsChanged);
  connect(m_frame_range_to, qOverload<int>(&QSpinBox::valueChanged), this,
          &FIFOPlayerWindow::OnLimitsChanged);

  connect(m_object_range_from, qOverload<int>(&QSpinBox::valueChanged), this,
          &FIFOPlayerWindow::OnLimitsChanged);
  connect(m_object_range_to, qOverload<int>(&QSpinBox::valueChanged), this,
          &FIFOPlayerWindow::OnLimitsChanged);
}

void FIFOPlayerWindow::AddDescriptions()
{
  static const char TR_MEMORY_UPDATES_DESCRIPTION[] = QT_TR_NOOP(
      "If enabled, then all memory updates happen at once before the first frame.<br><br>"
      "Causes issues with many fifologs, but can be useful for testing.<br><br>"
      "<dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
  static const char TR_LOOP_DESCRIPTION[] =
      QT_TR_NOOP("If unchecked, then playback of the fifolog stops after the final frame.<br><br>"
                 "This is generally only useful when a frame-dumping option is enabled.<br><br>"
                 "<dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");

  m_early_memory_updates->SetDescription(tr(TR_MEMORY_UPDATES_DESCRIPTION));
  m_loop->SetDescription(tr(TR_LOOP_DESCRIPTION));
}

void FIFOPlayerWindow::LoadRecording()
{
  QString path = DolphinFileDialog::getOpenFileName(this, tr("Open FIFO log"), QString(),
                                                    tr("Dolphin FIFO Log (*.dff)"));

  if (path.isEmpty())
    return;

  emit LoadFIFORequested(path);
}

void FIFOPlayerWindow::SaveRecording()
{
  QString path = DolphinFileDialog::getSaveFileName(this, tr("Save FIFO log"), QString(),
                                                    tr("Dolphin FIFO Log (*.dff)"));

  if (path.isEmpty())
    return;

  FifoDataFile* file = FifoRecorder::GetInstance().GetRecordedFile();

  bool result = file->Save(path.toStdString());

  if (!result)
  {
    ModalMessageBox::critical(this, tr("Error"), tr("Failed to save FIFO log."));
  }
}

void FIFOPlayerWindow::StartRecording()
{
  // Start recording
  FifoRecorder::GetInstance().StartRecording(m_frame_record_count->value(), [this] {
    QueueOnObject(this, [this] { OnRecordingDone(); });
  });

  UpdateControls();

  UpdateInfo();
}

void FIFOPlayerWindow::StopRecording()
{
  FifoRecorder::GetInstance().StopRecording();

  UpdateControls();
  UpdateInfo();
}

void FIFOPlayerWindow::OnEmulationStarted()
{
  UpdateControls();

  if (FifoPlayer::GetInstance().GetFile())
    OnFIFOLoaded();
}

void FIFOPlayerWindow::OnEmulationStopped()
{
  // If we have previously been recording, stop now.
  if (FifoRecorder::GetInstance().IsRecording())
    StopRecording();

  UpdateControls();
  // When emulation stops, switch away from the analyzer tab, as it no longer shows anything useful
  m_tab_widget->setCurrentWidget(m_main_widget);
  m_analyzer->Update();
}

void FIFOPlayerWindow::OnRecordingDone()
{
  UpdateInfo();
  UpdateControls();
}

void FIFOPlayerWindow::UpdateInfo()
{
  if (FifoPlayer::GetInstance().IsPlaying())
  {
    FifoDataFile* file = FifoPlayer::GetInstance().GetFile();
    m_info_label->setText(
        tr("%1 frame(s)\n%2 object(s)\nCurrent Frame: %3")
            .arg(QString::number(file->GetFrameCount()),
                 QString::number(FifoPlayer::GetInstance().GetCurrentFrameObjectCount()),
                 QString::number(FifoPlayer::GetInstance().GetCurrentFrameNum())));
    return;
  }

  if (FifoRecorder::GetInstance().IsRecordingDone())
  {
    FifoDataFile* file = FifoRecorder::GetInstance().GetRecordedFile();
    size_t fifo_bytes = 0;
    size_t mem_bytes = 0;

    for (u32 i = 0; i < file->GetFrameCount(); ++i)
    {
      fifo_bytes += file->GetFrame(i).fifoData.size();
      for (const auto& mem_update : file->GetFrame(i).memoryUpdates)
        mem_bytes += mem_update.data.size();
    }

    m_info_label->setText(tr("%1 FIFO bytes\n%2 memory bytes\n%3 frames")
                              .arg(QString::number(fifo_bytes), QString::number(mem_bytes),
                                   QString::number(file->GetFrameCount())));
    return;
  }

  if (Core::IsRunning() && FifoRecorder::GetInstance().IsRecording())
  {
    m_info_label->setText(tr("Recording..."));
    return;
  }

  m_info_label->setText(tr("No file loaded / recorded."));
}

void FIFOPlayerWindow::OnFIFOLoaded()
{
  FifoDataFile* file = FifoPlayer::GetInstance().GetFile();

  auto object_count = FifoPlayer::GetInstance().GetMaxObjectCount();
  auto frame_count = file->GetFrameCount();

  m_frame_range_to->setMaximum(frame_count - 1);
  m_object_range_to->setMaximum(object_count - 1);

  m_frame_range_from->setValue(0);
  m_object_range_from->setValue(0);
  m_frame_range_to->setValue(frame_count - 1);
  m_object_range_to->setValue(object_count - 1);

  UpdateInfo();
  UpdateLimits();
  UpdateControls();

  m_analyzer->Update();
}

void FIFOPlayerWindow::OnConfigChanged()
{
  Config::SetBase(Config::MAIN_FIFOPLAYER_EARLY_MEMORY_UPDATES,
                  m_early_memory_updates->isChecked());
  Config::SetBase(Config::MAIN_FIFOPLAYER_LOOP_REPLAY, m_loop->isChecked());
}

void FIFOPlayerWindow::OnLimitsChanged()
{
  FifoPlayer& player = FifoPlayer::GetInstance();

  player.SetFrameRangeStart(m_frame_range_from->value());
  player.SetFrameRangeEnd(m_frame_range_to->value());
  player.SetObjectRangeStart(m_object_range_from->value());
  player.SetObjectRangeEnd(m_object_range_to->value());
  UpdateLimits();
}

void FIFOPlayerWindow::UpdateLimits()
{
  m_frame_range_from->setMaximum(m_frame_range_to->value());
  m_frame_range_to->setMinimum(m_frame_range_from->value());
  m_object_range_from->setMaximum(m_object_range_to->value());
  m_object_range_to->setMinimum(m_object_range_from->value());
}

void FIFOPlayerWindow::UpdateControls()
{
  bool running = Core::IsRunning();
  bool is_recording = FifoRecorder::GetInstance().IsRecording();
  bool is_playing = FifoPlayer::GetInstance().IsPlaying();

  m_frame_range_from->setEnabled(is_playing);
  m_frame_range_from_label->setEnabled(is_playing);
  m_frame_range_to->setEnabled(is_playing);
  m_frame_range_to_label->setEnabled(is_playing);
  m_object_range_from->setEnabled(is_playing);
  m_object_range_from_label->setEnabled(is_playing);
  m_object_range_to->setEnabled(is_playing);
  m_object_range_to_label->setEnabled(is_playing);

  bool enable_frame_record_count = !is_playing && !is_recording;

  m_frame_record_count_label->setEnabled(enable_frame_record_count);
  m_frame_record_count->setEnabled(enable_frame_record_count);

  m_load->setEnabled(!running);
  m_record->setEnabled(running && !is_playing);

  m_stop->setVisible(running && is_recording);
  m_record->setVisible(!m_stop->isVisible());

  m_save->setEnabled(FifoRecorder::GetInstance().IsRecordingDone());
}

bool FIFOPlayerWindow::eventFilter(QObject* object, QEvent* event)
{
  // Close when escape is pressed
  if (event->type() == QEvent::KeyPress)
  {
    if (static_cast<QKeyEvent*>(event)->matches(QKeySequence::Cancel))
      hide();
  }

  return false;
}
