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

#ifndef COMPONENTS_OMNIBOX_BROWSER_ACTIONS_OMNIBOX_PEDAL_H_
#define COMPONENTS_OMNIBOX_BROWSER_ACTIONS_OMNIBOX_PEDAL_H_

#include <vector>

#include "base/gtest_prod_util.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/omnibox/browser/actions/omnibox_action.h"
#include "components/omnibox/browser/actions/omnibox_pedal_concepts.h"
#include "components/omnibox/browser/buildflags.h"
#include "url/gurl.h"

// Conceptually, a Pedal is a fixed action that can be taken by the user
// pressing a button or taking a new dedicated suggestion when some
// associated intention is detected in an omnibox match suggestion.
// The typical action is to navigate to an internal Chrome surface
// like settings, but other actions like translation or launching
// an incognito window are possible.  The intention is detected by
// checking trigger queries against suggested match queries.
class OmniboxPedal : public OmniboxAction {
 public:
  struct Token {
    // Token identifier from the common token dictionary.
    int id;

    // Index of the next unconsumed token node. Initially this is set to
    // the token's own index, indicating this token is unconsumed. Calls
    // to |TokenSequence::Consume| may then update it to greater values,
    // indicating that this token is consumed.
    size_t link;
  };

  // This is a specialized container for the sequence matching algorithm.
  // It is intended only for single-threaded access by OmniboxPedal classes.
  class TokenSequence {
   public:
    // Construct with reserved size; used when loading real data.
    explicit TokenSequence(size_t reserve_size);

    // Construct with given sequence of |token_ids|; used by tests.
    explicit TokenSequence(std::vector<int> token_ids);

    // Don't use copies. They were necessary with old algorithm,
    // but this structure is amenable to efficient resets on kept instances.
    TokenSequence(const TokenSequence&) = delete;
    TokenSequence& operator=(const TokenSequence&) = delete;
    TokenSequence(TokenSequence&&);
    TokenSequence& operator=(TokenSequence&&);
    ~TokenSequence();

    // Returns true if all tokens are consumed (true for empty sequences).
    bool IsFullyConsumed();

    // Returns the number of unconsumed tokens remaining. Used by tests.
    size_t CountUnconsumed() const;

    // Add token with given |id| to sequence.
    void Add(int id);

    // Clears all tokens from this sequence.
    inline void Clear() { tokens_.clear(); }

    // Initializes all links in sequence to their own index, indicating
    // unconsumed state for all. This is needed after calls to Erase.
    void ResetLinks();

    // Removes one or more instances of |erase_sequence| from this sequence
    // by erasing token items from the |tokens_| container.
    // Returns true if this sequence was changed; false if no match is found.
    bool Erase(const TokenSequence& erase_sequence, bool erase_only_once);

    // Consumes one or more instances of |consume_sequence| from this sequence
    // by updating links on matching tokens. The container is not modified,
    // only its contained elements may be mutated.
    // Returns true if matches were found and consumed; false if no match.
    bool Consume(const TokenSequence& consume_sequence, bool consume_only_once);

    // Returns the total number of tokens, regardless of consumed status.
    inline size_t Size() const { return tokens_.size(); }

    // Returns collection memory estimate for tracing.
    size_t EstimateMemoryUsage() const;

   private:
    // Returns true if this sequence, starting at |index|, matches given
    // |sequence|. The |index_mask| can be used to disregard consumed status (0)
    // or require that all tokens must be unconsumed to match (~0).
    bool MatchesAt(const TokenSequence& sequence,
                   size_t index,
                   size_t index_mask) const;

    // Follows links on tokens starting at |from_index| and returns the first
    // unconsumed index. Returns |Size()| if no unconsumed token is found.
    size_t WalkToUnconsumedIndexFrom(size_t from_index);

    // Storage for tokens.
    std::vector<Token> tokens_;
  };

  struct SynonymGroupSpec {
    bool required;
    bool match_once;
    int message_id;
  };

  class SynonymGroup {
   public:
    // Note: synonyms must be specified in decreasing order by length
    // so that longest matches will be detected first.  For example,
    // "incognito window" must come before "incognito" so that the " window"
    // part will also be covered by this group -- otherwise it would be left
    // intact and wrongly treated as uncovered by the checking algorithm.
    // See OmniboxPedal::IsConceptMatch for the logic that necessitates order.
    SynonymGroup(bool required, bool match_once, size_t reserve_size);
    SynonymGroup(SynonymGroup&&);
    ~SynonymGroup();
    SynonymGroup(const SynonymGroup&) = delete;
    SynonymGroup& operator=(const SynonymGroup&) = delete;
    SynonymGroup& operator=(SynonymGroup&&);

    // Removes one or more matching synonyms from given |remaining| sequence if
    // any are found.  Returns true if checking may continue; false if no more
    // checking is required because what remains cannot be a concept match.
    // Note, if |fully_erase| is true and this method returns true, the
    // |remaining| container has changed structure so ResetLinks must be called.
    // This method doesn't call ResetLinks in that case, for efficiency.
    bool EraseMatchesIn(TokenSequence& remaining, bool fully_erase) const;

    // Add a synonym token sequence to this group.
    void AddSynonym(TokenSequence synonym);

    // When runtime data was preprocessed by pedal_processor,
    // it avoided the need to sort at runtime in Chromium, but with
    // the TC-based l10n technique, data loading needs to be robust
    // enough to handle various forms and orders in translation data.
    // Hence, a call to `SortSynonyms` is required after all calls
    // to `AddSynonym` are complete. We may eliminate this step
    // if we implement post-processing of the .xtb translation files.
    void SortSynonyms();

