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

// Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com

#include "xfa/fde/cfde_texteditengine.h"

#include <algorithm>
#include <utility>

#include "core/fxcrt/fx_extension.h"
#include "core/fxcrt/span_util.h"
#include "core/fxge/text_char_pos.h"
#include "third_party/base/check.h"
#include "third_party/base/check_op.h"
#include "third_party/base/numerics/safe_conversions.h"
#include "xfa/fde/cfde_textout.h"
#include "xfa/fde/cfde_wordbreak_data.h"
#include "xfa/fgas/font/cfgas_gefont.h"

namespace {

class InsertOperation final : public CFDE_TextEditEngine::Operation {
 public:
  InsertOperation(CFDE_TextEditEngine* engine,
                  size_t start_idx,
                  const WideString& added_text)
      : engine_(engine), start_idx_(start_idx), added_text_(added_text) {}

  ~InsertOperation() override = default;

  void Redo() const override {
    engine_->Insert(start_idx_, added_text_,
                    CFDE_TextEditEngine::RecordOperation::kSkipRecord);
  }

  void Undo() const override {
    engine_->Delete(start_idx_, added_text_.GetLength(),
                    CFDE_TextEditEngine::RecordOperation::kSkipRecord);
  }

 private:
  UnownedPtr<CFDE_TextEditEngine> engine_;
  size_t start_idx_;
  WideString added_text_;
};

class DeleteOperation final : public CFDE_TextEditEngine::Operation {
 public:
  DeleteOperation(CFDE_TextEditEngine* engine,
                  size_t start_idx,
                  const WideString& removed_text)
      : engine_(engine), start_idx_(start_idx), removed_text_(removed_text) {}

  ~DeleteOperation() override = default;

  void Redo() const override {
    engine_->Delete(start_idx_, removed_text_.GetLength(),
                    CFDE_TextEditEngine::RecordOperation::kSkipRecord);
  }

  void Undo() const override {
    engine_->Insert(start_idx_, removed_text_,
                    CFDE_TextEditEngine::RecordOperation::kSkipRecord);
  }

 private:
  UnownedPtr<CFDE_TextEditEngine> engine_;
  size_t start_idx_;
  WideString removed_text_;
};

class ReplaceOperation final : public CFDE_TextEditEngine::Operation {
 public:
  ReplaceOperation(CFDE_TextEditEngine* engine,
                   size_t start_idx,
                   const WideString& removed_text,
                   const WideString& added_text)
      : insert_op_(engine, start_idx, added_text),
        delete_op_(engine, start_idx, removed_text) {}

  ~ReplaceOperation() override = default;

  void Redo() const override {
    delete_op_.Redo();
    insert_op_.Redo();
  }

  void Undo() const override {
    insert_op_.Undo();
    delete_op_.Undo();
  }

 private:
  InsertOperation insert_op_;
  DeleteOperation delete_op_;
};

int GetBreakFlagsFor(WordBreakProperty current, WordBreakProperty next) {
  if (current == WordBreakProperty::kMidLetter) {
    if (next == WordBreakProperty::kALetter)
      return 1;
  } else if (current == WordBreakProperty::kMidNum) {
    if (next == WordBreakProperty::kNumeric)
      return 2;
  } else if (current == WordBreakProperty::kMidNumLet) {
    if (next == WordBreakProperty::kALetter)
      return 1;
    if (next == WordBreakProperty::kNumeric)
      return 2;
  }
  return 0;
}

bool BreakFlagsChanged(int flags, WordBreakProperty previous) {
  return (flags != 1 || previous != WordBreakProperty::kALetter) &&
         (flags != 2 || previous != WordBreakProperty::kNumeric);
}

}  // namespace

CFDE_TextEditEngine::CFDE_TextEditEngine() {
  content_.resize(gap_size_);
  operation_buffer_.resize(max_edit_operations_);

  text_break_.SetFontSize(font_size_);
  text_break_.SetLineBreakTolerance(2.0f);
  text_break_.SetTabWidth(36);
}

CFDE_TextEditEngine::~CFDE_TextEditEngine() = default;

void CFDE_TextEditEngine::Clear() {
  text_length_ = 0;
  gap_position_ = 0;
  gap_size_ = kGapSize;

  content_.clear();
  content_.resize(gap_size_);

  ClearSelection();
  ClearOperationRecords();
}

void CFDE_TextEditEngine::SetMaxEditOperationsForTesting(size_t max) {
  max_edit_operations_ = max;
  operation_buffer_.resize(max);

  ClearOperationRecords();
}

void CFDE_TextEditEngine::AdjustGap(size_t idx, size_t length) {
  static const size_t char_size = sizeof(WideString::CharType);

  // Move the gap, if necessary.
  if (idx < gap_position_) {
    memmove(content_.data() + idx + gap_size_, content_.data() + idx,
            (gap_position_ - idx) * char_size);
    gap_position_ = idx;
  } else if (idx > gap_position_) {
    memmove(content_.data() + gap_position_,
            content_.data() + gap_position_ + gap_size_,
            (idx - gap_position_) * char_size);
    gap_position_ = idx;
  }

  // If the gap is too small, make it bigger.
  if (length >= gap_size_) {
    size_t new_gap_size = length + kGapSize;
    content_.resize(text_length_ + new_gap_size);

    memmove(content_.data() + gap_position_ + new_gap_size,
            content_.data() + gap_position_ + gap_size_,
            (text_length_ - gap_position_) * char_size);

    gap_size_ = new_gap_size;
  }
}

