/***********************************************************************
* fapplication.cpp - Manages the application events                    *
*                                                                      *
* This file is part of the FINAL CUT widget toolkit                    *
*                                                                      *
* Copyright 2013-2021 Markus Gans                                      *
*                                                                      *
* FINAL CUT is free software; you can redistribute it and/or modify    *
* it under the terms of the GNU Lesser General Public License as       *
* published by the Free Software Foundation; either version 3 of       *
* the License, or (at your option) any later version.                  *
*                                                                      *
* FINAL CUT 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 Lesser General Public License for more details.                  *
*                                                                      *
* You should have received a copy of the GNU Lesser General Public     *
* License along with this program.  If not, see                        *
* <http://www.gnu.org/licenses/>.                                      *
***********************************************************************/

#include <chrono>
#include <fstream>
#include <iostream>
#include <memory>
#include <ostream>
#include <string>
#include <thread>

#include "final/fapplication.h"
#include "final/fevent.h"
#include "final/flog.h"
#include "final/flogger.h"
#include "final/fmenu.h"
#include "final/fmenubar.h"
#include "final/fmessagebox.h"
#include "final/fmouse.h"
#include "final/fstartoptions.h"
#include "final/fstatusbar.h"
#include "final/ftermdata.h"
#include "final/ftermios.h"
#include "final/fwidgetcolors.h"
#include "final/fwindow.h"