    // Estimates RAM usage in bytes for this synonym group.
    size_t EstimateMemoryUsage() const;

    // Erases sequences in ignore group from all synonyms in this group.
    void EraseIgnoreGroup(const SynonymGroup& ignore_group);

    // Returns true if this synonym group contains nontrivial data that can
    // be used by the matching algorithm.
    bool IsValid() const;

   protected:
    // If this is true, a synonym of the group must be present for triggering.
    // If false, then presence is simply allowed and does not inhibit triggering
    // (any text not covered by groups would stop trigger).
    bool required_;

    // If this is true, then only the rightmost instance of first synonym found
    // will be taken from text being checked, and additional instances will
    // inhibit trigger because repetition actually changes meaning.  If false,
    // then all instances of all synonyms are taken (repetition is meaningless).
    bool match_once_;

    // The set of interchangeable alternative representations for this group:
    // when trying to clear browsing data, a user may think of 'erase', 'clear',
    // 'delete', etc.  Even though these are not strictly synonymous in natural
    // language, they are considered equivalent within the context of intention
    // to perform this Pedal's action.
    std::vector<TokenSequence> synonyms_;
  };

  OmniboxPedal(OmniboxPedalId id, LabelStrings strings, GURL url);

  // Downcasts the given OmniboxAction to an OmniboxPedal if the supplied
  // instance represents one, otherwise returns nullptr.
  static const OmniboxPedal* FromAction(const OmniboxAction* action);

  // Called after the OmniboxPedalProvider finishes loading all pedals data.
  // This can be used to override implementation bits based on flags, etc.
  virtual void OnLoaded();

  // Sets the destination URL for the Pedal.
  void SetNavigationUrl(const GURL& url);

#if defined(SUPPORT_PEDALS_VECTOR_ICONS)
  // Returns the default vector icon to use for Pedals that do not specify one.
  static const gfx::VectorIcon& GetDefaultVectorIcon();
#endif

  // Add a verbatim token sequence for direct matching.
  // This can improve user experience of omnibox pedals by ensuring that
  // button text entered verbatim is always a sufficient trigger. Since
  // button text labels are not always within the specified set of triggers,
  // it may be possible to discover a pedal, memorize the button
  // label, and then go seeking it out again with the button label, but
  // not find it. With the verbatim sequence, learning a pedal by label will
  // always make the pedal available with that exact input (ignoring case and
  // the common ignore group).
  void AddVerbatimSequence(TokenSequence sequence);

  // Move a synonym group into this Pedal's collection.
  void AddSynonymGroup(SynonymGroup&& group);

  // Specify synonym groups to load from localization strings.
  // `locale_is_english` provides a hint about which locale is being loaded,
  // used to support both synonym-groups and whole-phrase localization.
  virtual std::vector<SynonymGroupSpec> SpecifySynonymGroups(
      bool locale_is_english) const;

  OmniboxPedalId PedalId() const { return id_; }

  // Sometimes pedals report different IDs for metrics, either to enable
  // feature discrimination (e.g. incognito mode) or to unify metrics
  // of closely related pedals (e.g. a ChromeOS specialization of a pedal).
  virtual OmniboxPedalId GetMetricsId() const;

  // If a sufficient set of triggering synonym groups are present in
  // match_sequence then it's a concept match and this returns true.  If a
  // required group is not present, or if match_sequence contains extraneous
  // tokens not covered by any synonym group, then it's not a concept match and
  // this returns false. |match_sequence| is consumed/mutated by this method.
  bool IsConceptMatch(TokenSequence& match_sequence) const;

  // OmniboxAction overrides:
  void RecordActionShown(size_t position, bool executed) const override;
#if defined(SUPPORT_PEDALS_VECTOR_ICONS)
  const gfx::VectorIcon& GetVectorIcon() const override;
#endif
  size_t EstimateMemoryUsage() const override;
  OmniboxActionId ActionId() const override;

#if BUILDFLAG(IS_ANDROID)
  base::android::ScopedJavaLocalRef<jobject> GetOrCreateJavaObject(
      JNIEnv* env) const override;
#endif

 protected:
  FRIEND_TEST_ALL_PREFIXES(OmniboxPedalTest, SynonymGroupErasesFirstMatchOnly);
  FRIEND_TEST_ALL_PREFIXES(OmniboxPedalTest, SynonymGroupsDriveConceptMatches);
  FRIEND_TEST_ALL_PREFIXES(OmniboxPedalTest,
                           VerbatimSynonymGroupDrivesConceptMatches);
  FRIEND_TEST_ALL_PREFIXES(OmniboxPedalImplementationsTest,
                           UnorderedSynonymExpressionsAreConceptMatches);

  ~OmniboxPedal() override;

  OmniboxPedalId id_;

  // Before standard synonym group matching, we can check the verbatim
  // syonym group for direct matches, e.g. with the button label text.
  SynonymGroup verbatim_synonym_group_;

  std::vector<SynonymGroup> synonym_groups_;
};

// This is a simple pedal suitable only for use by tests.
class TestOmniboxPedalClearBrowsingData : public OmniboxPedal {
 public:
  explicit TestOmniboxPedalClearBrowsingData();

  std::vector<SynonymGroupSpec> SpecifySynonymGroups(
      bool locale_is_english) const override;

 protected:
  ~TestOmniboxPedalClearBrowsingData() override = default;
};

#endif  // COMPONENTS_OMNIBOX_BROWSER_ACTIONS_OMNIBOX_PEDAL_H_
