////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2012-2024 The Octave Project Developers
//
// See the file COPYRIGHT.md in the top-level directory of this
// distribution or <https://octave.org/copyright/>.
//
// This file is part of Octave.
//
// Octave 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.
//
// Octave 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 Octave; see the file COPYING.  If not, see
// <https://www.gnu.org/licenses/>.
//
////////////////////////////////////////////////////////////////////////

#if defined (HAVE_CONFIG_H)
#  include "config.h"
#endif

#include <QAction>
#include <QApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QScreen>
#include <QStyle>
#include <QToolBar>
#include <QMenuBar>
#include <QWindow>

#include "gui-preferences-dw.h"
#include "gui-preferences-global.h"
#include "gui-preferences-mw.h"
#include "gui-preferences-sc.h"
#include "gui-settings.h"
#include "gui-utils.h"
#include "main-window.h"
#include "octave-dock-widget.h"

OCTAVE_BEGIN_NAMESPACE(octave)

label_dock_widget::label_dock_widget (QWidget *p)
  : QDockWidget (p), m_default_float_button (nullptr),
    m_default_close_button (nullptr)
{
  QStyle *st = style ();
  m_icon_size = 0.75*st->pixelMetric (QStyle::PM_SmallIconSize);

  // keep track of the original buttons on the default title bar,
  // the button further left is considered "float"
  QList<QAbstractButton *> buttonlist = findChildren<QAbstractButton *> ();
  if (buttonlist.size () == 2)
    {
      if (buttonlist.at (0)->x () < buttonlist.at (1)->x ())
        {
          m_default_float_button = buttonlist.at (0);
          m_default_close_button = buttonlist.at (1);
        }
      else
        {
          m_default_float_button = buttonlist.at (1);
          m_default_close_button = buttonlist.at (0);
        }
    }

  gui_settings settings;

  // the custom (extra) title bar of the widget
  m_title_widget = new QWidget ();

  m_dock_action = new QAction
    (settings.icon ("widget-undock", true), "", this);
  m_dock_action->setToolTip (tr ("Undock Widget"));
  m_dock_button = new QToolButton (m_title_widget);
  m_dock_button->setDefaultAction (m_dock_action);
  m_dock_button->setFocusPolicy (Qt::NoFocus);
  m_dock_button->setIconSize (QSize (m_icon_size, m_icon_size));

  m_close_action = new QAction
    (settings.icon ("widget-close", true), "", this);
  m_close_action->setToolTip (tr ("Close Widget"));
  m_close_button = new QToolButton (m_title_widget);
  m_close_button->setDefaultAction (m_close_action);
  m_close_button->setFocusPolicy (Qt::NoFocus);
  m_close_button->setIconSize (QSize (m_icon_size, m_icon_size));

  QString css_button = QString ("QToolButton {background: transparent; border: 0px;}");
  m_dock_button->setStyleSheet (css_button);
  m_close_button->setStyleSheet (css_button);

  QHBoxLayout *h_layout = new QHBoxLayout ();
  h_layout->addStretch (100);
  h_layout->addWidget (m_dock_button);
  h_layout->addWidget (m_close_button);
  h_layout->setSpacing (10);
  h_layout->setContentsMargins (5, 2, 2, 2);

  m_title_widget->setLayout (h_layout);

  if (p && (p->objectName () == gui_obj_name_main_window))
    {
      // Only connect the when a parent (main window) is given
      // copy & paste handling
      connect (p, SIGNAL (copyClipboard_signal ()),
               this, SLOT (copyClipboard ()));
      connect (p, SIGNAL (pasteClipboard_signal ()),
               this, SLOT (pasteClipboard ()));
      connect (p, SIGNAL (selectAll_signal ()),
               this, SLOT (selectAll ()));

      // undo handling
      connect (p, SIGNAL (undo_signal ()), this, SLOT (do_undo ()));
    }
}

// set the title in the dockwidgets title bar
void
label_dock_widget::set_title (const QString& title)
{
  QHBoxLayout *h_layout
    = static_cast<QHBoxLayout *> (m_title_widget->layout ());
  QLabel *label = new QLabel (title, m_title_widget);
  label->setStyleSheet ("background-color: transparent;");
  h_layout->insertWidget (0, label);
  setTitleBarWidget (m_title_widget);
  setWindowTitle (title);
}

