// Aseprite
// Copyright (C) 2019-2024  Igara Studio S.A.
// Copyright (C) 2001-2018  David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "app/ui_context.h"

#include "app/app.h"
#include "app/doc.h"
#include "app/site.h"
#include "app/ui/color_bar.h"
#include "app/ui/doc_view.h"
#include "app/ui/editor/editor.h"
#include "app/ui/input_chain.h"
#include "app/ui/main_window.h"
#include "app/ui/preview_editor.h"
#include "app/ui/status_bar.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui/workspace.h"
#include "app/ui/workspace_tabs.h"
#include "doc/sprite.h"
#include "ui/system.h"

#include <algorithm>

namespace app {

UIContext* UIContext::m_instance = nullptr;

UIContext::UIContext()
  : m_closedDocs(preferences())
{
  ASSERT(m_instance == nullptr);
  m_instance = this;
}

UIContext::~UIContext()
{
  ASSERT(m_instance == this);
  m_instance = nullptr;

  // The context must be empty at this point. (It's to check if the UI
  // is working correctly, i.e. closing all files when the user can
  // take any action about it.)
  //
  // Note: This assert is commented because it's really common to hit
  // it when the program crashes by any other reason, and we would
  // like to see that other reason instead of this assert.

  //ASSERT(documents().empty());
}

bool UIContext::isUIAvailable() const
{
  return App::instance()->isGui();
}

DocView* UIContext::activeView() const
{
  if (!isUIAvailable())
    return nullptr;

  // Bypass the active workspace view.
  if (m_targetView)
    return m_targetView;

  Workspace* workspace = App::instance()->workspace();
  if (!workspace)
    return nullptr;

  WorkspaceView* view = workspace->activeView();
  if (DocView* docView = dynamic_cast<DocView*>(view))
    return docView;
  else
    return nullptr;
}

void UIContext::setActiveView(DocView* docView)
{
  MainWindow* mainWin = App::instance()->mainWindow();

  // This can happen when the main window is being destroyed when we
  // close the app, and the active view is changing because we are
  // closing down every single tab.
  if (!mainWin)
    return;

  // Prioritize workspace for user input.
  App::instance()->inputChain().prioritize(mainWin->getWorkspace(), nullptr);

  // Do nothing cases: 1) the view is already selected, or 2) the view
  // is the a preview.
  if (m_lastSelectedView == docView ||
      (docView && docView->isPreview()))
    return;

  Editor* editor = nullptr;
  if (docView) {
    editor = docView->editor();
    mainWin->getTabsBar()->selectTab(docView);

    if (mainWin->getWorkspace()->activeView() != docView)
      mainWin->getWorkspace()->setActiveView(docView);

    if (editor)
      editor->requestFocus();
  }

  // This is the only place where we change the Editor::m_activeEditor
  // value.
  //
  // TODO probably Editor should have an observer to update its active
  //      editor value.
  Editor::_setActiveEditor(editor);

  mainWin->getTimeline()->updateUsingEditor(editor);
  mainWin->getPreviewEditor()->updateUsingEditor(editor);

  // Change the image-type of color bar.
  ColorBar::instance()->setPixelFormat(app_get_current_pixel_format());

  // Restore the palette of the selected document.
  app_refresh_screen();

  // Change the main frame title.
  App::instance()->updateDisplayTitleBar();

  m_lastSelectedView = docView;

  // TODO all the calls to functions like updateUsingEditor(),
  // setPixelFormat(), app_refresh_screen(), updateDisplayTitleBar(),
  // etc. could be replaced with the Transaction class, which is a
  // DocObserver and handles updates on the screen processing the
  // observed changes.
  notifyActiveSiteChanged();
}

void UIContext::onSetActiveDocument(Doc* document, bool notify)
{
  notify = (notify && lastSelectedDoc() != document);
  app::Context::onSetActiveDocument(document, false);

  DocView* docView = getFirstDocView(document);
  if (docView) {     // The view can be null if we are in --batch mode
    setActiveView(docView);
    notify = false;
  }

  if (notify)
    notifyActiveSiteChanged();
}

void UIContext::onSetActiveLayer(doc::Layer* layer)
{
  if (DocView* docView = activeView()) {
    if (Editor* editor = docView->editor())
      editor->setLayer(layer);
  }
  else if (!isUIAvailable())
    Context::onSetActiveLayer(layer);
}

void UIContext::onSetActiveFrame(const doc::frame_t frame)
{
  if (DocView* docView = activeView()) {
    if (Editor* editor = docView->editor())
      editor->setFrame(frame);
  }
  else if (!isUIAvailable())
    Context::onSetActiveFrame(frame);
}

void UIContext::onSetRange(const DocRange& range)
{
  Timeline* timeline =
    (App::instance()->mainWindow() ?
     App::instance()->mainWindow()->getTimeline(): nullptr);
  if (timeline) {
    timeline->setRange(range);
  }
  else if (!isUIAvailable()) {
    Context::onSetRange(range);
  }
}

void UIContext::onSetSelectedColors(const doc::PalettePicks& picks)
{
  if (activeView()) {
    if (ColorBar* colorBar = ColorBar::instance())
      colorBar->getPaletteView()->setSelectedEntries(picks);
  }
  else if (!isUIAvailable())
    Context::onSetSelectedColors(picks);
}

void UIContext::onSetSelectedTiles(const doc::PalettePicks& picks)
{
  if (activeView()) {
    if (ColorBar* colorBar = ColorBar::instance())
      colorBar->getTilesView()->setSelectedEntries(picks);
  }
  else if (!isUIAvailable())
    Context::onSetSelectedTiles(picks);
}

DocView* UIContext::getFirstDocView(Doc* document) const
{
  Workspace* workspace = App::instance()->workspace();
  if (!workspace) // Workspace (main window) can be null if we are in --batch mode
    return nullptr;

  for (WorkspaceView* view : *workspace) {
    if (DocView* docView = dynamic_cast<DocView*>(view)) {
      if (docView->document() == document) {
        return docView;
      }
    }
  }

  return nullptr;
}

DocViews UIContext::getAllDocViews(Doc* document) const
{
  DocViews docViews;
  // The workspace can be nullptr when we are running in batch mode.
  if (Workspace* workspace = App::instance()->workspace()) {
    for (WorkspaceView* view : *workspace) {
      if (DocView* docView = dynamic_cast<DocView*>(view)) {
        if (docView->document() == document) {
          docViews.push_back(docView);
        }
      }
    }
  }
  return docViews;
}

Editors UIContext::getAllEditorsIncludingPreview(Doc* document) const
{
  std::vector<Editor*> editors;
  for (DocView* docView : getAllDocViews(document)) {
    if (docView->editor())
      editors.push_back(docView->editor());
  }

  if (MainWindow* mainWin = App::instance()->mainWindow()) {
    PreviewEditorWindow* previewWin = mainWin->getPreviewEditor();
    if (previewWin) {
      Editor* miniEditor = previewWin->previewEditor();
      if (miniEditor && miniEditor->document() == document)
        editors.push_back(miniEditor);
    }
  }
  return editors;
}

Editor* UIContext::activeEditor()
{
  DocView* view = activeView();
  if (view)
    return view->editor();
  else
    return NULL;
}

Editor* UIContext::getEditorFor(Doc* document)
{
  if (auto view = getFirstDocView(document))
    return view->editor();
  else
    return nullptr;
}

bool UIContext::hasClosedDocs()
{
  return m_closedDocs.hasClosedDocs();
}

void UIContext::reopenLastClosedDoc()
{
  if (Doc* doc = m_closedDocs.reopenLastClosedDoc()) {
    // Put the document in the context again.
    doc->setContext(this);
  }
}

std::vector<Doc*> UIContext::getAndRemoveAllClosedDocs()
{
  return m_closedDocs.getAndRemoveAllClosedDocs();
}

void UIContext::onAddDocument(Doc* doc)
{
  app::Context::onAddDocument(doc);

  // We don't create views in batch mode.
  if (!App::instance()->isGui())
    return;

  // Add a new view for this document
  DocView* view = new DocView(
    lastSelectedDoc(),
    DocView::Normal,
    App::instance()->mainWindow()->getPreviewEditor());

  // Add a tab with the new view for the document
  App::instance()->workspace()->addView(view);

  setActiveView(view);
  view->editor()->setDefaultScroll();
}

void UIContext::onRemoveDocument(Doc* doc)
{
  app::Context::onRemoveDocument(doc);

  // We don't destroy views in batch mode.
  if (isUIAvailable()) {
    Workspace* workspace = App::instance()->workspace();

    for (DocView* docView : getAllDocViews(doc)) {
      workspace->removeView(docView);
      delete docView;
    }
  }
}

void UIContext::onGetActiveSite(Site* site) const
{
  // We can use the activeView only from the UI thread.
#ifdef _DEBUG
  if (isUIAvailable())
    ui::assert_ui_thread();
#endif

  DocView* view = activeView();
  if (view) {
    view->getSite(site);

    if (site->sprite()) {
      // Selected range in the timeline. We use it only if the
      // timeline is visible. A common scenario might be
      // undoing/redoing actions where the range is re-selected, that
      // could enable the range even if the timeline is hidden. In
      // this way we avoid using the timeline selection unexpectedly.
      Timeline* timeline = App::instance()->timeline();
      if (timeline &&
          timeline->isVisible() &&
          timeline->range().enabled()) {
        site->range(timeline->range());
      }
      else {
        ColorBar* colorBar = ColorBar::instance();
        if (colorBar &&
            colorBar->getPaletteView()->getSelectedEntriesCount() > 0) {
          site->focus(Site::InColorBar);

          doc::PalettePicks picks;
          colorBar->getPaletteView()->getSelectedEntries(picks);
          site->selectedColors(picks);
        }
        else if (colorBar &&
                 colorBar->getTilesView()->getSelectedEntriesCount() > 0) {
          site->focus(Site::InColorBar);

          doc::PalettePicks picks;
          colorBar->getTilesView()->getSelectedEntries(picks);
          site->selectedTiles(picks);
        }
        else {
          site->focus(Site::InEditor);
        }
      }
    }
  }
  else if (!isUIAvailable()) {
    return app::Context::onGetActiveSite(site);
  }
}

void UIContext::onCloseDocument(Doc* doc)
{
  ASSERT(doc != nullptr);
  ASSERT(doc->context() == nullptr);

  m_closedDocs.addClosedDoc(doc);
}

} // namespace app
