#include "flutter_window.h"
#include "utils.h"
#include "file_info.h"
#include "file_icon.h"

#include <optional>

#include "flutter/generated_plugin_registrant.h"

void trayMessageHandler(TrayIcon *, UINT);
HICON getIconHandle(std::string);
const flutter::EncodableValue* ValueOrNull(const flutter::EncodableMap& map, const char* key);

static std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> appChannel;
static std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> trayChannel;

FlutterWindow::FlutterWindow(const flutter::DartProject& project)
    : project_(project) {}

FlutterWindow::~FlutterWindow() {}

bool FlutterWindow::OnCreate() {
  if (!Win32Window::OnCreate()) {
    return false;
  }

  RECT frame = GetClientArea();

  // The size here must match the window dimensions to avoid unnecessary surface
  // creation / destruction in the startup path.
  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
      frame.right - frame.left, frame.bottom - frame.top, project_);
  // Ensure that basic setup of the controller was successful.
  if (!flutter_controller_->engine() || !flutter_controller_->view()) {
    return false;
  }
  RegisterPlugins(flutter_controller_->engine());
  SetChildContent(flutter_controller_->view()->GetNativeWindow());

  // register channel
  registerMethodChannel(flutter_controller_->engine());

  // setup trayicon
  m_tray.setUserData(this);
  m_tray.setListener(trayMessageHandler);

  flutter_controller_->engine()->SetNextFrameCallback([&]() {
    this->Show();
  });

  // Flutter can complete the first frame before the "show window" callback is
  // registered. The following call ensures a frame is pending to ensure the
  // window is shown. It is a no-op if the first frame hasn't completed yet.
  flutter_controller_->ForceRedraw();

  return true;
}

void FlutterWindow::OnDestroy() {
	m_tray.setVisible(false);

  if (flutter_controller_) {
    flutter_controller_ = nullptr;
  }

  Win32Window::OnDestroy();
}

void FlutterWindow::SetMinimumSize(SIZE size) {
  min_window_size_ = size;
}

LRESULT
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
                              WPARAM const wparam,
                              LPARAM const lparam) noexcept {
  // my custom message handler
  std::optional<LRESULT> r = handleMessage(hwnd, message, wparam, lparam);
  if (r) {    
    return *r;
  }

  // Give Flutter, including plugins, an opportunity to handle window messages.
  if (flutter_controller_) {
    std::optional<LRESULT> result =
        flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
                                                      lparam);
    if (result) {
      return *result;
    }
  }

  switch (message) {
    case WM_FONTCHANGE:
      flutter_controller_->engine()->ReloadSystemFonts();
      break;
    case WM_GETMINMAXINFO:
      if (min_window_size_.cx > 0 && min_window_size_.cy > 0) {
        LPMINMAXINFO info = reinterpret_cast<LPMINMAXINFO>(lparam);
        info->ptMinTrackSize.x = min_window_size_.cx;
        info->ptMinTrackSize.y = min_window_size_.cy;
      }
      break;
  }

  return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
}

void FlutterWindow::registerMethodChannel(flutter::FlutterEngine* engine)
{
  appChannel = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
    engine->messenger(), "app.flashlink/app",
    &flutter::StandardMethodCodec::GetInstance());
  appChannel->SetMethodCallHandler([this](const auto& call, auto result) {
    handleAppMethodCall(call, std::move(result));
  });

  trayChannel = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
    engine->messenger(), "app.flashlink/tray",
    &flutter::StandardMethodCodec::GetInstance());
  trayChannel->SetMethodCallHandler([this](const auto& call, auto result) {
    handleTrayMethodCall(call, std::move(result));
  });
}

void FlutterWindow::handleAppMethodCall(
  const flutter::MethodCall<flutter::EncodableValue>& method_call,
  std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
  if (method_call.method_name().compare("activeWindow") == 0) {
    onActiveWindow();
    result->Success(flutter::EncodableValue(true));
    //
  } else if (method_call.method_name().compare("hideWindow") == 0) {
    onHideWindow();
    result->Success(flutter::EncodableValue(true));
    //
  } else if (method_call.method_name().compare("toggleWindow") == 0) {
    onToggleWindow();
    result->Success(flutter::EncodableValue(true));
    //
  } else if (method_call.method_name().compare("destroyWindow") == 0) {
    Destroy();
    result->Success(flutter::EncodableValue(true));
    //
  } else if (method_call.method_name().compare("extractFileIcon") == 0) {
    extractFileIcon(method_call, std::move(result));
    //
  } else if (method_call.method_name().compare("getFileDescription") == 0) {
    getFileDescription(method_call, std::move(result));
    //
  } else {
    result->NotImplemented();
  }
}