static QString
qdockwidget_css (const QString& close_icon, const QString& close_tooltip,
                 const QString& float_icon, const QString& float_tooltip,
                 int icon_size, const QString& titlebar_foreground,
                 const QString& titlebar_background)
{
  return QString ("QDockWidget\n"
                  "{\n"
                  "%6"
                  "  border: none;\n"
                  "  titlebar-close-icon: url(%1);\n"
                  "  titlebar-normal-icon: url(%2);\n"
                  "}\n"
                  "\n"
                  "QDockWidget::close-button, QDockWidget::float-button\n"
                  "{\n"
                  "  border: none;\n"
                  "  icon-size: %3px;\n"
                  "}\n"
                  "\n"
                  "QAbstractButton#qt_dockwidget_closebutton\n"
                  "{\n"
                  "  qproperty-toolTip: \"%4\";\n"
                  "}\n"
                  "\n"
                  "QAbstractButton#qt_dockwidget_floatbutton\n"
                  "{\n"
                  "  qproperty-toolTip: \"%5\";\n"
                  "}\n"
                  "\n"
                  "QDockWidget::title {\n"
                  "  text-align: left;\n"
                  "%7"
                  "  padding-left: 1px;\n"
                  "}\n"
                  "\n"
                  "QDockWidget::close-button\n"
                  "{\n"
                  "  right: %8px;\n"
                  "  top: 3px;\n"
                  "}\n"
                  "\n"
                  "QDockWidget::float-button\n"
                  "{\n"
                  "  right: %9px;\n"
                  "  top: 3px;\n"
                  "}\n"
                  ).arg (close_icon).arg (float_icon).arg (icon_size)
                   .arg (close_tooltip).arg (float_tooltip)
                   .arg (titlebar_foreground).arg (titlebar_background)
                   .arg ((icon_size*2)/3).arg((icon_size*7)/3);
}

octave_dock_widget::octave_dock_widget (const QString& obj_name, QWidget *p)
  : label_dock_widget (p), m_main_window (nullptr),  m_adopted (false),
    m_custom_style (false), m_focus_follows_mouse (false),
    m_recent_float_geom (), m_recent_dock_geom (),
    m_waiting_for_mouse_button_release (false)
{
  setObjectName (obj_name);

  // FIXME: Can we avoid the cast here?
  m_main_window = dynamic_cast<main_window *> (p);

  m_predecessor_widget = nullptr;

  connect (this, &octave_dock_widget::topLevelChanged,
           this, &octave_dock_widget::toplevel_change);
  connect (this, &octave_dock_widget::visibilityChanged,
           this, &octave_dock_widget::handle_visibility);

  if (m_default_float_button != nullptr)
    {
      disconnect (m_default_float_button, 0, 0, 0);
      connect (m_default_float_button, &QAbstractButton::clicked,
               this, &octave_dock_widget::make_window);
    }
  connect (this, &octave_dock_widget::queue_make_window,
           this, &octave_dock_widget::make_window, Qt::QueuedConnection);
  connect (this, &octave_dock_widget::queue_make_widget,
           this, [=] () { make_widget (); }, Qt::QueuedConnection);

  gui_settings settings;

  settings.set_shortcut (m_dock_action, sc_dock_widget_dock);
  m_dock_action->setShortcutContext (Qt::WidgetWithChildrenShortcut);
  addAction (m_dock_action);
  connect (m_dock_action, &QAction::triggered,
           this, &octave_dock_widget::make_window);

  settings.set_shortcut (m_close_action, sc_dock_widget_close);
  m_close_action->setShortcutContext (Qt::WidgetWithChildrenShortcut);
  addAction (m_close_action);
  connect (m_close_action, &QAction::triggered,
           this, &octave_dock_widget::change_visibility);

  m_close_action->setToolTip (tr ("Hide Widget"));

  setStyleSheet (qdockwidget_css
                 (global_icon_paths.at (ICON_THEME_OCTAVE) + "widget-close.png",
                  QString ("Close Widget"),
                  global_icon_paths.at (ICON_THEME_OCTAVE) + "widget-undock.png",
                  QString ("Undock Widget"), m_icon_size, "", ""));

  if (widget ())
    widget ()->setToolTip ("");

  m_icon_color = "";
  m_title_3d = 50;

  installEventFilter (this);

  setFocusPolicy (Qt::StrongFocus);

  setFeatures (QDockWidget::DockWidgetClosable
               | QDockWidget::DockWidgetMovable
               | QDockWidget::DockWidgetFloatable);

  handle_settings ();
}

