// 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.

#include "components/services/app_service/public/cpp/app_registry_cache.h"

#include <optional>
#include <string_view>
#include <utility>

#include "base/observer_list.h"
#include "base/sequence_checker.h"
#include "components/services/app_service/public/cpp/app.h"
#include "components/services/app_service/public/cpp/app_registry_cache_wrapper.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "components/services/app_service/public/cpp/types_util.h"

namespace apps {

AppRegistryCache::Observer::Observer() = default;

AppRegistryCache::Observer::~Observer() {
  CHECK(!IsInObserverList());
}

AppRegistryCache::AppRegistryCache() : account_id_(EmptyAccountId()) {}

AppRegistryCache::~AppRegistryCache() {
  for (auto& obs : observers_) {
    obs.OnAppRegistryCacheWillBeDestroyed(this);
  }
  DCHECK(observers_.empty());
  AppRegistryCacheWrapper::Get().RemoveAppRegistryCache(this);
}

void AppRegistryCache::AddObserver(Observer* observer) {
  DCHECK(observer);
  observers_.AddObserver(observer);
}

void AppRegistryCache::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

AppType AppRegistryCache::GetAppType(std::string_view app_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);

  auto d_iter = deltas_in_progress_.find(app_id);
  if (d_iter != deltas_in_progress_.end()) {
    return d_iter->second->app_type;
  }
  auto s_iter = states_.find(app_id);
  return (s_iter != states_.end()) ? s_iter->second->app_type
                                   : AppType::kUnknown;
}

std::vector<AppPtr> AppRegistryCache::GetAllApps() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);

  std::vector<AppPtr> apps;
  for (const auto& s_iter : states_) {
    const App* state = s_iter.second.get();
    apps.push_back(state->Clone());

    auto d_iter = deltas_in_progress_.find(s_iter.first);
    const App* delta =
        (d_iter != deltas_in_progress_.end()) ? d_iter->second : nullptr;
    AppUpdate::Merge(apps[apps.size() - 1].get(), delta);
  }
  for (const auto& d_iter : deltas_in_progress_) {
    if (!states_.contains(d_iter.first)) {
      // Call AppUpdate::Merge to set the init value for the icon key's
      // `update_version` to keep the consistency as AppRegistryCache's
      // `states_`.
      auto app =
          std::make_unique<App>(d_iter.second->app_type, d_iter.second->app_id);
      AppUpdate::Merge(app.get(), d_iter.second);
      apps.push_back(std::move(app));
    }
  }
  return apps;
}

void AppRegistryCache::SetAccountId(const AccountId& account_id) {
  account_id_ = account_id;
}

const std::set<AppType>& AppRegistryCache::InitializedAppTypes() const {
  return initialized_app_types_;
}

bool AppRegistryCache::IsAppTypeInitialized(apps::AppType app_type) const {
  return initialized_app_types_.contains(app_type);
}

bool AppRegistryCache::IsAppTypePublished(apps::AppType app_type) const {
  return published_app_types_.contains(app_type);
}

bool AppRegistryCache::IsAppInstalled(std::string_view app_id) const {
  bool installed = false;
  ForOneApp(app_id, [&installed](const AppUpdate& update) {
    installed = apps_util::IsInstalled(update.Readiness());
  });
  return installed;
}

void AppRegistryCache::ReinitializeForTesting() {
  states_.clear();
  deltas_in_progress_.clear();
  deltas_pending_.clear();
  in_progress_initialized_app_types_.clear();
  published_app_types_.clear();

  // We can't clear initialized_app_types_ here as observers
  // expect each type to be initialized only once.
}

void AppRegistryCache::OnAppsForTesting(std::vector<AppPtr> deltas,
                                        apps::AppType app_type,
                                        bool should_notify_initialized) {
  OnApps(std::move(deltas), app_type, should_notify_initialized);
}

void AppRegistryCache::OnApps(std::vector<AppPtr> deltas,
                              apps::AppType app_type,
                              bool should_notify_initialized) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);

  if (should_notify_initialized) {
    DCHECK_NE(apps::AppType::kUnknown, app_type);

    if (!IsAppTypePublished(app_type)) {
      published_app_types_.insert(app_type);
      for (auto& obs : observers_) {
        obs.OnAppTypePublishing(deltas, app_type);
      }
    }

    if (!IsAppTypeInitialized(app_type)) {
      in_progress_initialized_app_types_.insert(app_type);
    }
  }

  OnApps(std::move(deltas));
}

void AppRegistryCache::OnApps(std::vector<AppPtr> deltas) {
  if (!deltas_in_progress_.empty()) {
    std::move(deltas.begin(), deltas.end(),
              std::back_inserter(deltas_pending_));
    return;
  }

  DoOnApps(std::move(deltas));
  while (!deltas_pending_.empty()) {
    std::vector<AppPtr> pending;
    pending.swap(deltas_pending_);
    DoOnApps(std::move(pending));
  }

  OnAppTypeInitialized();
}

