// Copyright 2012 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/download/download_query.h"

#include <stdint.h>

#include <algorithm>
#include <limits>
#include <memory>
#include <string>
#include <vector>

#include "base/check_op.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/i18n/case_conversion.h"
#include "base/i18n/string_search.h"
#include "base/i18n/time_formatting.h"
#include "base/memory/raw_ref.h"
#include "base/notreached.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_ostream_operators.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/download/public/common/download_item.h"
#include "components/url_formatter/url_formatter.h"
#include "third_party/re2/src/re2/re2.h"
#include "url/gurl.h"

using download::DownloadDangerType;
using download::DownloadItem;

namespace {

// Templatized base::Value::GetAs*().
template <typename T> bool GetAs(const base::Value& in, T* out);
template<> bool GetAs(const base::Value& in, bool* out) {
  if (out && in.is_bool()) {
    *out = in.GetBool();
    return true;
  }
  return in.is_bool();
}
template <>
bool GetAs(const base::Value& in, double* out) {
  // `GetIfDouble()` incapsulates type verification logic.
  const std::optional<double> maybe_value = in.GetIfDouble();
  if (maybe_value.has_value()) {
    *out = maybe_value.value();
    return true;
  }
  return false;
}
template<> bool GetAs(const base::Value& in, std::string* out) {
  auto is_string = in.is_string();
  if (is_string)
    *out = in.GetString();
  return is_string;
}
template <>
bool GetAs(const base::Value& in, std::u16string* out) {
  auto is_string = in.is_string();
  if (is_string)
    *out = base::UTF8ToUTF16(in.GetString());
  return is_string;
}
template <>
bool GetAs(const base::Value& in, std::vector<std::u16string>* out) {
  out->clear();
  if (!in.is_list())
    return false;
  for (const auto& value : in.GetList()) {
    if (!value.is_string()) {
      out->clear();
      return false;
    }
    out->push_back(base::UTF8ToUTF16(value.GetString()));
  }
  return true;
}

// The next several functions are helpers for making Callbacks that access
// DownloadItem fields.

int64_t GetStartTimeMsEpoch(const DownloadItem& item) {
  return (item.GetStartTime() - base::Time::UnixEpoch()).InMilliseconds();
}

int64_t GetEndTimeMsEpoch(const DownloadItem& item) {
  return (item.GetEndTime() - base::Time::UnixEpoch()).InMilliseconds();
}

std::string GetStartTime(const DownloadItem& item) {
  return base::TimeFormatAsIso8601(item.GetStartTime());
}

std::string GetEndTime(const DownloadItem& item) {
  return base::TimeFormatAsIso8601(item.GetEndTime());
}

bool GetDangerAccepted(const DownloadItem& item) {
  return (item.GetDangerType() ==
          download::DOWNLOAD_DANGER_TYPE_USER_VALIDATED);
}

bool GetExists(const DownloadItem& item) {
  return !item.GetFileExternallyRemoved();
}

std::u16string GetFilename(const DownloadItem& item) {
  // This filename will be compared with strings that could be passed in by the
  // user, who only sees LossyDisplayNames.
  return item.GetTargetFilePath().LossyDisplayName();
}

std::string GetFilenameUTF8(const DownloadItem& item) {
  return base::UTF16ToUTF8(GetFilename(item));
}

std::string GetOriginalUrl(const DownloadItem& item) {
  return item.GetOriginalUrl().spec();
}

std::string GetUrl(const DownloadItem& item) {
  return item.GetURL().spec();
}

DownloadItem::DownloadState GetState(const DownloadItem& item) {
  return item.GetState();
}

DownloadDangerType GetDangerType(const DownloadItem& item) {
  return item.GetDangerType();
}

double GetReceivedBytes(const DownloadItem& item) {
  return item.GetReceivedBytes();
}

double GetTotalBytes(const DownloadItem& item) {
  return item.GetTotalBytes();
}

std::string GetMimeType(const DownloadItem& item) {
  return item.GetMimeType();
}

bool IsPaused(const DownloadItem& item) {
  return item.IsPaused();
}

enum ComparisonType {LT, EQ, GT};

// Returns true if |item| matches the filter specified by |value|, |cmptype|,
// and |accessor|. |accessor| is conceptually a function that takes a
// DownloadItem and returns one of its fields, which is then compared to
// |value|.
template <typename ValueType>
bool FieldMatches(
    const ValueType& value,
    ComparisonType cmptype,
    const base::RepeatingCallback<ValueType(const DownloadItem&)>& accessor,
    const DownloadItem& item) {
  switch (cmptype) {
    case LT: return accessor.Run(item) < value;
    case EQ: return accessor.Run(item) == value;
    case GT: return accessor.Run(item) > value;
  }
  NOTREACHED();
}

// Helper for building a Callback to FieldMatches<>().
template <typename ValueType> DownloadQuery::FilterCallback BuildFilter(
    const base::Value& value, ComparisonType cmptype,
    ValueType (*accessor)(const DownloadItem&)) {
  ValueType cpp_value;
  if (!GetAs(value, &cpp_value)) return DownloadQuery::FilterCallback();
  return base::BindRepeating(&FieldMatches<ValueType>, cpp_value, cmptype,
                             base::BindRepeating(accessor));
}

// Returns true if |accessor.Run(item)| matches |pattern|.
bool FindRegex(
    RE2* pattern,
    const base::RepeatingCallback<std::string(const DownloadItem&)>& accessor,
    const DownloadItem& item) {
  return RE2::PartialMatch(accessor.Run(item), *pattern);
}

// Helper for building a Callback to FindRegex().
DownloadQuery::FilterCallback BuildRegexFilter(
    const base::Value& regex_value,
    std::string (*accessor)(const DownloadItem&)) {
  std::string regex_str;
  if (!GetAs(regex_value, &regex_str)) return DownloadQuery::FilterCallback();
  std::unique_ptr<RE2> pattern(new RE2(regex_str));
  if (!pattern->ok()) return DownloadQuery::FilterCallback();
  return base::BindRepeating(&FindRegex, base::Owned(pattern.release()),
                             base::BindRepeating(accessor));
}

// Returns a ComparisonType to indicate whether a field in |left| is less than,
// greater than or equal to the same field in |right|.
template <typename ValueType>
ComparisonType Compare(
    const base::RepeatingCallback<ValueType(const DownloadItem&)>& accessor,
    const DownloadItem& left,
    const DownloadItem& right) {
  ValueType left_value = accessor.Run(left);
  ValueType right_value = accessor.Run(right);
  if (left_value > right_value) return GT;
  if (left_value < right_value) return LT;
  DCHECK_EQ(left_value, right_value);
  return EQ;
}

}  // anonymous namespace