size_t CFDE_TextEditEngine::CountCharsExceedingSize(const WideString& text,
                                                    size_t num_to_check) {
  if (!limit_horizontal_area_ && !limit_vertical_area_)
    return 0;

  auto text_out = std::make_unique<CFDE_TextOut>();
  text_out->SetLineSpace(line_spacing_);
  text_out->SetFont(font_);
  text_out->SetFontSize(font_size_);

  FDE_TextStyle style;
  style.single_line_ = !is_multiline_;

  CFX_RectF text_rect;
  if (is_linewrap_enabled_) {
    style.line_wrap_ = true;
    text_rect.width = available_width_;
  } else {
    text_rect.width = kPageWidthMax;
  }
  text_out->SetStyles(style);

  size_t length = text.GetLength();
  WideStringView temp = text.AsStringView();

  float vertical_height = line_spacing_ * visible_line_count_;
  size_t chars_exceeding_size = 0;
  // TODO(dsinclair): Can this get changed to a binary search?
  for (size_t i = 0; i < num_to_check; i++) {
    text_out->CalcLogicSize(temp, &text_rect);
    if (limit_horizontal_area_ && text_rect.width <= available_width_)
      break;
    if (limit_vertical_area_ && text_rect.height <= vertical_height)
      break;

    ++chars_exceeding_size;

    --length;
    temp = temp.First(length);
  }

  return chars_exceeding_size;
}

void CFDE_TextEditEngine::Insert(size_t idx,
                                 const WideString& request_text,
                                 RecordOperation add_operation) {
  WideString text = request_text;
  if (text.IsEmpty())
    return;

  idx = std::min(idx, text_length_);

  TextChange change;
  change.selection_start = idx;
  change.selection_end = idx;
  change.text = text;
  change.previous_text = GetText();
  change.cancelled = false;

  if (delegate_ && (add_operation != RecordOperation::kSkipRecord &&
                    add_operation != RecordOperation::kSkipNotify)) {
    delegate_->OnTextWillChange(&change);
    if (change.cancelled)
      return;

    text = change.text;
    idx = change.selection_start;

    // Delegate extended the selection, so delete it before we insert.
    if (change.selection_end != change.selection_start)
      DeleteSelectedText(RecordOperation::kSkipRecord);

    // Delegate may have changed text entirely, recheck.
    idx = std::min(idx, text_length_);
  }

  size_t length = text.GetLength();
  if (length == 0)
    return;

  // If we're going to be too big we insert what we can and notify the
  // delegate we've filled the text after the insert is done.
  bool exceeded_limit = false;

  // Currently we allow inserting a number of characters over the text limit if
  // we're skipping notify. This means we're setting the formatted text into the
  // engine. Otherwise, if you enter 123456789 for an SSN into a field
  // with a 9 character limit and we reformat to 123-45-6789 we'll truncate
  // the 89 when inserting into the text edit. See https://crbug.com/pdfium/1089
  if (has_character_limit_ && text_length_ + length > character_limit_) {
    if (add_operation == RecordOperation::kSkipNotify) {
      // Raise the limit to allow subsequent changes to expanded text.
      character_limit_ = text_length_ + length;
    } else {
      // Truncate the text to comply with the limit.
      CHECK(text_length_ <= character_limit_);
      length = character_limit_ - text_length_;
      exceeded_limit = true;
    }
  }
  AdjustGap(idx, length);

  if (validation_enabled_ || limit_horizontal_area_ || limit_vertical_area_) {
    WideString str;
    if (gap_position_ > 0)
      str += WideStringView(content_.data(), gap_position_);

    str += text;

    if (text_length_ - gap_position_ > 0) {
      str += WideStringView(content_.data() + gap_position_ + gap_size_,
                            text_length_ - gap_position_);
    }

    if (validation_enabled_ && delegate_ && !delegate_->OnValidate(str)) {
      // TODO(dsinclair): Notify delegate of validation failure?
      return;
    }

    // Check if we've limited the horizontal/vertical area, and if so determine
    // how many of our characters would be outside the area.
    size_t chars_exceeding = CountCharsExceedingSize(str, length);
    if (chars_exceeding > 0) {
      // If none of the characters will fit, notify and exit.
      if (chars_exceeding == length) {
        if (delegate_)
          delegate_->NotifyTextFull();
        return;
      }

      // Some, but not all, chars will fit, insert them and then notify
      // we're full.
      exceeded_limit = true;
      length -= chars_exceeding;
    }
  }

  if (add_operation == RecordOperation::kInsertRecord) {
    AddOperationRecord(
        std::make_unique<InsertOperation>(this, gap_position_, text));
  }

  WideString previous_text;
  if (delegate_)
    previous_text = GetText();

  // Copy the new text into the gap.
  fxcrt::spancpy(pdfium::make_span(content_).subspan(gap_position_),
                 text.span().first(length));
  gap_position_ += length;
  gap_size_ -= length;
  text_length_ += length;

  is_dirty_ = true;

  // Inserting text resets the selection.
  ClearSelection();

  if (delegate_) {
    if (exceeded_limit)
      delegate_->NotifyTextFull();

    delegate_->OnTextChanged();
  }
}

