// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "windows_lifecycle_manager.h"

#include <TlHelp32.h>
#include <WinUser.h>
#include <Windows.h>
#include <tchar.h>

#include "flutter/shell/platform/windows/flutter_windows_engine.h"

namespace flutter {

WindowsLifecycleManager::WindowsLifecycleManager(FlutterWindowsEngine* engine)
    : engine_(engine), process_close_(false) {}

WindowsLifecycleManager::~WindowsLifecycleManager() {}

void WindowsLifecycleManager::Quit(std::optional<HWND> hwnd,
                                   std::optional<WPARAM> wparam,
                                   std::optional<LPARAM> lparam,
                                   UINT exit_code) {
  if (!hwnd.has_value()) {
    ::PostQuitMessage(exit_code);
  } else {
    BASE_CHECK(wparam.has_value() && lparam.has_value());
    sent_close_messages_[std::make_tuple(*hwnd, *wparam, *lparam)]++;
    DispatchMessage(*hwnd, WM_CLOSE, *wparam, *lparam);
  }
}

void WindowsLifecycleManager::DispatchMessage(HWND hwnd,
                                              UINT message,
                                              WPARAM wparam,
                                              LPARAM lparam) {
  PostMessage(hwnd, message, wparam, lparam);
}

bool WindowsLifecycleManager::WindowProc(HWND hwnd,
                                         UINT msg,
                                         WPARAM wpar,
                                         LPARAM lpar,
                                         LRESULT* result) {
  switch (msg) {
    // When WM_CLOSE is received from the final window of an application, we
    // send a request to the framework to see if the app should exit. If it
    // is, we re-dispatch a new WM_CLOSE message. In order to allow the new
    // message to reach other delegates, we ignore it here.
    case WM_CLOSE: {
      if (!process_close_) {
        return false;
      }
      auto key = std::make_tuple(hwnd, wpar, lpar);
      auto itr = sent_close_messages_.find(key);
      if (itr != sent_close_messages_.end()) {
        if (itr->second == 1) {
          sent_close_messages_.erase(itr);
        } else {
          sent_close_messages_[key]--;
        }
        return false;
      }
      if (IsLastWindowOfProcess()) {
        engine_->RequestApplicationQuit(hwnd, wpar, lpar,
                                        AppExitType::cancelable);
        return true;
      }
      break;
    }

    // DWM composition can be disabled on Windows 7.
    // Notify the engine as this can result in screen tearing.
    case WM_DWMCOMPOSITIONCHANGED:
      engine_->OnDwmCompositionChanged();
      break;

    case WM_SIZE:
      if (wpar == SIZE_MAXIMIZED || wpar == SIZE_RESTORED) {
        OnWindowStateEvent(hwnd, WindowStateEvent::kShow);
      } else if (wpar == SIZE_MINIMIZED) {
        OnWindowStateEvent(hwnd, WindowStateEvent::kHide);
      }
      break;

    case WM_SHOWWINDOW:
      if (!wpar) {
        OnWindowStateEvent(hwnd, WindowStateEvent::kHide);
      } else {
        OnWindowStateEvent(hwnd, WindowStateEvent::kShow);
      }
      break;

    case WM_DESTROY:
      OnWindowStateEvent(hwnd, WindowStateEvent::kHide);
      break;
  }
  return false;
}

class ThreadSnapshot {
 public:
  ThreadSnapshot() {
    thread_snapshot_ = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
  }
  ~ThreadSnapshot() {
    if (thread_snapshot_ != INVALID_HANDLE_VALUE) {
      ::CloseHandle(thread_snapshot_);
    }
  }

  std::optional<THREADENTRY32> GetFirstThread() {
    if (thread_snapshot_ == INVALID_HANDLE_VALUE) {
      FML_LOG(ERROR) << "Failed to get thread snapshot";
      return std::nullopt;
    }
    THREADENTRY32 thread;
    thread.dwSize = sizeof(thread);
    if (!::Thread32First(thread_snapshot_, &thread)) {
      DWORD error_num = ::GetLastError();
      char msg[256];
      ::FormatMessageA(
          FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL,
          error_num, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), msg, 256,
          nullptr);
      FML_LOG(ERROR) << "Failed to get thread(" << error_num << "): " << msg;
      return std::nullopt;
    }
    return thread;
  }

  bool GetNextThread(THREADENTRY32& thread) {
    if (thread_snapshot_ == INVALID_HANDLE_VALUE) {
      return false;
    }
    return ::Thread32Next(thread_snapshot_, &thread);
  }

 private:
  HANDLE thread_snapshot_;
};

