/***

  Olive - Non-Linear Video Editor
  Copyright (C) 2020 Olive Team

  This program 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 3 of the License, or
  (at your option) any later version.

  This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

***/

#include "mainwindow.h"

#include <QApplication>
#include <QDebug>
#include <QDesktopWidget>
#include <QMessageBox>

#ifdef Q_OS_LINUX
#include <QOffscreenSurface>
#endif

#include "mainmenu.h"
#include "mainstatusbar.h"

namespace olive {

MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent)
{
  // Resizes main window to desktop geometry on startup. Fixes the following issues:
  // * Qt on Windows has a bug that "de-maximizes" the window when widgets are added, resizing the
  //   window beforehand works around that issue and we just set it to whatever size is available.
  // * On Linux, it seems the window starts off at a vastly different size and then maximizes
  //   which throws off the proportions and makes the resulting layout wonky.
  resize(qApp->desktop()->availableGeometry(this).size());

#ifdef Q_OS_WINDOWS
  // Set up taskbar button progress bar (used for some modal tasks like exporting)
  taskbar_btn_id_ = RegisterWindowMessage("TaskbarButtonCreated");
  taskbar_interface_ = nullptr;
#endif

  // Create empty central widget - we don't actually want a central widget (so we set its maximum
  // size to 0,0) but some of Qt's docking/undocking fails without it
  QWidget* centralWidget = new QWidget(this);
  centralWidget->setMaximumSize(QSize(0, 0));
  setCentralWidget(centralWidget);

  // Set tabs to be on top of panels (default behavior is bottom)
  setTabPosition(Qt::AllDockWidgetAreas, QTabWidget::North);

  // Allow panels to be tabbed within each other
  setDockNestingEnabled(true);

  // Create and set main menu
  MainMenu* main_menu = new MainMenu(this);
  setMenuBar(main_menu);

  // Create and set status bar
  MainStatusBar* status_bar = new MainStatusBar(this);
  status_bar->ConnectTaskManager(TaskManager::instance());
  connect(status_bar, &MainStatusBar::DoubleClicked, this, &MainWindow::StatusBarDoubleClicked);
  setStatusBar(status_bar);

  // Create standard panels
  node_panel_ = PanelManager::instance()->CreatePanel<NodePanel>(this);
  footage_viewer_panel_ = PanelManager::instance()->CreatePanel<FootageViewerPanel>(this);
  param_panel_ = PanelManager::instance()->CreatePanel<ParamPanel>(this);
  curve_panel_ = PanelManager::instance()->CreatePanel<CurvePanel>(this);
  table_panel_ = PanelManager::instance()->CreatePanel<NodeTablePanel>(this);
  sequence_viewer_panel_ = PanelManager::instance()->CreatePanel<SequenceViewerPanel>(this);
  pixel_sampler_panel_ = PanelManager::instance()->CreatePanel<PixelSamplerPanel>(this);
  AppendProjectPanel();
  tool_panel_ = PanelManager::instance()->CreatePanel<ToolPanel>(this);
  task_man_panel_ = PanelManager::instance()->CreatePanel<TaskManagerPanel>(this);
  AppendTimelinePanel();
  audio_monitor_panel_ = PanelManager::instance()->CreatePanel<AudioMonitorPanel>(this);

  // Make node-related connections
  connect(node_panel_, &NodePanel::NodesSelected, param_panel_, &ParamPanel::SelectNodes);
  connect(node_panel_, &NodePanel::NodesDeselected, param_panel_, &ParamPanel::DeselectNodes);
  connect(node_panel_, &NodePanel::NodesSelected, table_panel_, &NodeTablePanel::SelectNodes);
  connect(node_panel_, &NodePanel::NodesDeselected, table_panel_, &NodeTablePanel::DeselectNodes);
  connect(param_panel_, &ParamPanel::RequestSelectNode, node_panel_, &NodePanel::Select);
  connect(param_panel_, &ParamPanel::FocusedNodeChanged, sequence_viewer_panel_, &ViewerPanel::SetGizmos);

  // Connect time signals together
  connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, param_panel_, &ParamPanel::SetTimestamp);
  connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, table_panel_, &NodeTablePanel::SetTimestamp);
  connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, curve_panel_, &NodeTablePanel::SetTimestamp);
  connect(param_panel_, &ParamPanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTimestamp);
  connect(param_panel_, &ParamPanel::TimeChanged, table_panel_, &NodeTablePanel::SetTimestamp);
  connect(param_panel_, &ParamPanel::TimeChanged, curve_panel_, &NodeTablePanel::SetTimestamp);
  connect(curve_panel_, &ParamPanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTimestamp);
  connect(curve_panel_, &ParamPanel::TimeChanged, table_panel_, &NodeTablePanel::SetTimestamp);
  connect(curve_panel_, &ParamPanel::TimeChanged, param_panel_, &NodeTablePanel::SetTimestamp);

  // Connect node order signals
  connect(param_panel_, &ParamPanel::NodeOrderChanged, curve_panel_, &CurvePanel::SetNodes);

  connect(PanelManager::instance(), &PanelManager::FocusedPanelChanged, this, &MainWindow::FocusedPanelChanged);

  sequence_viewer_panel_->ConnectTimeBasedPanel(param_panel_);
  sequence_viewer_panel_->ConnectTimeBasedPanel(curve_panel_);

  footage_viewer_panel_->ConnectPixelSamplerPanel(pixel_sampler_panel_);
  sequence_viewer_panel_->ConnectPixelSamplerPanel(pixel_sampler_panel_);

  UpdateTitle();

  QMetaObject::invokeMethod(this, "SetDefaultLayout", Qt::QueuedConnection);
}

