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

#include "components/ntp_snippets/reading_list/reading_list_suggestions_provider.h"

#include <algorithm>
#include <memory>
#include <vector>

#include "base/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "components/ntp_snippets/category.h"
#include "components/reading_list/core/reading_list_entry.h"
#include "components/reading_list/core/reading_list_model.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/url_formatter.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image.h"

namespace ntp_snippets {

namespace {
// Max number of entries to return.
const int kMaxEntries = 3;

bool CompareEntries(const ReadingListEntry* lhs, const ReadingListEntry* rhs) {
  return lhs->UpdateTime() > rhs->UpdateTime();
}
}  // namespace

ReadingListSuggestionsProvider::ReadingListSuggestionsProvider(
    ContentSuggestionsProvider::Observer* observer,
    ReadingListModel* reading_list_model)
    : ContentSuggestionsProvider(observer),
      category_status_(CategoryStatus::AVAILABLE_LOADING),
      provided_category_(
          Category::FromKnownCategory(KnownCategories::READING_LIST)),
      reading_list_model_(reading_list_model),
      scoped_observer_(this) {
  observer->OnCategoryStatusChanged(this, provided_category_, category_status_);

  // If the ReadingListModel is loaded, this will trigger a call to
  // ReadingListModelLoaded. Keep it as last instruction.
  scoped_observer_.Add(reading_list_model_);
}

ReadingListSuggestionsProvider::~ReadingListSuggestionsProvider() {}

CategoryStatus ReadingListSuggestionsProvider::GetCategoryStatus(
    Category category) {
  DCHECK_EQ(category, provided_category_);
  return category_status_;
}

CategoryInfo ReadingListSuggestionsProvider::GetCategoryInfo(
    Category category) {
  DCHECK_EQ(category, provided_category_);

  return CategoryInfo(l10n_util::GetStringUTF16(
                          IDS_NTP_READING_LIST_SUGGESTIONS_SECTION_HEADER),
                      ContentSuggestionsCardLayout::FULL_CARD,
                      ContentSuggestionsAdditionalAction::VIEW_ALL,
                      /*show_if_empty=*/false,
                      l10n_util::GetStringUTF16(
                          IDS_NTP_READING_LIST_SUGGESTIONS_SECTION_EMPTY));
}

void ReadingListSuggestionsProvider::DismissSuggestion(
    const ContentSuggestion::ID& suggestion_id) {
  if (!reading_list_model_) {
    return;
  }

  DCHECK(reading_list_model_->loaded());
  GURL url(suggestion_id.id_within_category());
  SetDismissedState(url, true);
}

void ReadingListSuggestionsProvider::FetchSuggestionImage(
    const ContentSuggestion::ID& suggestion_id,
    ImageFetchedCallback callback) {
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), gfx::Image()));
}

void ReadingListSuggestionsProvider::FetchSuggestionImageData(
    const ContentSuggestion::ID& suggestion_id,
    ImageDataFetchedCallback callback) {
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), std::string()));
}

void ReadingListSuggestionsProvider::Fetch(
    const Category& category,
    const std::set<std::string>& known_suggestion_ids,
    FetchDoneCallback callback) {
  LOG(DFATAL) << "ReadingListSuggestionsProvider has no |Fetch| functionality!";
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(callback),
                     Status(StatusCode::PERMANENT_ERROR,
                            "ReadingListSuggestionsProvider has no |Fetch| "
                            "functionality!"),
                     std::vector<ContentSuggestion>()));
}

void ReadingListSuggestionsProvider::ClearHistory(
    base::Time begin,
    base::Time end,
    const base::Callback<bool(const GURL& url)>& filter) {
  // Ignored, Reading List does not depend on history.
}

void ReadingListSuggestionsProvider::ClearCachedSuggestions() {
  // Ignored.
}

void ReadingListSuggestionsProvider::GetDismissedSuggestionsForDebugging(
    Category category,
    DismissedSuggestionsCallback callback) {
  if (!reading_list_model_ || reading_list_model_->IsPerformingBatchUpdates()) {
    std::move(callback).Run(std::vector<ContentSuggestion>());
    return;
  }

  DCHECK(reading_list_model_->loaded());
  std::vector<const ReadingListEntry*> entries;
  for (const GURL& url : reading_list_model_->Keys()) {
    const ReadingListEntry* entry = reading_list_model_->GetEntryByURL(url);
    if (entry->ContentSuggestionsExtra()->dismissed) {
      entries.emplace_back(entry);
    }
  }

  std::sort(entries.begin(), entries.end(), CompareEntries);

  std::vector<ContentSuggestion> suggestions;
  for (const ReadingListEntry* entry : entries) {
    suggestions.emplace_back(ConvertEntry(entry));
  }

  std::move(callback).Run(std::move(suggestions));
}

void ReadingListSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
    Category category) {
  for (const auto& url : reading_list_model_->Keys()) {
    SetDismissedState(url, false);
  }
}

void ReadingListSuggestionsProvider::ReadingListModelLoaded(
    const ReadingListModel* model) {
  DCHECK(model == reading_list_model_);
  FetchReadingListInternal();
}

void ReadingListSuggestionsProvider::ReadingListModelBeingDeleted(
    const ReadingListModel* model) {
  DCHECK(model == reading_list_model_);
  scoped_observer_.Remove(reading_list_model_);
  reading_list_model_ = nullptr;
}

void ReadingListSuggestionsProvider::ReadingListDidApplyChanges(
    ReadingListModel* model) {
  DCHECK(model == reading_list_model_);
  if (model->IsPerformingBatchUpdates())
    return;

  FetchReadingListInternal();
}

void ReadingListSuggestionsProvider::ReadingListModelCompletedBatchUpdates(
    const ReadingListModel* model) {
  DCHECK(model == reading_list_model_);

  FetchReadingListInternal();
}

void ReadingListSuggestionsProvider::FetchReadingListInternal() {
  if (!reading_list_model_ || reading_list_model_->IsPerformingBatchUpdates()) {
    return;
  }

  DCHECK(reading_list_model_->loaded());
  std::vector<const ReadingListEntry*> entries;
  for (const GURL& url : reading_list_model_->Keys()) {
    const ReadingListEntry* entry = reading_list_model_->GetEntryByURL(url);
    if (!entry->IsRead() && !entry->ContentSuggestionsExtra()->dismissed) {
      entries.emplace_back(entry);
    }
  }

  if (entries.size() > kMaxEntries) {
    // Get the |kMaxEntries| most recent entries.
    std::partial_sort(entries.begin(), entries.begin() + kMaxEntries,
                      entries.end(), CompareEntries);
    entries.resize(kMaxEntries);
  } else {
    std::sort(entries.begin(), entries.end(), CompareEntries);
  }

  std::vector<ContentSuggestion> suggestions;
  for (const ReadingListEntry* entry : entries) {
    suggestions.emplace_back(ConvertEntry(entry));
  }

  NotifyStatusChanged(CategoryStatus::AVAILABLE);
  observer()->OnNewSuggestions(this, provided_category_,
                               std::move(suggestions));
}

ContentSuggestion ReadingListSuggestionsProvider::ConvertEntry(
    const ReadingListEntry* entry) {
  ContentSuggestion suggestion(provided_category_, entry->URL().spec(),
                               entry->URL());

  if (!entry->Title().empty()) {
    suggestion.set_title(base::UTF8ToUTF16(entry->Title()));
  } else {
    suggestion.set_title(url_formatter::FormatUrl(entry->URL()));
  }
  suggestion.set_publisher_name(
      url_formatter::FormatUrl(entry->URL().GetOrigin()));
  int64_t entry_time = entry->DistillationTime();
  if (entry_time == 0) {
    entry_time = entry->CreationTime();
  }

  suggestion.set_publish_date(
      base::Time::FromDoubleT(entry_time / base::Time::kMicrosecondsPerSecond));

  auto extra = std::make_unique<ReadingListSuggestionExtra>();
  extra->favicon_page_url =
      entry->DistilledURL().is_valid() ? entry->DistilledURL() : entry->URL();
  suggestion.set_reading_list_suggestion_extra(std::move(extra));

  return suggestion;
}

void ReadingListSuggestionsProvider::NotifyStatusChanged(
    CategoryStatus new_status) {
  if (category_status_ == new_status) {
    return;
  }
  category_status_ = new_status;
  observer()->OnCategoryStatusChanged(this, provided_category_, new_status);
}

void ReadingListSuggestionsProvider::SetDismissedState(const GURL& url,
                                                       bool dismissed) {
  reading_list::ContentSuggestionsExtra extra;
  extra.dismissed = dismissed;
  reading_list_model_->SetContentSuggestionsExtra(url, extra);
}

}  // namespace ntp_snippets
