/*
 * Copyright (C) 2010 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * 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/>.
 *
 * Authored by: Gord Allott <gord.allott@canonical.com>
 */

#include "HudController.h"

#include <NuxCore/Logger.h>
#include <Nux/HLayout.h>
#include <UnityCore/Variant.h>

#include "unity-shared/AnimationUtils.h"
#include "unity-shared/ApplicationManager.h"
#include "unity-shared/WindowManager.h"
#include "unity-shared/PanelStyle.h"
#include "unity-shared/ThemeSettings.h"
#include "unity-shared/UBusMessages.h"
#include "unity-shared/UnitySettings.h"
#include "unity-shared/UScreen.h"

#include "MultiMonitor.h"

#include "config.h"


namespace unity
{
namespace hud
{
namespace
{
DECLARE_LOGGER(logger, "unity.hud.controller");
const unsigned FADE_DURATION = 90;
}

Controller::Controller(Controller::ViewCreator const& create_view,
                       Controller::WindowCreator const& create_window)
  : launcher_locked_out(false)
  , multiple_launchers(true)
  , hud_service_("com.canonical.hud", "/com/canonical/hud")
  , visible_(false)
  , need_show_(false)
  , view_(nullptr)
  , monitor_index_(0)
  , create_view_(create_view)
  , create_window_(create_window)
  , timeline_animator_(Settings::Instance().low_gfx() ? 0 : FADE_DURATION)
{
  LOG_DEBUG(logger) << "hud startup";

  // As a default, the create_view_ function should just create a view.
  if (create_view == nullptr)
  {
    create_view_ = [] {
      return new hud::View;
    };
  }

  // As a default. the create_window_ function should just create a base window.
  if (create_window_ == nullptr)
  {
    create_window_ = [this]() {
      return new ResizingBaseWindow("Hud",
                                    [this](nux::Geometry const& geo) {
                                      if (view_)
                                        return GetInputWindowGeometry();
                                      return geo;
                                    });
    };
  }

  SetupWindow();
  UScreen::GetDefault()->changed.connect(sigc::track_obj([this] (int, std::vector<nux::Geometry> const&) { Relayout(true); }, *this));

  ubus.RegisterInterest(UBUS_HUD_CLOSE_REQUEST, sigc::mem_fun(this, &Controller::OnExternalHideHud));

  //!!FIXME!! - just hijacks the dash close request so we get some more requests than normal,
  ubus.RegisterInterest(UBUS_OVERLAY_CLOSE_REQUEST, sigc::mem_fun(this, &Controller::OnExternalHideHud));

  ubus.RegisterInterest(UBUS_OVERLAY_SHOWN, [this] (GVariant *data) {
    unity::glib::String overlay_identity;
    gboolean can_maximise = FALSE;
    gint32 overlay_monitor = 0;
    int width, height;
    g_variant_get(data, UBUS_OVERLAY_FORMAT_STRING, &overlay_identity, &can_maximise, &overlay_monitor, &width, &height);

    if (overlay_identity.Str() != "hud")
    {
      HideHud();
    }
  });

  WindowManager& wm = WindowManager::Default();
  wm.screen_ungrabbed.connect(sigc::mem_fun(this, &Controller::OnScreenUngrabbed));
  wm.initiate_spread.connect(sigc::mem_fun(this, &Controller::HideHud));
  wm.screen_viewport_switch_started.connect(sigc::mem_fun(this, &Controller::HideHud));

  hud_service_.queries_updated.connect(sigc::mem_fun(this, &Controller::OnQueriesFinished));
  timeline_animator_.updated.connect(sigc::mem_fun(this, &Controller::OnViewShowHideFrame));

  Settings::Instance().dpi_changed.connect(sigc::mem_fun(this, &Controller::OnDPIChanged));
  Settings::Instance().launcher_position.changed.connect(sigc::hide(sigc::bind(sigc::mem_fun(this, &Controller::Relayout), false)));

  EnsureHud();
}

void Controller::SetupWindow()
{
  // Since BaseWindow is a View it is initially unowned.  This means that the first
  // reference that is taken grabs ownership of the pointer.  Since the smart pointer
  // references it, it becomes the owner, so no need to adopt the pointer here.
  window_ = create_window_();
  window_->SetBackgroundColor(nux::Color(0.0f, 0.0f, 0.0f, 0.0f));
  window_->SetConfigureNotifyCallback(&Controller::OnWindowConfigure, this);
  window_->ShowWindow(false);
  window_->SetOpacity(0.0f);
  window_->mouse_down_outside_pointer_grab_area.connect(
    sigc::mem_fun(this, &Controller::OnMouseDownOutsideWindow));

  if (nux::GetWindowThread()->IsEmbeddedWindow())
  {
    /* FIXME - first time we load our windows there is a race that causes the
     * input window not to actually get input, this side steps that by causing
     * an input window show and hide before we really need it. */
    WindowManager& wm = WindowManager::Default();
    wm.SaveInputFocus();
    window_->EnableInputWindow(true, "Hud", true, false);
    window_->EnableInputWindow(false, "Hud", true, false);
    wm.RestoreInputFocus();
  }
}

void Controller::SetupHudView()
{
  LOG_DEBUG(logger) << "SetupHudView called";
  view_ = create_view_();
  view_->scale = Settings::Instance().em(monitor_index_)->DPIScale();

  layout_ = new nux::VLayout(NUX_TRACKER_LOCATION);
  layout_->AddView(view_, 1, nux::MINOR_POSITION_START);
  window_->SetLayout(layout_);

  window_->UpdateInputWindowGeometry();

  view_->mouse_down_outside_pointer_grab_area.connect(sigc::mem_fun(this, &Controller::OnMouseDownOutsideWindow));

  LOG_DEBUG(logger) << "connecting to signals";
  view_->search_changed.connect(sigc::mem_fun(this, &Controller::OnSearchChanged));
  view_->search_activated.connect(sigc::mem_fun(this, &Controller::OnSearchActivated));
  view_->query_activated.connect(sigc::mem_fun(this, &Controller::OnQueryActivated));
  view_->query_selected.connect(sigc::mem_fun(this, &Controller::OnQuerySelected));
  view_->layout_changed.connect(sigc::bind(sigc::mem_fun(this, &Controller::Relayout), false));
  // Add to the debug introspection.
  AddChild(view_);
}

int Controller::GetIdealMonitor()
{
  int ideal_monitor;
  if (window_->IsVisible())
    ideal_monitor = monitor_index_;
  else
    ideal_monitor = UScreen::GetDefault()->GetMonitorWithMouse();
  return ideal_monitor;
}

bool Controller::IsLockedToLauncher(int monitor)
{
  if (launcher_locked_out && Settings::Instance().launcher_position() == LauncherPosition::LEFT)
  {
    int primary_monitor = UScreen::GetDefault()->GetPrimaryMonitor();

    if (multiple_launchers || (!multiple_launchers && primary_monitor == monitor))
    {
      return true;
    }
  }

  return false;
}

void Controller::EnsureHud()
{
  if (!window_)
  {
    LOG_DEBUG(logger) << "Initializing Hud Window";
    SetupWindow();
  }

  if (!view_)
  {
    LOG_DEBUG(logger) << "Initializing Hud View";
    SetupHudView();
    Relayout();
  }
}

void Controller::SetIcon(std::string const& icon_name)
{
  LOG_DEBUG(logger) << "setting icon to - " << icon_name;
  int launcher_size = unity::Settings::Instance().LauncherSize(monitor_index_);

  if (view_)
  {
    double scale = view_->scale();
    int tsize = tile_size().CP(scale);
    view_->SetIcon(icon_name, tsize, icon_size().CP(scale), launcher_size - tsize);
  }

  ubus.SendMessage(UBUS_HUD_ICON_CHANGED, g_variant_new_string(icon_name.c_str()));
}

nux::BaseWindow* Controller::window() const
{
  return window_.GetPointer();
}

nux::ObjectPtr<AbstractView> Controller::HudView() const
{
  return nux::ObjectPtr<AbstractView>(view_);
}

// We update the @geo that's sent in with our desired width and height
void Controller::OnWindowConfigure(int window_width, int window_height,
                                       nux::Geometry& geo, void* data)
{
  Controller* self = static_cast<Controller*>(data);
  geo = self->GetIdealWindowGeometry();
}

nux::Geometry Controller::GetIdealWindowGeometry()
{
  int ideal_monitor = GetIdealMonitor();
  auto monitor_geo = UScreen::GetDefault()->GetMonitorGeometry(ideal_monitor);

  panel::Style &panel_style = panel::Style::Instance();
  int panel_height = panel_style.PanelHeight(ideal_monitor);

  // We want to cover as much of the screen as possible to grab any mouse events
  // outside of our window
  nux::Geometry geo(monitor_geo.x,
                    monitor_geo.y + panel_height,
                    monitor_geo.width,
                    monitor_geo.height - panel_height);

  if (IsLockedToLauncher(ideal_monitor))
  {
    int launcher_width = unity::Settings::Instance().LauncherSize(ideal_monitor);
    geo.x += launcher_width;
    geo.width -= launcher_width;
  }

  return geo;
}

void Controller::Relayout(bool check_monitor)
{
  EnsureHud();

  if (check_monitor)
    monitor_index_ = CLAMP(GetIdealMonitor(), 0, static_cast<int>(UScreen::GetDefault()->GetMonitors().size()-1));

  nux::Geometry const& geo = GetIdealWindowGeometry();

  view_->QueueDraw();
  window_->SetGeometry(geo);
  panel::Style &panel_style = panel::Style::Instance();

  int horizontal_offset = 0;

  if (Settings::Instance().launcher_position() == LauncherPosition::LEFT)
    horizontal_offset = unity::Settings::Instance().LauncherSize(monitor_index_);

  view_->ShowEmbeddedIcon(!IsLockedToLauncher(monitor_index_));
  view_->SetMonitorOffset(horizontal_offset, panel_style.PanelHeight(monitor_index_));
}

void Controller::OnMouseDownOutsideWindow(int x, int y,
                                          unsigned long bflags, unsigned long kflags)
{
  LOG_DEBUG(logger) << "OnMouseDownOutsideWindow called";
  HideHud();
}

void Controller::OnScreenUngrabbed()
{
  LOG_DEBUG(logger) << "OnScreenUngrabbed called";
  if (need_show_)
  {
    nux::GetWindowCompositor().SetKeyFocusArea(view_->default_focus());

    window_->PushToFront();
    window_->SetInputFocus();
    EnsureHud();
    ShowHud();
  }
}

void Controller::OnExternalShowHud(GVariant* variant)
{
  EnsureHud();
  visible_ ? HideHud() : ShowHud();
}

void Controller::OnExternalHideHud(GVariant* variant)
{
  LOG_DEBUG(logger) << "External Hiding the hud";
  HideHud();
}

void Controller::ShowHideHud()
{
  EnsureHud();
  visible_ ? HideHud() : ShowHud();
}

void Controller::ReFocusKeyInput()
{
  if (visible_)
  {
    window_->PushToFront();
    window_->SetInputFocus();
  }
}

bool Controller::IsVisible()
{
  return visible_;
}

void Controller::ShowHud()
{
  WindowManager& wm = WindowManager::Default();
  LOG_DEBUG(logger) << "Showing the hud";
  EnsureHud();

  if (visible_ || wm.IsExpoActive() || wm.IsScaleActive())
    return;

  if (wm.IsScreenGrabbed())
  {
    need_show_ = true;
    return;
  }

  unsigned int ideal_monitor = GetIdealMonitor();

  if (ideal_monitor != monitor_index_)
  {
    Relayout();
    monitor_index_ = ideal_monitor;
    view_->scale = Settings::Instance().em(monitor_index_)->DPIScale();
  }

  view_->ShowEmbeddedIcon(!IsLockedToLauncher(monitor_index_));
  view_->AboutToShow();

  ApplicationManager& app_manager = ApplicationManager::Default();
  ApplicationPtr active_application;
  ApplicationWindowPtr active_window = app_manager.GetActiveWindow();
  if (active_window)
    active_application = active_window->application();

  if (active_application)
  {
    focused_app_icon_ = active_application->icon();
  }
  else
  {
    focused_app_icon_ = theme::Settings::Get()->ThemedFilePath("launcher_bfb", {PKGDATADIR});
  }

  wm.SaveInputFocus();

  LOG_DEBUG(logger) << "Taking application icon: " << focused_app_icon_;
  SetIcon(focused_app_icon_);
  FocusWindow();

  view_->ResetToDefault();
  need_show_ = true;
  visible_ = true;

  StartShowHideTimeline();

  // hide the launcher
  ubus.SendMessage(UBUS_LAUNCHER_LOCK_HIDE, glib::Variant(true));

  auto const& view_content_geometry = view_->GetContentGeometry();
  GVariant* info = g_variant_new(UBUS_OVERLAY_FORMAT_STRING, "hud", FALSE, monitor_index_,
                                 view_content_geometry.width, view_content_geometry.height);
  ubus.SendMessage(UBUS_OVERLAY_SHOWN, info);

  nux::GetWindowCompositor().SetKeyFocusArea(view_->default_focus());
  window_->SetEnterFocusInputArea(view_->default_focus());
}

void Controller::FocusWindow()
{
  window_->ShowWindow(true);
  window_->PushToFront();

  if (nux::GetWindowThread()->IsEmbeddedWindow())
  {
    window_->EnableInputWindow(true, "Hud", true, false);
    window_->UpdateInputWindowGeometry();
  }

  window_->SetInputFocus();
  window_->QueueDraw();
}

void Controller::HideHud()
{
  LOG_DEBUG (logger) << "hiding the hud";
  if (!visible_)
    return;

  need_show_ = false;
  EnsureHud();
  view_->AboutToHide();
  view_->ShowEmbeddedIcon(false);
  window_->CaptureMouseDownAnyWhereElse(false);
  window_->EnableInputWindow(false, "Hud", true, false);
  visible_ = false;

  auto& wc = nux::GetWindowCompositor();
  auto *key_focus_area = wc.GetKeyFocusArea();
  if (key_focus_area && key_focus_area->IsChildOf(view_))
    wc.SetKeyFocusArea(nullptr, nux::KEY_NAV_NONE);

  WindowManager::Default().RestoreInputFocus();

  StartShowHideTimeline();
  hud_service_.CloseQuery();

  //unhide the launcher
  ubus.SendMessage(UBUS_LAUNCHER_LOCK_HIDE, glib::Variant(false));

  auto const& view_content_geometry = view_->GetContentGeometry();
  GVariant* info = g_variant_new(UBUS_OVERLAY_FORMAT_STRING, "hud", FALSE, monitor_index_,
                                 view_content_geometry.width, view_content_geometry.height);
  ubus.SendMessage(UBUS_OVERLAY_HIDDEN, info);
}

void Controller::StartShowHideTimeline()
{
  EnsureHud();
  animation::StartOrReverseIf(timeline_animator_, visible_);
}

void Controller::OnViewShowHideFrame(double opacity)
{
  window_->SetOpacity(opacity);

  if (opacity == 0.0f && !visible_)
  {
    window_->ShowWindow(false);
  }
  else if (opacity == 1.0f && visible_)
  {
    // ensure the text entry is focused
    nux::GetWindowCompositor().SetKeyFocusArea(view_->default_focus());
  }
}

void Controller::OnActivateRequest(GVariant* variant)
{
  EnsureHud();
  ShowHud();
}

void Controller::OnSearchChanged(std::string search_string)
{
  // we're using live_search_reached, so this is called 40ms after the text
  // is input in the search bar
  LOG_DEBUG(logger) << "Search Changed";

  last_search_ = search_string;
  hud_service_.RequestQuery(last_search_);
}

void Controller::OnSearchActivated(std::string search_string)
{
  unsigned int timestamp = nux::GetGraphicsDisplay()->GetCurrentEvent().x11_timestamp;
  hud_service_.ExecuteQueryBySearch(search_string, timestamp);
  ubus.SendMessage(UBUS_HUD_CLOSE_REQUEST);
}

void Controller::OnQueryActivated(Query::Ptr query)
{
  LOG_DEBUG(logger) << "Activating query, " << query->formatted_text;
  unsigned int timestamp = nux::GetGraphicsDisplay()->GetCurrentEvent().x11_timestamp;
  hud_service_.ExecuteQuery(query, timestamp);
  ubus.SendMessage(UBUS_HUD_CLOSE_REQUEST);
}

void Controller::OnQuerySelected(Query::Ptr query)
{
  LOG_DEBUG(logger) << "Selected query, " << query->formatted_text;
  SetIcon(query->icon_name);
}

void Controller::OnQueriesFinished(Hud::Queries queries)
{
  view_->SetQueries(queries);
  std::string icon_name = focused_app_icon_;
  for (auto query = queries.begin(); query != queries.end(); query++)
  {
    if (!(*query)->icon_name.empty())
    {
      icon_name = (*query)->icon_name;
      break;
    }
  }

  SetIcon(icon_name);
  view_->SearchFinished();
}

void Controller::OnDPIChanged()
{
  if (view_)
    view_->scale = Settings::Instance().em(monitor_index_)->DPIScale();
}

// Introspectable
std::string Controller::GetName() const
{
  return "HudController";
}

void Controller::AddProperties(debug::IntrospectionData& introspection)
{
  introspection
    .add(window_ ? window_->GetGeometry() : nux::Geometry())
    .add("ideal_monitor", GetIdealMonitor())
    .add("visible", visible_)
    .add("hud_monitor", monitor_index_)
    .add("locked_to_launcher", IsLockedToLauncher(monitor_index_));
}

nux::Geometry Controller::GetInputWindowGeometry()
{
  EnsureHud();
  nux::Geometry const& window_geo(window_->GetGeometry());
  nux::Geometry const& view_content_geo(view_->GetContentGeometry());
  return nux::Geometry(window_geo.x, window_geo.y, view_content_geo.width, view_content_geo.height);
}



}
}
