// Copyright (c) 2015 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#include "atom/browser/api/atom_api_session.h"

#include <string>
#include <vector>

#include "atom/browser/api/atom_api_cookies.h"
#include "atom/browser/api/atom_api_download_item.h"
#include "atom/browser/api/atom_api_web_contents.h"
#include "atom/browser/api/save_page_handler.h"
#include "atom/browser/atom_browser_context.h"
#include "atom/browser/atom_browser_main_parts.h"
#include "atom/common/native_mate_converters/callback.h"
#include "atom/common/native_mate_converters/gurl_converter.h"
#include "atom/common/native_mate_converters/file_path_converter.h"
#include "atom/common/node_includes.h"
#include "base/files/file_path.h"
#include "base/prefs/pref_service.h"
#include "base/strings/string_util.h"
#include "base/thread_task_runner_handle.h"
#include "brightray/browser/net/devtools_network_conditions.h"
#include "brightray/browser/net/devtools_network_controller.h"
#include "chrome/common/pref_names.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "native_mate/dictionary.h"
#include "native_mate/object_template_builder.h"
#include "net/base/load_flags.h"
#include "net/disk_cache/disk_cache.h"
#include "net/proxy/proxy_service.h"
#include "net/proxy/proxy_config_service_fixed.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_getter.h"

using content::BrowserThread;
using content::StoragePartition;

namespace {

struct ClearStorageDataOptions {
  GURL origin;
  uint32 storage_types = StoragePartition::REMOVE_DATA_MASK_ALL;
  uint32 quota_types = StoragePartition::QUOTA_MANAGED_STORAGE_MASK_ALL;
};

uint32 GetStorageMask(const std::vector<std::string>& storage_types) {
  uint32 storage_mask = 0;
  for (const auto& it : storage_types) {
    auto type = base::StringToLowerASCII(it);
    if (type == "appcache")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_APPCACHE;
    else if (type == "cookies")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_COOKIES;
    else if (type == "filesystem")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_FILE_SYSTEMS;
    else if (type == "indexdb")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_INDEXEDDB;
    else if (type == "localstorage")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_LOCAL_STORAGE;
    else if (type == "shadercache")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_SHADER_CACHE;
    else if (type == "websql")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_WEBSQL;
    else if (type == "serviceworkers")
      storage_mask |= StoragePartition::REMOVE_DATA_MASK_SERVICE_WORKERS;
  }
  return storage_mask;
}

uint32 GetQuotaMask(const std::vector<std::string>& quota_types) {
  uint32 quota_mask = 0;
  for (const auto& it : quota_types) {
    auto type = base::StringToLowerASCII(it);
    if (type == "temporary")
      quota_mask |= StoragePartition::QUOTA_MANAGED_STORAGE_MASK_TEMPORARY;
    else if (type == "persistent")
      quota_mask |= StoragePartition::QUOTA_MANAGED_STORAGE_MASK_PERSISTENT;
    else if (type == "syncable")
      quota_mask |= StoragePartition::QUOTA_MANAGED_STORAGE_MASK_SYNCABLE;
  }
  return quota_mask;
}

}  // namespace

namespace mate {

template<>
struct Converter<ClearStorageDataOptions> {
  static bool FromV8(v8::Isolate* isolate,
                     v8::Local<v8::Value> val,
                     ClearStorageDataOptions* out) {
    mate::Dictionary options;
    if (!ConvertFromV8(isolate, val, &options))
      return false;
    options.Get("origin", &out->origin);
    std::vector<std::string> types;
    if (options.Get("storages", &types))
      out->storage_types = GetStorageMask(types);
    if (options.Get("quotas", &types))
      out->quota_types = GetQuotaMask(types);
    return true;
  }
};

template<>
struct Converter<net::ProxyConfig> {
  static bool FromV8(v8::Isolate* isolate,
                     v8::Local<v8::Value> val,
                     net::ProxyConfig* out) {
    std::string proxy;
    if (!ConvertFromV8(isolate, val, &proxy))
      return false;
    auto pac_url = GURL(proxy);
    if (pac_url.is_valid()) {
      out->set_pac_url(pac_url);
    } else {
      out->proxy_rules().ParseFromString(proxy);
    }
    return true;
  }
};

}  // namespace mate

namespace atom {

namespace api {

namespace {

// The wrapSession funtion which is implemented in JavaScript
using WrapSessionCallback = base::Callback<void(v8::Local<v8::Value>)>;
WrapSessionCallback g_wrap_session;

class ResolveProxyHelper {
 public:
  ResolveProxyHelper(AtomBrowserContext* browser_context,
                     const GURL& url,
                     Session::ResolveProxyCallback callback)
      : callback_(callback),
        original_thread_(base::ThreadTaskRunnerHandle::Get()) {
    scoped_refptr<net::URLRequestContextGetter> context_getter =
        browser_context->GetRequestContext();
    context_getter->GetNetworkTaskRunner()->PostTask(
        FROM_HERE,
        base::Bind(&ResolveProxyHelper::ResolveProxy,
                   base::Unretained(this), context_getter, url));
  }

