// Copyright (c) 2012 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 "chrome/browser/web_resource/notification_promo.h"

#include <cmath>
#include <vector>

#include "base/bind.h"
#include "base/prefs/pref_registry_simple.h"
#include "base/prefs/pref_service.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/sys_info.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/web_resource/promo_resource_service.h"
#include "chrome/common/chrome_version_info.h"
#include "chrome/common/pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "content/public/browser/user_metrics.h"
#include "net/base/url_util.h"
#include "ui/base/device_form_factor.h"
#include "url/gurl.h"

using base::UserMetricsAction;

namespace {

const int kDefaultGroupSize = 100;

const char promo_server_url[] = "https://clients3.google.com/crsignal/client";

// The name of the preference that stores the promotion object.
const char kPrefPromoObject[] = "promo";

// Keys in the kPrefPromoObject dictionary; used only here.
const char kPrefPromoText[] = "text";
const char kPrefPromoPayload[] = "payload";
const char kPrefPromoStart[] = "start";
const char kPrefPromoEnd[] = "end";
const char kPrefPromoNumGroups[] = "num_groups";
const char kPrefPromoSegment[] = "segment";
const char kPrefPromoIncrement[] = "increment";
const char kPrefPromoIncrementFrequency[] = "increment_frequency";
const char kPrefPromoIncrementMax[] = "increment_max";
const char kPrefPromoMaxViews[] = "max_views";
const char kPrefPromoGroup[] = "group";
const char kPrefPromoViews[] = "views";
const char kPrefPromoClosed[] = "closed";

// Returns a string suitable for the Promo Server URL 'osname' value.
std::string PlatformString() {
#if defined(OS_WIN)
  return "win";
#elif defined(OS_ANDROID)
  ui::DeviceFormFactor form_factor = ui::GetDeviceFormFactor();
  return std::string("android-") +
      (form_factor == ui::DEVICE_FORM_FACTOR_TABLET ? "tablet" : "phone");
#elif defined(OS_IOS)
  ui::DeviceFormFactor form_factor = ui::GetDeviceFormFactor();
  return std::string("ios-") +
      (form_factor == ui::DEVICE_FORM_FACTOR_TABLET ? "tablet" : "phone");
#elif defined(OS_MACOSX)
  return "mac";
#elif defined(OS_CHROMEOS)
  return "chromeos";
#elif defined(OS_LINUX)
  return "linux";
#else
  return "none";
#endif
}

// Returns a string suitable for the Promo Server URL 'dist' value.
const char* ChannelString() {
#if defined (OS_WIN)
  // GetChannel hits the registry on Windows. See http://crbug.com/70898.
  // TODO(achuith): Move NotificationPromo::PromoServerURL to the blocking pool.
  base::ThreadRestrictions::ScopedAllowIO allow_io;
#endif
  const chrome::VersionInfo::Channel channel =
      chrome::VersionInfo::GetChannel();
  switch (channel) {
    case chrome::VersionInfo::CHANNEL_CANARY:
      return "canary";
    case chrome::VersionInfo::CHANNEL_DEV:
      return "dev";
    case chrome::VersionInfo::CHANNEL_BETA:
      return "beta";
    case chrome::VersionInfo::CHANNEL_STABLE:
      return "stable";
    default:
      return "none";
  }
}

struct PromoMapEntry {
  NotificationPromo::PromoType promo_type;
  const char* promo_type_str;
};

const PromoMapEntry kPromoMap[] = {
    { NotificationPromo::NO_PROMO, "" },
    { NotificationPromo::NTP_NOTIFICATION_PROMO, "ntp_notification_promo" },
    { NotificationPromo::NTP_BUBBLE_PROMO, "ntp_bubble_promo" },
    { NotificationPromo::MOBILE_NTP_SYNC_PROMO, "mobile_ntp_sync_promo" },
};

// Convert PromoType to appropriate string.
const char* PromoTypeToString(NotificationPromo::PromoType promo_type) {
  for (size_t i = 0; i < arraysize(kPromoMap); ++i) {
    if (kPromoMap[i].promo_type == promo_type)
      return kPromoMap[i].promo_type_str;
  }
  NOTREACHED();
  return "";
}

// Deep-copies a node, replacing any "value" that is a key
// into "strings" dictionary with its value from "strings".
// E.g. for
//   {promo_action_args:['MSG_SHORT']} + strings:{MSG_SHORT:'yes'}
// it will return
//   {promo_action_args:['yes']}
// |node| - a value to be deep copied and resolved.
// |strings| - a dictionary of strings to be used for resolution.
// Returns a _new_ object that is a deep copy with replacements.
// TODO(aruslan): http://crbug.com/144320 Consider moving it to values.cc/h.
base::Value* DeepCopyAndResolveStrings(
    const base::Value* node,
    const base::DictionaryValue* strings) {
  switch (node->GetType()) {
    case base::Value::TYPE_LIST: {
      const base::ListValue* list = static_cast<const base::ListValue*>(node);
      base::ListValue* copy = new base::ListValue;
      for (base::ListValue::const_iterator it = list->begin();
           it != list->end();
           ++it) {
        base::Value* child_copy = DeepCopyAndResolveStrings(*it, strings);
        copy->Append(child_copy);
      }
      return copy;
    }

    case base::Value::TYPE_DICTIONARY: {
      const base::DictionaryValue* dict =
          static_cast<const base::DictionaryValue*>(node);
      base::DictionaryValue* copy = new base::DictionaryValue;
      for (base::DictionaryValue::Iterator it(*dict);
           !it.IsAtEnd();
           it.Advance()) {
        base::Value* child_copy = DeepCopyAndResolveStrings(&it.value(),
                                                            strings);
        copy->SetWithoutPathExpansion(it.key(), child_copy);
      }
      return copy;
    }

    case base::Value::TYPE_STRING: {
      std::string value;
      bool rv = node->GetAsString(&value);
      DCHECK(rv);
      std::string actual_value;
      if (!strings || !strings->GetString(value, &actual_value))
        actual_value = value;
      return new base::StringValue(actual_value);
    }

    default:
      // For everything else, just make a copy.
      return node->DeepCopy();
  }
}

void AppendQueryParameter(GURL* url,
                          const std::string& param,
                          const std::string& value) {
  *url = net::AppendQueryParameter(*url, param, value);
}

}  // namespace