void FlutterWindow::handleTrayMethodCall(
  const flutter::MethodCall<flutter::EncodableValue>& method_call,
  std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
  if (method_call.method_name().compare("setSystemTray") == 0) {
    setSystemTray(method_call, std::move(result));
    //
  } else if (method_call.method_name().compare("resetSystemTray") == 0) {
    resetSystemTray(method_call, std::move(result));
    //
  } else if (method_call.method_name().compare("setContextMenu") == 0) {
    setContextMenu(method_call, std::move(result));
    //
  } else if (method_call.method_name().compare("popupContextMenu") == 0) {
    popUpContextMenu(method_call, std::move(result));
    //
  } else {
    result->NotImplemented();
  }
}

std::optional<LRESULT> FlutterWindow::handleMessage(HWND window, UINT const message, 
  WPARAM const wparam, LPARAM const lparam)
{
  if (message == WM_CLOSE) {
    if (m_tray.isVisible()) {
      onHideWindow();
    } else {
      Destroy();
    }    
    return std::make_optional<LRESULT>(0);
  }

  if (message == WM_COMMAND) {
    flutter::EncodableMap eventData = flutter::EncodableMap();
    eventData[flutter::EncodableValue("id")] = flutter::EncodableValue((int)wparam);
    trayChannel->InvokeMethod("onTrayMenuItemClick",
                          std::make_unique<flutter::EncodableValue>(eventData));
  }

  return std::nullopt;
}

void FlutterWindow::onActiveWindow()
{
  bool isVisible = (::GetWindowLong(GetHandle(), GWL_STYLE) & WS_VISIBLE) != 0;
  if (isVisible) {
    WINDOWPLACEMENT windowPlacement;
    GetWindowPlacement(GetHandle(), &windowPlacement);
    if (windowPlacement.showCmd == SW_SHOWMINIMIZED) {
      PostMessage(GetHandle(), WM_SYSCOMMAND, SC_RESTORE, 0);
    }
  } else {
    onShowWindow();
  }
}

void FlutterWindow::onToggleWindow()
{
  bool isVisible = (::GetWindowLong(GetHandle(), GWL_STYLE) & WS_VISIBLE) != 0;
  if (isVisible) {
    WINDOWPLACEMENT windowPlacement;
    GetWindowPlacement(GetHandle(), &windowPlacement);
    if (windowPlacement.showCmd == SW_SHOWMINIMIZED) {
      PostMessage(GetHandle(), WM_SYSCOMMAND, SC_RESTORE, 0);
    } else {
      onHideWindow();
    }
  } else {
    onShowWindow();
  }
}

void FlutterWindow::onShowWindow()
{
  ShowWindow(GetHandle(), SW_SHOW);
  SetForegroundWindow(GetHandle());
  auto args = std::make_unique<flutter::EncodableValue>(nullptr);
  appChannel->InvokeMethod("onWindowShow", std::move(args));
}

void FlutterWindow::onHideWindow()
{
  ShowWindow(GetHandle(), SW_HIDE);
  auto args = std::make_unique<flutter::EncodableValue>(nullptr);
  appChannel->InvokeMethod("onWindowHide", std::move(args));
}

void FlutterWindow::onTrayIconLButtonDown()
{
  auto args = std::make_unique<flutter::EncodableValue>(nullptr);
  trayChannel->InvokeMethod("onTrayIconLButtonDown", std::move(args));
}

void FlutterWindow::onTrayIconLButtonUp()
{
  auto args = std::make_unique<flutter::EncodableValue>(nullptr);
  trayChannel->InvokeMethod("onTrayIconLButtonUp", std::move(args));
}

void FlutterWindow::onTrayIconRButtonDown()
{
  auto args = std::make_unique<flutter::EncodableValue>(nullptr);
  trayChannel->InvokeMethod("onTrayIconRButtonDown", std::move(args));
}

void FlutterWindow::onTrayIconRButtonUp()
{
  auto args = std::make_unique<flutter::EncodableValue>(nullptr);
  trayChannel->InvokeMethod("onTrayIconRButtonUp", std::move(args));
}

void FlutterWindow::setSystemTray(
  const flutter::MethodCall<flutter::EncodableValue>& method_call,
  std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
    const flutter::EncodableMap& args =
      std::get<flutter::EncodableMap>(*method_call.arguments());
    std::string tooltip = std::get<std::string>(args.at(flutter::EncodableValue("tooltip")));
    std::string icon = std::get<std::string>(args.at(flutter::EncodableValue("icon")));

    m_tray.setName((LPCWSTR)(Utf16FromUtf8(tooltip.c_str()).c_str()));
    m_tray.setIcon(getIconHandle(icon));
    m_tray.setVisible(true);
    result->Success(flutter::EncodableValue(true));
}

void FlutterWindow::resetSystemTray(
  const flutter::MethodCall<flutter::EncodableValue>& method_call,
  std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
  m_tray.setVisible(false);
  result->Success(flutter::EncodableValue(true));
}

void FlutterWindow::setContextMenu(
    const flutter::MethodCall<flutter::EncodableValue>& method_call,
    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
  const flutter::EncodableMap& args =
    std::get<flutter::EncodableMap>(*method_call.arguments());

  m_menuHandle = CreatePopupMenu();

  createMenu(m_menuHandle, std::get<flutter::EncodableMap>(
    args.at(flutter::EncodableValue("menu"))));
  
  result->Success(flutter::EncodableValue(true));
}