void CFDE_TextEditEngine::AddOperationRecord(std::unique_ptr<Operation> op) {
  size_t last_insert_position = next_operation_index_to_insert_ == 0
                                    ? max_edit_operations_ - 1
                                    : next_operation_index_to_insert_ - 1;

  // If our undo record is not the last thing we inserted then we need to
  // remove all the undo records between our insert position and the undo marker
  // and make that our new insert position.
  if (next_operation_index_to_undo_ != last_insert_position) {
    if (next_operation_index_to_undo_ > last_insert_position) {
      // Our Undo position is ahead of us, which means we need to clear out the
      // head of the queue.
      while (last_insert_position != 0) {
        operation_buffer_[last_insert_position].reset();
        --last_insert_position;
      }
      operation_buffer_[0].reset();

      // Moving this will let us then clear out the end, setting the undo
      // position to before the insert position.
      last_insert_position = max_edit_operations_ - 1;
    }

    // Clear out the vector from undo position to our set insert position.
    while (next_operation_index_to_undo_ != last_insert_position) {
      operation_buffer_[last_insert_position].reset();
      --last_insert_position;
    }
  }

  // We're now pointing at the next thing we want to Undo, so insert at the
  // next position in the queue.
  ++last_insert_position;
  if (last_insert_position >= max_edit_operations_)
    last_insert_position = 0;

  operation_buffer_[last_insert_position] = std::move(op);
  next_operation_index_to_insert_ =
      (last_insert_position + 1) % max_edit_operations_;
  next_operation_index_to_undo_ = last_insert_position;
}

void CFDE_TextEditEngine::ClearOperationRecords() {
  for (auto& record : operation_buffer_)
    record.reset();

  next_operation_index_to_undo_ = max_edit_operations_ - 1;
  next_operation_index_to_insert_ = 0;
}

size_t CFDE_TextEditEngine::GetIndexLeft(size_t pos) const {
  if (pos == 0)
    return 0;
  --pos;

  while (pos != 0) {
    // We want to be on the location just before the \r or \n
    wchar_t ch = GetChar(pos - 1);
    if (ch != '\r' && ch != '\n')
      break;

    --pos;
  }
  return pos;
}

size_t CFDE_TextEditEngine::GetIndexRight(size_t pos) const {
  if (pos >= text_length_)
    return text_length_;
  ++pos;

  wchar_t ch = GetChar(pos);
  // We want to be on the location after the \r\n.
  while (pos < text_length_ && (ch == '\r' || ch == '\n')) {
    ++pos;
    ch = GetChar(pos);
  }

  return pos;
}

size_t CFDE_TextEditEngine::GetIndexUp(size_t pos) const {
  size_t line_start = GetIndexAtStartOfLine(pos);
  if (line_start == 0)
    return pos;

  // Determine how far along the line we were.
  size_t dist = pos - line_start;

  // Move to the end of the preceding line.
  wchar_t ch;
  do {
    --line_start;
    ch = GetChar(line_start);
  } while (line_start != 0 && (ch == '\r' || ch == '\n'));

  if (line_start == 0)
    return dist;

  // Get the start of the line prior to the current line.
  size_t prior_start = GetIndexAtStartOfLine(line_start);

  // Prior line is shorter then next line, and we're past the end of that line
  // return the end of line.
  if (prior_start + dist > line_start)
    return GetIndexAtEndOfLine(line_start);

  return prior_start + dist;
}

size_t CFDE_TextEditEngine::GetIndexDown(size_t pos) const {
  size_t line_end = GetIndexAtEndOfLine(pos);
  if (line_end == text_length_)
    return pos;

  wchar_t ch;
  do {
    ++line_end;
    ch = GetChar(line_end);
  } while (line_end < text_length_ && (ch == '\r' || ch == '\n'));

  if (line_end == text_length_)
    return line_end;

  // Determine how far along the line we are.
  size_t dist = pos - GetIndexAtStartOfLine(pos);

  // Check if next line is shorter then current line. If so, return end
  // of next line.
  size_t next_line_end = GetIndexAtEndOfLine(line_end);
  if (line_end + dist > next_line_end)
    return next_line_end;

  return line_end + dist;
}

size_t CFDE_TextEditEngine::GetIndexAtStartOfLine(size_t pos) const {
  if (pos == 0)
    return 0;

  wchar_t ch = GetChar(pos);
  // What to do.
  if (ch == '\r' || ch == '\n')
    return pos;

  do {
    // We want to be on the location just after the \r\n
    ch = GetChar(pos - 1);
    if (ch == '\r' || ch == '\n')
      break;

    --pos;
  } while (pos > 0);

  return pos;
}