NotificationPromo::NotificationPromo()
    : prefs_(g_browser_process->local_state()),
      promo_type_(NO_PROMO),
      promo_payload_(new base::DictionaryValue()),
      start_(0.0),
      end_(0.0),
      num_groups_(kDefaultGroupSize),
      initial_segment_(0),
      increment_(1),
      time_slice_(0),
      max_group_(0),
      max_views_(0),
      group_(0),
      views_(0),
      closed_(false),
      new_notification_(false) {
  DCHECK(prefs_);
}

NotificationPromo::~NotificationPromo() {}

void NotificationPromo::InitFromJson(const base::DictionaryValue& json,
                                     PromoType promo_type) {
  promo_type_ = promo_type;
  const base::ListValue* promo_list = NULL;
  DVLOG(1) << "InitFromJson " << PromoTypeToString(promo_type_);
  if (!json.GetList(PromoTypeToString(promo_type_), &promo_list))
    return;

  // No support for multiple promos yet. Only consider the first one.
  const base::DictionaryValue* promo = NULL;
  if (!promo_list->GetDictionary(0, &promo))
    return;

  // Date.
  const base::ListValue* date_list = NULL;
  if (promo->GetList("date", &date_list)) {
    const base::DictionaryValue* date;
    if (date_list->GetDictionary(0, &date)) {
      std::string time_str;
      base::Time time;
      if (date->GetString("start", &time_str) &&
          base::Time::FromString(time_str.c_str(), &time)) {
        start_ = time.ToDoubleT();
        DVLOG(1) << "start str=" << time_str
                 << ", start_="<< base::DoubleToString(start_);
      }
      if (date->GetString("end", &time_str) &&
          base::Time::FromString(time_str.c_str(), &time)) {
        end_ = time.ToDoubleT();
        DVLOG(1) << "end str =" << time_str
                 << ", end_=" << base::DoubleToString(end_);
      }
    }
  }

  // Grouping.
  const base::DictionaryValue* grouping = NULL;
  if (promo->GetDictionary("grouping", &grouping)) {
    grouping->GetInteger("buckets", &num_groups_);
    grouping->GetInteger("segment", &initial_segment_);
    grouping->GetInteger("increment", &increment_);
    grouping->GetInteger("increment_frequency", &time_slice_);
    grouping->GetInteger("increment_max", &max_group_);

    DVLOG(1) << "num_groups_ = " << num_groups_
             << ", initial_segment_ = " << initial_segment_
             << ", increment_ = " << increment_
             << ", time_slice_ = " << time_slice_
             << ", max_group_ = " << max_group_;
  }

  // Strings.
  const base::DictionaryValue* strings = NULL;
  promo->GetDictionary("strings", &strings);

  // Payload.
  const base::DictionaryValue* payload = NULL;
  if (promo->GetDictionary("payload", &payload)) {
    base::Value* ppcopy = DeepCopyAndResolveStrings(payload, strings);
    DCHECK(ppcopy && ppcopy->IsType(base::Value::TYPE_DICTIONARY));
    promo_payload_.reset(static_cast<base::DictionaryValue*>(ppcopy));
  }

  if (!promo_payload_->GetString("promo_message_short", &promo_text_) &&
      strings) {
    // For compatibility with the legacy desktop version,
    // if no |payload.promo_message_short| is specified,
    // the first string in |strings| is used.
    base::DictionaryValue::Iterator iter(*strings);
    iter.value().GetAsString(&promo_text_);
  }
  DVLOG(1) << "promo_text_=" << promo_text_;

  promo->GetInteger("max_views", &max_views_);
  DVLOG(1) << "max_views_ " << max_views_;

  CheckForNewNotification();
}