void FlutterWindow::popUpContextMenu(
    const flutter::MethodCall<flutter::EncodableValue>& method_call,
    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
	POINT pt;
	if (GetCursorPos(&pt)) {
    ::SetForegroundWindow(GetHandle());
		TrackPopupMenu(m_menuHandle, TPM_BOTTOMALIGN | TPM_LEFTALIGN, 
      pt.x, pt.y, 0, GetHandle(), NULL);
	}
  result->Success(flutter::EncodableValue(true));
}

void FlutterWindow::extractFileIcon(
  const flutter::MethodCall<flutter::EncodableValue>& method_call,
  std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
  const flutter::EncodableMap& args =
    std::get<flutter::EncodableMap>(*method_call.arguments());
  std::string fileName = std::get<std::string>(args.at(flutter::EncodableValue("fileName")));
  std::string iconName = std::get<std::string>(args.at(flutter::EncodableValue("iconName")));

  auto ret = SaveIcon(fileName, iconName, 32, 0);
  result->Success(flutter::EncodableValue(ret));
}

void FlutterWindow::getFileDescription(
  const flutter::MethodCall<flutter::EncodableValue>& method_call,
  std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
  const flutter::EncodableMap& args =
    std::get<flutter::EncodableMap>(*method_call.arguments());
  std::wstring fileName = Utf16FromUtf8(std::get<std::string>(args.at(flutter::EncodableValue("fileName"))).c_str());
  std::wstring description;
  FileInfoUtils::GetFileDescription(fileName, description);

  std::wcout << fileName << " " << description << std::endl;

  result->Success(flutter::EncodableValue(utf8_encode(description)));
}

void FlutterWindow::createMenu(HMENU menu, flutter::EncodableMap args) {
  flutter::EncodableList items = std::get<flutter::EncodableList>(
      args.at(flutter::EncodableValue("items")));

  int count = GetMenuItemCount(menu);
  for (int i = 0; i < count; i++) {
    // always remove at 0 because they shift every time
    RemoveMenu(menu, 0, MF_BYPOSITION);
  }

  for (flutter::EncodableValue item_value : items) {
    flutter::EncodableMap item_map =
        std::get<flutter::EncodableMap>(item_value);
    int id = std::get<int>(item_map.at(flutter::EncodableValue("id")));
    std::string type =
        std::get<std::string>(item_map.at(flutter::EncodableValue("type")));
    std::string label =
        std::get<std::string>(item_map.at(flutter::EncodableValue("label")));
    auto* checked = std::get_if<bool>(ValueOrNull(item_map, "checked"));
    bool disabled =
        std::get<bool>(item_map.at(flutter::EncodableValue("disabled")));

    UINT_PTR item_id = id;
    UINT uFlags = MF_STRING;

    if (disabled) {
      uFlags |= MF_GRAYED;
    }

    if (type.compare("separator") == 0) {
      AppendMenuW(menu, MF_SEPARATOR, item_id, NULL);
    } else {
      if (type.compare("checkbox") == 0) {
        if (checked == nullptr) {
          // skip
        } else {
          uFlags |= (*checked == true ? MF_CHECKED : MF_UNCHECKED);
        }
      } else if (type.compare("submenu") == 0) {
        uFlags |= MF_POPUP;
        HMENU sub_menu = ::CreatePopupMenu();
        createMenu(sub_menu, std::get<flutter::EncodableMap>(item_map.at(
                                  flutter::EncodableValue("submenu"))));
        item_id = reinterpret_cast<UINT_PTR>(sub_menu);
      }
      AppendMenuW(menu, uFlags, item_id, (LPCWSTR)(Utf16FromUtf8(label.c_str()).c_str()));
    }
  }
}

void trayMessageHandler(TrayIcon *pTrayIcon, UINT uMsg)
{
	switch (uMsg) {
  case WM_LBUTTONDOWN: 
    ((FlutterWindow *)pTrayIcon->getUserData())->onTrayIconLButtonDown();
    break;
  case WM_LBUTTONUP: 
    ((FlutterWindow *)pTrayIcon->getUserData())->onTrayIconLButtonUp();
    break;
	case WM_RBUTTONDOWN:
    ((FlutterWindow *)pTrayIcon->getUserData())->onTrayIconRButtonDown();
    break;
  case WM_RBUTTONUP:
    ((FlutterWindow *)pTrayIcon->getUserData())->onTrayIconRButtonUp();
    break;
	}
}

HICON getIconHandle(std::string iconPath)
{
  return static_cast<HICON>(LoadImage(NULL, (LPCWSTR)(Utf16FromUtf8(iconPath.c_str()).c_str()),
		IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE | LR_SHARED));
}

const flutter::EncodableValue* ValueOrNull(const flutter::EncodableMap& map, const char* key)
{
  auto it = map.find(flutter::EncodableValue(key));
  if (it == map.end()) {
    return nullptr;
  }
  return &(it->second);
}