  void OnResolveProxyCompleted(int result) {
    std::string proxy;
    if (result == net::OK)
      proxy = proxy_info_.ToPacString();
    original_thread_->PostTask(FROM_HERE,
                               base::Bind(callback_, proxy));
    delete this;
  }

 private:
  void ResolveProxy(scoped_refptr<net::URLRequestContextGetter> context_getter,
                    const GURL& url) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

    net::ProxyService* proxy_service =
        context_getter->GetURLRequestContext()->proxy_service();
    net::CompletionCallback completion_callback =
        base::Bind(&ResolveProxyHelper::OnResolveProxyCompleted,
                   base::Unretained(this));

    // Start the request.
    int result = proxy_service->ResolveProxy(
        url, net::LOAD_NORMAL, &proxy_info_, completion_callback,
        &pac_req_, nullptr, net::BoundNetLog());

    // Completed synchronously.
    if (result != net::ERR_IO_PENDING)
      completion_callback.Run(result);
  }

  Session::ResolveProxyCallback callback_;
  net::ProxyInfo proxy_info_;
  net::ProxyService::PacRequest* pac_req_;
  scoped_refptr<base::SingleThreadTaskRunner> original_thread_;

  DISALLOW_COPY_AND_ASSIGN(ResolveProxyHelper);
};

// Runs the callback in UI thread.
template <typename ...T>
void RunCallbackInUI(const base::Callback<void(T...)>& callback, T... result) {
  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE, base::Bind(callback, result...));
}

// Callback of HttpCache::GetBackend.
void OnGetBackend(disk_cache::Backend** backend_ptr,
                  const net::CompletionCallback& callback,
                  int result) {
  if (result != net::OK) {
    RunCallbackInUI(callback, result);
  } else if (backend_ptr && *backend_ptr) {
    (*backend_ptr)->DoomAllEntries(base::Bind(&RunCallbackInUI<int>, callback));
  } else {
    RunCallbackInUI<int>(callback, net::ERR_FAILED);
  }
}

void ClearHttpCacheInIO(
    const scoped_refptr<net::URLRequestContextGetter>& context_getter,
    const net::CompletionCallback& callback) {
  auto request_context = context_getter->GetURLRequestContext();
  auto http_cache = request_context->http_transaction_factory()->GetCache();
  if (!http_cache)
    RunCallbackInUI<int>(callback, net::ERR_FAILED);

  // Call GetBackend and make the backend's ptr accessable in OnGetBackend.
  using BackendPtr = disk_cache::Backend*;
  BackendPtr* backend_ptr = new BackendPtr(nullptr);
  net::CompletionCallback on_get_backend =
      base::Bind(&OnGetBackend, base::Owned(backend_ptr), callback);
  int rv = http_cache->GetBackend(backend_ptr, on_get_backend);
  if (rv != net::ERR_IO_PENDING)
    on_get_backend.Run(net::OK);
}

void SetProxyInIO(net::URLRequestContextGetter* getter,
                  const net::ProxyConfig& config,
                  const base::Closure& callback) {
  auto proxy_service = getter->GetURLRequestContext()->proxy_service();
  proxy_service->ResetConfigService(new net::ProxyConfigServiceFixed(config));
  // Refetches and applies the new pac script if provided.
  proxy_service->ForceReloadProxyConfig();
  RunCallbackInUI(callback);
}

}  // namespace

Session::Session(AtomBrowserContext* browser_context)
    : browser_context_(browser_context) {
  AttachAsUserData(browser_context);

  // Observe DownloadManger to get download notifications.
  content::BrowserContext::GetDownloadManager(browser_context)->
      AddObserver(this);
}

Session::~Session() {
  content::BrowserContext::GetDownloadManager(browser_context())->
      RemoveObserver(this);
  Destroy();
}

void Session::OnDownloadCreated(content::DownloadManager* manager,
                                content::DownloadItem* item) {
  auto web_contents = item->GetWebContents();
  if (SavePageHandler::IsSavePageTypes(item->GetMimeType()))
    return;
  bool prevent_default = Emit(
      "will-download",
      DownloadItem::Create(isolate(), item),
      api::WebContents::CreateFrom(isolate(), web_contents));
  if (prevent_default) {
    item->Cancel(true);
    item->Remove();
  }
}

bool Session::IsDestroyed() const {
  return !browser_context_;
}

void Session::Destroy() {
  browser_context_ = nullptr;
}

void Session::ResolveProxy(const GURL& url, ResolveProxyCallback callback) {
  new ResolveProxyHelper(browser_context(), url, callback);
}

void Session::ClearCache(const net::CompletionCallback& callback) {
  BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
      base::Bind(&ClearHttpCacheInIO,
                 make_scoped_refptr(browser_context_->GetRequestContext()),
                 callback));
}