namespace finalcut
{

namespace internal
{

struct var
{
  static FApplication* app_object;  // Global application object
  static bool          exit_loop;   // Flag to exit the local event loop
};

FApplication*  var::app_object {nullptr};
bool           var::exit_loop  {false};

}  // namespace internal

// Static attributes
FWidget*       FWidget::main_widget          {nullptr};  // main application widget
FWidget*       FWidget::active_window        {nullptr};  // the active window
FWidget*       FWidget::focus_widget         {nullptr};  // has keyboard input focus
FWidget*       FWidget::clicked_widget       {nullptr};  // is focused by click
FWidget*       FWidget::open_menu            {nullptr};  // currently open menu
FWidget*       FWidget::move_size_widget     {nullptr};  // move/size by keyboard
FWidget*       FApplication::keyboard_widget {nullptr};  // has the keyboard focus
int            FApplication::loop_level      {0};        // event loop level
int            FApplication::quit_code       {EXIT_SUCCESS};
bool           FApplication::quit_now        {false};
uInt64         FApplication::next_event_wait {5000};     // 5 ms (200 Hz)
TimeValue      FApplication::time_last_event {};


//----------------------------------------------------------------------
// class FApplication
//----------------------------------------------------------------------

// constructors and destructor
//----------------------------------------------------------------------
FApplication::FApplication (const int& arg_c, char* arg_v[])
  : FWidget{processParameters(Args(arg_v, arg_v + arg_c))}
  , app_args{arg_v, arg_v + arg_c}
{
  if ( quit_now )
    return;

  if ( internal::var::app_object )
  {
    auto& fterm_data = FTermData::getInstance();
    fterm_data.setExitMessage("FApplication: There should be "
                              "only one application object");
    FApplication::exit(EXIT_FAILURE);
    return;
  }

  // First define the application object
  internal::var::app_object = this;

  init();
}

//----------------------------------------------------------------------
FApplication::~FApplication()  // destructor
{
  internal::var::app_object = nullptr;

  if ( eventInQueue() )
    event_queue.clear();

  destroyLog();
}


// public methods of FApplication
//----------------------------------------------------------------------
FApplication* FApplication::getApplicationObject()
{
  return internal::var::app_object;
}

//----------------------------------------------------------------------
FWidget* FApplication::getKeyboardWidget()
{
  return keyboard_widget;
}

//----------------------------------------------------------------------
FApplication::FLogPtr& FApplication::getLog()
{
  // Global logger object
  static auto logger_ptr = new FLogPtr();

  if ( logger_ptr && logger_ptr->get() == nullptr )
  {
    *logger_ptr = std::make_shared<FLogger>();

    // Set the logger as rdbuf of clog
    std::clog.rdbuf(logger_ptr->get());
  }

  return *logger_ptr;
}

//----------------------------------------------------------------------
void FApplication::setLog (const FLogPtr& log)
{
  FLogPtr& logger = getLog();
  logger.reset();
  logger = log;

  // Set the logger as rdbuf of clog
  std::clog.rdbuf(logger.get());
}

//----------------------------------------------------------------------
bool FApplication::isQuit()
{
  return internal::var::app_object ? quit_now : true;
}

//----------------------------------------------------------------------
int FApplication::exec()  // run
{
  if ( quit_now )
  {
    quit_now = false;
    return quit_code;
  }

  quit_code = EXIT_SUCCESS;
  enterLoop();
  return quit_code;
}

//----------------------------------------------------------------------
int FApplication::enterLoop()  // event loop
{
  loop_level++;
  quit_now = false;

  const bool old_app_exit_loop = internal::var::exit_loop;
  internal::var::exit_loop = false;

  while ( ! (quit_now || internal::var::exit_loop) )
    processNextEvent();

  internal::var::exit_loop = old_app_exit_loop;
  loop_level--;
  return 0;
}

//----------------------------------------------------------------------
void FApplication::exitLoop() const
{
  internal::var::exit_loop = true;
}

//----------------------------------------------------------------------
void FApplication::exit (int retcode)
{
  quit_now  = true;
  quit_code = retcode;
}

//----------------------------------------------------------------------
void FApplication::quit() const
{
  FApplication::exit(0);
}

//----------------------------------------------------------------------
bool FApplication::sendEvent (FObject* receiver, FEvent* event )
{
  if ( quit_now || internal::var::exit_loop || ! (bool(receiver) && bool(event)) )
    return false;

  if ( ! isEventProcessable (receiver, event) )
    return false;

  // Sends the event event directly to receiver
  bool ret = receiver->event(event);
  event->send = true;
  return ret;
}

//----------------------------------------------------------------------
void FApplication::queueEvent (FObject* receiver, FEvent* event)
{
  if ( ! (bool(receiver) && bool(event)) )
    return;

  // queue this event
  event->queued = true;
  event_queue.emplace_back (receiver, event);
}

//----------------------------------------------------------------------
void FApplication::sendQueuedEvents()
{
  while ( eventInQueue() )
  {
    const EventPair& event_pair = event_queue.front();
    event_pair.second->queued = false;
    sendEvent(event_pair.first, event_pair.second);
    event_queue.pop_front();
  }
}

//----------------------------------------------------------------------
bool FApplication::eventInQueue() const
{
  if ( internal::var::app_object )
    return ( ! event_queue.empty() );
  else
    return false;
}

//----------------------------------------------------------------------
bool FApplication::removeQueuedEvent (const FObject* receiver)
{
  if ( ! eventInQueue() )
    return false;

  if ( ! receiver )
    return false;

  bool retval{false};
  auto iter = event_queue.begin();

  while ( iter != event_queue.end() )
  {
    if ( iter->first == receiver )
    {
      iter = event_queue.erase(iter);
      retval = true;
    }
    else
      ++iter;
  }

  return retval;
}

//----------------------------------------------------------------------
void FApplication::initTerminal()
{
  if ( ! isQuit() )
    FWidget::initTerminal();
}

//----------------------------------------------------------------------
void FApplication::setDefaultTheme()
{
  if ( FTerm::getMaxColor() < 16 )  // for 8 color mode
  {
    if ( getStartOptions().color_change )
      FTerm::setColorPaletteTheme<default8ColorPalette>();

    setColorTheme<default8ColorTheme>();
  }
  else
  {
    if ( getStartOptions().color_change )
      FTerm::setColorPaletteTheme<default16ColorPalette>();

    setColorTheme<default16ColorTheme>();
  }
}

//----------------------------------------------------------------------
void FApplication::setDarkTheme()
{
  if ( getStartOptions().color_change )
    FTerm::setColorPaletteTheme<default16DarkColorPalette>();

  if ( FTerm::getMaxColor() < 16 )  // for 8 color mode
    setColorTheme<default8ColorDarkTheme>();
  else
    setColorTheme<default16ColorDarkTheme>();
}

//----------------------------------------------------------------------
void FApplication::setLogFile (const FString& filename)
{
  auto& log_stream = getStartOptions().logfile_stream;
  log_stream.open(filename.c_str(), std::ofstream::out);

  if ( log_stream.is_open() )
  {
    // Get the global logger object
    FLog& log = *FApplication::getLog();
    log.setOutputStream(log_stream);
    log.enableTimestamp();
    log.setLineEnding (FLog::LineEnding::LF);
  }
  else
  {
    auto& fterm_data = FTermData::getInstance();
    fterm_data.setExitMessage ( "Could not open log file \""
                              + filename + "\"" );
    exit(EXIT_FAILURE);
  }
}

//----------------------------------------------------------------------
void FApplication::setKeyboardWidget (FWidget* widget)
{
  keyboard_widget = widget;
}

//----------------------------------------------------------------------
void FApplication::closeConfirmationDialog (FWidget* w, FCloseEvent* ev)
{
  internal::var::app_object->unsetMoveSizeMode();
  const FMessageBox::ButtonType ret = \
      FMessageBox::info ( w, "Quit"
                        , "Do you really want\n"
                          "to quit the program ?"
                        , FMessageBox::ButtonType::Yes
                        , FMessageBox::ButtonType::No );

  if ( ret == FMessageBox::ButtonType::Yes )
    ev->accept();
  else
  {
    ev->ignore();

    // Status bar restore after closing the FMessageBox
    if ( getStatusBar() )
      getStatusBar()->drawMessage();
  }
}


// protected methods of FApplication
//----------------------------------------------------------------------
void FApplication::processExternalUserEvent()
{
  // This method can be overloaded and replaced by own code
}


// private methods of FApplication
//----------------------------------------------------------------------
void FApplication::init()
{
  // FApplication cannot have a second child widget
  setMaxChildren(1);

  // Initialize the last event time
  time_last_event = TimeValue{};

  // Initialize keyboard
  auto& keyboard = FKeyboard::getInstance();
  auto cmd1 = [this] () { this->keyPressed(); };
  auto cmd2 = [this] () { this->keyReleased(); };
  auto cmd3 = [this] () { this->escapeKeyPressed(); };
  auto cmd4 = [this] () { this->mouseTracking(); };
  FKeyboardCommand key_cmd1 (cmd1);
  FKeyboardCommand key_cmd2 (cmd2);
  FKeyboardCommand key_cmd3 (cmd3);
  FKeyboardCommand key_cmd4 (cmd4);
  keyboard.setPressCommand (key_cmd1);
  keyboard.setReleaseCommand (key_cmd2);
  keyboard.setEscPressedCommand (key_cmd3);
  keyboard.setMouseTrackingCommand (key_cmd4);
  // Set the keyboard keypress timeout
  keyboard.setKeypressTimeout (key_timeout);

  // Initialize mouse control
  auto& mouse = FMouseControl::getInstance();
  auto cmd = [this] (const FMouseData& md) { this->mouseEvent(md); };
  FMouseCommand mouse_cmd (cmd);
  mouse.setEventCommand (mouse_cmd);
  // Set stdin number for a gpm-mouse
  mouse.setStdinNo (FTermios::getStdIn());
  // Set the default double click interval
  mouse.setDblclickInterval (dblclick_interval);

  // Initialize logging
  if ( ! getStartOptions().logfile_stream.is_open() )
    getLog()->setLineEnding(FLog::LineEnding::CRLF);
}

//----------------------------------------------------------------------
void FApplication::setTerminalEncoding (const FString& enc_str)
{
  const FString& enc = enc_str.toLower();

  if ( enc.includes("utf8") )
    getStartOptions().encoding = Encoding::UTF8;
  else if ( enc.includes("vt100") )
    getStartOptions().encoding = Encoding::VT100;
  else if ( enc.includes("pc") )
    getStartOptions().encoding = Encoding::PC;
  else if ( enc.includes("ascii") )
    getStartOptions().encoding = Encoding::ASCII;
  else if ( enc.includes("help") )
    showParameterUsage();
  else
  {
    auto& fterm_data = FTermData::getInstance();
    fterm_data.setExitMessage ( "Unknown encoding \"" + enc_str
                              + "\"\n(Valid encodings are utf8, "
                              + "vt100, pc and ascii)" );
    exit(EXIT_FAILURE);
  }
}

//----------------------------------------------------------------------
inline void FApplication::setLongOptions (std::vector<CmdOption>& long_options)
{
  long_options =
  {
    {"encoding",                 required_argument, nullptr,  'e' },
    {"log-file",                 required_argument, nullptr,  'l' },
    {"no-mouse",                 no_argument,       nullptr,  'm' },
    {"no-optimized-cursor",      no_argument,       nullptr,  'o' },
    {"no-terminal-detection",    no_argument,       nullptr,  'd' },
    {"no-terminal-data-request", no_argument,       nullptr,  'r' },
    {"no-color-change",          no_argument,       nullptr,  'c' },
    {"no-sgr-optimizer",         no_argument,       nullptr,  's' },
    {"vgafont",                  no_argument,       nullptr,  'v' },
    {"newfont",                  no_argument,       nullptr,  'n' },
    {"dark-theme",               no_argument,       nullptr,  't' },

  #if defined(__FreeBSD__) || defined(__DragonFly__)
    {"no-esc-for-alt-meta",      no_argument,       nullptr,  'E' },
    {"no-cursorstyle-change",    no_argument,       nullptr,  'C' },
  #elif defined(__NetBSD__) || defined(__OpenBSD__)
    {"no-esc-for-alt-meta",      no_argument,       nullptr,  'E' },
  #endif

    {nullptr,                    0,                 nullptr,  0   }
  };
}

//----------------------------------------------------------------------
inline void FApplication::setCmdOptionsMap (CmdMap& cmd_map)
{
  auto enc = [] (const FString& s) { FApplication::setTerminalEncoding(s); };
  auto log = [] (const FString& s) { FApplication::setLogFile(s); };
  auto opt = &FApplication::getStartOptions;

  // --encoding
  cmd_map['e'] = [enc] (const char* arg) { enc(FString(arg)); };
  // --log-file
  cmd_map['l'] = [log] (const char* arg) { log(FString(arg)); };
  // --no-mouse
  cmd_map['m'] = [opt] (const char*) { opt().mouse_support = false; };
  // --no-optimized-cursor
  cmd_map['o'] = [opt] (const char*) { opt().cursor_optimisation = false; };
  // --no-terminal-detection
  cmd_map['d'] = [opt] (const char*) { opt().terminal_detection = false; };
  // --no-terminal-data-request
  cmd_map['r'] = [opt] (const char*) { opt().terminal_data_request = false; };
  // --no-color-change
  cmd_map['c'] = [opt] (const char*) { opt().color_change = false; };
  // --no-sgr-optimizer
  cmd_map['s'] = [opt] (const char*) { opt().sgr_optimizer = false; };
  // --vgafont
  cmd_map['v'] = [opt] (const char*) { opt().vgafont = true; };
  // --newfont
  cmd_map['n'] = [opt] (const char*) { opt().newfont = true; };
  // --dark-theme
  cmd_map['t'] = [opt] (const char*) { opt().dark_theme = true; };
#if defined(__FreeBSD__) || defined(__DragonFly__)
  // --no-esc-for-alt-meta
  cmd_map['E'] = [opt] (const char*) { opt().meta_sends_escape = false; };
  // --no-cursorstyle-change
  cmd_map['C'] = [opt] (const char*) { opt().change_cursorstyle = false; };
#elif defined(__NetBSD__) || defined(__OpenBSD__)
  // --no-esc-for-alt-meta
  cmd_map['E'] = [opt] (const char*) { opt().meta_sends_escape = false; };
#endif
}

//----------------------------------------------------------------------
void FApplication::cmdOptions (const Args& args)
{
  // Interpret the command line options

  CmdMap cmd_map{};
  setCmdOptionsMap(cmd_map);
  auto argc = args.size();
  std::vector<const char*> argv(argc);
  std::transform ( args.begin()
                 , args.end()
                 , argv.begin()
                 , [] (const std::string& str)
                   {
                     return str.data();
                   }
                 );

  while ( true )
  {
    opterr = 0;
    int idx{0};
    std::vector<CmdOption> long_options{};
    setLongOptions(long_options);
    auto p = reinterpret_cast<const struct option*>(long_options.data());
    auto argv_data = const_cast<char* const*>(argv.data());
    const int opt = getopt_long (int(argc), argv_data, "", p, &idx);

    if ( opt == -1 )
      break;

    const auto& entry = cmd_map[opt];

    if ( entry )
      entry(optarg);
  }

  cmd_map.clear();
}

//----------------------------------------------------------------------
inline FStartOptions& FApplication::getStartOptions()
{
  return FStartOptions::getInstance();
}

//----------------------------------------------------------------------
void FApplication::showParameterUsage()
{
  std::cout \
    << "Generic options:\n"
    << "  -h, --help                "
    << "    Display this help and exit\n"
    << "\n"
    << "FINAL CUT options:\n"
    << "  --encoding=<MODE>         "
    << "    Sets the character encoding mode\n"
    << "                            "
    << "    {utf8, vt100, pc, ascii}\n"
    << "  --log-file=<FILE>         "
    << "    Writes log output to FILE\n"
    << "  --no-mouse                "
    << "    Disable mouse support\n"
    << "  --no-optimized-cursor     "
    << "    Disable cursor optimization\n"
    << "  --no-terminal-detection   "
    << "    Disable terminal detection\n"
    << "  --no-terminal-data-request"
    << "    Do not determine terminal font and title\n"
    << "  --no-color-change         "
    << "    Do not redefine the color palette\n"
    << "  --no-sgr-optimizer        "
    << "    Do not optimize SGR sequences\n"
    << "  --vgafont                 "
    << "    Set the standard vga 8x16 font\n"
    << "  --newfont                 "
    << "    Enables the graphical font\n"
    << "  --dark-theme              "
    << "    Enables the dark theme\n"

#if defined(__FreeBSD__) || defined(__DragonFly__)
    << "\n"
    << "FreeBSD console options:\n"
    << "  --no-esc-for-alt-meta     "
    << "    Do not send a ESC prefix for the alt/meta key\n"
    << "  --no-cursorstyle-change   "
    << "    Do not change the current cursor style\n"
#elif defined(__NetBSD__) || defined(__OpenBSD__)
    << "\n"
    << "NetBSD/OpenBSD console options:\n"
    << "  --no-esc-for-alt-meta     "
    << "    Do not send a ESC prefix for the alt/meta key\n"
#endif

    << std::endl;  // newline character + flushes the output stream
}

//----------------------------------------------------------------------
inline void FApplication::destroyLog()
{
  // Reset the rdbuf of clog
  std::clog.rdbuf(default_clog_rdbuf);

  // Delete the logger
  const FLogPtr* logger = &(getLog());
  delete logger;
}

//----------------------------------------------------------------------
inline void FApplication::findKeyboardWidget() const
{
  // Find the widget that has the keyboard focus

  FWidget* widget{nullptr};
  auto focus = getFocusWidget();
  auto move_size = getMoveSizeWidget();

  if ( focus )
  {
    if ( move_size )
      widget = move_size;
    else
      widget = focus;
  }
  else
  {
    widget = getMainWidget();

    if ( widget && widget->numOfChildren() >= 1 )
      widget->focusFirstChild();
  }

  keyboard_widget = widget;
}

//----------------------------------------------------------------------
inline bool FApplication::isKeyPressed() const
{
  auto& mouse = FMouseControl::getInstance();
  auto& keyboard = FKeyboard::getInstance();

  if ( mouse.isGpmMouseEnabled() )
    return mouse.getGpmKeyPressed(keyboard.hasUnprocessedInput());

  return (keyboard.isKeyPressed() || keyboard.hasPendingInput());
}

//----------------------------------------------------------------------
void FApplication::keyPressed()
{
  performKeyboardAction();
}

//----------------------------------------------------------------------
void FApplication::keyReleased() const
{
  sendKeyUpEvent (keyboard_widget);
}

//----------------------------------------------------------------------
void FApplication::escapeKeyPressed() const
{
  sendEscapeKeyPressEvent();
}

//----------------------------------------------------------------------
void FApplication::mouseTracking() const
{
  performMouseAction();
}

//----------------------------------------------------------------------
inline void FApplication::performKeyboardAction()
{
  const auto& keyboard = FKeyboard::getInstance();

  if ( keyboard.getKey() == FKey::Ctrl_l )  // Ctrl-L (redraw the screen)
  {
    redraw();
  }
  else
  {
    const bool acceptKeyDown = sendKeyDownEvent (keyboard_widget);
    const bool acceptKeyPress = sendKeyPressEvent (keyboard_widget);

    if ( ! (acceptKeyDown || acceptKeyPress) )
      sendKeyboardAccelerator();
  }
}

//----------------------------------------------------------------------
inline void FApplication::performMouseAction() const
{
  auto& mouse = FMouseControl::getInstance();
  auto& keyboard = FKeyboard::getInstance();
  const auto key = keyboard.getKey();
  auto& buffer = keyboard.getKeyBuffer();

  if ( key == FKey::X11mouse )
  {
    mouse.setRawData (FMouse::MouseType::X11, buffer);
  }
  else if ( key == FKey::Extended_mouse )
  {
    mouse.setRawData (FMouse::MouseType::Sgr, buffer);
  }
  else if ( key == FKey::Urxvt_mouse )
  {
    mouse.setRawData (FMouse::MouseType::Urxvt, buffer);
  }

  keyboard.hasUnprocessedInput() = mouse.hasUnprocessedInput();
  queuingMouseInput();
}

//----------------------------------------------------------------------
void FApplication::mouseEvent (const FMouseData& md)
{
  determineClickedWidget (md);

  if ( FWidget::getClickedWidget() )
  {
    unsetMoveSizeMode();
    closeDropDown (md);
    unselectMenubarItems (md);
    sendMouseEvent (md);
  }
}

//----------------------------------------------------------------------
inline void FApplication::sendEscapeKeyPressEvent() const
{
  // Send an escape key press event
  FKeyEvent k_press_ev (Event::KeyPress, FKey::Escape);
  sendEvent (keyboard_widget, &k_press_ev);
}

//----------------------------------------------------------------------
inline bool FApplication::sendKeyDownEvent (FWidget* widget) const
{
  // Send key down event
  const auto& keyboard = FKeyboard::getInstance();
  FKeyEvent k_down_ev (Event::KeyDown, keyboard.getKey());
  sendEvent (widget, &k_down_ev);
  return k_down_ev.isAccepted();
}

//----------------------------------------------------------------------
inline bool FApplication::sendKeyPressEvent (FWidget* widget) const
{
  // Send key press event
  const auto& keyboard = FKeyboard::getInstance();
  FKeyEvent k_press_ev (Event::KeyPress, keyboard.getKey());
  sendEvent (widget, &k_press_ev);
  return k_press_ev.isAccepted();
}

//----------------------------------------------------------------------
inline bool FApplication::sendKeyUpEvent (FWidget* widget) const
{
  // Send key up event
  const auto& keyboard = FKeyboard::getInstance();
  FKeyEvent k_up_ev (Event::KeyUp, keyboard.getKey());
  sendEvent (widget, &k_up_ev);
  return k_up_ev.isAccepted();
}

//----------------------------------------------------------------------
inline void FApplication::sendKeyboardAccelerator()
{
  if ( FWidget::getOpenMenu() )
    return;

  // Switch to a specific dialog with Meta + 1..9
  bool accpt = processDialogSwitchAccelerator();

  // Windows keyboard accelerator
  if ( ! accpt )
  {
    auto window = static_cast<const FWidget*>(getActiveWindow());

    if ( window )
      accpt = processAccelerator(*window);
  }

  // Global keyboard accelerator
  if ( ! accpt )
  {
    auto root_widget = getRootWidget();

    if ( root_widget )
      processAccelerator(*root_widget);
  }
}

//----------------------------------------------------------------------
inline bool FApplication::hasDataInQueue() const
{
  const auto& keyboard = FKeyboard::getInstance();
  const auto& mouse = FMouseControl::getInstance();

  if ( keyboard.hasDataInQueue()
    || mouse.hasDataInQueue()
    || FTerm::hasChangedTermSize() )
    return true;

  return false;
}

//----------------------------------------------------------------------
void FApplication::queuingKeyboardInput() const
{
  if ( quit_now
    || internal::var::exit_loop
    || FTerm::hasChangedTermSize() )
    return;

  findKeyboardWidget();
  auto& keyboard = FKeyboard::getInstance();
  keyboard.escapeKeyHandling();  // special case: Esc key
  keyboard.clearKeyBufferOnTimeout();

  if ( isKeyPressed() )
    keyboard.fetchKeyCode();
}

//----------------------------------------------------------------------
void FApplication::queuingMouseInput() const
{
  auto& mouse = FMouseControl::getInstance();

  if ( quit_now
    || internal::var::exit_loop
    || ! mouse.hasData()
    || FTerm::hasChangedTermSize() )
    return;

  auto& keyboard = FKeyboard::getInstance();
  auto time_keypressed = keyboard.getKeyPressedTime();
  mouse.processEvent (time_keypressed);
  keyboard.hasUnprocessedInput() = mouse.hasUnprocessedInput();
  mouse.clearEvent();
}

//----------------------------------------------------------------------
void FApplication::processKeyboardEvent() const
{
  if ( quit_now
    || internal::var::exit_loop
    || FTerm::hasChangedTermSize() )
    return;

  auto& keyboard = FKeyboard::getInstance();
  keyboard.processQueuedInput();
}

//----------------------------------------------------------------------
void FApplication::processMouseEvent() const
{
  if ( quit_now
    || internal::var::exit_loop
    || FTerm::hasChangedTermSize() )
    return;

  FMouseControl::getInstance().processQueuedInput();
}

//----------------------------------------------------------------------
bool FApplication::processDialogSwitchAccelerator() const
{
  const auto& keyboard = FKeyboard::getInstance();

  if ( keyboard.getKey() >= FKey::Meta_1
    && keyboard.getKey() <= FKey::Meta_9 )
  {
    const FKey key = keyboard.getKey();
    const auto n = std::size_t(key - FKey::Meta_0);
    const std::size_t s = getDialogList()->size();

    if ( s > 0 && s >= n )
    {
      // unset the move/size mode
      auto move_size = getMoveSizeWidget();

      if ( move_size )
      {
        auto w = move_size;
        setMoveSizeWidget(nullptr);
        w->redraw();
      }

      FAccelEvent a_ev (Event::Accelerator, getFocusWidget());
      sendEvent (getDialogList()->at(n - 1), &a_ev);
      return true;
    }
  }

  return false;
}

//----------------------------------------------------------------------
bool FApplication::processAccelerator (const FWidget& widget) const
{
  if ( widget.getAcceleratorList().empty() )
    return false;

  for (auto&& item : widget.getAcceleratorList())
  {
    if ( item.key == FKeyboard::getInstance().getKey() )
    {
      // unset the move/size mode
      auto move_size = getMoveSizeWidget();

      if ( move_size )
      {
        setMoveSizeWidget(nullptr);
        move_size->redraw();
      }

      FAccelEvent a_ev (Event::Accelerator, getFocusWidget());
      sendEvent (item.object, &a_ev);
      return a_ev.isAccepted();
    }

    if ( quit_now || internal::var::exit_loop )
      break;
  }

  return false;
}

//----------------------------------------------------------------------
void FApplication::determineClickedWidget (const FMouseData& md)
{
  clicked_widget = FWidget::getClickedWidget();

  if ( clicked_widget )
    return;  // The clicked widget was already found

  if ( ! md.isLeftButtonPressed()
    && ! md.isLeftButtonDoubleClick()
    && ! md.isRightButtonPressed()
    && ! md.isMiddleButtonPressed()
    && ! md.isWheelUp()
    && ! md.isWheelDown() )
    return;

  const auto& mouse_position = md.getPos();

  // Determine the window object on the current click position
  auto window = FWindow::getWindowWidgetAt (mouse_position);

  if ( window )
  {
    // Determine the widget at the current click position
    auto child = window->childWidgetAt(mouse_position);
    clicked_widget = ( child != nullptr ) ? child : window;
    setClickedWidget (clicked_widget);
  }
}

//----------------------------------------------------------------------
void FApplication::unsetMoveSizeMode() const
{
  // Unset the move/size mode

  auto& move_size = getMoveSizeWidget();

  if ( move_size )
  {
    FWidget* w{nullptr};
    std::swap(w, move_size);  // Clear move_size_widget
    w->redraw();
  }
}

//----------------------------------------------------------------------
void FApplication::closeDropDown (const FMouseData& md) const
{
  // Close the open menu

  if ( md.isMoved() )
    return;

  const auto& mouse_position = md.getPos();
  finalcut::closeDropDown (this, mouse_position);
}

//----------------------------------------------------------------------
void FApplication::unselectMenubarItems (const FMouseData& md) const
{
  // Unselect the menu bar items

  const auto& openmenu = FWidget::getOpenMenu();
  auto menu_bar = FWidget::getMenuBar();

  if ( openmenu || md.isMoved() )
    return;

  if ( ! (menu_bar && menu_bar->hasSelectedItem()) )
    return;

  const auto& mouse_position = md.getPos();

  if ( ! menu_bar->getTermGeometry().contains(mouse_position) )
  {
    if ( FWidget::getStatusBar() )
      FWidget::getStatusBar()->clearMessage();

    menu_bar->resetMenu();
    menu_bar->redraw();

    // No widget was been clicked
    if ( ! FWidget::getClickedWidget() )
      FWindow::switchToPrevWindow(this);

    if ( FWidget::getStatusBar() )
      FWidget::getStatusBar()->drawMessage();
  }
}

//----------------------------------------------------------------------
void FApplication::sendMouseEvent (const FMouseData& md) const
{
  const auto& mouse_position = md.getPos();
  MouseButton key_state{MouseButton::None};

  if ( md.isShiftKeyPressed() )
    key_state |= MouseButton::Shift;

  if ( md.isControlKeyPressed() )
    key_state |= MouseButton::Control;

  if ( md.isMetaKeyPressed() )
    key_state |= MouseButton::Meta;

  const auto& widgetMousePos = clicked_widget->termToWidgetPos(mouse_position);

  if ( md.isMoved() )
  {
    sendMouseMoveEvent (md, widgetMousePos, mouse_position, key_state);
  }
  else
  {
    sendMouseLeftClickEvent (md, widgetMousePos, mouse_position, key_state);
    sendMouseRightClickEvent (md, widgetMousePos, mouse_position, key_state);
    sendMouseMiddleClickEvent (md, widgetMousePos, mouse_position, key_state);
  }

  sendWheelEvent (md, widgetMousePos, mouse_position);
}

//----------------------------------------------------------------------
void FApplication::sendMouseMoveEvent ( const FMouseData& md
                                      , const FPoint& widgetMousePos
                                      , const FPoint& mouse_position
                                      , MouseButton key_state ) const
{
  if ( md.isLeftButtonPressed() )
  {
    FMouseEvent m_down_ev ( Event::MouseMove
                          , widgetMousePos
                          , mouse_position
                          , MouseButton::Left | key_state );
    sendEvent (clicked_widget, &m_down_ev);
  }

  if ( md.isRightButtonPressed() )
  {
    FMouseEvent m_down_ev ( Event::MouseMove
                          , widgetMousePos
                          , mouse_position
                          , MouseButton::Right | key_state );
    sendEvent (clicked_widget, &m_down_ev);
  }

  if ( md.isMiddleButtonPressed() )
  {
    FMouseEvent m_down_ev ( Event::MouseMove
                          , widgetMousePos
                          , mouse_position
                          , MouseButton::Middle | key_state );
    sendEvent (clicked_widget, &m_down_ev);
  }
}

//----------------------------------------------------------------------
void FApplication::sendMouseLeftClickEvent ( const FMouseData& md
                                           , const FPoint& widgetMousePos
                                           , const FPoint& mouse_position
                                           , MouseButton key_state ) const
{
  if ( md.isLeftButtonDoubleClick() )
  {
    FMouseEvent m_dblclick_ev ( Event::MouseDoubleClick
                              , widgetMousePos
                              , mouse_position
                              , MouseButton::Left | key_state );
    sendEvent (clicked_widget, &m_dblclick_ev);
  }
  else if ( md.isLeftButtonPressed() )
  {
    FMouseEvent m_down_ev ( Event::MouseDown
                          , widgetMousePos
                          , mouse_position
                          , MouseButton::Left | key_state );
    sendEvent (clicked_widget, &m_down_ev);
  }
  else if ( md.isLeftButtonReleased() )
  {
    FMouseEvent m_up_ev ( Event::MouseUp
                        , widgetMousePos
                        , mouse_position
                        , MouseButton::Left | key_state );
    auto released_widget = clicked_widget;

    if ( ! md.isRightButtonPressed()
      && ! md.isMiddleButtonPressed() )
      setClickedWidget(nullptr);

    sendEvent (released_widget, &m_up_ev);
  }
}

//----------------------------------------------------------------------
void FApplication::sendMouseRightClickEvent ( const FMouseData& md
                                            , const FPoint& widgetMousePos
                                            , const FPoint& mouse_position
                                            , MouseButton key_state ) const
{
  if ( md.isRightButtonPressed() )
  {
    FMouseEvent m_down_ev ( Event::MouseDown
                          , widgetMousePos
                          , mouse_position
                          , MouseButton::Right | key_state );
    sendEvent (clicked_widget, &m_down_ev);
  }
  else if ( md.isRightButtonReleased() )
  {
    FMouseEvent m_up_ev ( Event::MouseUp
                        , widgetMousePos
                        , mouse_position
                        , MouseButton::Right | key_state );
    auto released_widget = clicked_widget;

    if ( ! md.isLeftButtonPressed()
      && ! md.isMiddleButtonPressed() )
      setClickedWidget(nullptr);

    sendEvent (released_widget, &m_up_ev);
  }
}

//----------------------------------------------------------------------
void FApplication::sendMouseMiddleClickEvent ( const FMouseData& md
                                             , const FPoint& widgetMousePos
                                             , const FPoint& mouse_position
                                             , MouseButton key_state ) const
{
  if ( md.isMiddleButtonPressed() )
  {
    FMouseEvent m_down_ev ( Event::MouseDown
                          , widgetMousePos
                          , mouse_position
                          , MouseButton::Middle | key_state );
    sendEvent (clicked_widget, &m_down_ev);

    // gnome-terminal sends no released on middle click
    if ( FTerm::isGnomeTerminal() )
      setClickedWidget(nullptr);
  }
  else if ( md.isMiddleButtonReleased() )
  {
    FMouseEvent m_up_ev ( Event::MouseUp
                        , widgetMousePos
                        , mouse_position
                        , MouseButton::Middle | key_state );
    auto released_widget = clicked_widget;

    if ( ! md.isLeftButtonPressed()
      && ! md.isRightButtonPressed() )
    {
      setClickedWidget(nullptr);
    }

    sendEvent (released_widget, &m_up_ev);
  }
}

//----------------------------------------------------------------------
void FApplication::sendWheelEvent ( const FMouseData& md
                                  , const FPoint& widgetMousePos
                                  , const FPoint& mouse_position ) const
{
  if ( md.isWheelUp() )
  {
    FWheelEvent wheel_ev ( Event::MouseWheel
                         , widgetMousePos
                         , mouse_position
                         , MouseWheel::Up );
    auto scroll_over_widget = clicked_widget;
    setClickedWidget(nullptr);
    sendEvent(scroll_over_widget, &wheel_ev);
  }

  if ( md.isWheelDown() )
  {
    FWheelEvent wheel_ev ( Event::MouseWheel
                         , widgetMousePos
                         , mouse_position
                         , MouseWheel::Down );
    auto scroll_over_widget = clicked_widget;
    setClickedWidget(nullptr);
    sendEvent (scroll_over_widget, &wheel_ev);
  }
}

//----------------------------------------------------------------------
FWidget* FApplication::processParameters (const Args& args)
{
  if ( args.size() > 1 && (args[1] == "--help" || args[1] == "-h") )
  {
    showParameterUsage();
    FApplication::exit(EXIT_SUCCESS);
  }

  cmdOptions (args);
  return nullptr;
}

//----------------------------------------------------------------------
void FApplication::processResizeEvent() const
{
  if ( ! FTerm::hasChangedTermSize() )  // A SIGWINCH signal was received
    return;

  FTerm::detectTermSize();  // Detect and save the current terminal size
  auto& mouse = FMouseControl::getInstance();
  mouse.setMaxWidth (uInt16(getDesktopWidth()));
  mouse.setMaxHeight (uInt16(getDesktopHeight()));
  FResizeEvent r_ev(Event::Resize);
  sendEvent(internal::var::app_object, &r_ev);

  if ( r_ev.isAccepted() )
    FTerm::changeTermSizeFinished();
}

//----------------------------------------------------------------------
void FApplication::processCloseWidget()
{
  if ( ! getWidgetCloseList() || getWidgetCloseList()->empty() )
    return;

  setTerminalUpdates (FVTerm::TerminalUpdate::Stop);
  auto iter = getWidgetCloseList()->begin();

  while ( iter != getWidgetCloseList()->end() && *iter )
  {
    delete *iter;
    ++iter;
  }

  getWidgetCloseList()->clear();
  setTerminalUpdates (FVTerm::TerminalUpdate::Start);
}

//----------------------------------------------------------------------
void FApplication::processLogger() const
{
  // Synchronizing the stream buffer with the logging output

  auto logger = getLog();

  if ( ! logger->str().empty() )
    logger->pubsync();

  logger->flush();
}

//----------------------------------------------------------------------
bool FApplication::processNextEvent()
{
  uInt num_events{0};
  bool is_timeout = isNextEventTimeout();

  if ( is_timeout || hasDataInQueue() )
  {
    time_last_event = FObject::getCurrentTime();
    queuingKeyboardInput();
    queuingMouseInput();
    processKeyboardEvent();
    processMouseEvent();
    processResizeEvent();
    processCloseWidget();
    processTerminalUpdate();  // after terminal changes
    flush();
    processLogger();
  }

  processExternalUserEvent();

  if ( is_timeout )
  {
    sendQueuedEvents();
    num_events += processTimerEvent();
  }

  return ( num_events > 0 );
}

//----------------------------------------------------------------------
void FApplication::performTimerAction (FObject* receiver, FEvent* event)
{
  sendEvent (receiver, event);
}

//----------------------------------------------------------------------
bool FApplication::isEventProcessable ( FObject* receiver
                                      , const FEvent* event )
{
  if ( ! receiver->isWidget() )  // No restrictions for non-widgets
    return true;

  auto widget = static_cast<FWidget*>(receiver);

  if ( getModalDialogCounter() > 0 )
  {
    const FWidget* window;

    if ( widget->isWindowWidget() )
      window = widget;
    else
      window = FWindow::getWindowWidget(widget);

    // block events for widgets in non modal windows
    if ( window
      && ! window->getFlags().modal
      && ! window->isMenuWidget() )
    {
      constexpr std::array<const Event, 13> blocked_events
      {{
        Event::KeyPress,
        Event::KeyUp,
        Event::KeyDown,
        Event::MouseDown,
        Event::MouseUp,
        Event::MouseDoubleClick,
        Event::MouseWheel,
        Event::MouseMove,
        Event::FocusIn,
        Event::FocusOut,
        Event::ChildFocusIn,
        Event::ChildFocusOut,
        Event::Accelerator
      }};

      if ( std::any_of( blocked_events.cbegin()
                      , blocked_events.cend()
                      , [&event](const Event& ev)
                        {
                          return ev == event->getType();
                        } ) )
      {
        return false;
      }
    }
  }

  // Throw away mouse events for disabled widgets
  if ( event->getType() >= Event::MouseDown
    && event->getType() <= Event::MouseMove
    && ! widget->isEnabled() )
    return false;

  return true;
}

//----------------------------------------------------------------------
bool FApplication::isNextEventTimeout()
{
  return FObject::isTimeout (time_last_event, next_event_wait);
}


// FLog non-member operators
//----------------------------------------------------------------------
std::ostream& operator << (std::ostream& outstr, FLog::LogLevel l)
{
  *FApplication::getLog() << l;
  return outstr;
}

}  // namespace finalcut