// AddSorter() creates a Sorter and pushes it onto sorters_. A Sorter is a
// direction and a Callback to Compare<>(). After filtering, Search() makes a
// DownloadComparator functor from the sorters_ and passes the
// DownloadComparator to std::partial_sort. std::partial_sort calls the
// DownloadComparator with different pairs of DownloadItems.  DownloadComparator
// iterates over the sorters until a callback returns ComparisonType LT or GT.
// DownloadComparator returns true or false depending on that ComparisonType and
// the sorter's direction in order to indicate to std::partial_sort whether the
// left item is after or before the right item. If all sorters return EQ, then
// DownloadComparator compares GetId. A DownloadQuery may have zero or more
// Sorters, but there is one DownloadComparator per call to Search().

struct DownloadQuery::Sorter {
  using SortType = base::RepeatingCallback<ComparisonType(const DownloadItem&,
                                                          const DownloadItem&)>;

  template <typename ValueType>
  static Sorter Build(DownloadQuery::SortDirection adirection,
                      ValueType (*accessor)(const DownloadItem&)) {
    return Sorter(adirection,
                  base::BindRepeating(&Compare<ValueType>,
                                      base::BindRepeating(accessor)));
  }

  Sorter(DownloadQuery::SortDirection adirection, const SortType& asorter)
      : direction(adirection), sorter(asorter) {}
  ~Sorter() = default;

  DownloadQuery::SortDirection direction;
  SortType sorter;
};

class DownloadQuery::DownloadComparator {
 public:
  explicit DownloadComparator(const DownloadQuery::SorterVector& terms)
      : terms_(terms) {}

  // Returns true if |left| sorts before |right|.
  bool operator()(const DownloadItem* left, const DownloadItem* right);

 private:
  const raw_ref<const DownloadQuery::SorterVector> terms_;

  // std::sort requires this class to be copyable.
};

