/*
 * Copyright (C) 2009 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "web/TextFinder.h"

#include "core/dom/Range.h"
#include "core/dom/shadow/ShadowRoot.h"
#include "core/editing/Editor.h"
#include "core/editing/VisibleSelection.h"
#include "core/editing/iterators/SearchBuffer.h"
#include "core/editing/markers/DocumentMarker.h"
#include "core/editing/markers/DocumentMarkerController.h"
#include "core/frame/FrameView.h"
#include "core/layout/LayoutObject.h"
#include "core/layout/TextAutosizer.h"
#include "core/page/Page.h"
#include "modules/accessibility/AXObject.h"
#include "modules/accessibility/AXObjectCacheImpl.h"
#include "platform/RuntimeEnabledFeatures.h"
#include "platform/Timer.h"
#include "public/platform/WebVector.h"
#include "public/web/WebAXObject.h"
#include "public/web/WebFindOptions.h"
#include "public/web/WebFrameClient.h"
#include "public/web/WebViewClient.h"
#include "web/FindInPageCoordinates.h"
#include "web/WebLocalFrameImpl.h"
#include "web/WebViewImpl.h"
#include "wtf/CurrentTime.h"

namespace blink {

TextFinder::FindMatch::FindMatch(Range* range, int ordinal)
    : m_range(range), m_ordinal(ordinal) {}

DEFINE_TRACE(TextFinder::FindMatch) {
  visitor->trace(m_range);
}

class TextFinder::DeferredScopeStringMatches
    : public GarbageCollectedFinalized<TextFinder::DeferredScopeStringMatches> {
 public:
  static DeferredScopeStringMatches* create(TextFinder* textFinder,
                                            int identifier,
                                            const WebString& searchText,
                                            const WebFindOptions& options,
                                            bool reset) {
    return new DeferredScopeStringMatches(textFinder, identifier, searchText,
                                          options, reset);
  }

  DEFINE_INLINE_TRACE() { visitor->trace(m_textFinder); }

  void dispose() { m_timer.stop(); }

 private:
  DeferredScopeStringMatches(TextFinder* textFinder,
                             int identifier,
                             const WebString& searchText,
                             const WebFindOptions& options,
                             bool reset)
      : m_timer(this, &DeferredScopeStringMatches::doTimeout),
        m_textFinder(textFinder),
        m_identifier(identifier),
        m_searchText(searchText),
        m_options(options),
        m_reset(reset) {
    m_timer.startOneShot(0.0, BLINK_FROM_HERE);
  }

  void doTimeout(TimerBase*) {
    m_textFinder->callScopeStringMatches(this, m_identifier, m_searchText,
                                         m_options, m_reset);
  }

  Timer<DeferredScopeStringMatches> m_timer;
  Member<TextFinder> m_textFinder;
  const int m_identifier;
  const WebString m_searchText;
  const WebFindOptions m_options;
  const bool m_reset;
};

bool TextFinder::find(int identifier,
                      const WebString& searchText,
                      const WebFindOptions& options,
                      bool wrapWithinFrame,
                      bool* activeNow) {
  if (!options.findNext)
    unmarkAllTextMatches();
  else
    setMarkerActive(m_activeMatch.get(), false);

  if (m_activeMatch &&
      &m_activeMatch->ownerDocument() != ownerFrame().frame()->document())
    m_activeMatch = nullptr;

  // If the user has selected something since the last Find operation we want
  // to start from there. Otherwise, we start searching from where the last Find
  // operation left off (either a Find or a FindNext operation).
  VisibleSelection selection(ownerFrame().frame()->selection().selection());
  bool activeSelection = !selection.isNone();
  if (activeSelection) {
    m_activeMatch = firstRangeOf(selection);
    ownerFrame().frame()->selection().clear();
  }

  DCHECK(ownerFrame().frame());
  DCHECK(ownerFrame().frame()->view());
  const FindOptions findOptions =
      (options.forward ? 0 : Backwards) |
      (options.matchCase ? 0 : CaseInsensitive) |
      (wrapWithinFrame ? WrapAround : 0) |
      (options.wordStart ? AtWordStarts : 0) |
      (options.medialCapitalAsWordStart ? TreatMedialCapitalAsWordStart : 0) |
      (options.findNext ? 0 : StartInSelection);
  m_activeMatch = ownerFrame().frame()->editor().findStringAndScrollToVisible(
      searchText, m_activeMatch.get(), findOptions);

  if (!m_activeMatch) {
    // If we're finding next the next active match might not be in the current
    // frame.  In this case we don't want to clear the matches cache.
    if (!options.findNext)
      clearFindMatchesCache();

    ownerFrame().frameView()->invalidatePaintForTickmarks();
    return false;
  }

  // If the user is browsing a page with autosizing, adjust the zoom to the
  // column where the next hit has been found. Doing this when autosizing is
  // not set will result in a zoom reset on small devices.
  if (ownerFrame()
          .frame()
          ->document()
          ->textAutosizer()
          ->pageNeedsAutosizing()) {
    ownerFrame().viewImpl()->zoomToFindInPageRect(
        ownerFrame().frameView()->contentsToRootFrame(
            enclosingIntRect(LayoutObject::absoluteBoundingBoxRectForRange(
                m_activeMatch.get()))));
  }

  bool wasActiveFrame = m_currentActiveMatchFrame;
  m_currentActiveMatchFrame = true;

  bool isActive = setMarkerActive(m_activeMatch.get(), true);
  if (activeNow)
    *activeNow = isActive;

  // Make sure no node is focused. See http://crbug.com/38700.
  ownerFrame().frame()->document()->clearFocusedElement();

  // Set this frame as focused.
  ownerFrame().viewImpl()->setFocusedFrame(&ownerFrame());

  if (!options.findNext || activeSelection || !isActive) {
    // This is either an initial Find operation, a Find-next from a new
    // start point due to a selection, or new matches were found during
    // Find-next due to DOM alteration (that couldn't be set as active), so
    // we set the flag to ask the scoping effort to find the active rect for
    // us and report it back to the UI.
    m_locatingActiveRect = true;
  } else {
    if (!wasActiveFrame) {
      if (options.forward)
        m_activeMatchIndex = 0;
      else
        m_activeMatchIndex = m_lastMatchCount - 1;
    } else {
      if (options.forward)
        ++m_activeMatchIndex;
      else
        --m_activeMatchIndex;

      if (m_activeMatchIndex + 1 > m_lastMatchCount)
        m_activeMatchIndex = 0;
      else if (m_activeMatchIndex < 0)
        m_activeMatchIndex = m_lastMatchCount - 1;
    }
    WebRect selectionRect = ownerFrame().frameView()->contentsToRootFrame(
        m_activeMatch->boundingBox());
    reportFindInPageSelection(selectionRect, m_activeMatchIndex + 1,
                              identifier);
  }

  // We found something, so the result of the previous scoping may be outdated.
  m_lastFindRequestCompletedWithNoMatches = false;

  return true;
}

void TextFinder::clearActiveFindMatch() {
  m_currentActiveMatchFrame = false;
  setMarkerActive(m_activeMatch.get(), false);
  resetActiveMatch();
}

void TextFinder::stopFindingAndClearSelection() {
  cancelPendingScopingEffort();

  // Remove all markers for matches found and turn off the highlighting.
  ownerFrame().frame()->document()->markers().removeMarkers(
      DocumentMarker::TextMatch);
  ownerFrame().frame()->editor().setMarkedTextMatchesAreHighlighted(false);
  clearFindMatchesCache();
  resetActiveMatch();

  // Let the frame know that we don't want tickmarks anymore.
  ownerFrame().frameView()->invalidatePaintForTickmarks();
}

void TextFinder::reportFindInPageResultToAccessibility(int identifier) {
  if (!m_activeMatch)
    return;

  AXObjectCacheImpl* axObjectCache = toAXObjectCacheImpl(
      ownerFrame().frame()->document()->existingAXObjectCache());
  if (!axObjectCache)
    return;

  AXObject* startObject = axObjectCache->get(m_activeMatch->startContainer());
  AXObject* endObject = axObjectCache->get(m_activeMatch->endContainer());
  if (!startObject || !endObject)
    return;

  // Notify the client of new text marker data.
  axObjectCache->postNotification(
      startObject, AXObjectCache::AXNotification::AXChildrenChanged);
  if (startObject != endObject)
    axObjectCache->postNotification(
        endObject, AXObjectCache::AXNotification::AXChildrenChanged);

  if (ownerFrame().client()) {
    ownerFrame().client()->handleAccessibilityFindInPageResult(
        identifier, m_activeMatchIndex + 1, WebAXObject(startObject),
        m_activeMatch->startOffset(), WebAXObject(endObject),
        m_activeMatch->endOffset());
  }
}

void TextFinder::scopeStringMatches(int identifier,
                                    const WebString& searchText,
                                    const WebFindOptions& options,
                                    bool reset) {
  // TODO(dglazkov): The reset/continue cases need to be untangled into two
  // separate functions. This collation of logic is unnecessary and adds to
  // overall complexity of the code.
  if (reset) {
    // This is a brand new search, so we need to reset everything.
    // Scoping is just about to begin.
    m_scopingInProgress = true;

    // Need to keep the current identifier locally in order to finish the
    // request in case the frame is detached during the process.
    m_findRequestIdentifier = identifier;

    // Clear highlighting for this frame.
    unmarkAllTextMatches();

    // Clear the tickmarks and results cache.
    clearFindMatchesCache();

    // Clear the counters from last operation.
    m_lastMatchCount = 0;
    m_nextInvalidateAfter = 0;
    m_resumeScopingFromRange = nullptr;

    // The view might be null on detached frames.
    LocalFrame* frame = ownerFrame().frame();
    if (frame && frame->page())
      m_frameScoping = true;

    // Now, defer scoping until later to allow find operation to finish quickly.
    scopeStringMatchesSoon(
        identifier, searchText, options,
        false);  // false means just reset, so don't do it again.
    return;
  }

  if (!shouldScopeMatches(searchText, options)) {
    finishCurrentScopingEffort(identifier);
    return;
  }

  PositionInFlatTree searchStart =
      PositionInFlatTree::firstPositionInNode(ownerFrame().frame()->document());
  PositionInFlatTree searchEnd =
      PositionInFlatTree::lastPositionInNode(ownerFrame().frame()->document());
  DCHECK_EQ(searchStart.document(), searchEnd.document());

  if (m_resumeScopingFromRange) {
    // This is a continuation of a scoping operation that timed out and didn't
    // complete last time around, so we should start from where we left off.
    DCHECK(m_resumeScopingFromRange->collapsed());
    searchStart = fromPositionInDOMTree<EditingInFlatTreeStrategy>(
        m_resumeScopingFromRange->endPosition());
    if (searchStart.document() != searchEnd.document())
      return;
  }

  // TODO(dglazkov): The use of updateStyleAndLayoutIgnorePendingStylesheets
  // needs to be audited.  see http://crbug.com/590369 for more details.
  searchStart.document()->updateStyleAndLayoutIgnorePendingStylesheets();

  // This timeout controls how long we scope before releasing control. This
  // value does not prevent us from running for longer than this, but it is
  // periodically checked to see if we have exceeded our allocated time.
  const double maxScopingDuration = 0.1;  // seconds

  int matchCount = 0;
  bool timedOut = false;
  double startTime = currentTime();
  do {
    // Find next occurrence of the search string.
    // FIXME: (http://crbug.com/6818) This WebKit operation may run for longer
    // than the timeout value, and is not interruptible as it is currently
    // written. We may need to rewrite it with interruptibility in mind, or
    // find an alternative.
    const EphemeralRangeInFlatTree result =
        findPlainText(EphemeralRangeInFlatTree(searchStart, searchEnd),
                      searchText, options.matchCase ? 0 : CaseInsensitive);
    if (result.isCollapsed()) {
      // Not found.
      break;
    }
    Range* resultRange = Range::create(
        result.document(), toPositionInDOMTree(result.startPosition()),
        toPositionInDOMTree(result.endPosition()));
    if (resultRange->collapsed()) {
      // resultRange will be collapsed if the matched text spans over multiple
      // TreeScopes.  FIXME: Show such matches to users.
      searchStart = result.endPosition();
      continue;
    }

    ++matchCount;

    // Catch a special case where Find found something but doesn't know what
    // the bounding box for it is. In this case we set the first match we find
    // as the active rect.
    IntRect resultBounds = resultRange->boundingBox();
    IntRect activeSelectionRect;
    if (m_locatingActiveRect) {
      activeSelectionRect =
          m_activeMatch.get() ? m_activeMatch->boundingBox() : resultBounds;
    }

    // If the Find function found a match it will have stored where the
    // match was found in m_activeSelectionRect on the current frame. If we
    // find this rect during scoping it means we have found the active
    // tickmark.
    bool foundActiveMatch = false;
    if (m_locatingActiveRect && (activeSelectionRect == resultBounds)) {
      // We have found the active tickmark frame.
      m_currentActiveMatchFrame = true;
      foundActiveMatch = true;
      // We also know which tickmark is active now.
      m_activeMatchIndex = matchCount - 1;
      // To stop looking for the active tickmark, we set this flag.
      m_locatingActiveRect = false;

      // Notify browser of new location for the selected rectangle.
      reportFindInPageSelection(
          ownerFrame().frameView()->contentsToRootFrame(resultBounds),
          m_activeMatchIndex + 1, identifier);
    }

    ownerFrame().frame()->document()->markers().addTextMatchMarker(
        EphemeralRange(resultRange), foundActiveMatch);

    m_findMatchesCache.append(
        FindMatch(resultRange, m_lastMatchCount + matchCount));

    // Set the new start for the search range to be the end of the previous
    // result range. There is no need to use a VisiblePosition here,
    // since findPlainText will use a TextIterator to go over the visible
    // text nodes.
    searchStart = result.endPosition();

    m_resumeScopingFromRange = Range::create(
        result.document(), toPositionInDOMTree(result.endPosition()),
        toPositionInDOMTree(result.endPosition()));
    timedOut = (currentTime() - startTime) >= maxScopingDuration;
  } while (!timedOut);

  // Remember what we search for last time, so we can skip searching if more
  // letters are added to the search string (and last outcome was 0).
  m_lastSearchString = searchText;

  if (matchCount > 0) {
    ownerFrame().frame()->editor().setMarkedTextMatchesAreHighlighted(true);

    m_lastMatchCount += matchCount;

    // Let the frame know how many matches we found during this pass.
    ownerFrame().increaseMatchCount(matchCount, identifier);
  }

  if (timedOut) {
    // If we found anything during this pass, we should redraw. However, we
    // don't want to spam too much if the page is extremely long, so if we
    // reach a certain point we start throttling the redraw requests.
    if (matchCount > 0)
      invalidateIfNecessary();

    // Scoping effort ran out of time, lets ask for another time-slice.
    scopeStringMatchesSoon(identifier, searchText, options,
                           false);  // don't reset.
    return;                         // Done for now, resume work later.
  }

  finishCurrentScopingEffort(identifier);
}

void TextFinder::flushCurrentScopingEffort(int identifier) {
  if (!ownerFrame().frame() || !ownerFrame().frame()->page())
    return;

  m_frameScoping = false;
  ownerFrame().increaseMatchCount(0, identifier);
}

void TextFinder::finishCurrentScopingEffort(int identifier) {
  flushCurrentScopingEffort(identifier);

  m_scopingInProgress = false;
  m_lastFindRequestCompletedWithNoMatches = !m_lastMatchCount;

  // This frame is done, so show any scrollbar tickmarks we haven't drawn yet.
  ownerFrame().frameView()->invalidatePaintForTickmarks();
}

void TextFinder::cancelPendingScopingEffort() {
  for (DeferredScopeStringMatches* deferredWork : m_deferredScopingWork)
    deferredWork->dispose();
  m_deferredScopingWork.clear();

  m_activeMatchIndex = -1;

  // Last request didn't complete.
  if (m_scopingInProgress)
    m_lastFindRequestCompletedWithNoMatches = false;

  m_scopingInProgress = false;
}

void TextFinder::increaseMatchCount(int identifier, int count) {
  if (count)
    ++m_findMatchMarkersVersion;

  m_totalMatchCount += count;

  // Update the UI with the latest findings.
  if (ownerFrame().client())
    ownerFrame().client()->reportFindInPageMatchCount(
        identifier, m_totalMatchCount, !m_frameScoping || !m_totalMatchCount);
}

void TextFinder::reportFindInPageSelection(const WebRect& selectionRect,
                                           int activeMatchOrdinal,
                                           int identifier) {
  // Update the UI with the latest selection rect.
  if (ownerFrame().client())
    ownerFrame().client()->reportFindInPageSelection(
        identifier, activeMatchOrdinal, selectionRect);

  // Update accessibility too, so if the user commits to this query
  // we can move accessibility focus to this result.
  reportFindInPageResultToAccessibility(identifier);
}

void TextFinder::resetMatchCount() {
  if (m_totalMatchCount > 0)
    ++m_findMatchMarkersVersion;

  m_totalMatchCount = 0;
  m_frameScoping = false;
}

void TextFinder::clearFindMatchesCache() {
  if (!m_findMatchesCache.isEmpty())
    ++m_findMatchMarkersVersion;

  m_findMatchesCache.clear();
  m_findMatchRectsAreValid = false;
}

void TextFinder::updateFindMatchRects() {
  IntSize currentContentsSize = ownerFrame().contentsSize();
  if (m_contentsSizeForCurrentFindMatchRects != currentContentsSize) {
    m_contentsSizeForCurrentFindMatchRects = currentContentsSize;
    m_findMatchRectsAreValid = false;
  }

  size_t deadMatches = 0;
  for (FindMatch& match : m_findMatchesCache) {
    if (!match.m_range->boundaryPointsValid() ||
        !match.m_range->startContainer()->isConnected())
      match.m_rect = FloatRect();
    else if (!m_findMatchRectsAreValid)
      match.m_rect = findInPageRectFromRange(match.m_range.get());

    if (match.m_rect.isEmpty())
      ++deadMatches;
  }

  // Remove any invalid matches from the cache.
  if (deadMatches) {
    HeapVector<FindMatch> filteredMatches;
    filteredMatches.reserveCapacity(m_findMatchesCache.size() - deadMatches);

    for (const FindMatch& match : m_findMatchesCache) {
      if (!match.m_rect.isEmpty())
        filteredMatches.append(match);
    }

    m_findMatchesCache.swap(filteredMatches);
  }

  // Invalidate the rects in child frames. Will be updated later during
  // traversal.
  if (!m_findMatchRectsAreValid)
    for (WebFrame* child = ownerFrame().firstChild(); child;
         child = child->nextSibling())
      toWebLocalFrameImpl(child)->ensureTextFinder().m_findMatchRectsAreValid =
          false;

  m_findMatchRectsAreValid = true;
}

WebFloatRect TextFinder::activeFindMatchRect() {
  if (!m_currentActiveMatchFrame || !m_activeMatch)
    return WebFloatRect();

  return WebFloatRect(findInPageRectFromRange(activeMatch()));
}

void TextFinder::findMatchRects(WebVector<WebFloatRect>& outputRects) {
  updateFindMatchRects();

  Vector<WebFloatRect> matchRects;
  matchRects.reserveCapacity(matchRects.size() + m_findMatchesCache.size());
  for (const FindMatch& match : m_findMatchesCache) {
    DCHECK(!match.m_rect.isEmpty());
    matchRects.append(match.m_rect);
  }

  outputRects = matchRects;
}

int TextFinder::selectNearestFindMatch(const WebFloatPoint& point,
                                       WebRect* selectionRect) {
  int index = nearestFindMatch(point, nullptr);
  if (index != -1)
    return selectFindMatch(static_cast<unsigned>(index), selectionRect);

  return -1;
}

int TextFinder::nearestFindMatch(const FloatPoint& point,
                                 float* distanceSquared) {
  updateFindMatchRects();

  int nearest = -1;
  float nearestDistanceSquared = FLT_MAX;
  for (size_t i = 0; i < m_findMatchesCache.size(); ++i) {
    DCHECK(!m_findMatchesCache[i].m_rect.isEmpty());
    FloatSize offset = point - m_findMatchesCache[i].m_rect.center();
    float width = offset.width();
    float height = offset.height();
    float currentDistanceSquared = width * width + height * height;
    if (currentDistanceSquared < nearestDistanceSquared) {
      nearest = i;
      nearestDistanceSquared = currentDistanceSquared;
    }
  }

  if (distanceSquared)
    *distanceSquared = nearestDistanceSquared;

  return nearest;
}

int TextFinder::selectFindMatch(unsigned index, WebRect* selectionRect) {
  SECURITY_DCHECK(index < m_findMatchesCache.size());

  Range* range = m_findMatchesCache[index].m_range;
  if (!range->boundaryPointsValid() || !range->startContainer()->isConnected())
    return -1;

  // Check if the match is already selected.
  if (!m_currentActiveMatchFrame || !m_activeMatch ||
      !areRangesEqual(m_activeMatch.get(), range)) {
    m_activeMatchIndex = m_findMatchesCache[index].m_ordinal - 1;

    // Set this frame as the active frame (the one with the active highlight).
    m_currentActiveMatchFrame = true;
    ownerFrame().viewImpl()->setFocusedFrame(&ownerFrame());

    if (m_activeMatch)
      setMarkerActive(m_activeMatch.get(), false);
    m_activeMatch = range;
    setMarkerActive(m_activeMatch.get(), true);

    // Clear any user selection, to make sure Find Next continues on from the
    // match we just activated.
    ownerFrame().frame()->selection().clear();

    // Make sure no node is focused. See http://crbug.com/38700.
    ownerFrame().frame()->document()->clearFocusedElement();
  }

  IntRect activeMatchRect;
  IntRect activeMatchBoundingBox = enclosingIntRect(
      LayoutObject::absoluteBoundingBoxRectForRange(m_activeMatch.get()));

  if (!activeMatchBoundingBox.isEmpty()) {
    if (m_activeMatch->firstNode() &&
        m_activeMatch->firstNode()->layoutObject()) {
      m_activeMatch->firstNode()->layoutObject()->scrollRectToVisible(
          LayoutRect(activeMatchBoundingBox),
          ScrollAlignment::alignCenterIfNeeded,
          ScrollAlignment::alignCenterIfNeeded, UserScroll);
    }

    // Zoom to the active match.
    activeMatchRect =
        ownerFrame().frameView()->contentsToRootFrame(activeMatchBoundingBox);
    ownerFrame().viewImpl()->zoomToFindInPageRect(activeMatchRect);
  }

  if (selectionRect)
    *selectionRect = activeMatchRect;

  return m_activeMatchIndex + 1;
}

TextFinder* TextFinder::create(WebLocalFrameImpl& ownerFrame) {
  return new TextFinder(ownerFrame);
}

TextFinder::TextFinder(WebLocalFrameImpl& ownerFrame)
    : m_ownerFrame(&ownerFrame),
      m_currentActiveMatchFrame(false),
      m_activeMatchIndex(-1),
      m_resumeScopingFromRange(nullptr),
      m_lastMatchCount(-1),
      m_totalMatchCount(-1),
      m_frameScoping(false),
      m_findRequestIdentifier(-1),
      m_nextInvalidateAfter(0),
      m_findMatchMarkersVersion(0),
      m_locatingActiveRect(false),
      m_scopingInProgress(false),
      m_lastFindRequestCompletedWithNoMatches(false),
      m_findMatchRectsAreValid(false) {}

TextFinder::~TextFinder() {}

bool TextFinder::setMarkerActive(Range* range, bool active) {
  if (!range || range->collapsed())
    return false;
  return ownerFrame().frame()->document()->markers().setMarkersActive(
      EphemeralRange(range), active);
}

void TextFinder::unmarkAllTextMatches() {
  LocalFrame* frame = ownerFrame().frame();
  if (frame && frame->page() &&
      frame->editor().markedTextMatchesAreHighlighted())
    frame->document()->markers().removeMarkers(DocumentMarker::TextMatch);
}

bool TextFinder::shouldScopeMatches(const String& searchText,
                                    const WebFindOptions& options) {
  // Don't scope if we can't find a frame or a view.
  // The user may have closed the tab/application, so abort.
  LocalFrame* frame = ownerFrame().frame();
  if (!frame || !frame->view() || !frame->page())
    return false;

  DCHECK(frame->document());
  DCHECK(frame->view());

  if (options.force)
    return true;

  if (!ownerFrame().hasVisibleContent())
    return false;

  // If the frame completed the scoping operation and found 0 matches the last
  // time it was searched, then we don't have to search it again if the user is
  // just adding to the search string or sending the same search string again.
  if (m_lastFindRequestCompletedWithNoMatches &&
      !m_lastSearchString.isEmpty()) {
    // Check to see if the search string prefixes match.
    String previousSearchPrefix =
        searchText.substring(0, m_lastSearchString.length());

    if (previousSearchPrefix == m_lastSearchString)
      return false;  // Don't search this frame, it will be fruitless.
  }

  return true;
}

void TextFinder::scopeStringMatchesSoon(int identifier,
                                        const WebString& searchText,
                                        const WebFindOptions& options,
                                        bool reset) {
  m_deferredScopingWork.append(DeferredScopeStringMatches::create(
      this, identifier, searchText, options, reset));
}

void TextFinder::callScopeStringMatches(DeferredScopeStringMatches* caller,
                                        int identifier,
                                        const WebString& searchText,
                                        const WebFindOptions& options,
                                        bool reset) {
  size_t index = m_deferredScopingWork.find(caller);
  m_deferredScopingWork.remove(index);

  scopeStringMatches(identifier, searchText, options, reset);
}

void TextFinder::invalidateIfNecessary() {
  if (m_lastMatchCount <= m_nextInvalidateAfter)
    return;

  // FIXME: (http://crbug.com/6819) Optimize the drawing of the tickmarks and
  // remove this. This calculation sets a milestone for when next to
  // invalidate the scrollbar and the content area. We do this so that we
  // don't spend too much time drawing the scrollbar over and over again.
  // Basically, up until the first 500 matches there is no throttle.
  // After the first 500 matches, we set set the milestone further and
  // further out (750, 1125, 1688, 2K, 3K).
  static const int startSlowingDownAfter = 500;
  static const int slowdown = 750;

  int i = m_lastMatchCount / startSlowingDownAfter;
  m_nextInvalidateAfter += i * slowdown;
  ownerFrame().frameView()->invalidatePaintForTickmarks();
}

void TextFinder::flushCurrentScoping() {
  flushCurrentScopingEffort(m_findRequestIdentifier);
}

DEFINE_TRACE(TextFinder) {
  visitor->trace(m_ownerFrame);
  visitor->trace(m_activeMatch);
  visitor->trace(m_resumeScopingFromRange);
  visitor->trace(m_deferredScopingWork);
  visitor->trace(m_findMatchesCache);
}

}  // namespace blink