void
octave_dock_widget::init_window_menu_entry ()
{
  emit active_changed (isVisible ());  // emit once for init of window menu
}

// make the widget floating
void
octave_dock_widget::make_window (bool widget_was_dragged)
{
  bool vis = isVisible ();

  // prevent follow-up calls by clearing state variable
  m_waiting_for_mouse_button_release = false;

  set_focus_predecessor ();  // set focus previously active widget if tabbed

  // Before unparenting, get current geometry for restoring if dragged
  QRect geom = geometry ();

  // the widget has to be reparented (parent = 0), preferably
  // from a non-toplevel widget otherwise may not have full
  // decorations, e.g., no taskbar icon and always in front
  if (isFloating ())
    setFloating (false);

  if (m_main_window)
    {
      // Before making it a separate (no more parent) floating widget,
      // remove the dock widget from the main window. This ensures
      // that tabbed widgets keep their focus when it is re-docked
      // later
      m_main_window->removeDockWidget (this);
    }

  setParent (0, Qt::CustomizeWindowHint | Qt::WindowTitleHint |
             Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint | Qt::Window);

  // restore the last geometry when floating only if we have not dragged
  // the window outside the main window
  if (! widget_was_dragged)
    geom = m_recent_float_geom.isNull () ? QRect (50,100,480,480)
                                         : m_recent_float_geom;
  setGeometry (geom);

  // adjust the (un)dock action
  disconnect (m_dock_action, 0, this, 0);
  connect (m_dock_action, &QAction::triggered,
           this, &octave_dock_widget::make_widget);

  // adjust the (un)dock icon
  if (titleBarWidget ())
    {
      gui_settings settings;

      m_dock_action->setIcon (settings.icon ("widget-dock" + m_icon_color, true));
      m_dock_action->setToolTip (tr ("Dock Widget"));
    }
  else
    {
      disconnect (m_default_float_button, 0, this, 0);
      connect (m_default_float_button, &QAbstractButton::clicked,
               this, &octave_dock_widget::make_widget);
    }

  raise ();
  activateWindow ();

  if (vis)
    {
      show ();
      setFocus ();
      set_style (true);
    }

  emit topLevelChanged (true);  // Be sure signal is emitted
}

// dock the widget
void
octave_dock_widget::make_widget (bool)
{
  gui_settings settings;

  bool vis = isVisible ();

  // Since floating widget has no parent, we have to read it

  if (m_main_window)
    {
      settings.setValue (mw_state.settings_key (), m_main_window->saveState ());

      // Stay window, otherwise will bounce back to window by default
      // because there is no layout information for this widget in the
      // saved settings.
      setParent (m_main_window, Qt::Window);
      m_main_window->addDockWidget (Qt::BottomDockWidgetArea, this);
      m_adopted = false;
      // recover old window states, hide and re-show new added widget
      m_main_window->restoreState (settings.value (mw_state.settings_key ()).toByteArray ());
      setFloating (false);
      // restore size using setGeometry instead of restoreGeometry
      // following this post:
      // https://forum.qt.io/topic/79326/qdockwidget-restoregeometry-not-working-correctly-when-qmainwindow-is-maximized/5
      setGeometry (m_recent_dock_geom);
    }

  // adjust the (un)dock icon
  disconnect (m_dock_action, 0, this, 0);
  connect (m_dock_action, &QAction::triggered,
           this, &octave_dock_widget::make_window);
  if (titleBarWidget ())
    {
      m_dock_action->setIcon (settings.icon ("widget-undock" + m_icon_color, true));
      m_dock_action->setToolTip (tr ("Undock Widget"));
    }
  else
    {
      disconnect (m_default_float_button, 0, this, 0);
      connect (m_default_float_button, &QAbstractButton::clicked,
               this, &octave_dock_widget::make_window);
    }

  raise ();
  activateWindow ();

  if (vis)
    {
      show ();
      setFocus ();
      set_style (true);
    }
}