void Session::ClearStorageData(mate::Arguments* args) {
  // clearStorageData([options, ]callback)
  ClearStorageDataOptions options;
  args->GetNext(&options);
  base::Closure callback;
  if (!args->GetNext(&callback)) {
    args->ThrowError();
    return;
  }

  auto storage_partition =
      content::BrowserContext::GetStoragePartition(browser_context(), nullptr);
  storage_partition->ClearData(
      options.storage_types, options.quota_types, options.origin,
      content::StoragePartition::OriginMatcherFunction(),
      base::Time(), base::Time::Max(), callback);
}

void Session::SetProxy(const net::ProxyConfig& config,
                       const base::Closure& callback) {
  auto getter = browser_context_->GetRequestContext();
  BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
      base::Bind(&SetProxyInIO, base::Unretained(getter), config, callback));
}

void Session::SetDownloadPath(const base::FilePath& path) {
  browser_context_->prefs()->SetFilePath(
      prefs::kDownloadDefaultDirectory, path);
}

void Session::EnableNetworkEmulation(const mate::Dictionary& options) {
  scoped_ptr<brightray::DevToolsNetworkConditions> conditions;
  bool offline = false;
  double latency, download_throughput, upload_throughput;
  if (options.Get("offline", &offline) && offline) {
    conditions.reset(new brightray::DevToolsNetworkConditions(offline));
  } else {
    options.Get("latency", &latency);
    options.Get("downloadThroughput", &download_throughput);
    options.Get("uploadThroughput", &upload_throughput);
    conditions.reset(
        new brightray::DevToolsNetworkConditions(false,
                                                 latency,
                                                 download_throughput,
                                                 upload_throughput));
  }
  auto controller = browser_context_->GetDevToolsNetworkController();

  BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
      base::Bind(&brightray::DevToolsNetworkController::SetNetworkState,
                 base::Unretained(controller),
                 std::string(),
                 base::Passed(&conditions)));
}

void Session::DisableNetworkEmulation() {
  scoped_ptr<brightray::DevToolsNetworkConditions> conditions(
      new brightray::DevToolsNetworkConditions(false));
  auto controller = browser_context_->GetDevToolsNetworkController();

  BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
      base::Bind(&brightray::DevToolsNetworkController::SetNetworkState,
                 base::Unretained(controller),
                 std::string(),
                 base::Passed(&conditions)));
}

v8::Local<v8::Value> Session::Cookies(v8::Isolate* isolate) {
  if (cookies_.IsEmpty()) {
    auto handle = atom::api::Cookies::Create(isolate, browser_context());
    cookies_.Reset(isolate, handle.ToV8());
  }
  return v8::Local<v8::Value>::New(isolate, cookies_);
}

mate::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
    v8::Isolate* isolate) {
  return mate::ObjectTemplateBuilder(isolate)
      .SetMethod("resolveProxy", &Session::ResolveProxy)
      .SetMethod("clearCache", &Session::ClearCache)
      .SetMethod("clearStorageData", &Session::ClearStorageData)
      .SetMethod("setProxy", &Session::SetProxy)
      .SetMethod("setDownloadPath", &Session::SetDownloadPath)
      .SetMethod("enableNetworkEmulation", &Session::EnableNetworkEmulation)
      .SetMethod("disableNetworkEmulation", &Session::DisableNetworkEmulation)
      .SetProperty("cookies", &Session::Cookies);
}

// static
mate::Handle<Session> Session::CreateFrom(
    v8::Isolate* isolate, AtomBrowserContext* browser_context) {
  auto existing = TrackableObject::FromWrappedClass(isolate, browser_context);
  if (existing)
    return mate::CreateHandle(isolate, static_cast<Session*>(existing));

  auto handle = mate::CreateHandle(isolate, new Session(browser_context));
  g_wrap_session.Run(handle.ToV8());
  return handle;
}

// static
mate::Handle<Session> Session::FromPartition(
    v8::Isolate* isolate, const std::string& partition, bool in_memory) {
  auto browser_context = brightray::BrowserContext::From(partition, in_memory);
  return CreateFrom(isolate,
                    static_cast<AtomBrowserContext*>(browser_context.get()));
}

void ClearWrapSession() {
  g_wrap_session.Reset();
}

void SetWrapSession(const WrapSessionCallback& callback) {
  g_wrap_session = callback;

  // Cleanup the wrapper on exit.
  atom::AtomBrowserMainParts::Get()->RegisterDestructionCallback(
      base::Bind(ClearWrapSession));
}

}  // namespace api

}  // namespace atom

namespace {

void Initialize(v8::Local<v8::Object> exports, v8::Local<v8::Value> unused,
                v8::Local<v8::Context> context, void* priv) {
  v8::Isolate* isolate = context->GetIsolate();
  mate::Dictionary dict(isolate, exports);
  dict.SetMethod("fromPartition", &atom::api::Session::FromPartition);
  dict.SetMethod("_setWrapSession", &atom::api::SetWrapSession);
}

}  // namespace

NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_browser_session, Initialize)