MainWindow::~MainWindow()
{
#ifdef Q_OS_WINDOWS
  if (taskbar_interface_) {
    taskbar_interface_->Release();
  }
#endif
}

void MainWindow::LoadLayout(const MainWindowLayoutInfo &info)
{
  foreach (Folder* folder, info.open_folders()) {
    FolderOpen(folder->project(), folder, true);
  }

  foreach (const MainWindowLayoutInfo::OpenSequence& sequence, info.open_sequences()) {
    TimelinePanel* panel = OpenSequence(sequence.sequence, info.open_sequences().size() == 1);
    panel->RestoreSplitterState(sequence.panel_state);
  }

  restoreState(info.state());
}

MainWindowLayoutInfo MainWindow::SaveLayout() const
{
  MainWindowLayoutInfo info;

  foreach (ProjectPanel* panel, folder_panels_) {
    if (panel->project()) {
      info.add_folder(static_cast<Folder*>(panel->get_root_index().internalPointer()));
    }
  }

  foreach (TimelinePanel* panel, timeline_panels_) {
    if (panel->GetConnectedViewer()) {
      info.add_sequence({static_cast<Sequence*>(panel->GetConnectedViewer()->parent()),
                         panel->SaveSplitterState()});
    }
  }

  info.set_state(saveState());

  return info;
}

TimelinePanel* MainWindow::OpenSequence(Sequence *sequence, bool enable_focus)
{
  // See if this sequence is already open, and switch to it if so
  foreach (TimelinePanel* tl, timeline_panels_) {
    if (tl->GetConnectedViewer() == sequence->viewer_output()) {
      tl->raise();
      return tl;
    }
  }

  // See if we have any sequences open or not
  TimelinePanel* panel;

  if (!timeline_panels_.first()->GetConnectedViewer()) {
    panel = timeline_panels_.first();
  } else {
    panel = AppendTimelinePanel();
    enable_focus = false;
  }

  panel->ConnectViewerNode(sequence->viewer_output());

  if (enable_focus) {
    TimelineFocused(sequence->viewer_output());
  }

  return panel;
}