bool DownloadQuery::DownloadComparator::operator()(const DownloadItem* left,
                                                   const DownloadItem* right) {
  for (auto term = terms_->begin(); term != terms_->end(); ++term) {
    switch (term->sorter.Run(*left, *right)) {
      case LT:
        return term->direction == DownloadQuery::ASCENDING;
      case GT:
        return term->direction == DownloadQuery::DESCENDING;
      case EQ:
        break;  // break the switch but not the loop
    }
  }
  CHECK(left == right || left->GetId() != right->GetId());
  return left->GetId() < right->GetId();
}

// static
bool DownloadQuery::MatchesQuery(const std::vector<std::u16string>& query_terms,
                                 const DownloadItem& item) {
  if (query_terms.empty())
    return true;

  std::u16string original_url_raw(
      base::UTF8ToUTF16(item.GetOriginalUrl().spec()));
  std::u16string url_raw(base::UTF8ToUTF16(item.GetURL().spec()));
  // Try to also match query with above URLs formatted in user display friendly
  // way. This will unescape characters (including spaces) and trim all extra
  // data (like username and password) from raw url so that for example raw url
  // "http://some.server.org/example%20download/file.zip" will be matched with
  // search term "example download".
  std::u16string original_url_formatted(
      url_formatter::FormatUrl(item.GetOriginalUrl()));
  std::u16string url_formatted(url_formatter::FormatUrl(item.GetURL()));
  std::u16string path(item.GetTargetFilePath().LossyDisplayName());

  for (auto it = query_terms.begin(); it != query_terms.end(); ++it) {
    std::u16string term = base::i18n::ToLower(*it);
    if (!base::i18n::StringSearchIgnoringCaseAndAccents(term, original_url_raw,
                                                        nullptr, nullptr) &&
        !base::i18n::StringSearchIgnoringCaseAndAccents(
            term, original_url_formatted, nullptr, nullptr) &&
        !base::i18n::StringSearchIgnoringCaseAndAccents(term, url_raw, nullptr,
                                                        nullptr) &&
        !base::i18n::StringSearchIgnoringCaseAndAccents(term, url_formatted,
                                                        nullptr, nullptr) &&
        !base::i18n::StringSearchIgnoringCaseAndAccents(term, path, nullptr,
                                                        nullptr)) {
      return false;
    }
  }
  return true;
}

DownloadQuery::DownloadQuery() : limit_(std::numeric_limits<uint32_t>::max()) {}
DownloadQuery::~DownloadQuery() = default;

// AddFilter() pushes a new FilterCallback to filters_. Most FilterCallbacks are
// Callbacks to FieldMatches<>(). Search() iterates over given DownloadItems,
// discarding items for which any filter returns false. A DownloadQuery may have
// zero or more FilterCallbacks.

bool DownloadQuery::AddFilter(const DownloadQuery::FilterCallback& value) {
  if (value.is_null()) return false;
  filters_.push_back(value);
  return true;
}

void DownloadQuery::AddFilter(DownloadItem::DownloadState state) {
  AddFilter(base::BindRepeating(&FieldMatches<DownloadItem::DownloadState>,
                                state, EQ, base::BindRepeating(&GetState)));
}

void DownloadQuery::AddFilter(DownloadDangerType danger) {
  AddFilter(base::BindRepeating(&FieldMatches<DownloadDangerType>, danger, EQ,
                                base::BindRepeating(&GetDangerType)));
}