size_t CFDE_TextEditEngine::GetIndexAtEndOfLine(size_t pos) const {
  if (pos >= text_length_)
    return text_length_;

  wchar_t ch = GetChar(pos);
  // Not quite sure which way to go here?
  if (ch == '\r' || ch == '\n')
    return pos;

  // We want to be on the location of the first \r or \n.
  do {
    ++pos;
    ch = GetChar(pos);
  } while (pos < text_length_ && (ch != '\r' && ch != '\n'));

  return pos;
}

void CFDE_TextEditEngine::LimitHorizontalScroll(bool val) {
  ClearOperationRecords();
  limit_horizontal_area_ = val;
}

void CFDE_TextEditEngine::LimitVerticalScroll(bool val) {
  ClearOperationRecords();
  limit_vertical_area_ = val;
}

bool CFDE_TextEditEngine::CanUndo() const {
  return operation_buffer_[next_operation_index_to_undo_] != nullptr &&
         next_operation_index_to_undo_ != next_operation_index_to_insert_;
}

bool CFDE_TextEditEngine::CanRedo() const {
  size_t idx = (next_operation_index_to_undo_ + 1) % max_edit_operations_;
  return idx != next_operation_index_to_insert_ &&
         operation_buffer_[idx] != nullptr;
}

bool CFDE_TextEditEngine::Redo() {
  if (!CanRedo())
    return false;

  next_operation_index_to_undo_ =
      (next_operation_index_to_undo_ + 1) % max_edit_operations_;
  operation_buffer_[next_operation_index_to_undo_]->Redo();
  return true;
}

bool CFDE_TextEditEngine::Undo() {
  if (!CanUndo())
    return false;

  operation_buffer_[next_operation_index_to_undo_]->Undo();
  next_operation_index_to_undo_ = next_operation_index_to_undo_ == 0
                                      ? max_edit_operations_ - 1
                                      : next_operation_index_to_undo_ - 1;
  return true;
}

void CFDE_TextEditEngine::Layout() {
  if (!is_dirty_)
    return;

  is_dirty_ = false;
  RebuildPieces();
}

CFX_RectF CFDE_TextEditEngine::GetContentsBoundingBox() {
  // Layout if necessary.
  Layout();
  return contents_bounding_box_;
}

void CFDE_TextEditEngine::SetAvailableWidth(size_t width) {
  if (width == available_width_)
    return;

  ClearOperationRecords();

  available_width_ = width;
  if (is_linewrap_enabled_)
    text_break_.SetLineWidth(width);
  if (is_comb_text_)
    SetCombTextWidth();

  is_dirty_ = true;
}

void CFDE_TextEditEngine::SetHasCharacterLimit(bool limit) {
  if (has_character_limit_ == limit)
    return;

  has_character_limit_ = limit;
  character_limit_ = std::max(character_limit_, text_length_);
  if (is_comb_text_)
    SetCombTextWidth();

  is_dirty_ = true;
}

void CFDE_TextEditEngine::SetCharacterLimit(size_t limit) {
  if (character_limit_ == limit)
    return;

  ClearOperationRecords();

  character_limit_ = std::max(limit, text_length_);
  if (is_comb_text_)
    SetCombTextWidth();

  is_dirty_ = true;
}

void CFDE_TextEditEngine::SetFont(RetainPtr<CFGAS_GEFont> font) {
  if (font_ == font)
    return;

  font_ = std::move(font);
  text_break_.SetFont(font_);
  is_dirty_ = true;
}

RetainPtr<CFGAS_GEFont> CFDE_TextEditEngine::GetFont() const {
  return font_;
}

void CFDE_TextEditEngine::SetFontSize(float size) {
  if (font_size_ == size)
    return;

  font_size_ = size;
  text_break_.SetFontSize(font_size_);
  is_dirty_ = true;
}

void CFDE_TextEditEngine::SetTabWidth(float width) {
  int32_t old_tab_width = text_break_.GetTabWidth();
  text_break_.SetTabWidth(width);
  if (old_tab_width == text_break_.GetTabWidth())
    return;

  is_dirty_ = true;
}

void CFDE_TextEditEngine::SetAlignment(uint32_t alignment) {
  if (alignment == character_alignment_)
    return;

  character_alignment_ = alignment;
  text_break_.SetAlignment(alignment);
  is_dirty_ = true;
}

void CFDE_TextEditEngine::SetVisibleLineCount(size_t count) {
  if (visible_line_count_ == count)
    return;

  visible_line_count_ = std::max(static_cast<size_t>(1), count);
  is_dirty_ = true;
}

void CFDE_TextEditEngine::EnableMultiLine(bool val) {
  if (is_multiline_ == val)
    return;

  is_multiline_ = val;
  Mask<CFGAS_Break::LayoutStyle> style = text_break_.GetLayoutStyles();
  if (is_multiline_)
    style.Clear(CFGAS_Break::LayoutStyle::kSingleLine);
  else
    style |= CFGAS_Break::LayoutStyle::kSingleLine;

  text_break_.SetLayoutStyles(style);
  is_dirty_ = true;
}