void NotificationPromo::CheckForNewNotification() {
  NotificationPromo old_promo;
  old_promo.InitFromPrefs(promo_type_);
  const double old_start = old_promo.start_;
  const double old_end = old_promo.end_;
  const std::string old_promo_text = old_promo.promo_text_;

  new_notification_ =
      old_start != start_ || old_end != end_ || old_promo_text != promo_text_;
  if (new_notification_)
    OnNewNotification();
}

void NotificationPromo::OnNewNotification() {
  DVLOG(1) << "OnNewNotification";
  // Create a new promo group.
  group_ = base::RandInt(0, num_groups_ - 1);
  WritePrefs();
}

// static
void NotificationPromo::RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(kPrefPromoObject);
}

// static
void NotificationPromo::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  // TODO(dbeam): Registered only for migration. Remove in M28 when
  // we're reasonably sure all prefs are gone.
  // http://crbug.com/168887
  registry->RegisterDictionaryPref(
      kPrefPromoObject, user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
}

// static
void NotificationPromo::MigrateUserPrefs(PrefService* user_prefs) {
  user_prefs->ClearPref(kPrefPromoObject);
}

void NotificationPromo::WritePrefs() {
  base::DictionaryValue* ntp_promo = new base::DictionaryValue;
  ntp_promo->SetString(kPrefPromoText, promo_text_);
  ntp_promo->Set(kPrefPromoPayload, promo_payload_->DeepCopy());
  ntp_promo->SetDouble(kPrefPromoStart, start_);
  ntp_promo->SetDouble(kPrefPromoEnd, end_);

  ntp_promo->SetInteger(kPrefPromoNumGroups, num_groups_);
  ntp_promo->SetInteger(kPrefPromoSegment, initial_segment_);
  ntp_promo->SetInteger(kPrefPromoIncrement, increment_);
  ntp_promo->SetInteger(kPrefPromoIncrementFrequency, time_slice_);
  ntp_promo->SetInteger(kPrefPromoIncrementMax, max_group_);

  ntp_promo->SetInteger(kPrefPromoMaxViews, max_views_);

  ntp_promo->SetInteger(kPrefPromoGroup, group_);
  ntp_promo->SetInteger(kPrefPromoViews, views_);
  ntp_promo->SetBoolean(kPrefPromoClosed, closed_);

  base::ListValue* promo_list = new base::ListValue;
  promo_list->Set(0, ntp_promo);  // Only support 1 promo for now.

  base::DictionaryValue promo_dict;
  promo_dict.MergeDictionary(prefs_->GetDictionary(kPrefPromoObject));
  promo_dict.Set(PromoTypeToString(promo_type_), promo_list);
  prefs_->Set(kPrefPromoObject, promo_dict);
  DVLOG(1) << "WritePrefs " << promo_dict;
}

