// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/devtools/protocol/browser_handler_android.h"

#include <set>
#include <vector>

#include "base/functional/bind.h"
#include "chrome/browser/android/devtools_manager_delegate_android.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "ui/base/base_window.h"
#include "ui/gfx/geometry/rect.h"

using protocol::Response;

namespace {
static constexpr char kNotImplemented[] = "Not implemented";

struct ResolvedWindowState {
  std::string name;
  bool is_minimized;
};

ResolvedWindowState ResolveWindowState(ui::BaseWindow* window) {
  bool is_minimized = window->IsMinimized();
  bool is_maximized = window->IsMaximized();

  // A backgrounded fixed-size Android task is not minimized in the CDP sense.
  if (is_minimized) {
    ui::WindowResizePrecheckResult resize_precheck_result;
    if (!window->CanResize(resize_precheck_result) &&
        resize_precheck_result !=
            ui::WindowResizePrecheckResult::kAndroidNoActivity) {
      is_minimized = false;
      is_maximized = true;
    }
  }

  return {
      BrowserHandlerAndroid::ComputeWindowStateString(
          window->IsFullscreen(), is_maximized, is_minimized),
      is_minimized,
  };
}

// CDP window-state mutations are only observable for resizable Android tasks.
// Fixed-size tasks continue to report maximized even when backgrounded.
Response CheckCanChangeWindow(ui::BaseWindow* window, bool is_pending) {
  ui::WindowResizePrecheckResult resize_precheck_result;
  if (window->CanResize(resize_precheck_result) ||
      (is_pending && resize_precheck_result ==
                         ui::WindowResizePrecheckResult::kAndroidNoActivity)) {
    // A DevTools-created Android task can be addressed before its Activity is
    // attached. ChromeAndroidTask queues state and bounds changes in this case.
    return Response::Success();
  }
  return Response::ServerError(
      "Window state or bounds cannot be changed in the current Android "
      "configuration");
}
}  // namespace

// static
ui::BaseWindow* BrowserHandlerAndroid::FindBrowserWindowById(int window_id) {
  for (BrowserWindowInterface* bwi : GetAllBrowserWindowInterfaces()) {
    if (bwi->GetSessionID().id() == window_id) {
      return bwi->GetWindow();
    }
  }
  return nullptr;
}

// static
std::unique_ptr<protocol::Browser::Bounds>
BrowserHandlerAndroid::BuildBrowserWindowBounds(ui::BaseWindow* window) {
  ResolvedWindowState window_state = ResolveWindowState(window);

  const gfx::Rect bounds = window_state.is_minimized
                               ? window->GetRestoredBounds()
                               : window->GetBounds();

  return protocol::Browser::Bounds::Create()
      .SetLeft(bounds.x())
      .SetTop(bounds.y())
      .SetWidth(bounds.width())
      .SetHeight(bounds.height())
      .SetWindowState(window_state.name)
      .Build();
}

BrowserHandlerAndroid::BrowserHandlerAndroid(
    protocol::UberDispatcher* dispatcher,
    const std::string& target_id)
    : target_id_(target_id) {
  CHECK(dispatcher);
  protocol::Browser::Dispatcher::wire(dispatcher, this);
}

BrowserHandlerAndroid::~BrowserHandlerAndroid() = default;

void BrowserHandlerAndroid::TrackBrowserWindow(
    BrowserWindowInterface* browser_window) {
  CHECK(browser_window);
  const SessionID& window_id = browser_window->GetSessionID();
  CHECK(window_id.is_valid());

  auto tracked_window = tracked_browser_windows_.find(window_id.id());
  if (tracked_window != tracked_browser_windows_.end() &&
      tracked_window->second != browser_window) {
    window_close_subscriptions_.erase(tracked_window->second);
  }
  tracked_browser_windows_.insert_or_assign(window_id.id(), browser_window);

  if (!window_close_subscriptions_.contains(browser_window)) {
    window_close_subscriptions_.insert_or_assign(
        browser_window,
        browser_window->RegisterBrowserDidClose(
            base::BindRepeating(&BrowserHandlerAndroid::OnBrowserWindowClosed,
                                base::Unretained(this))));
  }
}