// dock the widget
void
octave_dock_widget::default_dock (bool)
{
  setFloating (false);
}

// set the widget which previously had focus when tabified
void
octave_dock_widget::set_predecessor_widget (octave_dock_widget *prev_widget)
{
  m_predecessor_widget = prev_widget;
}

void
octave_dock_widget::set_main_window (main_window *mw)
{
  m_main_window = mw;

  if (m_main_window)
    {
      connect (m_main_window, &main_window::copyClipboard_signal,
               this, &octave_dock_widget::copyClipboard);

      connect (m_main_window, &main_window::pasteClipboard_signal,
               this, &octave_dock_widget::pasteClipboard);

      connect (m_main_window, &main_window::selectAll_signal,
               this, &octave_dock_widget::selectAll);

      connect (m_main_window, &main_window::undo_signal,
               this, &octave_dock_widget::do_undo);
    }
}

// close event
void
octave_dock_widget::closeEvent (QCloseEvent *e)
{
  emit active_changed (false);
  set_focus_predecessor ();
  save_settings ();

  QDockWidget::closeEvent (e);
}

// get focus widget
QWidget *
octave_dock_widget::focusWidget ()
{
  QWidget *w = QApplication::focusWidget ();
  if (w && w->focusProxy ()) w = w->focusProxy ();
  return w;
}

bool
octave_dock_widget::event (QEvent *event)
{
  // low-level check of whether docked-widget became a window via
  // double-click or via drag-and-drop
  if ( (event->type () == QEvent::MouseButtonDblClick && ! isFloating ())
       || (event->type () == QEvent::ActivationChange
           && m_waiting_for_mouse_button_release))
    {
      bool retval = QDockWidget::event (event);
      if (isFloating () && parent () != 0)
        {
          m_waiting_for_mouse_button_release = false;
          emit queue_make_window (event->type () != QEvent::MouseButtonDblClick);
        }
      return retval;
    }

  return QDockWidget::event (event);
}

void
octave_dock_widget::handle_settings ()
{
  gui_settings settings;

  m_focus_follows_mouse = settings.bool_value (dw_focus_follows_mouse);

  m_custom_style
    = settings.bool_value (dw_title_custom_style);

  m_title_3d = settings.int_value (dw_title_3d);

  m_fg_color = settings.color_value (dw_title_fg_color);

  m_fg_color_active = settings.color_value (dw_title_fg_color_active);

  m_bg_color = settings.color_value (dw_title_bg_color);

  m_bg_color_active = settings.color_value (dw_title_bg_color_active);

  QColor bcol (m_bg_color);
  QColor bcola (m_bg_color_active);

  if (! m_custom_style)
    {
      bcol = QWidget::palette ().color (m_title_widget->backgroundRole ());
      bcola = bcol;
    }

  int r, g, b;
  bcol.getRgb (&r, &g, &b);
  if (r+g+b < 400)
    m_icon_color = "-light";
  else
    m_icon_color = "";

  bcola.getRgb (&r, &g, &b);
  if (r+g+b < 400)
    m_icon_color_active = "-light";
  else
    m_icon_color_active = "";

  int x, y, w, h;
  QGuiApplication::primaryScreen ()->availableGeometry ().getRect (&x, &y, &w, &h);
  QRect default_floating_size = QRect (x+16, y+32, w/3, h/2);

  QRect default_dock_size;
  if (m_main_window)
    {
      // We have a main window, dock size depends on size of main window
      m_main_window->geometry ().getRect (&x, &y, &w, &h);
      default_dock_size = QRect (x+16, y+32, w/3, h/3);
    }
  else
    {
      // No main window, default dock size should never be used
      default_dock_size = QRect (0, 0, w/10, h/10);
    }

  m_recent_float_geom
    = settings.value (dw_float_geometry.settings_key ().arg (objectName ()),
                       default_floating_size).toRect ();

  adjust_to_screen (m_recent_float_geom, default_floating_size);

  // The following is required for ensure smooth transition from old
  // saveGeomety to new QRect setting (see comment for restoring size
  // of docked widgets)
  QVariant dock_geom
    = settings.value (dw_dock_geometry.settings_key ().arg (objectName ()),
                      default_dock_size);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  if (dock_geom.canConvert (QMetaType (QMetaType::QRect)))
#else
  if (dock_geom.canConvert (QMetaType::QRect))
#endif
    m_recent_dock_geom = dock_geom.toRect ();
  else
    m_recent_dock_geom = dw_dock_geometry.def ().toRect ();

  notice_settings ();  // call individual handler

  set_style (false);

  // Compacter design
  QToolBar *toolbar = findChild <QToolBar *> ();
  if (toolbar)
    toolbar->setStyleSheet (toolbar->styleSheet () + global_toolbar_style);

  QMenuBar *menubar = findChild <QMenuBar *> ();
  if (menubar)
    menubar->setStyleSheet (menubar->styleSheet () + global_menubar_style);

}