void CFDE_TextEditEngine::EnableLineWrap(bool val) {
  if (is_linewrap_enabled_ == val)
    return;

  is_linewrap_enabled_ = val;
  text_break_.SetLineWidth(is_linewrap_enabled_ ? available_width_
                                                : kPageWidthMax);
  is_dirty_ = true;
}

void CFDE_TextEditEngine::SetCombText(bool enable) {
  if (is_comb_text_ == enable)
    return;

  is_comb_text_ = enable;

  Mask<CFGAS_Break::LayoutStyle> style = text_break_.GetLayoutStyles();
  if (enable) {
    style |= CFGAS_Break::LayoutStyle::kCombText;
    SetCombTextWidth();
  } else {
    style.Clear(CFGAS_Break::LayoutStyle::kCombText);
  }
  text_break_.SetLayoutStyles(style);
  is_dirty_ = true;
}

void CFDE_TextEditEngine::SetCombTextWidth() {
  size_t width = available_width_;
  if (has_character_limit_)
    width /= character_limit_;

  text_break_.SetCombWidth(width);
}

void CFDE_TextEditEngine::SelectAll() {
  if (text_length_ == 0)
    return;

  has_selection_ = true;
  selection_.start_idx = 0;
  selection_.count = text_length_;
}

void CFDE_TextEditEngine::ClearSelection() {
  has_selection_ = false;
  selection_.start_idx = 0;
  selection_.count = 0;
}

void CFDE_TextEditEngine::SetSelection(size_t start_idx, size_t count) {
  if (count == 0) {
    ClearSelection();
    return;
  }

  if (start_idx > text_length_)
    return;
  if (start_idx + count > text_length_)
    count = text_length_ - start_idx;

  has_selection_ = true;
  selection_.start_idx = start_idx;
  selection_.count = count;
}

WideString CFDE_TextEditEngine::GetSelectedText() const {
  if (!has_selection_)
    return WideString();

  WideString text;
  if (selection_.start_idx < gap_position_) {
    // Fully on left of gap.
    if (selection_.start_idx + selection_.count < gap_position_) {
      text += WideStringView(content_.data() + selection_.start_idx,
                             selection_.count);
      return text;
    }

    // Pre-gap text
    text += WideStringView(content_.data() + selection_.start_idx,
                           gap_position_ - selection_.start_idx);

    if (selection_.count - (gap_position_ - selection_.start_idx) > 0) {
      // Post-gap text
      text += WideStringView(
          content_.data() + gap_position_ + gap_size_,
          selection_.count - (gap_position_ - selection_.start_idx));
    }

    return text;
  }

  // Fully right of gap
  text += WideStringView(content_.data() + gap_size_ + selection_.start_idx,
                         selection_.count);
  return text;
}

WideString CFDE_TextEditEngine::DeleteSelectedText(
    RecordOperation add_operation) {
  if (!has_selection_)
    return WideString();

  return Delete(selection_.start_idx, selection_.count, add_operation);
}

WideString CFDE_TextEditEngine::Delete(size_t start_idx,
                                       size_t length,
                                       RecordOperation add_operation) {
  if (start_idx >= text_length_)
    return WideString();

  TextChange change;
  change.text.clear();
  change.cancelled = false;
  if (delegate_ && (add_operation != RecordOperation::kSkipRecord &&
                    add_operation != RecordOperation::kSkipNotify)) {
    change.previous_text = GetText();
    change.selection_start = start_idx;
    change.selection_end = start_idx + length;

    delegate_->OnTextWillChange(&change);
    if (change.cancelled)
      return WideString();

    // Delegate may have changed the selection range.
    start_idx = change.selection_start;
    length = change.selection_end - change.selection_start;

    // Delegate may have changed text entirely, recheck.
    if (start_idx >= text_length_)
      return WideString();
  }

  length = std::min(length, text_length_ - start_idx);
  AdjustGap(start_idx + length, 0);

  WideString ret;
  ret += WideStringView(content_.data() + start_idx, length);

  if (add_operation == RecordOperation::kInsertRecord) {
    AddOperationRecord(std::make_unique<DeleteOperation>(this, start_idx, ret));
  }

  WideString previous_text = GetText();

  gap_position_ = start_idx;
  gap_size_ += length;

  text_length_ -= length;
  is_dirty_ = true;
  ClearSelection();

  // The JS requested the insertion of text instead of just a deletion.
  if (!change.text.IsEmpty())
    Insert(start_idx, change.text, RecordOperation::kSkipRecord);

  if (delegate_)
    delegate_->OnTextChanged();

  return ret;
}