bool DownloadQuery::AddFilter(DownloadQuery::FilterType type,
                              const base::Value& value) {
  switch (type) {
    case FILTER_BYTES_RECEIVED:
      return AddFilter(BuildFilter<double>(value, EQ, &GetReceivedBytes));
    case FILTER_DANGER_ACCEPTED:
      return AddFilter(BuildFilter<bool>(value, EQ, &GetDangerAccepted));
    case FILTER_EXISTS:
      return AddFilter(BuildFilter<bool>(value, EQ, &GetExists));
    case FILTER_FILENAME:
      return AddFilter(BuildFilter<std::u16string>(value, EQ, &GetFilename));
    case FILTER_FILENAME_REGEX:
      return AddFilter(BuildRegexFilter(value, &GetFilenameUTF8));
    case FILTER_MIME:
      return AddFilter(BuildFilter<std::string>(value, EQ, &GetMimeType));
    case FILTER_PAUSED:
      return AddFilter(BuildFilter<bool>(value, EQ, &IsPaused));
    case FILTER_QUERY: {
      std::vector<std::u16string> query_terms;
      return GetAs(value, &query_terms) &&
             (query_terms.empty() ||
              AddFilter(
                  base::BindRepeating(&MatchesQuery, std::move(query_terms))));
    }
    case FILTER_ENDED_AFTER:
      return AddFilter(BuildFilter<std::string>(value, GT, &GetEndTime));
    case FILTER_ENDED_BEFORE:
      return AddFilter(BuildFilter<std::string>(value, LT, &GetEndTime));
    case FILTER_END_TIME:
      return AddFilter(BuildFilter<std::string>(value, EQ, &GetEndTime));
    case FILTER_STARTED_AFTER:
      return AddFilter(BuildFilter<std::string>(value, GT, &GetStartTime));
    case FILTER_STARTED_BEFORE:
      return AddFilter(BuildFilter<std::string>(value, LT, &GetStartTime));
    case FILTER_START_TIME:
      return AddFilter(BuildFilter<std::string>(value, EQ, &GetStartTime));
    case FILTER_TOTAL_BYTES:
      return AddFilter(BuildFilter<double>(value, EQ, &GetTotalBytes));
    case FILTER_TOTAL_BYTES_GREATER:
      return AddFilter(BuildFilter<double>(value, GT, &GetTotalBytes));
    case FILTER_TOTAL_BYTES_LESS:
      return AddFilter(BuildFilter<double>(value, LT, &GetTotalBytes));
    case FILTER_ORIGINAL_URL:
      return AddFilter(BuildFilter<std::string>(value, EQ, &GetOriginalUrl));
    case FILTER_ORIGINAL_URL_REGEX:
      return AddFilter(BuildRegexFilter(value, &GetOriginalUrl));
    case FILTER_URL:
      return AddFilter(BuildFilter<std::string>(value, EQ, &GetUrl));
    case FILTER_URL_REGEX:
      return AddFilter(BuildRegexFilter(value, &GetUrl));
  }
  return false;
}

bool DownloadQuery::Matches(const DownloadItem& item) const {
  for (auto filter = filters_.begin(); filter != filters_.end(); ++filter) {
    if (!filter->Run(item))
      return false;
  }
  return true;
}

void DownloadQuery::AddSorter(DownloadQuery::SortType type,
                              DownloadQuery::SortDirection direction) {
  switch (type) {
    case SORT_END_TIME:
      sorters_.push_back(Sorter::Build<int64_t>(direction, &GetEndTimeMsEpoch));
      break;
    case SORT_START_TIME:
      sorters_.push_back(
          Sorter::Build<int64_t>(direction, &GetStartTimeMsEpoch));
      break;
    case SORT_ORIGINAL_URL:
      sorters_.push_back(
          Sorter::Build<std::string>(direction, &GetOriginalUrl));
      break;
    case SORT_URL:
      sorters_.push_back(Sorter::Build<std::string>(direction, &GetUrl));
      break;
    case SORT_FILENAME:
      sorters_.push_back(
          Sorter::Build<std::u16string>(direction, &GetFilename));
      break;
    case SORT_DANGER:
      sorters_.push_back(Sorter::Build<DownloadDangerType>(
          direction, &GetDangerType));
      break;
    case SORT_DANGER_ACCEPTED:
      sorters_.push_back(Sorter::Build<bool>(direction, &GetDangerAccepted));
      break;
    case SORT_EXISTS:
      sorters_.push_back(Sorter::Build<bool>(direction, &GetExists));
      break;
    case SORT_STATE:
      sorters_.push_back(Sorter::Build<DownloadItem::DownloadState>(
          direction, &GetState));
      break;
    case SORT_PAUSED:
      sorters_.push_back(Sorter::Build<bool>(direction, &IsPaused));
      break;
    case SORT_MIME:
      sorters_.push_back(Sorter::Build<std::string>(direction, &GetMimeType));
      break;
    case SORT_BYTES_RECEIVED:
      sorters_.push_back(Sorter::Build<double>(direction, &GetReceivedBytes));
      break;
    case SORT_TOTAL_BYTES:
      sorters_.push_back(Sorter::Build<double>(direction, &GetTotalBytes));
      break;
  }
}

void DownloadQuery::FinishSearch(DownloadQuery::DownloadVector* results) const {
 if (!sorters_.empty()) {
    std::partial_sort(results->begin(),
                      results->begin() + std::min(limit_, results->size()),
                      results->end(),
                      DownloadComparator(sorters_));
  }

  if (results->size() > limit_)
    results->resize(limit_);
}