void AppRegistryCache::DoOnApps(std::vector<AppPtr> deltas) {
  // Merge any deltas elements that have the same app_id. If an observer's
  // OnAppUpdate calls back into this AppRegistryCache then we can therefore
  // present a single delta for any given app_id.
  bool is_pending = false;
  for (auto& delta : deltas) {
    if (is_pending) {
      deltas_pending_.push_back(std::move(delta));
      continue;
    }
    if (delta->readiness == Readiness::kRemoved) {
      // As soon as we see a removed update, we need to defer future merges to
      // avoid cases where a removed update is merged with a future non-removed
      // update.
      //
      // For example, for below apps deltas:
      // 1. {App1: ready}
      // 2. {App1: uninstalled, App1: removed}
      // 3. {App1: disabled by policy, App2: ready}
      //
      // The expected result for OnAppUpdate is:
      // {App1: ready}
      // {App1: uninstalled}
      // Then App1 is removed in AppRegistry, GetAppType returns kUnknown.
      // {App1: is added as disabled by policy}
      // {App2: ready}
      is_pending = true;
    }

    auto d_iter = deltas_in_progress_.find(delta->app_id);
    if (d_iter != deltas_in_progress_.end()) {
      if (delta->readiness == Readiness::kRemoved) {
        // Ensure that removed deltas are *not* merged, so that the last update
        // before the merge is sent separately.
        deltas_pending_.push_back(std::move(delta));
      } else {
        // Call AppUpdate::MergeDelta to merge the icon key's `update_version`.
        AppUpdate::MergeDelta(d_iter->second, delta.get());
      }
    } else {
      deltas_in_progress_[delta->app_id] = delta.get();
    }
  }

  // The remaining for loops range over the deltas_in_progress_ map, not
  // the deltas vector, so that OnAppUpdate is called only once per unique
  // app_id.

  // Notify the observers for every de-duplicated delta.
  for (const auto& d_iter : deltas_in_progress_) {
    // Do not update subscribers for removed apps.
    if (d_iter.second->readiness == Readiness::kRemoved) {
      continue;
    }
    auto s_iter = states_.find(d_iter.first);
    App* state = (s_iter != states_.end()) ? s_iter->second.get() : nullptr;
    App* delta = d_iter.second;

    // Skip OnAppUpdate if there is no change.
    if (!AppUpdate::IsChanged(state, delta)) {
      continue;
    }

    for (auto& obs : observers_) {
      obs.OnAppUpdate(AppUpdate(state, delta, account_id_));
    }
  }

  // Update the states for every de-duplicated delta.
  for (const auto& d_iter : deltas_in_progress_) {
    auto s_iter = states_.find(d_iter.first);
    apps::App* state =
        (s_iter != states_.end()) ? s_iter->second.get() : nullptr;
    apps::App* delta = d_iter.second;

    if (delta->readiness != Readiness::kRemoved) {
      if (state) {
        AppUpdate::Merge(state, delta);
      } else {
        // Call AppUpdate::Merge to set the init value for the icon key's
        // `update_version`.
        auto app = std::make_unique<App>(delta->app_type, delta->app_id);
        AppUpdate::Merge(app.get(), delta);
        states_.insert(std::make_pair(delta->app_id, std::move(app)));
      }
    } else {
      DCHECK(!state || state->readiness != Readiness::kReady);
      states_.erase(d_iter.first);
    }
  }
  deltas_in_progress_.clear();
}

void AppRegistryCache::InitApps(apps::AppType app_type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);

  CHECK_NE(apps::AppType::kUnknown, app_type);

  if (!IsAppTypeInitialized(app_type)) {
    in_progress_initialized_app_types_.insert(app_type);
  }

  OnAppTypeInitialized();
}

void AppRegistryCache::OnAppTypeInitialized() {
  if (in_progress_initialized_app_types_.empty()) {
    return;
  }

  // In observer's OnAppTypeInitialized callback, `OnApp` might be called to
  // update the app, then this OnAppTypeInitialized might be called again. So we
  // need to check the initialized `app_type` first, and remove it from
  // `in_progress_initialized_app_types_` to prevent the dead loop.
  std::set<AppType> in_progress_initialized_app_types;
  for (auto app_type : in_progress_initialized_app_types_) {
    in_progress_initialized_app_types.insert(app_type);
  }

  for (auto app_type : in_progress_initialized_app_types) {
    in_progress_initialized_app_types_.erase(app_type);
    initialized_app_types_.insert(app_type);
    for (auto& obs : observers_) {
      obs.OnAppTypeInitialized(app_type);
    }
  }
}

std::optional<AppUpdate> AppRegistryCache::GetAppUpdate(
    std::string_view app_id) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);

  auto s_iter = states_.find(app_id);
  const App* state = (s_iter != states_.end()) ? s_iter->second.get() : nullptr;

  auto d_iter = deltas_in_progress_.find(app_id);
  const App* delta =
      (d_iter != deltas_in_progress_.end()) ? d_iter->second : nullptr;

  if (state || delta) {
    return AppUpdate(state, delta, account_id_);
  }
  return std::nullopt;
}

}  // namespace apps