void
octave_dock_widget::handle_active_dock_changed (octave_dock_widget *w_old,
                                                octave_dock_widget *w_new)
{
  if (m_custom_style && this == w_old)
    {
      set_style (false);
      update ();
    }

  if (m_custom_style && this == w_new)
    {
      set_style (true);
      update ();
    }
}

void
octave_dock_widget::save_settings ()
{
  gui_settings settings;

  // save state of this dock-widget
  QString name = objectName ();

  store_geometry ();

  // conditional needed?
  if (! m_recent_float_geom.isNull ())
    settings.setValue (dw_float_geometry.settings_key ().arg (name), m_recent_float_geom);

  if (! m_recent_dock_geom.isEmpty ())
    settings.setValue (dw_dock_geometry.settings_key ().arg (name), m_recent_dock_geom);
  settings.setValue (dw_is_visible.settings_key ().arg (name), isVisible ()); // store visibility
  settings.setValue (dw_is_floating.settings_key ().arg (name), isFloating ()); // store floating
  settings.setValue (dw_is_minimized.settings_key ().arg (name), isMinimized ()); // store minimized

  settings.sync ();
}

bool octave_dock_widget::eventFilter (QObject *obj, QEvent *e)
{
  // Ignore double clicks into window decoration elements
  if (e->type () == QEvent::NonClientAreaMouseButtonDblClick)
    {
      e->ignore ();
      return true;
    }

  // Detect mouse enter events if "focus follows mouse" is desired
  // for widgets docked to the main window (non floating) and activate
  // the widget currently under the mouse
  if (m_focus_follows_mouse && ! isFloating () && (e->type () == QEvent::Enter))
    setFocus ();

  return QDockWidget::eventFilter (obj, e);
}

void
octave_dock_widget::store_geometry ()
{
  if (isFloating ())
    {
      if (! parent ())
        m_recent_float_geom = geometry ();
    }
  else
    {
      m_recent_dock_geom = geometry ();
    }
}

void
octave_dock_widget::moveEvent (QMoveEvent *event)
{
  store_geometry ();

  QDockWidget::moveEvent (event);
}

void
octave_dock_widget::resizeEvent (QResizeEvent *event)
{
  store_geometry ();

  QDockWidget::resizeEvent (event);
}

// slot for hiding the widget
void
octave_dock_widget::change_visibility (bool)
{
  setVisible (false);
  emit active_changed (false);
}

void octave_dock_widget::activate ()
{
  if (! isVisible ())
    setVisible (true);

  setFocus ();
  activateWindow ();
  raise ();
}

void octave_dock_widget::handle_visibility (bool visible)
{
  if (visible)
    {
      emit active_changed (true);
      if (! isFloating ())
        setFocus ();
    }
}