void MainWindow::CloseSequence(Sequence *sequence)
{
  // We defer to RemoveTimelinePanel() to close the panels, which may delete and remove indices from timeline_panels_.
  // We make a copy so that our array here doesn't get ruined by what RemoveTimelinePanel() does
  QList<TimelinePanel*> copy = timeline_panels_;

  foreach (TimelinePanel* tp, copy) {
    if (tp->GetConnectedViewer() == sequence->viewer_output()) {
      RemoveTimelinePanel(tp);
    }
  }
}

bool MainWindow::IsSequenceOpen(Sequence *sequence) const
{
  foreach (TimelinePanel* tp, timeline_panels_) {
    if (tp->GetConnectedViewer() == sequence->viewer_output()) {
      return true;
    }
  }

  return false;
}

void MainWindow::FolderOpen(Project* p, Item *i, bool floating)
{
  ProjectPanel* panel = PanelManager::instance()->CreatePanel<ProjectPanel>(this);

  // Set custom name to distinguish it from regular ProjectPanels
  panel->setObjectName(QStringLiteral("FolderPanel"));

  SetUniquePanelID<ProjectPanel>(panel, folder_panels_);

  panel->set_project(p);
  panel->set_root(i);

  // Tabify with source project panel
  foreach (ProjectPanel* proj_panel, project_panels_) {
    if (proj_panel->project() == p) {
      tabifyDockWidget(proj_panel, panel);
      break;
    }
  }

  panel->setFloating(floating);

  panel->show();
  panel->raise();

  // If the panel is closed, just destroy it
  panel->SetSignalInsteadOfClose(true);
  panel->setProperty("parent_list", reinterpret_cast<quintptr>(&folder_panels_));
  connect(panel, &ProjectPanel::CloseRequested, this, &MainWindow::FloatingPanelCloseRequested);

  folder_panels_.append(panel);
}

ScopePanel *MainWindow::AppendScopePanel()
{
  return AppendFloatingPanelInternal<ScopePanel>(scope_panels_);
}

void MainWindow::SetFullscreen(bool fullscreen)
{
  if (fullscreen) {
    setWindowState(windowState() | Qt::WindowFullScreen);
  } else {
    setWindowState(windowState() & ~Qt::WindowFullScreen);
  }
}

void MainWindow::ToggleMaximizedPanel()
{
  if (premaximized_state_.isEmpty()) {
    // Assume nothing is maximized at the moment

    // Find the currently focused panel
    PanelWidget* currently_hovered = PanelManager::instance()->CurrentlyHovered();

    // If no panel is hovered, fallback to the currently active panel
    if (!currently_hovered) {
      currently_hovered = PanelManager::instance()->CurrentlyFocused();

      // If no panel is hovered or focused, do nothing
      if (!currently_hovered) {
        return;
      }
    }

    // If this panel is not actually on the main window, this is a no-op
    if (currently_hovered->isFloating()) {
      return;
    }

    // Save the current state so it can be restored later
    premaximized_state_ = saveState();

    // For every other panel that is on the main window, hide it
    foreach (PanelWidget* panel, PanelManager::instance()->panels()) {
      if (!panel->isFloating() && panel != currently_hovered) {
        panel->setVisible(false);
      }
    }
  } else {
    // Assume we are currently maximized, restore the state
    restoreState(premaximized_state_);
    premaximized_state_.clear();
  }
}

void MainWindow::ProjectOpen(Project *p)
{
  // See if this project is already open, and switch to it if so
  foreach (ProjectPanel* pl, project_panels_) {
    if (pl->project() == p) {
      pl->raise();
      return;
    }
  }

  ProjectPanel* panel;

  if (!project_panels_.first()->project()) {
    panel = project_panels_.first();
  } else {
    panel = AppendProjectPanel();
  }

  panel->set_project(p);
}