void CFDE_TextEditEngine::ReplaceSelectedText(const WideString& requested_rep) {
  WideString rep = requested_rep;

  if (delegate_) {
    TextChange change;
    change.selection_start = selection_.start_idx;
    change.selection_end = selection_.start_idx + selection_.count;
    change.text = rep;
    change.previous_text = GetText();
    change.cancelled = false;

    delegate_->OnTextWillChange(&change);
    if (change.cancelled)
      return;

    rep = change.text;
    selection_.start_idx = change.selection_start;
    selection_.count = change.selection_end - change.selection_start;
  }

  size_t start_idx = selection_.start_idx;
  WideString txt = DeleteSelectedText(RecordOperation::kSkipRecord);
  Insert(gap_position_, rep, RecordOperation::kSkipRecord);

  AddOperationRecord(
      std::make_unique<ReplaceOperation>(this, start_idx, txt, rep));
}

WideString CFDE_TextEditEngine::GetText() const {
  WideString str;
  if (gap_position_ > 0)
    str += WideStringView(content_.data(), gap_position_);
  if (text_length_ - gap_position_ > 0) {
    str += WideStringView(content_.data() + gap_position_ + gap_size_,
                          text_length_ - gap_position_);
  }
  return str;
}

size_t CFDE_TextEditEngine::GetLength() const {
  return text_length_;
}

wchar_t CFDE_TextEditEngine::GetChar(size_t idx) const {
  if (idx >= text_length_)
    return L'\0';
  if (password_mode_)
    return password_alias_;

  return idx < gap_position_
             ? content_[idx]
             : content_[gap_position_ + gap_size_ + (idx - gap_position_)];
}

int32_t CFDE_TextEditEngine::GetWidthOfChar(size_t idx) {
  // Recalculate the widths if necessary.
  Layout();
  return idx < char_widths_.size() ? char_widths_[idx] : 0;
}

size_t CFDE_TextEditEngine::GetIndexForPoint(const CFX_PointF& point) {
  // Recalculate the widths if necessary.
  Layout();

  auto start_it = text_piece_info_.begin();
  for (; start_it < text_piece_info_.end(); ++start_it) {
    if (start_it->rtPiece.top <= point.y &&
        point.y < start_it->rtPiece.bottom())
      break;
  }
  // We didn't find the point before getting to the end of the text, return
  // end of text.
  if (start_it == text_piece_info_.end())
    return text_length_;

  auto end_it = start_it;
  for (; end_it < text_piece_info_.end(); ++end_it) {
    // We've moved past where the point should be and didn't find anything.
    // Return the start of the current piece as the location.
    if (end_it->rtPiece.bottom() <= point.y || point.y < end_it->rtPiece.top)
      break;
  }
  // Make sure the end iterator is pointing to our text pieces.
  if (end_it == text_piece_info_.end())
    --end_it;

  size_t start_it_idx = start_it->nStart;
  for (; start_it <= end_it; ++start_it) {
    bool piece_contains_point_vertically =
        (point.y >= start_it->rtPiece.top &&
         point.y < start_it->rtPiece.bottom());
    if (!piece_contains_point_vertically)
      continue;

    std::vector<CFX_RectF> rects = GetCharRects(*start_it);
    for (size_t i = 0; i < rects.size(); ++i) {
      bool character_contains_point_horizontally =
          (point.x >= rects[i].left && point.x < rects[i].right());
      if (!character_contains_point_horizontally)
        continue;

      // When clicking on the left half of a character, the cursor should be
      // moved before it. If the click was on the right half of that character,
      // move the cursor after it.
      bool closer_to_left =
          (point.x - rects[i].left < rects[i].right() - point.x);
      size_t caret_pos = closer_to_left ? i : i + 1;
      size_t pos = start_it->nStart + caret_pos;
      if (pos >= text_length_)
        return text_length_;

      wchar_t wch = GetChar(pos);
      if (wch == L'\n' || wch == L'\r') {
        if (wch == L'\n' && pos > 0 && GetChar(pos - 1) == L'\r')
          --pos;
        return pos;
      }

      // TODO(dsinclair): Old code had a before flag set based on bidi?
      return pos;
    }

    // Point is not within the horizontal range of any characters, it's
    // afterwards. Return the position after the last character.
    // The last line has nCount equal to the number of characters + 1 (sentinel
    // character maybe?). Restrict to the text_length_ to account for that.
    size_t pos = std::min(
        static_cast<size_t>(start_it->nStart + start_it->nCount), text_length_);

    // If the line is not the last one and it was broken right after a breaking
    // whitespace (space or line break), the cursor should not be placed after
    // the whitespace, but before it. If the cursor is moved after the
    // whitespace, it goes to the beginning of the next line.
    bool is_last_line = (std::next(start_it) == text_piece_info_.end());
    if (!is_last_line && pos > 0) {
      wchar_t previous_char = GetChar(pos - 1);
      if (previous_char == L' ' || previous_char == L'\n' ||
          previous_char == L'\r') {
        --pos;
      }
    }

    return pos;
  }

  if (start_it == text_piece_info_.end())
    return start_it_idx;
  if (start_it == end_it)
    return start_it->nStart;

  // We didn't find the point before going over all of the pieces, we want to
  // return the start of the piece after the point.
  return end_it->nStart;
}