void
octave_dock_widget::toplevel_change (bool toplevel)
{
  QObject *dockobj;
  const char *docksig;

  if (titleBarWidget ())
    {
      dockobj = m_dock_action;
      docksig = SIGNAL (triggered (bool));
    }
  else
    {
      dockobj = m_default_float_button;
      docksig = SIGNAL (clicked (bool));
    }

  if (toplevel)
    {
      // This is a fallback in case the attempt to create a floated
      // top-level window fails and the QDockWidget remains a child
      // of the QMainWindow.
      connect (dockobj, docksig, this, SLOT (default_dock (bool)));

      // Could be dragging window, so must wait until there is a
      // change in focus.
      if (parent () != 0)
        m_waiting_for_mouse_button_release = true;
    }
  else
    {
      // If a drag-and-drop within the QMainWindow occurred, want to remain a widget.
      m_waiting_for_mouse_button_release = false;

      // Making into a widget immediately will mangle the double-click
      // status and cause problems on followup button clicks.
      if (parent () == 0)
        emit queue_make_widget ();
    }
}

void
octave_dock_widget::set_style (bool active)
{
  QString css_foreground;
  QString css_background;
  QString css_button;
  QString dock_icon;

  QString icon_col = m_icon_color;

  QString close_tooltip = "Close Widget";
  QString dock_tooltip;

  if (isFloating ())
    {
      dock_icon = "widget-dock";
      dock_tooltip = "Dock Widget";
    }
  else
    {
      dock_icon = "widget-undock";
      dock_tooltip = "Undock Widget";
    }

#if defined (Q_OS_MAC)
  QString alignment = "center";
#else
  QString alignment = "center left";
#endif
  if (m_custom_style)
    {

      QColor bg_col, fg_col;

      if (active)
        {
          bg_col = m_bg_color_active;
          fg_col = m_fg_color_active;
          icon_col = m_icon_color_active;
        }
      else
        {
          bg_col = m_bg_color;
          fg_col = m_fg_color;
          icon_col = m_icon_color;
        }

      QColor bg_col_top, bg_col_bottom;
      if (m_title_3d > 0)
        {
          bg_col_top = bg_col.lighter (100 + m_title_3d);
          bg_col_bottom = bg_col.darker (100 + m_title_3d);
        }
      else
        {
          bg_col_top = bg_col.darker (100 - m_title_3d);
          bg_col_bottom = bg_col.lighter (100 - m_title_3d);
        }

      css_foreground = QString ("  color: %1;\n").arg (fg_col.name ());

      css_background =
        QString ("  background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,"
                 " stop: 0 %1, stop: 0.60 %2, stop: 0.95 %2 stop: 1.0 %3);\n").
        arg (bg_col_top.name ()).
        arg (bg_col.name ()).
        arg (bg_col_bottom.name ());
    }
  else
    {
      css_foreground = "";
      css_background = "";
    }

  QString full_dock_icon = dock_icon + icon_col;
  QString full_close_icon = "widget-close" + icon_col;
  if (titleBarWidget ())
    {
      gui_settings settings;

      titleBarWidget ()->setStyleSheet (css_foreground + css_background);
      css_button = QString ("QToolButton {background: transparent; border: 0px;}");
      m_dock_button->setStyleSheet (css_button);
      m_close_button->setStyleSheet (css_button);
      m_dock_action->setIcon (settings.icon (full_dock_icon, true));
      m_close_action->setIcon (settings.icon (full_close_icon, true));
    }
  else
    {
      setStyleSheet (qdockwidget_css (global_icon_paths.at (ICON_THEME_OCTAVE) + full_close_icon + ".png",
                                      close_tooltip,
                                      global_icon_paths.at (ICON_THEME_OCTAVE) + full_dock_icon + ".png",
                                      dock_tooltip,
                                      m_icon_size,
                                      css_foreground,
                                      css_background));
    }
}

// set focus to previously active widget in tabbed widget stack
void
octave_dock_widget::set_focus_predecessor ()
{
  // only != 0 if widget was tabbed
  if (m_predecessor_widget && m_predecessor_widget->isVisible ())
    m_predecessor_widget->setFocus ();

  m_predecessor_widget = nullptr;
}

OCTAVE_END_NAMESPACE(octave)