void MainWindow::ProjectClose(Project *p)
{
  // Close any open sequences from project
  QVector<Item*> open_sequences = p->get_items_of_type(Item::kSequence);

  foreach (Item* item, open_sequences) {
    Sequence* seq = static_cast<Sequence*>(item);

    if (IsSequenceOpen(seq)) {
      CloseSequence(seq);
    }
  }

  // Close any open footage in footage viewer
  QVector<Item*> footage = p->get_items_of_type(Item::kFootage);
  QVector<Footage*> footage_in_viewer = footage_viewer_panel_->GetSelectedFootage();

  if (!footage_in_viewer.isEmpty()) {
    // FootageViewer only has the one footage item
    Footage* f = footage_in_viewer.first();

    foreach (Item* i, footage) {
      if (f == i) {
        footage_viewer_panel_->SetFootage(nullptr);
        break;
      }
    }
  }

  // Close any extra folder panels
  foreach (ProjectPanel* panel, folder_panels_) {
    if (panel->project() == p) {
      panel->close();
    }
  }

  // Close project from project panel
  foreach (ProjectPanel* panel, project_panels_) {
    if (panel->project() == p) {
      RemoveProjectPanel(panel);
    }
  }
}

void MainWindow::SetApplicationProgressStatus(ProgressStatus status)
{
#if defined(Q_OS_WINDOWS)
  if (taskbar_interface_) {
    switch (status) {
    case kProgressShow:
      taskbar_interface_->SetProgressState(reinterpret_cast<HWND>(this->winId()), TBPF_NORMAL);
      break;
    case kProgressNone:
      taskbar_interface_->SetProgressState(reinterpret_cast<HWND>(this->winId()), TBPF_NOPROGRESS);
      break;
    case kProgressError:
      taskbar_interface_->SetProgressState(reinterpret_cast<HWND>(this->winId()), TBPF_ERROR);
      break;
    }
  }

#elif defined(Q_OS_MAC)
#endif
}

void MainWindow::SetApplicationProgressValue(int value)
{
#if defined(Q_OS_WINDOWS)
  if (taskbar_interface_) {
    taskbar_interface_->SetProgressValue(reinterpret_cast<HWND>(this->winId()), value, 100);
  }
#elif defined(Q_OS_MAC)
#endif
}

void MainWindow::closeEvent(QCloseEvent *e)
{
  // Try to close all projects (this will return false if the user chooses not to close)
  if (!Core::instance()->CloseAllProjects(false)) {
    e->ignore();
    return;
  }

  PanelManager::instance()->DeleteAllPanels();

  QMainWindow::closeEvent(e);
}

#ifdef Q_OS_WINDOWS
bool MainWindow::nativeEvent(const QByteArray &eventType, void *message, long *result)
{
  if (static_cast<MSG*>(message)->message == taskbar_btn_id_) {
    // Attempt to create taskbar button progress handle
    HRESULT hr = CoCreateInstance(CLSID_TaskbarList,
                                  NULL,
                                  CLSCTX_INPROC_SERVER,
                                  IID_ITaskbarList3,
                                  reinterpret_cast<void**>(&taskbar_interface_));

    if (SUCCEEDED(hr)) {
      hr = taskbar_interface_->HrInit();

      if (FAILED(hr)) {
        taskbar_interface_->Release();
        taskbar_interface_ = nullptr;
      }
    }
  }

  return QMainWindow::nativeEvent(eventType, message, result);
}
#endif

void MainWindow::StatusBarDoubleClicked()
{
  task_man_panel_->show();
  task_man_panel_->raise();
}

#ifdef Q_OS_LINUX
void MainWindow::ShowNouveauWarning()
{
  QMessageBox::warning(this,
                       tr("Driver Warning"),
                       tr("Olive has detected your system is using the Nouveau graphics driver.\n\nThis driver is "
                          "known to have stability and performance issues with Olive. It is highly recommended "
                          "you install the proprietary NVIDIA driver before continuing to use Olive."),
                       QMessageBox::Ok);
}
#endif

void MainWindow::UpdateTitle()
{
  if (Core::instance()->GetActiveProject()) {
    setWindowTitle(QStringLiteral("%1 %2 - [*]%3").arg(QApplication::applicationName(),
                                                       QApplication::applicationVersion(),
                                                       Core::instance()->GetActiveProject()->pretty_filename()));
  } else {
    setWindowTitle(QStringLiteral("%1 %2").arg(QApplication::applicationName(),
                                               QApplication::applicationVersion()));
  }
}

