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

#include "components/feed/core/v2/surface_updater.h"

#include <tuple>
#include <utility>

#include "base/check.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "components/feed/core/proto/v2/ui.pb.h"
#include "components/feed/core/proto/v2/wire/reliability_logging_enums.pb.h"
#include "components/feed/core/proto/v2/xsurface.pb.h"
#include "components/feed/core/v2/enums.h"
#include "components/feed/core/v2/feed_stream.h"
#include "components/feed/core/v2/feed_stream_surface.h"
#include "components/feed/core/v2/launch_reliability_logger.h"
#include "components/feed/core/v2/metrics_reporter.h"
#include "components/feed/core/v2/stream_surface_set.h"
#include "components/feed/core/v2/types.h"

namespace feed {
namespace {

using DrawState = SurfaceUpdater::DrawState;
using StreamUpdateType = LaunchReliabilityLogger::StreamUpdateType;

// Give each kind of zero state a unique name, so that the UI knows if it
// changes.
const char* GetZeroStateSliceId(feedui::ZeroStateSlice::Type type) {
  switch (type) {
    case feedui::ZeroStateSlice::NO_CARDS_AVAILABLE:
      return "no-cards";
    case feedui::ZeroStateSlice::CANT_REFRESH:  // fall-through
    default:
      return "cant-refresh";
  }
}

void AddSharedState(const StreamModel& model,
                    const std::string& shared_state_id,
                    feedui::StreamUpdate* stream_update) {
  const std::string* shared_state_data =
      model.FindSharedStateData(shared_state_id);
  DCHECK(shared_state_data);
  feedui::SharedState* added_shared_state =
      stream_update->add_new_shared_states();
  added_shared_state->set_id(shared_state_id);
  added_shared_state->set_xsurface_shared_state(*shared_state_data);
}

void AddSliceUpdate(const StreamModel& model,
                    ContentRevision content_revision,
                    bool is_content_new,
                    feedui::StreamUpdate* stream_update) {
  if (is_content_new) {
    feedui::Slice* slice = stream_update->add_updated_slices()->mutable_slice();
    slice->set_slice_id(ToString(content_revision));
    const feedstore::Content* content = model.FindContent(content_revision);
    DCHECK(content);
    slice->mutable_xsurface_slice()->set_xsurface_frame(content->frame());
  } else {
    stream_update->add_updated_slices()->set_slice_id(
        ToString(content_revision));
  }
}

void AddLoadingSpinner(bool is_initial_load,
                       int load_more_indicator_id,
                       feedui::StreamUpdate* update) {
  feedui::Slice* slice = update->add_updated_slices()->mutable_slice();
  slice->mutable_loading_spinner_slice()->set_is_at_top(is_initial_load);
  // The slice ID is checked on Java code to find out the loading spinner view.
  // An incremental ID is added to the slice ID in order to differentiate from
  // pvrevious spinners.
  slice->set_slice_id(
      is_initial_load
          ? "loading-spinner"
          : base::StrCat({"load-more-spinner",
                          base::NumberToString(load_more_indicator_id)}));
}

struct StreamUpdateAndType {
  feedui::StreamUpdate stream_update;
  StreamUpdateType type = StreamUpdateType::kNone;
};

StreamUpdateAndType MakeStreamUpdate(
    const std::vector<std::string>& updated_shared_state_ids,
    const base::flat_set<ContentRevision>& already_sent_content,
    const StreamModel* model,
    const DrawState& state,
    int load_more_indicator_id) {
  DCHECK(!state.loading_initial || !state.loading_more)
      << "logic bug: requested both top and bottom spinners.";

  StreamUpdateAndType update;

  // Add content from the model, if it's loaded.
  bool has_content = false;
  if (model) {
    for (ContentRevision content_revision : model->GetContentList()) {
      const bool is_updated = already_sent_content.count(content_revision) == 0;
      AddSliceUpdate(*model, content_revision, is_updated,
                     &update.stream_update);
      has_content = true;
      update.type = StreamUpdateType::kContent;
    }
    for (const std::string& name : updated_shared_state_ids) {
      AddSharedState(*model, name, &update.stream_update);
    }
  }

  feedui::ZeroStateSlice::Type zero_state_type = state.zero_state_type;
  // If there are no cards, and we aren't loading, force a zero-state.
  // This happens when a model is loaded, but it has no content.
  if (!state.loading_initial && !has_content &&
      state.zero_state_type == feedui::ZeroStateSlice::UNKNOWN) {
    zero_state_type = feedui::ZeroStateSlice::NO_CARDS_AVAILABLE;
  }

  if (zero_state_type != feedui::ZeroStateSlice::UNKNOWN) {
    feedui::Slice* slice =
        update.stream_update.add_updated_slices()->mutable_slice();
    slice->mutable_zero_state_slice()->set_type(zero_state_type);
    slice->set_slice_id(GetZeroStateSliceId(zero_state_type));
    update.type = StreamUpdateType::kZeroState;
  } else {
    // Add the initial-load spinner if applicable.
    if (state.loading_initial) {
      AddLoadingSpinner(/*is_initial_load=*/true, load_more_indicator_id,
                        &update.stream_update);
      update.type = StreamUpdateType::kInitialLoadingSpinner;
    }
    // Add a loading-more spinner if applicable.
    if (state.loading_more) {
      AddLoadingSpinner(/*is_initial_load=*/false, load_more_indicator_id,
                        &update.stream_update);
      update.type = StreamUpdateType::kLoadingMoreSpinner;
    }
  }

  if (model) {
    update.stream_update.set_fetch_time_ms(
        model->GetLastAddedTime().ToDeltaSinceWindowsEpoch().InMilliseconds());
    ToProto(model->GetLoggingParameters(),
            *update.stream_update.mutable_logging_parameters());
  } else {
    ToProto(LoggingParameters{},
            *update.stream_update.mutable_logging_parameters());
  }

  return update;
}

StreamUpdateAndType GetUpdateForNewSurface(const DrawState& state,
                                           const StreamModel* model) {
  std::vector<std::string> updated_shared_state_ids;
  if (model) {
    updated_shared_state_ids = model->GetSharedStateIds();
  }
  return MakeStreamUpdate(std::move(updated_shared_state_ids),
                          /*already_sent_content=*/{}, model, state, 0);
}

base::flat_set<ContentRevision> GetContentSet(const StreamModel* model) {
  if (!model)
    return {};
  const std::vector<ContentRevision>& content_list = model->GetContentList();
  return base::flat_set<ContentRevision>(content_list.begin(),
                                         content_list.end());
}

feedui::ZeroStateSlice::Type GetZeroStateType(LoadStreamStatus status) {
  switch (status) {
    case LoadStreamStatus::kNoResponseBody:
    case LoadStreamStatus::kProtoTranslationFailed:
    case LoadStreamStatus::kCannotLoadFromNetworkOffline:
    case LoadStreamStatus::kCannotLoadFromNetworkThrottled:
    case LoadStreamStatus::kNetworkFetchFailed:
    case LoadStreamStatus::kAccountTokenFetchFailedWrongAccount:
    case LoadStreamStatus::kAccountTokenFetchTimedOut:
    case LoadStreamStatus::kNetworkFetchTimedOut:
      return feedui::ZeroStateSlice::CANT_REFRESH;
    case LoadStreamStatus::kNotAWebFeedSubscriber_DEPRECATED:
    case LoadStreamStatus::kNoStatus:
    case LoadStreamStatus::kLoadedFromStore:
    case LoadStreamStatus::kLoadedFromNetwork:
    case LoadStreamStatus::kFailedWithStoreError:
    case LoadStreamStatus::kNoStreamDataInStore:
    case LoadStreamStatus::kModelAlreadyLoaded:
    case LoadStreamStatus::kDataInStoreIsStale:
    case LoadStreamStatus::kDataInStoreIsStaleTimestampInFuture:
    case LoadStreamStatus::
        kCannotLoadFromNetworkSupressedForHistoryDelete_DEPRECATED:
    case LoadStreamStatus::kLoadNotAllowedEulaNotAccepted:
    case LoadStreamStatus::kLoadNotAllowedArticlesListHidden:
    case LoadStreamStatus::kCannotParseNetworkResponseBody:
    case LoadStreamStatus::kLoadMoreModelIsNotLoaded:
    case LoadStreamStatus::kLoadNotAllowedDisabledByEnterprisePolicy:
    case LoadStreamStatus::kCannotLoadMoreNoNextPageToken:
    case LoadStreamStatus::kDataInStoreStaleMissedLastRefresh:
    case LoadStreamStatus::kLoadedStaleDataFromStoreDueToNetworkFailure:
    case LoadStreamStatus::kDataInStoreIsExpired:
    case LoadStreamStatus::kDataInStoreIsForAnotherUser:
    case LoadStreamStatus::kAbortWithPendingClearAll:
    case LoadStreamStatus::kAlreadyHaveUnreadContent:
    case LoadStreamStatus::kLoadNotAllowedDisabled:
    case LoadStreamStatus::kLoadNotAllowedDisabledByDse:
    case LoadStreamStatus::kNoCardReceived:
      break;
  }
  return feedui::ZeroStateSlice::NO_CARDS_AVAILABLE;
}

}  // namespace

bool SurfaceUpdater::DrawState::operator==(const DrawState& rhs) const {
  return std::tie(loading_more, loading_initial, zero_state_type) ==
         std::tie(rhs.loading_more, rhs.loading_initial, rhs.zero_state_type);
}

SurfaceUpdater::SurfaceUpdater(
    MetricsReporter* metrics_reporter,
    XsurfaceDatastoreDataReader* global_datastore_slice,
    StreamSurfaceSet* surfaces)
    : metrics_reporter_(metrics_reporter),
      surfaces_(surfaces),
      aggregate_data_({&surface_data_slice_, global_datastore_slice}),
      launch_reliability_logger_(surfaces) {
  aggregate_data_.AddObserver(this);
}

SurfaceUpdater::~SurfaceUpdater() {
  aggregate_data_.RemoveObserver(this);
}

void SurfaceUpdater::SetModel(StreamModel* model) {
  if (model_ == model)
    return;
  if (model_)
    model_->RemoveObserver(this);
  model_ = model;
  sent_content_.clear();
  if (model_) {
    model_->AddObserver(this);
    loading_initial_ = loading_initial_ && model_->GetContentList().empty();
    loading_more_ = false;
    // TODO(iwells): Avoid sending a second loading spinner in the "valid
    // response, zero cards" case.
    SendStreamUpdate(model_->GetSharedStateIds());
    last_draw_state_ = GetState();
  }
}

void SurfaceUpdater::OnUiUpdate(const StreamModel::UiUpdate& update) {
  DCHECK(model_);  // The update comes from the model.
  loading_initial_ = loading_initial_ && model_->GetContentList().empty();
  loading_more_ = loading_more_ && !update.content_list_changed;

  std::vector<std::string> updated_shared_state_ids;
  for (const StreamModel::UiUpdate::SharedStateInfo& info :
       update.shared_states) {
    if (info.updated)
      updated_shared_state_ids.push_back(info.shared_state_id);
  }

  SendStreamUpdate(updated_shared_state_ids);
}

void SurfaceUpdater::SurfaceAdded(
    SurfaceId surface_id,
    SurfaceRenderer* renderer,
    feedwire::DiscoverLaunchResult loading_not_allowed_reason) {
  ReliabilityLoggingBridge& logger = renderer->GetReliabilityLoggingBridge();
  logger.LogFeedLaunchOtherStart(base::TimeTicks::Now());

  if (loading_not_allowed_reason !=
      feedwire::DiscoverLaunchResult::CARDS_UNSPECIFIED) {
    logger.LogLaunchFinishedAfterStreamUpdate(loading_not_allowed_reason);
  }

  StreamUpdateAndType update = GetUpdateForNewSurface(GetState(), model_);
  launch_reliability_logger_.OnStreamUpdate(update.type, *renderer);
  SendUpdateToSurface(surface_id, renderer, update.stream_update);

  for (std::pair<std::string, std::string> datastore_entry :
       aggregate_data_.GetAllEntries()) {
    renderer->ReplaceDataStoreEntry(datastore_entry.first,
                                    datastore_entry.second);
  }
}

void SurfaceUpdater::SurfaceRemoved(SurfaceId surface_id) {}

void SurfaceUpdater::DatastoreEntryUpdated(XsurfaceDatastoreDataReader*,
                                           const std::string& key) {
  const std::string* value = aggregate_data_.FindEntry(key);
  DCHECK(value);
  for (auto& entry : *surfaces_)
    entry.renderer->ReplaceDataStoreEntry(key, *value);
}

void SurfaceUpdater::DatastoreEntryRemoved(XsurfaceDatastoreDataReader*,
                                           const std::string& key) {
  for (auto& entry : *surfaces_)
    entry.renderer->RemoveDataStoreEntry(key);
}

void SurfaceUpdater::LoadStreamStarted(bool manual_refreshing) {
  load_stream_failed_ = false;
  loading_initial_ = !manual_refreshing;
  load_stream_started_ = true;
  SendStreamUpdateIfNeeded();
}

void SurfaceUpdater::LoadStreamComplete(
    bool success,
    LoadStreamStatus load_stream_status,
    feedwire::DiscoverLaunchResult launch_result) {
  loading_initial_ = false;
  load_stream_status_ = load_stream_status;
  load_stream_failed_ = !success;

  if (ShouldSendStreamUpdate()) {
    if (launch_result != feedwire::DiscoverLaunchResult::CARDS_UNSPECIFIED) {
      launch_reliability_logger_.LogLaunchFinishedAfterStreamUpdate(
          launch_result);
    }
    SendStreamUpdate({});
  }

  load_stream_started_ = false;
}

int SurfaceUpdater::GetSliceIndexFromSliceId(const std::string& slice_id) {
  ContentRevision slice_rev = ToContentRevision(slice_id);
  if (slice_rev.is_null() || !model_)
    return -1;
  int index = 0;
  for (const ContentRevision& rev : model_->GetContentList()) {
    if (rev == slice_rev)
      return index;
    ++index;
  }
  return -1;
}

void SurfaceUpdater::SetLoadingMore(bool is_loading) {
  DCHECK(!loading_initial_)
      << "SetLoadingMore while still loading the initial state";
  loading_more_ = is_loading;
  if (loading_more_) {
    current_load_more_indicator_id_++;
  }
  SendStreamUpdateIfNeeded();
}

DrawState SurfaceUpdater::GetState() const {
  DrawState new_state;
  new_state.loading_more = loading_more_;
  new_state.loading_initial = loading_initial_;
  if (load_stream_failed_)
    new_state.zero_state_type = GetZeroStateType(load_stream_status_);
  return new_state;
}

bool SurfaceUpdater::ShouldSendStreamUpdate() const {
  return !(last_draw_state_ == GetState());
}

void SurfaceUpdater::SendStreamUpdateIfNeeded() {
  if (ShouldSendStreamUpdate())
    SendStreamUpdate({});
}

void SurfaceUpdater::SendStreamUpdate(
    const std::vector<std::string>& updated_shared_state_ids) {
  DrawState state = GetState();
  StreamUpdateAndType update =
      MakeStreamUpdate(updated_shared_state_ids, sent_content_, model_, state,
                       current_load_more_indicator_id_);

  if (load_stream_started_ || loading_more_) {
    launch_reliability_logger_.OnStreamUpdate(update.type);
  }

  for (auto& entry : *surfaces_) {
    SendUpdateToSurface(entry.surface_id, entry.renderer, update.stream_update);
  }

  sent_content_ = GetContentSet(model_);
  last_draw_state_ = state;
}

void SurfaceUpdater::SendUpdateToSurface(SurfaceId surface_id,
                                         SurfaceRenderer* renderer,
                                         const feedui::StreamUpdate& update) {
  renderer->StreamUpdate(update);

  // Call |MetricsReporter::SurfaceReceivedContent()| if appropriate.

  bool update_has_content = false;
  for (const feedui::StreamUpdate_SliceUpdate& slice_update :
       update.updated_slices()) {
    if (slice_update.has_slice() && slice_update.slice().has_xsurface_slice()) {
      update_has_content = true;
    }
  }
  if (!update_has_content)
    return;
  metrics_reporter_->SurfaceReceivedContent(surface_id);
}

void SurfaceUpdater::SetOfflinePageAvailability(const std::string& badge_id,
                                                bool available_offline) {
  feedxsurface::OfflineBadgeContent testbadge;
  if (available_offline) {
    std::string badge_serialized;
    testbadge.set_available_offline(available_offline);
    testbadge.SerializeToString(&badge_serialized);
    surface_data_slice_.UpdateDatastoreEntry(badge_id, badge_serialized);
  } else {
    surface_data_slice_.RemoveDatastoreEntry(badge_id);
  }
}

}  // namespace feed