Response BrowserHandlerAndroid::BuildBoundsForWindowId(
    int window_id,
    std::unique_ptr<protocol::Browser::Bounds>* out_bounds) {
  auto [window, _] = FindWindowById(window_id);
  if (!window) {
    return Response::ServerError("Browser window not found");
  }

  *out_bounds = BuildBrowserWindowBounds(window);
  return Response::Success();
}

BrowserHandlerAndroid::WindowLookupResult BrowserHandlerAndroid::FindWindowById(
    int window_id) {
  if (ui::BaseWindow* window = FindBrowserWindowById(window_id)) {
    auto tracked_window = tracked_browser_windows_.find(window_id);
    if (tracked_window != tracked_browser_windows_.end()) {
      BrowserWindowInterface* browser_window = tracked_window->second;
      tracked_browser_windows_.erase(tracked_window);
      window_close_subscriptions_.erase(browser_window);
    }
    return {window, false};
  }

  auto tracked_window = tracked_browser_windows_.find(window_id);
  if (tracked_window != tracked_browser_windows_.end()) {
    return {tracked_window->second->GetWindow(), true};
  }
  return {nullptr, false};
}

void BrowserHandlerAndroid::OnBrowserWindowClosed(
    BrowserWindowInterface* browser_window) {
  auto tracked_window =
      tracked_browser_windows_.find(browser_window->GetSessionID().id());
  if (tracked_window != tracked_browser_windows_.end() &&
      tracked_window->second == browser_window) {
    tracked_browser_windows_.erase(tracked_window);
  }
  window_close_subscriptions_.erase(browser_window);
}

// static
std::optional<int> BrowserHandlerAndroid::ResolveWindowIdForWebContents(
    content::WebContents* web_contents) {
  // Prefer the tab's own window id (covers pending ids).
  TabAndroid* tab = TabAndroid::FromWebContents(web_contents);
  if (tab) {
    SessionID wid = tab->GetWindowId();
    if (wid.is_valid()) {
      return wid.id();
    }
  }

  // Fallback: use the owning TabModel's session id.
  for (TabModel* model : TabModelList::models()) {
    for (int i = 0; i < model->GetTabCount(); ++i) {
      TabAndroid* model_tab = model->GetTabAt(i);
      if (model_tab && model_tab->web_contents() == web_contents) {
        return model->GetSessionId().id();
      }
    }
  }

  return std::nullopt;
}

// static
std::string BrowserHandlerAndroid::ComputeWindowStateString(bool is_fullscreen,
                                                            bool is_maximized,
                                                            bool is_minimized) {
  if (is_fullscreen) {
    return protocol::Browser::WindowStateEnum::Fullscreen;
  }
  if (is_maximized) {
    return protocol::Browser::WindowStateEnum::Maximized;
  }
  if (is_minimized) {
    return protocol::Browser::WindowStateEnum::Minimized;
  }
  return protocol::Browser::WindowStateEnum::Normal;
}

Response BrowserHandlerAndroid::GetWindowForTarget(
    std::optional<std::string> target_id,
    int* out_window_id,
    std::unique_ptr<protocol::Browser::Bounds>* out_bounds) {
  auto host =
      content::DevToolsAgentHost::GetForId(target_id.value_or(target_id_));
  if (!host) {
    return Response::ServerError("No matching target");
  }
  content::WebContents* web_contents = host->GetWebContents();
  if (!web_contents) {
    return Response::ServerError("No web contents in the target");
  }

  std::optional<int> window_id = ResolveWindowIdForWebContents(web_contents);
  if (!window_id.has_value()) {
    return Response::ServerError("Browser window not found");
  }
  *out_window_id = window_id.value();

  return BuildBoundsForWindowId(window_id.value(), out_bounds);
}

Response BrowserHandlerAndroid::GetWindowBounds(
    int window_id,
    std::unique_ptr<protocol::Browser::Bounds>* out_bounds) {
  return BuildBoundsForWindowId(window_id, out_bounds);
}

Response BrowserHandlerAndroid::Close() {
  return Response::MethodNotFound(kNotImplemented);
}