void MainWindow::TimelineCloseRequested()
{
  RemoveTimelinePanel(static_cast<TimelinePanel*>(sender()));
}

void MainWindow::ProjectCloseRequested()
{
  ProjectPanel* panel = static_cast<ProjectPanel*>(sender());
  Project* p = panel->project();

  Core::instance()->CloseProject(p, true);
}

void MainWindow::FloatingPanelCloseRequested()
{
  PanelWidget* panel = static_cast<PanelWidget*>(sender());

  quintptr list_ptr = panel->property("parent_list").value<quintptr>();
  QList<PanelWidget*>* list = reinterpret_cast< QList<PanelWidget*>* >(list_ptr);

  list->removeOne(panel);
  panel->deleteLater();
}

TimelinePanel* MainWindow::AppendTimelinePanel()
{
  TimelinePanel* panel = AppendPanelInternal<TimelinePanel>(timeline_panels_);

  connect(panel, &PanelWidget::CloseRequested, this, &MainWindow::TimelineCloseRequested);
  connect(panel, &TimelinePanel::TimeChanged, curve_panel_, &ParamPanel::SetTimestamp);
  connect(panel, &TimelinePanel::TimeChanged, param_panel_, &ParamPanel::SetTimestamp);
  connect(panel, &TimelinePanel::TimeChanged, table_panel_, &NodeTablePanel::SetTimestamp);
  connect(panel, &TimelinePanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTimestamp);
  connect(panel, &TimelinePanel::BlocksSelected, node_panel_, &NodePanel::SelectBlocks);
  connect(panel, &TimelinePanel::BlocksDeselected, node_panel_, &NodePanel::DeselectBlocks);
  connect(param_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp);
  connect(curve_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp);
  connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp);

  sequence_viewer_panel_->ConnectTimeBasedPanel(panel);

  return panel;
}

ProjectPanel *MainWindow::AppendProjectPanel()
{
  ProjectPanel* panel = AppendPanelInternal<ProjectPanel>(project_panels_);

  connect(panel, &PanelWidget::CloseRequested, this, &MainWindow::ProjectCloseRequested);
  connect(panel, &ProjectPanel::ProjectNameChanged, this, &MainWindow::UpdateTitle);

  return panel;
}

void MainWindow::RemoveTimelinePanel(TimelinePanel *panel)
{
  // Stop showing this timeline in the viewer
  TimelineFocused(nullptr);

  if (timeline_panels_.size() == 1) {
    // Leave our single remaining timeline panel open
    panel->ConnectViewerNode(nullptr);
  } else {
    timeline_panels_.removeOne(panel);
    panel->deleteLater();
  }
}

void MainWindow::RemoveProjectPanel(ProjectPanel *panel)
{
  if (project_panels_.size() == 1) {
    panel->set_project(nullptr);
  } else {
    project_panels_.removeOne(panel);
    panel->deleteLater();
  }
}

void MainWindow::TimelineFocused(ViewerOutput* viewer)
{
  sequence_viewer_panel_->ConnectViewerNode(viewer);
  param_panel_->ConnectViewerNode(viewer);
  curve_panel_->ConnectViewerNode(viewer);

  Sequence* seq = nullptr;

  if (viewer) {
    seq = static_cast<Sequence*>(viewer->parent());
  }

  node_panel_->SetGraph(seq);
}

void MainWindow::FocusedPanelChanged(PanelWidget *panel)
{
  TimelinePanel* timeline = dynamic_cast<TimelinePanel*>(panel);

  if (timeline) {
    TimelineFocused(timeline->GetConnectedViewer());
    return;
  }

  ProjectPanel* project = dynamic_cast<ProjectPanel*>(panel);

  if (project) {
    UpdateTitle();
    return;
  }
}