void NotificationPromo::InitFromPrefs(PromoType promo_type) {
  promo_type_ = promo_type;
  const base::DictionaryValue* promo_dict =
      prefs_->GetDictionary(kPrefPromoObject);
  if (!promo_dict)
    return;

  const base::ListValue* promo_list = NULL;
  promo_dict->GetList(PromoTypeToString(promo_type_), &promo_list);
  if (!promo_list)
    return;

  const base::DictionaryValue* ntp_promo = NULL;
  promo_list->GetDictionary(0, &ntp_promo);
  if (!ntp_promo)
    return;

  ntp_promo->GetString(kPrefPromoText, &promo_text_);
  const base::DictionaryValue* promo_payload = NULL;
  if (ntp_promo->GetDictionary(kPrefPromoPayload, &promo_payload))
    promo_payload_.reset(promo_payload->DeepCopy());

  ntp_promo->GetDouble(kPrefPromoStart, &start_);
  ntp_promo->GetDouble(kPrefPromoEnd, &end_);

  ntp_promo->GetInteger(kPrefPromoNumGroups, &num_groups_);
  ntp_promo->GetInteger(kPrefPromoSegment, &initial_segment_);
  ntp_promo->GetInteger(kPrefPromoIncrement, &increment_);
  ntp_promo->GetInteger(kPrefPromoIncrementFrequency, &time_slice_);
  ntp_promo->GetInteger(kPrefPromoIncrementMax, &max_group_);

  ntp_promo->GetInteger(kPrefPromoMaxViews, &max_views_);

  ntp_promo->GetInteger(kPrefPromoGroup, &group_);
  ntp_promo->GetInteger(kPrefPromoViews, &views_);
  ntp_promo->GetBoolean(kPrefPromoClosed, &closed_);
}

bool NotificationPromo::CheckAppLauncher() const {
#if !defined(ENABLE_APP_LIST)
  return true;
#else
  bool is_app_launcher_promo = false;
  if (!promo_payload_->GetBoolean("is_app_launcher_promo",
                                  &is_app_launcher_promo))
    return true;
  return !is_app_launcher_promo ||
         !prefs_->GetBoolean(prefs::kAppLauncherIsEnabled);
#endif  // !defined(ENABLE_APP_LIST)
}

bool NotificationPromo::CanShow() const {
  return !closed_ &&
         !promo_text_.empty() &&
         !ExceedsMaxGroup() &&
         !ExceedsMaxViews() &&
         CheckAppLauncher() &&
         base::Time::FromDoubleT(StartTimeForGroup()) < base::Time::Now() &&
         base::Time::FromDoubleT(EndTime()) > base::Time::Now();
}

// static
void NotificationPromo::HandleClosed(PromoType promo_type) {
  content::RecordAction(UserMetricsAction("NTPPromoClosed"));
  NotificationPromo promo;
  promo.InitFromPrefs(promo_type);
  if (!promo.closed_) {
    promo.closed_ = true;
    promo.WritePrefs();
  }
}

// static
bool NotificationPromo::HandleViewed(PromoType promo_type) {
  content::RecordAction(UserMetricsAction("NTPPromoShown"));
  NotificationPromo promo;
  promo.InitFromPrefs(promo_type);
  ++promo.views_;
  promo.WritePrefs();
  return promo.ExceedsMaxViews();
}

bool NotificationPromo::ExceedsMaxGroup() const {
  return (max_group_ == 0) ? false : group_ >= max_group_;
}

bool NotificationPromo::ExceedsMaxViews() const {
  return (max_views_ == 0) ? false : views_ >= max_views_;
}

// static
GURL NotificationPromo::PromoServerURL() {
  GURL url(promo_server_url);
  AppendQueryParameter(&url, "dist", ChannelString());
  AppendQueryParameter(&url, "osname", PlatformString());
  AppendQueryParameter(&url, "branding", chrome::VersionInfo().Version());
  AppendQueryParameter(&url, "osver", base::SysInfo::OperatingSystemVersion());
  DVLOG(1) << "PromoServerURL=" << url.spec();
  // Note that locale param is added by WebResourceService.
  return url;
}

double NotificationPromo::StartTimeForGroup() const {
  if (group_ < initial_segment_)
    return start_;
  return start_ +
      std::ceil(static_cast<float>(group_ - initial_segment_ + 1) / increment_)
      * time_slice_;
}

double NotificationPromo::EndTime() const {
  return end_;
}