// Copyright 2015 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/gcm_driver/instance_id/instance_id_impl.h"

#include <stdint.h>

#include <algorithm>
#include <memory>
#include <string>

#include "base/base64.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "components/gcm_driver/gcm_driver.h"
#include "crypto/random.h"

namespace instance_id {

namespace {

InstanceID::Result GCMClientResultToInstanceIDResult(
    gcm::GCMClient::Result result) {
  switch (result) {
    case gcm::GCMClient::SUCCESS:
      return InstanceID::SUCCESS;
    case gcm::GCMClient::INVALID_PARAMETER:
      return InstanceID::INVALID_PARAMETER;
    case gcm::GCMClient::GCM_DISABLED:
      return InstanceID::DISABLED;
    case gcm::GCMClient::ASYNC_OPERATION_PENDING:
      return InstanceID::ASYNC_OPERATION_PENDING;
    case gcm::GCMClient::NETWORK_ERROR:
      return InstanceID::NETWORK_ERROR;
    case gcm::GCMClient::SERVER_ERROR:
      return InstanceID::SERVER_ERROR;
    case gcm::GCMClient::UNKNOWN_ERROR:
      return InstanceID::UNKNOWN_ERROR;
    case gcm::GCMClient::TTL_EXCEEDED:
      NOTREACHED();
  }
  return InstanceID::UNKNOWN_ERROR;
}

}  // namespace

// static
std::unique_ptr<InstanceID> InstanceID::CreateInternal(
    const std::string& app_id,
    gcm::GCMDriver* gcm_driver) {
  return std::make_unique<InstanceIDImpl>(app_id, gcm_driver);
}

InstanceIDImpl::InstanceIDImpl(const std::string& app_id,
                               gcm::GCMDriver* gcm_driver)
    : InstanceID(app_id, gcm_driver) {
  Handler()->GetInstanceIDData(
      app_id, base::BindOnce(&InstanceIDImpl::GetInstanceIDDataCompleted,
                             weak_ptr_factory_.GetWeakPtr()));
}

InstanceIDImpl::~InstanceIDImpl() {
}

void InstanceIDImpl::GetID(GetIDCallback callback) {
  RunWhenReady(base::BindOnce(&InstanceIDImpl::DoGetID,
                              weak_ptr_factory_.GetWeakPtr(),
                              std::move(callback)));
}

void InstanceIDImpl::DoGetID(GetIDCallback callback) {
  EnsureIDGenerated();
  std::move(callback).Run(id_);
}

void InstanceIDImpl::GetCreationTime(GetCreationTimeCallback callback) {
  RunWhenReady(base::BindOnce(&InstanceIDImpl::DoGetCreationTime,
                              weak_ptr_factory_.GetWeakPtr(),
                              std::move(callback)));
}

void InstanceIDImpl::DoGetCreationTime(GetCreationTimeCallback callback) {
  std::move(callback).Run(creation_time_);
}

void InstanceIDImpl::GetToken(const std::string& authorized_entity,
                              const std::string& scope,
                              base::TimeDelta time_to_live,
                              std::set<Flags> flags,
                              GetTokenCallback callback) {
  DCHECK(!authorized_entity.empty());
  DCHECK(!scope.empty());

  RunWhenReady(base::BindOnce(&InstanceIDImpl::DoGetToken,
                              weak_ptr_factory_.GetWeakPtr(), authorized_entity,
                              scope, time_to_live, std::move(callback)));
}

void InstanceIDImpl::DoGetToken(
    const std::string& authorized_entity,
    const std::string& scope,
    base::TimeDelta time_to_live,
    GetTokenCallback callback) {
  EnsureIDGenerated();

  Handler()->GetToken(
      app_id(), authorized_entity, scope, time_to_live,
      base::BindOnce(&InstanceIDImpl::OnGetTokenCompleted,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void InstanceIDImpl::ValidateToken(const std::string& authorized_entity,
                                   const std::string& scope,
                                   const std::string& token,
                                   ValidateTokenCallback callback) {
  DCHECK(!authorized_entity.empty());
  DCHECK(!scope.empty());
  DCHECK(!token.empty());

  RunWhenReady(base::BindOnce(&InstanceIDImpl::DoValidateToken,
                              weak_ptr_factory_.GetWeakPtr(), authorized_entity,
                              scope, token, std::move(callback)));
}

void InstanceIDImpl::DoValidateToken(const std::string& authorized_entity,
                                     const std::string& scope,
                                     const std::string& token,
                                     ValidateTokenCallback callback) {
  if (id_.empty()) {
    std::move(callback).Run(false /* is_valid */);
    return;
  }

  Handler()->ValidateToken(app_id(), authorized_entity, scope, token,
                           std::move(callback));
}

void InstanceIDImpl::DeleteTokenImpl(const std::string& authorized_entity,
                                     const std::string& scope,
                                     DeleteTokenCallback callback) {
  DCHECK(!authorized_entity.empty());
  DCHECK(!scope.empty());

  RunWhenReady(base::BindOnce(&InstanceIDImpl::DoDeleteToken,
                              weak_ptr_factory_.GetWeakPtr(), authorized_entity,
                              scope, std::move(callback)));
}

void InstanceIDImpl::DoDeleteToken(const std::string& authorized_entity,
                                   const std::string& scope,
                                   DeleteTokenCallback callback) {
  // Nothing to delete if the ID has not been generated.
  if (id_.empty()) {
    std::move(callback).Run(InstanceID::INVALID_PARAMETER);
    return;
  }

  Handler()->DeleteToken(
      app_id(), authorized_entity, scope,
      base::BindOnce(&InstanceIDImpl::OnDeleteTokenCompleted,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void InstanceIDImpl::DeleteIDImpl(DeleteIDCallback callback) {
  RunWhenReady(base::BindOnce(&InstanceIDImpl::DoDeleteID,
                              weak_ptr_factory_.GetWeakPtr(),
                              std::move(callback)));
}

void InstanceIDImpl::DoDeleteID(DeleteIDCallback callback) {
  // Nothing to do if ID has not been generated.
  if (id_.empty()) {
    std::move(callback).Run(InstanceID::SUCCESS);
    return;
  }

  Handler()->DeleteAllTokensForApp(
      app_id(),
      base::BindOnce(&InstanceIDImpl::OnDeleteIDCompleted,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));

  Handler()->RemoveInstanceIDData(app_id());

  id_.clear();
  creation_time_ = base::Time();
}

void InstanceIDImpl::OnGetTokenCompleted(GetTokenCallback callback,
                                         const std::string& token,
                                         gcm::GCMClient::Result result) {
  std::move(callback).Run(token, GCMClientResultToInstanceIDResult(result));
}

void InstanceIDImpl::OnDeleteTokenCompleted(DeleteTokenCallback callback,
                                            gcm::GCMClient::Result result) {
  std::move(callback).Run(GCMClientResultToInstanceIDResult(result));
}

void InstanceIDImpl::OnDeleteIDCompleted(DeleteIDCallback callback,
                                         gcm::GCMClient::Result result) {
  std::move(callback).Run(GCMClientResultToInstanceIDResult(result));
}

void InstanceIDImpl::GetInstanceIDDataCompleted(
    const std::string& instance_id,
    const std::string& extra_data) {
  id_ = instance_id;

  if (extra_data.empty()) {
    creation_time_ = base::Time();
  } else {
    int64_t time_internal = 0LL;
    if (!base::StringToInt64(extra_data, &time_internal)) {
      DVLOG(1) << "Failed to parse the time data: " + extra_data;
      return;
    }
    creation_time_ = base::Time::FromInternalValue(time_internal);
  }

  delayed_task_controller_.SetReady();
}

void InstanceIDImpl::EnsureIDGenerated() {
  if (!id_.empty())
    return;

  // Now produce the ID in the following steps:

  // 1) Generates the random number in 8 bytes which is required by the server.
  //    We don't want to be strictly cryptographically secure. The server might
  //    reject the ID if there is a conflict or problem.
  uint8_t bytes[kInstanceIDByteLength];
  crypto::RandBytes(bytes);

  // 2) Transforms the first 4 bits to 0x7. Note that this is required by the
  //    server.
  bytes[0] &= 0x0f;
  bytes[0] |= 0x70;

  // 3) Encode the value in Android-compatible base64 scheme:
  //    * URL safe: '/' replaced by '_' and '+' replaced by '-'.
  //    * No padding: any trailing '=' will be removed.
  id_ = base::Base64Encode(bytes);
  std::replace(id_.begin(), id_.end(), '+', '-');
  std::replace(id_.begin(), id_.end(), '/', '_');
  std::erase(id_, '=');

  creation_time_ = base::Time::Now();

  // Save to the persistent store.
  Handler()->AddInstanceIDData(
      app_id(), id_, base::NumberToString(creation_time_.ToInternalValue()));
}

gcm::InstanceIDHandler* InstanceIDImpl::Handler() {
  gcm::InstanceIDHandler* handler =
      gcm_driver()->GetInstanceIDHandlerInternal();
  DCHECK(handler);
  return handler;
}

void InstanceIDImpl::RunWhenReady(base::OnceClosure task) {
  if (!delayed_task_controller_.CanRunTaskWithoutDelay())
    delayed_task_controller_.AddTask(std::move(task));
  else
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(task));
}

}  // namespace instance_id