std::vector<CFX_RectF> CFDE_TextEditEngine::GetCharRects(
    const FDE_TEXTEDITPIECE& piece) {
  if (piece.nCount < 1)
    return std::vector<CFX_RectF>();

  CFGAS_TxtBreak::Run tr;
  tr.pEdtEngine = this;
  tr.iStart = piece.nStart;
  tr.iLength = piece.nCount;
  tr.pFont = font_;
  tr.fFontSize = font_size_;
  tr.dwStyles = text_break_.GetLayoutStyles();
  tr.dwCharStyles = piece.dwCharStyles;
  tr.pRect = &piece.rtPiece;
  return text_break_.GetCharRects(tr);
}

std::vector<TextCharPos> CFDE_TextEditEngine::GetDisplayPos(
    const FDE_TEXTEDITPIECE& piece) {
  if (piece.nCount < 1)
    return std::vector<TextCharPos>();

  CFGAS_TxtBreak::Run tr;
  tr.pEdtEngine = this;
  tr.iStart = piece.nStart;
  tr.iLength = piece.nCount;
  tr.pFont = font_;
  tr.fFontSize = font_size_;
  tr.dwStyles = text_break_.GetLayoutStyles();
  tr.dwCharStyles = piece.dwCharStyles;
  tr.pRect = &piece.rtPiece;

  std::vector<TextCharPos> data(text_break_.GetDisplayPos(tr, nullptr));
  text_break_.GetDisplayPos(tr, data.data());
  return data;
}

void CFDE_TextEditEngine::RebuildPieces() {
  text_break_.EndBreak(CFGAS_Char::BreakType::kParagraph);
  text_break_.ClearBreakPieces();

  char_widths_.clear();
  text_piece_info_.clear();

  // Must have a font set in order to break the text.
  if (!CanGenerateCharacterInfo())
    return;

  bool initialized_bounding_box = false;
  contents_bounding_box_ = CFX_RectF();
  size_t current_piece_start = 0;
  float current_line_start = 0;

  CFDE_TextEditEngine::Iterator iter(this);
  while (!iter.IsEOF(false)) {
    iter.Next(false);

    CFGAS_Char::BreakType break_status = text_break_.AppendChar(
        password_mode_ ? password_alias_ : iter.GetChar());
    if (iter.IsEOF(false) && CFX_BreakTypeNoneOrPiece(break_status))
      break_status = text_break_.EndBreak(CFGAS_Char::BreakType::kParagraph);

    if (CFX_BreakTypeNoneOrPiece(break_status))
      continue;
    int32_t piece_count = text_break_.CountBreakPieces();
    for (int32_t i = 0; i < piece_count; ++i) {
      const CFGAS_BreakPiece* piece = text_break_.GetBreakPieceUnstable(i);

      FDE_TEXTEDITPIECE txtEdtPiece;
      txtEdtPiece.rtPiece.left = piece->GetStartPos() / 20000.0f;
      txtEdtPiece.rtPiece.top = current_line_start;
      txtEdtPiece.rtPiece.width = piece->GetWidth() / 20000.0f;
      txtEdtPiece.rtPiece.height = line_spacing_;
      txtEdtPiece.nStart =
          pdfium::base::checked_cast<int32_t>(current_piece_start);
      txtEdtPiece.nCount = piece->GetCharCount();
      txtEdtPiece.nBidiLevel = piece->GetBidiLevel();
      txtEdtPiece.dwCharStyles = piece->GetCharStyles();
      if (FX_IsOdd(piece->GetBidiLevel()))
        txtEdtPiece.dwCharStyles |= FX_TXTCHARSTYLE_OddBidiLevel;

      text_piece_info_.push_back(txtEdtPiece);

      if (initialized_bounding_box) {
        contents_bounding_box_.Union(txtEdtPiece.rtPiece);
      } else {
        contents_bounding_box_ = txtEdtPiece.rtPiece;
        initialized_bounding_box = true;
      }

      current_piece_start += txtEdtPiece.nCount;
      for (int32_t k = 0; k < txtEdtPiece.nCount; ++k)
        char_widths_.push_back(piece->GetChar(k)->m_iCharWidth);
    }

    current_line_start += line_spacing_;
    text_break_.ClearBreakPieces();
  }

  float delta = 0.0;
  bool bounds_smaller = contents_bounding_box_.width < available_width_;
  if (IsAlignedRight() && bounds_smaller) {
    delta = available_width_ - contents_bounding_box_.width;
  } else if (IsAlignedCenter() && bounds_smaller) {
    delta = (available_width_ - contents_bounding_box_.width) / 2.0f;
  }

  if (delta != 0.0) {
    float offset = delta - contents_bounding_box_.left;
    for (auto& info : text_piece_info_)
      info.rtPiece.Offset(offset, 0.0f);
    contents_bounding_box_.Offset(offset, 0.0f);
  }

  // Shrink the last piece down to the font_size.
  contents_bounding_box_.height -= line_spacing_ - font_size_;
  text_piece_info_.back().rtPiece.height = font_size_;
}