Response BrowserHandlerAndroid::SetWindowBounds(
    int window_id,
    std::unique_ptr<protocol::Browser::Bounds> window_bounds) {
  auto [window, is_pending] = FindWindowById(window_id);
  if (!window) {
    return Response::ServerError("Browser window not found");
  }

  gfx::Rect bounds = window->GetBounds();
  const bool set_bounds = window_bounds->HasLeft() || window_bounds->HasTop() ||
                          window_bounds->HasWidth() ||
                          window_bounds->HasHeight();
  if (set_bounds) {
    bounds.set_x(window_bounds->GetLeft(bounds.x()));
    bounds.set_y(window_bounds->GetTop(bounds.y()));
    bounds.set_width(window_bounds->GetWidth(bounds.width()));
    bounds.set_height(window_bounds->GetHeight(bounds.height()));
  }

  const std::string window_state =
      window_bounds->GetWindowState(protocol::Browser::WindowStateEnum::Normal);
  if (set_bounds &&
      window_state != protocol::Browser::WindowStateEnum::Normal) {
    return Response::InvalidParams(
        "The 'minimized', 'maximized' and 'fullscreen' states cannot be "
        "combined with 'left', 'top', 'width' or 'height'");
  }

  if (window_state == protocol::Browser::WindowStateEnum::Fullscreen) {
    return Response::ServerError("Fullscreen not supported on Android");
  }
  const std::string current_state = ResolveWindowState(window).name;
  if (window_state == protocol::Browser::WindowStateEnum::Maximized) {
    if (current_state == protocol::Browser::WindowStateEnum::Maximized) {
      return Response::Success();
    }
    if (current_state == protocol::Browser::WindowStateEnum::Minimized ||
        current_state == protocol::Browser::WindowStateEnum::Fullscreen) {
      return Response::ServerError(
          "To maximize a minimized or fullscreen "
          "window, restore it to normal state first.");
    }
    Response can_change = CheckCanChangeWindow(window, is_pending);
    if (!can_change.IsSuccess()) {
      return can_change;
    }
    window->Maximize();
    return Response::Success();
  }
  if (window_state == protocol::Browser::WindowStateEnum::Minimized) {
    if (current_state == protocol::Browser::WindowStateEnum::Minimized) {
      return Response::Success();
    }
    if (current_state == protocol::Browser::WindowStateEnum::Fullscreen) {
      return Response::ServerError(
          "To minimize a fullscreen window, restore it to normal "
          "state first.");
    }
    Response can_change = CheckCanChangeWindow(window, is_pending);
    if (!can_change.IsSuccess()) {
      return can_change;
    }
    window->Minimize();
    return Response::Success();
  }
  if (window_state != protocol::Browser::WindowStateEnum::Normal) {
    return Response::InvalidParams("Invalid windowState: " + window_state);
  }

  if (current_state == protocol::Browser::WindowStateEnum::Fullscreen) {
    return Response::ServerError("Cannot exit fullscreen on Android");
  }
  if (set_bounds) {
    Response can_change = CheckCanChangeWindow(window, is_pending);
    if (!can_change.IsSuccess()) {
      return can_change;
    }
    // Android's task implementation handles restoring a maximized/minimized
    // freeform window as part of applying explicit bounds.
    window->SetBounds(bounds);
  } else if (current_state == protocol::Browser::WindowStateEnum::Minimized ||
             current_state == protocol::Browser::WindowStateEnum::Maximized) {
    Response can_change = CheckCanChangeWindow(window, is_pending);
    if (!can_change.IsSuccess()) {
      return can_change;
    }
    window->Restore();
  }
  return Response::Success();
}

Response BrowserHandlerAndroid::SetContentsSize(int window_id,
                                                std::optional<int> width,
                                                std::optional<int> height) {
  return Response::MethodNotFound(kNotImplemented);
}

protocol::Response BrowserHandlerAndroid::SetDockTile(
    std::optional<std::string> label,
    std::optional<protocol::Binary> image) {
  return Response::MethodNotFound(kNotImplemented);
}

protocol::Response BrowserHandlerAndroid::ExecuteBrowserCommand(
    const protocol::Browser::BrowserCommandId& command_id) {
  return Response::MethodNotFound(kNotImplemented);
}

protocol::Response BrowserHandlerAndroid::AddPrivacySandboxEnrollmentOverride(
    const std::string& in_url) {
  return Response::MethodNotFound(kNotImplemented);
}