static int64_t NumWindowsForThread(const THREADENTRY32& thread) {
  int64_t num_windows = 0;
  ::EnumThreadWindows(
      thread.th32ThreadID,
      [](HWND hwnd, LPARAM lparam) {
        int64_t* windows_ptr = reinterpret_cast<int64_t*>(lparam);
        if (::GetParent(hwnd) == nullptr) {
          (*windows_ptr)++;
        }
        return *windows_ptr <= 1 ? TRUE : FALSE;
      },
      reinterpret_cast<LPARAM>(&num_windows));
  return num_windows;
}

bool WindowsLifecycleManager::IsLastWindowOfProcess() {
  DWORD pid = ::GetCurrentProcessId();
  ThreadSnapshot thread_snapshot;
  std::optional<THREADENTRY32> first_thread = thread_snapshot.GetFirstThread();
  if (!first_thread.has_value()) {
    FML_LOG(ERROR) << "No first thread found";
    return true;
  }

  int num_windows = 0;
  THREADENTRY32 thread = *first_thread;
  do {
    if (thread.th32OwnerProcessID == pid) {
      num_windows += NumWindowsForThread(thread);
      if (num_windows > 1) {
        return false;
      }
    }
  } while (thread_snapshot.GetNextThread(thread));

  return num_windows <= 1;
}

void WindowsLifecycleManager::BeginProcessingClose() {
  process_close_ = true;
}

// TODO(schectman): Wait until the platform channel is registered to send
// the platform message.
// https://github.com/flutter/flutter/issues/131616
void WindowsLifecycleManager::SetLifecycleState(AppLifecycleState state) {
  if (state_ == state) {
    return;
  }
  state_ = state;
  if (engine_) {
    const char* state_name = AppLifecycleStateToString(state);
    engine_->SendPlatformMessage("flutter/lifecycle",
                                 reinterpret_cast<const uint8_t*>(state_name),
                                 strlen(state_name), nullptr, nullptr);
  }
}

void WindowsLifecycleManager::OnWindowStateEvent(HWND hwnd,
                                                 WindowStateEvent event) {
  // Synthesize an unfocus event when a focused window is hidden.
  if (event == WindowStateEvent::kHide &&
      focused_windows_.find(hwnd) != focused_windows_.end()) {
    OnWindowStateEvent(hwnd, WindowStateEvent::kUnfocus);
  }

  std::lock_guard guard(state_update_lock_);
  switch (event) {
    case WindowStateEvent::kShow: {
      bool first_shown_window = visible_windows_.empty();
      auto pair = visible_windows_.insert(hwnd);
      if (first_shown_window && pair.second &&
          state_ == AppLifecycleState::kHidden) {
        SetLifecycleState(AppLifecycleState::kInactive);
      }
      break;
    }
    case WindowStateEvent::kHide: {
      bool present = visible_windows_.erase(hwnd);
      bool empty = visible_windows_.empty();
      if (present && empty &&
          (state_ == AppLifecycleState::kResumed ||
           state_ == AppLifecycleState::kInactive)) {
        SetLifecycleState(AppLifecycleState::kHidden);
      }
      break;
    }
    case WindowStateEvent::kFocus: {
      bool first_focused_window = focused_windows_.empty();
      auto pair = focused_windows_.insert(hwnd);
      if (first_focused_window && pair.second &&
          state_ == AppLifecycleState::kInactive) {
        SetLifecycleState(AppLifecycleState::kResumed);
      }
      break;
    }
    case WindowStateEvent::kUnfocus: {
      if (focused_windows_.erase(hwnd) && focused_windows_.empty() &&
          state_ == AppLifecycleState::kResumed) {
        SetLifecycleState(AppLifecycleState::kInactive);
      }
      break;
    }
  }
}

std::optional<LRESULT> WindowsLifecycleManager::ExternalWindowMessage(
    HWND hwnd,
    UINT message,
    WPARAM wparam,
    LPARAM lparam) {
  std::optional<flutter::WindowStateEvent> event = std::nullopt;

  // TODO (schectman): Handle WM_CLOSE messages.
  // https://github.com/flutter/flutter/issues/131497
  switch (message) {
    case WM_SHOWWINDOW:
      event = wparam ? flutter::WindowStateEvent::kShow
                     : flutter::WindowStateEvent::kHide;
      break;
    case WM_SIZE:
      switch (wparam) {
        case SIZE_MINIMIZED:
          event = flutter::WindowStateEvent::kHide;
          break;
        case SIZE_RESTORED:
        case SIZE_MAXIMIZED:
          event = flutter::WindowStateEvent::kShow;
          break;
      }
      break;
    case WM_SETFOCUS:
      event = flutter::WindowStateEvent::kFocus;
      break;
    case WM_KILLFOCUS:
      event = flutter::WindowStateEvent::kUnfocus;
      break;
    case WM_DESTROY:
      event = flutter::WindowStateEvent::kHide;
      break;
  }

  if (event.has_value()) {
    OnWindowStateEvent(hwnd, *event);
  }

  return std::nullopt;
}

}  // namespace flutter
