/***

  Olive - Non-Linear Video Editor
  Copyright (C) 2019 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 "core.h"

#include <QApplication>
#include <QClipboard>
#include <QCommandLineParser>
#include <QDebug>
#include <QFileDialog>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QMessageBox>
#include <QStyleFactory>

#include "audio/audiomanager.h"
#include "cli/clitask/clitaskdialog.h"
#include "common/filefunctions.h"
#include "common/xmlutils.h"
#include "config/config.h"
#include "dialog/about/about.h"
#include "dialog/export/export.h"
#include "dialog/sequence/sequence.h"
#include "dialog/task/task.h"
#include "dialog/preferences/preferences.h"
#include "dialog/projectproperties/projectproperties.h"
#include "node/factory.h"
#include "panel/panelmanager.h"
#include "panel/project/project.h"
#include "panel/viewer/viewer.h"
#include "render/backend/opengl/opengltexturecache.h"
#include "render/colormanager.h"
#include "render/diskmanager.h"
#include "render/pixelformat.h"
#include "render/shaderinfo.h"
#include "task/cache/cache.h"
#include "task/project/import/import.h"
#include "task/project/load/load.h"
#include "task/project/save/save.h"
#include "task/taskmanager.h"
#include "ui/style/style.h"
#include "undo/undostack.h"
#include "widget/menu/menushared.h"
#include "widget/taskview/taskviewitem.h"
#include "widget/viewer/viewer.h"
#include "window/mainwindow/mainwindow.h"

OLIVE_NAMESPACE_ENTER

Core Core::instance_;

Core::Core() :
  main_window_(nullptr),
  tool_(Tool::kPointer),
  addable_object_(Tool::kAddableEmpty),
  snapping_(true),
  gui_active_(false)
{
}

Core *Core::instance()
{
  return &instance_;
}

int Core::execute(QCoreApplication* a)
{
  int exit_code = 1;

  // Start core
  OLIVE_NAMESPACE::Core::instance()->Start();

  //
  // Parse command line arguments
  //

  QCommandLineParser parser;
  parser.addHelpOption();
  parser.addVersionOption();

  // Project from command line option
  // FIXME: What's the correct way to make a visually "optional" positional argument, or is manually adding square
  // brackets like this correct?
  parser.addPositionalArgument("[project]", tr("Project to open on startup"));

  // Create fullscreen option
  QCommandLineOption fullscreen_option({"f", "fullscreen"}, tr("Start in full screen mode"));
  parser.addOption(fullscreen_option);

  // Create headless export option
  QCommandLineOption headless_export_option({"x", "export"}, tr("Export project from command line"));
  parser.addOption(headless_export_option);

  // Parse options
  parser.process(*a);

  QStringList args = parser.positionalArguments();

  // Detect project to load on startup
  if (!args.isEmpty()) {
    startup_project_ = args.first();
  }

  gui_active_ = !parser.isSet(headless_export_option);

  if (gui_active_) {

    // Start GUI
    StartGUI(parser.isSet(fullscreen_option));

    // If we have a startup
    QMetaObject::invokeMethod(this, "OpenStartupProject", Qt::QueuedConnection);

    // Run application loop and receive exit code
    exit_code = a->exec();

  } else {

    if (parser.isSet(headless_export_option)) {
      // Start a headless export
      if (StartHeadlessExport()) {
        exit_code = 0;
      }
    }

  }



  // Clear core memory
  OLIVE_NAMESPACE::Core::instance()->Stop();

  return exit_code;
}

void Core::DeclareTypesForQt()
{
  qRegisterMetaType<rational>();
  qRegisterMetaType<OpenGLTexturePtr>();
  qRegisterMetaType<OpenGLTextureCache::ReferencePtr>();
  qRegisterMetaType<NodeValue>();
  qRegisterMetaType<NodeValueTable>();
  qRegisterMetaType<NodeValueDatabase>();
  qRegisterMetaType<FramePtr>();
  qRegisterMetaType<SampleBufferPtr>();
  qRegisterMetaType<AudioParams>();
  qRegisterMetaType<NodeKeyframe::Type>();
  qRegisterMetaType<Decoder::RetrieveState>();
  qRegisterMetaType<OLIVE_NAMESPACE::TimeRange>();
  qRegisterMetaType<Color>();
  qRegisterMetaType<OLIVE_NAMESPACE::ProjectPtr>();
  qRegisterMetaType<OLIVE_NAMESPACE::AudioVisualWaveform>();
  qRegisterMetaType<OLIVE_NAMESPACE::SampleJob>();
  qRegisterMetaType<OLIVE_NAMESPACE::ShaderJob>();
  qRegisterMetaType<OLIVE_NAMESPACE::GenerateJob>();
  qRegisterMetaType<OLIVE_NAMESPACE::VideoParams>();
  qRegisterMetaType<OLIVE_NAMESPACE::MainWindowLayoutInfo>();
}

void Core::Start()
{
  // Reset config (Config sets to default on construction already, but we do it again here as a workaround that fixes
  //               the fact that some of the config paths set by default rely on the app name having been set (in main())
  Config::Current().SetDefaults();

  // Load application config
  Config::Load();

  // Declare custom types for Qt signal/slot system
  DeclareTypesForQt();

  // Set up node factory/library
  NodeFactory::Initialize();

  // Set up color manager's default config
  ColorManager::SetUpDefaultConfig();

  // Initialize task manager
  TaskManager::CreateInstance();

  // Initialize OpenGL service
  OpenGLProxy::CreateInstance();

  //
  // Start application
  //

  qInfo() << "Using Qt version:" << qVersion();
}

void Core::Stop()
{
  // Save Config
  //Config::Save();

  // Save recently opened projects
  {
    QFile recent_projects_file(GetRecentProjectsFilePath());
    if (recent_projects_file.open(QFile::WriteOnly | QFile::Text)) {
      QTextStream ts(&recent_projects_file);

      foreach (const QString& s, recent_projects_) {
        ts << s << "\n";
      }

      recent_projects_file.close();
    }
  }

  OpenGLProxy::DestroyInstance();

  MenuShared::DestroyInstance();

  TaskManager::DestroyInstance();

  PanelManager::DestroyInstance();

  AudioManager::DestroyInstance();

  DiskManager::DestroyInstance();

  PixelFormat::DestroyInstance();

  NodeFactory::Destroy();

  delete main_window_;
}

MainWindow *Core::main_window()
{
  return main_window_;
}

UndoStack *Core::undo_stack()
{
  return &undo_stack_;
}

void Core::ImportFiles(const QStringList &urls, ProjectViewModel* model, Folder* parent)
{
  if (urls.isEmpty()) {
    QMessageBox::critical(main_window_, tr("Import error"), tr("Nothing to import"));
    return;
  }

  ProjectImportTask* pim = new ProjectImportTask(model, parent, urls);

  if (!pim->GetFileCount()) {
    // No files to import
    delete pim;
    return;
  }

  TaskDialog* task_dialog = new TaskDialog(pim, tr("Importing..."), main_window());

  connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::ImportTaskComplete);

  task_dialog->open();
}

const Tool::Item &Core::tool() const
{
  return tool_;
}

const Tool::AddableObject &Core::GetSelectedAddableObject() const
{
  return addable_object_;
}

const QString &Core::GetSelectedTransition() const
{
  return selected_transition_;
}

void Core::SetSelectedAddableObject(const Tool::AddableObject &obj)
{
  addable_object_ = obj;
}

void Core::SetSelectedTransitionObject(const QString &obj)
{
  selected_transition_ = obj;
}

void Core::ClearOpenRecentList()
{
  recent_projects_.clear();
}

void Core::CreateNewProject()
{
  // If we already have an empty/new project, switch to it
  foreach (ProjectPtr already_open, open_projects_) {
    if (already_open->is_new()) {
      AddOpenProject(already_open);
      return;
    }
  }

  AddOpenProject(std::make_shared<Project>());
}

const bool &Core::snapping() const
{
  return snapping_;
}

const QStringList &Core::GetRecentProjects() const
{
  return recent_projects_;
}

ProjectPtr Core::GetSharedPtrFromProject(Project *project) const
{
  foreach (ProjectPtr p, open_projects_) {
    if (p.get() == project) {
      return p;
    }
  }

  return nullptr;
}

void Core::SetTool(const Tool::Item &tool)
{
  tool_ = tool;

  emit ToolChanged(tool_);
}

void Core::SetSnapping(const bool &b)
{
  snapping_ = b;

  emit SnappingChanged(snapping_);
}

void Core::DialogAboutShow()
{
  AboutDialog a(main_window_);
  a.exec();
}

void Core::DialogImportShow()
{
  // Open dialog for user to select files
  QStringList files = QFileDialog::getOpenFileNames(main_window_,
                                                    tr("Import footage..."));

  // Check if the user actually selected files to import
  if (!files.isEmpty()) {

    // Locate the most recently focused Project panel (assume that's the panel the user wants to import into)
    ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused<ProjectPanel>();
    Project* active_project;

    if (active_project_panel == nullptr // Check that we found a Project panel
        || (active_project = active_project_panel->project()) == nullptr) { // and that we could find an active Project
      QMessageBox::critical(main_window_, tr("Failed to import footage"), tr("Failed to find active Project panel"));
      return;
    }

    // Get the selected folder in this panel
    Folder* folder = active_project_panel->GetSelectedFolder();

    ImportFiles(files, active_project_panel->model(), folder);
  }
}

void Core::DialogPreferencesShow()
{
  PreferencesDialog pd(main_window_, main_window_->menuBar());
  pd.exec();
}

void Core::DialogProjectPropertiesShow()
{
  ProjectPtr proj = GetActiveProject();

  if (proj) {
    ProjectPropertiesDialog ppd(proj.get(), main_window_);
    ppd.exec();
  } else {
    QMessageBox::critical(main_window_,
                          tr("No Active Project"),
                          tr("No project is currently open to set the properties for"),
                          QMessageBox::Ok);
  }
}

void Core::DialogExportShow()
{
  TimeBasedPanel* latest_time_based = PanelManager::instance()->MostRecentlyFocused<TimeBasedPanel>();

  if (latest_time_based && latest_time_based->GetConnectedViewer()) {
    if (latest_time_based->GetConnectedViewer()->GetLength() == 0) {
      QMessageBox::critical(main_window_,
                            tr("Error"),
                            tr("This Sequence is empty. There is nothing to export."),
                            QMessageBox::Ok);
    } else {
      ExportDialog ed(latest_time_based->GetConnectedViewer(), main_window_);
      ed.exec();
    }
  }
}

void Core::CreateNewFolder()
{
  // Locate the most recently focused Project panel (assume that's the panel the user wants to import into)
  ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused<ProjectPanel>();
  Project* active_project;

  if (active_project_panel == nullptr // Check that we found a Project panel
      || (active_project = active_project_panel->project()) == nullptr) { // and that we could find an active Project
    QMessageBox::critical(main_window_, tr("Failed to create new folder"), tr("Failed to find active project"));
    return;
  }

  // Get the selected folder in this panel
  Folder* folder = active_project_panel->GetSelectedFolder();

  // Create new folder
  ItemPtr new_folder = std::make_shared<Folder>();

  // Set a default name
  new_folder->set_name(tr("New Folder"));

  // Create an undoable command
  ProjectViewModel::AddItemCommand* aic = new ProjectViewModel::AddItemCommand(active_project_panel->model(),
                                                                               folder,
                                                                               new_folder);

  Core::instance()->undo_stack()->push(aic);

  // Trigger an automatic rename so users can enter the folder name
  active_project_panel->Edit(new_folder.get());
}

void Core::CreateNewSequence()
{
  ProjectPtr active_project = GetActiveProject();

  if (!active_project) {
    QMessageBox::critical(main_window_, tr("Failed to create new sequence"), tr("Failed to find active project"));
    return;
  }

  // Create new sequence
  SequencePtr new_sequence = CreateNewSequenceForProject(active_project.get());

  // Set all defaults for the sequence
  new_sequence->set_default_parameters();

  SequenceDialog sd(new_sequence.get(), SequenceDialog::kNew, main_window_);

  // Make sure SequenceDialog doesn't make an undo command for editing the sequence, since we make an undo command for
  // adding it later on
  sd.SetUndoable(false);

  if (sd.exec() == QDialog::Accepted) {
    // Create an undoable command
    ProjectViewModel::AddItemCommand* aic = new ProjectViewModel::AddItemCommand(GetActiveProjectModel(),
                                                                                 GetSelectedFolderInActiveProject(),
                                                                                 new_sequence);

    new_sequence->add_default_nodes();

    Core::instance()->undo_stack()->push(aic);

    Core::instance()->main_window()->OpenSequence(new_sequence.get());
  }
}

void Core::AddOpenProject(ProjectPtr p)
{
  // Ensure project is not open at the moment
  foreach (ProjectPtr already_open, open_projects_) {
    if (already_open == p) {
      // Signal UI to switch to this project
      emit ProjectOpened(p.get());
      return;
    }
  }

  // If we currently have an empty project, close it first
  if (!open_projects_.isEmpty() && open_projects_.last()->is_new()) {
    CloseProject(open_projects_.last(), false);
  }

  connect(p.get(), &Project::ModifiedChanged, this, &Core::ProjectWasModified);
  open_projects_.append(p);

  PushRecentlyOpenedProject(p->filename());

  emit ProjectOpened(p.get());
}

void Core::AddOpenProjectFromTask(Task *task)
{
  QList<ProjectPtr> projects = static_cast<ProjectLoadTask*>(task)->GetLoadedProjects();
  QList<MainWindowLayoutInfo> layouts = static_cast<ProjectLoadTask*>(task)->GetLoadedLayouts();

  for (int i=0; i<projects.size(); i++) {
    AddOpenProject(projects.at(i));
    main_window_->LoadLayout(layouts.at(i));
  }
}

void Core::ImportTaskComplete(Task* task)
{
  QUndoCommand *command = static_cast<ProjectImportTask*>(task)->GetCommand();

  undo_stack_.pushIfHasChildren(command);
}

bool Core::ConfirmImageSequence(const QString& filename)
{
  QMessageBox mb(main_window_);

  mb.setIcon(QMessageBox::Question);
  mb.setWindowTitle(tr("Possible image sequence detected"));
  mb.setText(tr("The file '%1' looks like it might be part of an image "
                "sequence. Would you like to import it as such?").arg(filename));

  mb.addButton(QMessageBox::Yes);
  mb.addButton(QMessageBox::No);

  return (mb.exec() == QMessageBox::Yes);
}

void Core::ProjectWasModified(bool e)
{
  //Project* p = static_cast<Project*>(sender());

  if (e) {
    // If this project is modified, we know for sure the window should show a "modified" flag (the * in the titlebar)
    main_window_->setWindowModified(true);
  } else {
    // If we just set this project to "not modified", see if all projects are not modified in which case we can hide
    // the modified flag
    foreach (ProjectPtr open, open_projects_) {
      if (open->is_modified()) {
        main_window_->setWindowModified(true);
        return;
      }
    }

    main_window_->setWindowModified(false);
  }
}

bool Core::StartHeadlessExport()
{
  if (startup_project_.isEmpty()) {
    qCritical().noquote() << tr("You must specify a project file to export");
    return false;
  }

  if (!QFileInfo::exists(startup_project_)) {
    qCritical().noquote() << tr("Specified project does not exist");
    return false;
  }

  // Start a load task and try running it
  ProjectLoadTask plm(startup_project_);
  CLITaskDialog task_dialog(&plm);

  if (task_dialog.Run()) {
    ProjectPtr p = plm.GetLoadedProjects().first();
    QList<ItemPtr> items = p->get_items_of_type(Item::kSequence);

    // Check if this project contains sequences
    if (items.isEmpty()) {
      qCritical().noquote() << tr("Project contains no sequences, nothing to export");
      return false;
    }

    SequencePtr sequence = nullptr;

    // Check if this project contains multiple sequences
    if (items.size() > 1) {
      qInfo().noquote() << tr("This project has multiple sequences. Which do you wish to export?");
      for (int i=0;i<items.size();i++) {
        std::cout << "[" << i << "] " << items.at(i)->name().toStdString();
      }

      QTextStream stream(stdin);
      QString sequence_read;
      int sequence_index = -1;
      QString quit_code = QStringLiteral("q");
      std::string prompt = tr("Enter number (or %1 to cancel): ").arg(quit_code).toStdString();
      forever {
        std::cout << prompt;

        stream.readLineInto(&sequence_read);

        if (!QString::compare(sequence_read, quit_code, Qt::CaseInsensitive)) {
          return false;
        }

        bool ok;
        sequence_index = sequence_read.toInt(&ok);

        if (ok && sequence_index >= 0 && sequence_index < items.size()) {
          break;
        } else {
          qCritical().noquote() << tr("Invalid sequence number");
        }
      }

      sequence = std::static_pointer_cast<Sequence>(items.at(sequence_index));
    } else {
      sequence = std::static_pointer_cast<Sequence>(items.first());
    }

    ExportParams params;
    ExportTask export_task(sequence->viewer_output(), p->color_manager(), params);
    CLITaskDialog export_dialog(&export_task);
    if (export_dialog.Run()) {
      qInfo().noquote() << tr("Export succeeded");
      return true;
    } else {
      qInfo().noquote() << tr("Export failed: %1").arg(export_task.GetError());
      return false;
    }
  } else {
    qCritical().noquote() << tr("Project failed to load: %1").arg(plm.GetError());
    return false;
  }
}

void Core::OpenStartupProject()
{
  // Load startup project
  if (!startup_project_.isEmpty() && !QFileInfo::exists(startup_project_)) {
    QMessageBox::warning(main_window_,
                         tr("Failed to open startup file"),
                         tr("The project \"%1\" doesn't exist. A new project will be started instead.").arg(startup_project_),
                         QMessageBox::Ok);

    startup_project_.clear();
  }

  if (startup_project_.isEmpty()) {
    // If no load project is set, create a new one on open
    CreateNewProject();
  } else {
    OpenProjectInternal(startup_project_);
  }
}

void Core::StartGUI(bool full_screen)
{
  // Set UI style
  qApp->setStyle(QStyleFactory::create("Fusion"));
  StyleManager::SetStyle(StyleManager::DefaultStyle());

  // Set up shared menus
  MenuShared::CreateInstance();

  // Since we're starting GUI mode, create a PanelFocusManager (auto-deletes with QObject)
  PanelManager::CreateInstance();

  // Initialize audio service
  AudioManager::CreateInstance();

  // Initialize disk service
  DiskManager::CreateInstance();

  // Initialize pixel service
  PixelFormat::CreateInstance();

  // Connect the PanelFocusManager to the application's focus change signal
  connect(qApp,
          &QApplication::focusChanged,
          PanelManager::instance(),
          &PanelManager::FocusChanged);

  // Create main window and open it
  main_window_ = new MainWindow();
  if (full_screen) {
    main_window_->showFullScreen();
  } else {
    main_window_->showMaximized();
  }

  // When a new project is opened, update the mainwindow
  connect(this, &Core::ProjectOpened, main_window_, &MainWindow::ProjectOpen);
  connect(this, &Core::ProjectClosed, main_window_, &MainWindow::ProjectClose);

  // Start autorecovery timer using the config value as its interval
  SetAutorecoveryInterval(Config::Current()["AutorecoveryInterval"].toInt());
  autorecovery_timer_.start();

  // Load recently opened projects list
  {
    QFile recent_projects_file(GetRecentProjectsFilePath());
    if (recent_projects_file.open(QFile::ReadOnly | QFile::Text)) {
      QTextStream ts(&recent_projects_file);

      while (!ts.atEnd()) {
        recent_projects_.append(ts.readLine());
      }

      recent_projects_file.close();
    }
  }
}

void Core::SaveProjectInternal(ProjectPtr project)
{
  // Create save manager
  ProjectSaveTask* psm = new ProjectSaveTask(project);
  TaskDialog* task_dialog = new TaskDialog(psm, tr("Save Project"), main_window_);

  connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::ProjectSaveSucceeded);

  task_dialog->open();
}

void Core::SaveAutorecovery()
{
  foreach (ProjectPtr p, open_projects_) {
    if (!p->has_autorecovery_been_saved()) {
      // FIXME: SAVE AN AUTORECOVERY PROJECT
      p->set_autorecovery_saved(true);
    }
  }
}

void Core::ProjectSaveSucceeded(Task* task)
{
  ProjectPtr p = static_cast<ProjectSaveTask*>(task)->GetProject();

  PushRecentlyOpenedProject(p->filename());

  p->set_modified(false);
}

ProjectPtr Core::GetActiveProject() const
{
  ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused<ProjectPanel>();

  if (active_project_panel && active_project_panel->project()) {
    return GetSharedPtrFromProject(active_project_panel->project());
  }

  return nullptr;
}

ProjectViewModel *Core::GetActiveProjectModel() const
{
  ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused<ProjectPanel>();

  if (active_project_panel) {
    return active_project_panel->model();
  } else {
    return nullptr;
  }
}

Folder *Core::GetSelectedFolderInActiveProject() const
{
  ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused<ProjectPanel>();

  if (active_project_panel) {
    return active_project_panel->GetSelectedFolder();
  } else {
    return nullptr;
  }
}

Timecode::Display Core::GetTimecodeDisplay() const
{
  return static_cast<Timecode::Display>(Config::Current()["TimecodeDisplay"].toInt());
}

void Core::SetTimecodeDisplay(Timecode::Display d)
{
  Config::Current()["TimecodeDisplay"] = d;

  emit TimecodeDisplayChanged(d);
}

void Core::SetAutorecoveryInterval(int minutes)
{
  // Convert minutes to milliseconds
  autorecovery_timer_.setInterval(minutes * 60000);
}

void Core::CopyStringToClipboard(const QString &s)
{
  QGuiApplication::clipboard()->setText(s);
}

QString Core::PasteStringFromClipboard()
{
  return QGuiApplication::clipboard()->text();
}

bool Core::SaveActiveProject()
{
  ProjectPtr active_project = GetActiveProject();

  if (active_project) {
    return SaveProject(active_project);
  }

  return false;
}

bool Core::SaveActiveProjectAs()
{
  ProjectPtr active_project = GetActiveProject();

  if (active_project) {
    return SaveProjectAs(active_project);
  }

  return false;
}

bool Core::SaveAllProjects()
{
  foreach (ProjectPtr p, open_projects_) {
    if (!SaveProject(p)) {
      return false;
    }
  }

  return true;
}

bool Core::CloseActiveProject()
{
  return CloseProject(GetActiveProject(), true);
}

bool Core::CloseAllExceptActiveProject()
{
  ProjectPtr active_proj = GetActiveProject();
  QList<ProjectPtr> copy = open_projects_;

  foreach (ProjectPtr p, copy) {
    if (p != active_proj) {
      if (!CloseProject(p, true)) {
        return false;
      }
    }
  }

  return true;
}

QList<rational> Core::SupportedFrameRates()
{
  QList<rational> frame_rates;

  frame_rates.append(rational(10, 1));            // 10 FPS
  frame_rates.append(rational(15, 1));            // 15 FPS
  frame_rates.append(rational(24000, 1001));      // 23.976 FPS
  frame_rates.append(rational(24, 1));            // 24 FPS
  frame_rates.append(rational(25, 1));            // 25 FPS
  frame_rates.append(rational(30000, 1001));      // 29.97 FPS
  frame_rates.append(rational(30, 1));            // 30 FPS
  frame_rates.append(rational(48000, 1001));      // 47.952 FPS
  frame_rates.append(rational(48, 1));            // 48 FPS
  frame_rates.append(rational(50, 1));            // 50 FPS
  frame_rates.append(rational(60000, 1001));      // 59.94 FPS
  frame_rates.append(rational(60, 1));            // 60 FPS

  return frame_rates;
}

QList<int> Core::SupportedSampleRates()
{
  QList<int> sample_rates;

  sample_rates.append(8000);         // 8000 Hz
  sample_rates.append(11025);        // 11025 Hz
  sample_rates.append(16000);        // 16000 Hz
  sample_rates.append(22050);        // 22050 Hz
  sample_rates.append(24000);        // 24000 Hz
  sample_rates.append(32000);        // 32000 Hz
  sample_rates.append(44100);        // 44100 Hz
  sample_rates.append(48000);        // 48000 Hz
  sample_rates.append(88200);        // 88200 Hz
  sample_rates.append(96000);        // 96000 Hz

  return sample_rates;
}

QList<uint64_t> Core::SupportedChannelLayouts()
{
  QList<uint64_t> channel_layouts;

  channel_layouts.append(AV_CH_LAYOUT_MONO);
  channel_layouts.append(AV_CH_LAYOUT_STEREO);
  channel_layouts.append(AV_CH_LAYOUT_2_1);
  channel_layouts.append(AV_CH_LAYOUT_5POINT1);
  channel_layouts.append(AV_CH_LAYOUT_7POINT1);

  return channel_layouts;
}

QList<int> Core::SupportedDividers()
{
  return {1, 2, 3, 4, 6, 8, 12, 16};
}

QString Core::FrameRateToString(const rational &frame_rate)
{
  return tr("%1 FPS").arg(frame_rate.toDouble());
}

QString Core::SampleRateToString(const int &sample_rate)
{
  return tr("%1 Hz").arg(sample_rate);
}

QString Core::ChannelLayoutToString(const uint64_t &layout)
{
  switch (layout) {
  case AV_CH_LAYOUT_MONO:
    return tr("Mono");
  case AV_CH_LAYOUT_STEREO:
    return tr("Stereo");
  case AV_CH_LAYOUT_2_1:
    return tr("2.1");
  case AV_CH_LAYOUT_5POINT1:
    return tr("5.1");
  case AV_CH_LAYOUT_7POINT1:
    return tr("7.1");
  default:
    return tr("Unknown (0x%1)").arg(layout, 1, 16);
  }
}

QString Core::GetProjectFilter()
{
  return QStringLiteral("%1 (*.ove)").arg(tr("Olive Project"));
}

QString Core::GetRecentProjectsFilePath()
{
  return QDir(FileFunctions::GetConfigurationLocation()).filePath(QStringLiteral("recent"));
}

bool Core::SaveProject(ProjectPtr p)
{
  if (p->filename().isEmpty()) {
    return SaveProjectAs(p);
  } else {
    SaveProjectInternal(p);

    return true;
  }
}

bool Core::SaveProjectAs(ProjectPtr p)
{
  QString fn = QFileDialog::getSaveFileName(main_window_,
                                            tr("Save Project As"),
                                            QString(),
                                            GetProjectFilter());

  if (!fn.isEmpty()) {
    QString extension(QStringLiteral(".ove"));
    if (!fn.endsWith(extension, Qt::CaseInsensitive)) {
      fn.append(extension);
    }

    p->set_filename(fn);

    SaveProjectInternal(p);

    return true;
  }

  return false;
}

void Core::PushRecentlyOpenedProject(const QString& s)
{
  if (s.isEmpty()) {
    return;
  }

  int existing_index = recent_projects_.indexOf(s);

  if (existing_index >= 0) {
    recent_projects_.move(existing_index, 0);
  } else {
    recent_projects_.prepend(s);
  }
}

void Core::OpenProjectInternal(const QString &filename)
{
  // See if this project is open already
  foreach (ProjectPtr p, open_projects_) {
    if (p->filename() == filename) {
      // This project is already open
      AddOpenProject(p);
      return;
    }
  }

  ProjectLoadTask* plm = new ProjectLoadTask(filename);

  TaskDialog* task_dialog = new TaskDialog(plm, tr("Load Project"), main_window());

  connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::AddOpenProjectFromTask);

  task_dialog->open();
}

int Core::CountFilesInFileList(const QFileInfoList &filenames)
{
  int file_count = 0;

  foreach (const QFileInfo& f, filenames) {
    // For some reason QDir::NoDotAndDotDot	doesn't work with entryInfoList, so we have to check manually
    if (f.fileName() == "." || f.fileName() == "..") {
      continue;
    } else if (f.isDir()) {
      QFileInfoList info_list = QDir(f.absoluteFilePath()).entryInfoList();

      file_count += CountFilesInFileList(info_list);
    } else {
      file_count++;
    }
  }

  return file_count;
}

QString GetRenderModePreferencePrefix(RenderMode::Mode mode, const QString &preference) {
  QString key;

  key.append((mode == RenderMode::kOffline) ? QStringLiteral("Offline") : QStringLiteral("Online"));
  key.append(preference);

  return key;
}

QVariant Core::GetPreferenceForRenderMode(RenderMode::Mode mode, const QString &preference)
{
  return Config::Current()[GetRenderModePreferencePrefix(mode, preference)];
}

void Core::SetPreferenceForRenderMode(RenderMode::Mode mode, const QString &preference, const QVariant &value)
{
  Config::Current()[GetRenderModePreferencePrefix(mode, preference)] = value;
}

void Core::LabelNodes(const QList<Node *> &nodes) const
{
  if (nodes.isEmpty()) {
    return;
  }

  bool ok;

  QString start_label = nodes.first()->GetLabel();

  for (int i=1; i<nodes.size(); i++) {
    if (nodes.at(i)->GetLabel() != start_label) {
      // Not all the nodes share the same name, so we'll start with a blank one
      start_label.clear();
      break;
    }
  }

  QString s = QInputDialog::getText(main_window_,
                                    tr("Label Node"),
                                    tr("Set node label"),
                                    QLineEdit::Normal,
                                    start_label,
                                    &ok);

  if (ok) {
    foreach (Node* n, nodes) {
      n->SetLabel(s);
    }
  }
}

SequencePtr Core::CreateNewSequenceForProject(Project* project) const
{
  SequencePtr new_sequence = std::make_shared<Sequence>();

  // Get default name for this sequence (in the format "Sequence N", the first that doesn't exist)
  int sequence_number = 1;
  QString sequence_name;
  do {
    sequence_name = tr("Sequence %1").arg(sequence_number);
    sequence_number++;
  } while (project->root()->ChildExistsWithName(sequence_name));
  new_sequence->set_name(sequence_name);

  return new_sequence;
}

void Core::OpenProjectFromRecentList(int index)
{
  const QString& open_fn = recent_projects_.at(index);

  if (QFileInfo::exists(open_fn)) {
    OpenProjectInternal(open_fn);
  } else if (QMessageBox::information(main_window(),
                                      tr("Cannot open recent project"),
                                      tr("The project \"%1\" doesn't exist. Would you like to remove this file from the recent list?").arg(open_fn),
                                      QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
    recent_projects_.removeAt(index);
  }
}

bool Core::CloseProject(ProjectPtr p, bool auto_open_new)
{
  CloseProjectBehavior b = kCloseProjectOnlyOne;
  return CloseProject(p, auto_open_new, b);
}

bool Core::CloseProject(ProjectPtr p, bool auto_open_new, CloseProjectBehavior &confirm_behavior)
{
  for (int i=0;i<open_projects_.size();i++) {
    if (open_projects_.at(i) == p) {

      if (p->is_modified() && confirm_behavior != kCloseProjectDontSave) {

        bool save_this_project;

        if (confirm_behavior == kCloseProjectAsk || confirm_behavior == kCloseProjectOnlyOne) {
          QMessageBox mb(main_window_);

          mb.setWindowModality(Qt::WindowModal);
          mb.setIcon(QMessageBox::Question);
          mb.setWindowTitle(tr("Unsaved Changes"));
          mb.setText(tr("The project '%1' has unsaved changes. Would you like to save them?")
                     .arg(p->name()));

          QPushButton* yes_btn = mb.addButton(tr("Save"), QMessageBox::YesRole);

          QPushButton* yes_to_all_btn;
          if (confirm_behavior == kCloseProjectOnlyOne) {
            yes_to_all_btn = nullptr;
          } else {
            yes_to_all_btn = mb.addButton(tr("Save All"), QMessageBox::YesRole);
          }

          mb.addButton(tr("Don't Save"), QMessageBox::NoRole);

          QPushButton* no_to_all_btn;
          if (confirm_behavior == kCloseProjectOnlyOne) {
            no_to_all_btn = nullptr;
          } else {
            no_to_all_btn = mb.addButton(tr("Don't Save All"), QMessageBox::NoRole);
          }

          QPushButton* cancel_btn = mb.addButton(QMessageBox::Cancel);

          mb.exec();

          if (mb.clickedButton() == cancel_btn) {
            // Stop closing projects if the user clicked cancel
            return false;
          } else if (mb.clickedButton() == yes_to_all_btn) {
            // Set flag that other CloseProject commands are going to use
            confirm_behavior = kCloseProjectSave;
          } else if (mb.clickedButton() == no_to_all_btn) {
            // Set flag that other CloseProject commands are going to use
            confirm_behavior = kCloseProjectDontSave;
          }

          save_this_project = (mb.clickedButton() == yes_btn || mb.clickedButton() == yes_to_all_btn);

        } else {
          // We must be saving this project
          save_this_project = true;
        }

        if (save_this_project && !SaveProject(p)) {
          // The save failed, stop closing projects
          return false;
        }

      }

      // For safety, the undo stack is cleared so no commands try to affect a freed project
      undo_stack_.clear();

      disconnect(p.get(), &Project::ModifiedChanged, this, &Core::ProjectWasModified);
      emit ProjectClosed(p.get());
      open_projects_.removeAt(i);
      break;
    }
  }

  // Ensure a project is always active
  if (auto_open_new && open_projects_.isEmpty()) {
    CreateNewProject();
  }

  return true;
}

bool Core::CloseAllProjects(bool auto_open_new)
{
  QList<ProjectPtr> copy = open_projects_;

  // See how many projects are modified so we can set "behavior" correctly
  // (i.e. whether to show "Yes/No To All" buttons or not)
  int modified_count = 0;
  foreach (ProjectPtr p, copy) {
    if (p->is_modified()) {
      modified_count++;
    }
  }

  CloseProjectBehavior behavior;

  if (modified_count > 1) {
    behavior = kCloseProjectAsk;
  } else {
    behavior = kCloseProjectOnlyOne;
  }

  foreach (ProjectPtr p, copy) {
    // If this is the only remaining project and the user hasn't chose "yes/no to all", hide those buttons
    if (modified_count == 1 && behavior == kCloseProjectAsk) {
      behavior = kCloseProjectOnlyOne;
    }

    if (!CloseProject(p, auto_open_new, behavior)) {
      return false;
    }

    modified_count--;
  }

  return true;
}

void Core::CacheActiveSequence(bool in_out_only)
{
  TimeBasedPanel* p = PanelManager::instance()->MostRecentlyFocused<TimeBasedPanel>();

  if (p && p->GetConnectedViewer()) {
    CacheTask* task = new CacheTask(p->GetConnectedViewer(),
                                    p->GetConnectedViewer()->video_params(),
                                    p->GetConnectedViewer()->audio_params(),
                                    in_out_only);

    // Stop any current auto-cache tasks
    ViewerWidget::StopAllBackgroundCacheTasks(true);
    ViewerWidget::SetBackgroundCacheTask(task);

    TaskDialog* dialog = new TaskDialog(task, tr("Caching Sequence"), main_window_);

    connect(dialog,
            &TaskDialog::TaskSucceeded,
            this,
            [] { ViewerWidget::SetBackgroundCacheTask(nullptr); });

    dialog->open();
  }
}

bool Core::CloseAllProjects()
{
  return CloseAllProjects(true);
}

void Core::OpenProject()
{
  QString file = QFileDialog::getOpenFileName(main_window_,
                                              tr("Open Project"),
                                              QString(),
                                              GetProjectFilter());

  if (!file.isEmpty()) {
    OpenProjectInternal(file);
  }
}

OLIVE_NAMESPACE_EXIT
