// 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.

#import "ios/chrome/browser/omnibox/model/omnibox_text_model.h"

#import "base/metrics/user_metrics.h"
#import "base/trace_event/trace_event.h"
#import "ios/chrome/browser/omnibox/model/omnibox_text_controller.h"

OmniboxTextModel::OmniboxTextModel(OmniboxClientIOS* client)
    : omnibox_client(client),
      focus_state(OMNIBOX_FOCUS_NONE),
      user_input_in_progress(false),
      user_text(u""),
      focus_resulted_in_navigation(false),
      just_deleted_text(false),
      inline_autocompletion(u""),
      paste_state(OmniboxPasteState::kNone) {}

OmniboxTextModel::~OmniboxTextModel() = default;

bool OmniboxTextModel::SetInputInProgressNoNotify(bool in_progress) {
  if (user_input_in_progress == in_progress) {
    return false;
  }

  user_input_in_progress = in_progress;
  if (user_input_in_progress) {
    time_user_first_modified_omnibox = base::TimeTicks::Now();
    base::RecordAction(base::UserMetricsAction("OmniboxInputInProgress"));
  }
  return true;
}

bool OmniboxTextModel::HasFocus() {
  return focus_state != OMNIBOX_FOCUS_NONE;
}

void OmniboxTextModel::KillFocus() {
  focus_state = OMNIBOX_FOCUS_NONE;
  last_omnibox_focus = base::TimeTicks();
  paste_state = OmniboxPasteState::kNone;
  if (omnibox_client) {
    omnibox_client->OnFocusChanged(focus_state, OMNIBOX_FOCUS_CHANGE_EXPLICIT);
  }
}

void OmniboxTextModel::OnSetFocus() {
  TRACE_EVENT0("omnibox", "OmniboxTextModel::OnSetFocus");
  last_omnibox_focus = base::TimeTicks::Now();
  focus_resulted_in_navigation = false;

  // If the omnibox lost focus while the caret was hidden and then regained
  // focus, OnSetFocus() is called and should restore visibility. Note that
  // focus can be regained without an accompanying call to
  // OmniboxViewIOS::SetFocus(), e.g. by tabbing in.
  SetFocusState(OMNIBOX_FOCUS_VISIBLE, OMNIBOX_FOCUS_CHANGE_EXPLICIT);

  if (user_input_in_progress || !in_revert) {
    omnibox_client->OnInputStateChanged();
  }
}

void OmniboxTextModel::SetFocusState(OmniboxFocusState state,
                                     OmniboxFocusChangeReason reason) {
  if (state == focus_state) {
    return;
  }

  focus_state = state;
  omnibox_client->OnFocusChanged(focus_state, reason);
}

void OmniboxTextModel::UpdateUserText(const std::u16string& text) {
  user_text = text;
  just_deleted_text = false;
  inline_autocompletion.clear();
}

bool OmniboxTextModel::UpdateStateAfterPossibleChange(
    const OmniboxStateChanges& state_changes) {
  // Update the paste state as appropriate: if we're just finishing a paste
  // that replaced all the text, preserve that information; otherwise, if we've
  // made some other edit, clear paste tracking.
  if (paste_state == OmniboxPasteState::kPasting) {
    paste_state = OmniboxPasteState::kPasted;

    GURL url = GURL(*(state_changes.new_text));
    if (url.is_valid() && omnibox_client) {
      omnibox_client->OnUserPastedInOmniboxResultingInValidURL();
    }
  } else if (state_changes.text_differs) {
    paste_state = OmniboxPasteState::kNone;
  }

  if (state_changes.text_differs || state_changes.selection_differs) {
    // Restore caret visibility whenever the user changes text or selection in
    // the omnibox.
    SetFocusState(OMNIBOX_FOCUS_VISIBLE, OMNIBOX_FOCUS_CHANGE_TYPING);
  }

  // If the user text does not need to be changed, return now, so we don't
  // change any other state, lest arrowing around the omnibox do something like
  // reset `just_deleted_text_`.  Note that modifying the selection accepts any
  // inline autocompletion, which results in a user text change.
  if (!state_changes.text_differs &&
      (!state_changes.selection_differs || inline_autocompletion.empty())) {
    return false;
  }

  UpdateUserText(*state_changes.new_text);
  just_deleted_text = state_changes.just_deleted_text;

  return true;
}

OmniboxStateChanges OmniboxTextModel::GetStateChanges(
    const OmniboxTextState& before,
    const OmniboxTextState& after) const {
  OmniboxStateChanges state_changes;
  state_changes.old_text = &before.text;
  state_changes.new_text = &after.text;
  state_changes.new_sel_start = after.sel_start;
  state_changes.new_sel_end = after.sel_end;
  const bool old_sel_empty = before.sel_start == before.sel_end;
  const bool new_sel_empty = after.sel_start == after.sel_end;
  const bool sel_same_ignoring_direction =
      std::min(before.sel_start, before.sel_end) ==
          std::min(after.sel_start, after.sel_end) &&
      std::max(before.sel_start, before.sel_end) ==
          std::max(after.sel_start, after.sel_end);
  state_changes.selection_differs =
      (!old_sel_empty || !new_sel_empty) && !sel_same_ignoring_direction;
  state_changes.text_differs = before.text != after.text;
  state_changes.just_deleted_text =
      before.text.length() > after.text.length() &&
      // Check that the cursor is at or before the start of the old selection.
      // This ensures that if the user selected text and typed, it's not
      // considered a deletion even if the new text is shorter.
      after.sel_start <= std::min(before.sel_start, before.sel_end);
  return state_changes;
}