void MainWindow::SetDefaultLayout()
{
  node_panel_->show();
  addDockWidget(Qt::TopDockWidgetArea, node_panel_);

  footage_viewer_panel_->show();
  addDockWidget(Qt::TopDockWidgetArea, footage_viewer_panel_);

  param_panel_->show();
  tabifyDockWidget(footage_viewer_panel_, param_panel_);
  footage_viewer_panel_->raise();

  curve_panel_->hide();
  curve_panel_->setFloating(true);
  addDockWidget(Qt::TopDockWidgetArea, curve_panel_);

  table_panel_->hide();
  table_panel_->setFloating(true);
  addDockWidget(Qt::TopDockWidgetArea, table_panel_);

  sequence_viewer_panel_->show();
  addDockWidget(Qt::TopDockWidgetArea, sequence_viewer_panel_);

  pixel_sampler_panel_->hide();
  pixel_sampler_panel_->setFloating(true);
  addDockWidget(Qt::TopDockWidgetArea, pixel_sampler_panel_);

  project_panels_.first()->show();
  addDockWidget(Qt::BottomDockWidgetArea, project_panels_.first());

  tool_panel_->show();
  addDockWidget(Qt::BottomDockWidgetArea, tool_panel_);

  timeline_panels_.first()->show();
  addDockWidget(Qt::BottomDockWidgetArea, timeline_panels_.first());

  task_man_panel_->hide();
  task_man_panel_->setFloating(true);
  addDockWidget(Qt::BottomDockWidgetArea, task_man_panel_);

  audio_monitor_panel_->show();
  addDockWidget(Qt::BottomDockWidgetArea, audio_monitor_panel_);

  resizeDocks({node_panel_, param_panel_, sequence_viewer_panel_},
  {width()/3, width()/3, width()/3},
              Qt::Horizontal);

  resizeDocks({project_panels_.first(), tool_panel_, timeline_panels_.first(), audio_monitor_panel_},
  {width()/4, 1, width(), 1},
              Qt::Horizontal);

  resizeDocks({node_panel_, project_panels_.first()},
  {height()/2, height()/2},
              Qt::Vertical);
}

void MainWindow::showEvent(QShowEvent *e)
{
  QMainWindow::showEvent(e);

#ifdef Q_OS_LINUX
  // Check for nouveau since that driver really doesn't work with Olive
  QOffscreenSurface surface;
  surface.create();
  QOpenGLContext context;
  context.create();
  context.makeCurrent(&surface);
  const char* vendor = reinterpret_cast<const char*>(context.functions()->glGetString(GL_VENDOR));
  qDebug() << "Using graphics driver:" << vendor;
  if (!strcmp(vendor, "nouveau")) {
    QMetaObject::invokeMethod(this, "ShowNouveauWarning", Qt::QueuedConnection);
  }
#endif
}

template<typename T>
T *MainWindow::AppendPanelInternal(QList<T*>& list)
{
  T* panel = PanelManager::instance()->CreatePanel<T>(this);

  SetUniquePanelID(panel, list);

  if (!list.isEmpty()) {
    tabifyDockWidget(list.last(), panel);
  }

  // For some reason raise() on its own doesn't do anything, we need both
  panel->show();
  panel->raise();

  list.append(panel);

  // Let us handle the panel closing rather than the panel itself
  panel->SetSignalInsteadOfClose(true);

  return panel;
}

template<typename T>
void MainWindow::SetUniquePanelID(T *panel, const QList<T *> &list)
{
  // Set unique object name so it can be identified by QMainWindow's save and restore state functions
  panel->setObjectName(panel->objectName().append(QString::number(list.size())));
}

template<typename T>
T *MainWindow::AppendFloatingPanelInternal(QList<T *> &list)
{
  T* panel = PanelManager::instance()->CreatePanel<T>(this);

  SetUniquePanelID(panel, list);

  panel->setFloating(true);
  panel->show();

  panel->SetSignalInsteadOfClose(true);
  connect(panel, &PanelWidget::CloseRequested, this, &MainWindow::FloatingPanelCloseRequested);

  panel->setProperty("parent_list", reinterpret_cast<quintptr>(&list));

  return panel;
}

}