std::pair<int32_t, CFX_RectF> CFDE_TextEditEngine::GetCharacterInfo(
    int32_t start_idx) {
  DCHECK(start_idx >= 0);
  DCHECK(static_cast<size_t>(start_idx) <= text_length_);

  // Make sure the current available data is fresh.
  Layout();

  auto it = text_piece_info_.begin();
  for (; it != text_piece_info_.end(); ++it) {
    if (it->nStart <= start_idx && start_idx < it->nStart + it->nCount)
      break;
  }
  CHECK_NE(it, text_piece_info_.end());
  return {it->nBidiLevel, GetCharRects(*it)[start_idx - it->nStart]};
}

std::vector<CFX_RectF> CFDE_TextEditEngine::GetCharacterRectsInRange(
    int32_t start_idx,
    int32_t count) {
  // Make sure the current available data is fresh.
  Layout();

  auto it = text_piece_info_.begin();
  for (; it != text_piece_info_.end(); ++it) {
    if (it->nStart <= start_idx && start_idx < it->nStart + it->nCount)
      break;
  }
  if (it == text_piece_info_.end())
    return std::vector<CFX_RectF>();

  int32_t end_idx = start_idx + count - 1;
  std::vector<CFX_RectF> rects;
  while (it != text_piece_info_.end()) {
    // If we end inside the current piece, extract what we need and we're done.
    if (it->nStart <= end_idx && end_idx < it->nStart + it->nCount) {
      std::vector<CFX_RectF> arr = GetCharRects(*it);
      CFX_RectF piece = arr[0];
      piece.Union(arr[end_idx - it->nStart]);
      rects.push_back(piece);
      break;
    }
    rects.push_back(it->rtPiece);
    ++it;
  }

  return rects;
}

std::pair<size_t, size_t> CFDE_TextEditEngine::BoundsForWordAt(
    size_t idx) const {
  if (idx > text_length_)
    return {0, 0};

  CFDE_TextEditEngine::Iterator iter(this);
  iter.SetAt(idx);

  size_t start_idx = iter.FindNextBreakPos(true);
  size_t end_idx = iter.FindNextBreakPos(false);
  return {start_idx, end_idx - start_idx + 1};
}

CFDE_TextEditEngine::Iterator::Iterator(const CFDE_TextEditEngine* engine)
    : engine_(engine) {}

CFDE_TextEditEngine::Iterator::~Iterator() = default;

void CFDE_TextEditEngine::Iterator::Next(bool bPrev) {
  if (bPrev && current_position_ == -1)
    return;
  if (!bPrev && current_position_ > -1 &&
      static_cast<size_t>(current_position_) == engine_->GetLength()) {
    return;
  }

  if (bPrev)
    --current_position_;
  else
    ++current_position_;
}

wchar_t CFDE_TextEditEngine::Iterator::GetChar() const {
  return engine_->GetChar(current_position_);
}

void CFDE_TextEditEngine::Iterator::SetAt(size_t nIndex) {
  nIndex = std::min(nIndex, engine_->GetLength());
  current_position_ = pdfium::base::checked_cast<int32_t>(nIndex);
}

bool CFDE_TextEditEngine::Iterator::IsEOF(bool bPrev) const {
  return bPrev ? current_position_ == -1
               : current_position_ > -1 &&
                     static_cast<size_t>(current_position_) ==
                         engine_->GetLength();
}

size_t CFDE_TextEditEngine::Iterator::FindNextBreakPos(bool bPrev) {
  if (IsEOF(bPrev))
    return current_position_ > -1 ? current_position_ : 0;

  WordBreakProperty ePreType = WordBreakProperty::kNone;
  if (!IsEOF(!bPrev)) {
    Next(!bPrev);
    ePreType = FX_GetWordBreakProperty(GetChar());
    Next(bPrev);
  }

  WordBreakProperty eCurType = FX_GetWordBreakProperty(GetChar());
  bool bFirst = true;
  while (!IsEOF(bPrev)) {
    Next(bPrev);

    WordBreakProperty eNextType = FX_GetWordBreakProperty(GetChar());
    bool wBreak = FX_CheckStateChangeForWordBreak(eCurType, eNextType);
    if (wBreak) {
      if (IsEOF(bPrev)) {
        Next(!bPrev);
        break;
      }
      if (bFirst) {
        int32_t nFlags = GetBreakFlagsFor(eCurType, eNextType);
        if (nFlags > 0) {
          if (BreakFlagsChanged(nFlags, ePreType)) {
            Next(!bPrev);
            break;
          }
          Next(bPrev);
          wBreak = false;
        }
      }
      if (wBreak) {
        int32_t nFlags = GetBreakFlagsFor(eNextType, eCurType);
        if (nFlags <= 0) {
          Next(!bPrev);
          break;
        }

        Next(bPrev);
        eNextType = FX_GetWordBreakProperty(GetChar());
        if (BreakFlagsChanged(nFlags, eNextType)) {
          Next(!bPrev);
          Next(!bPrev);
          break;
        }
      }
    }
    eCurType = eNextType;
    bFirst = false;
  }
  return current_position_ > -1 ? current_position_ : 0;
}
