// 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 "libcef/browser/download_manager_delegate.h"

#include "include/cef_download_handler.h"
#include "libcef/browser/alloy/alloy_browser_host_impl.h"
#include "libcef/browser/context.h"
#include "libcef/browser/download_item_impl.h"
#include "libcef/browser/thread_util.h"

#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/common/chrome_constants.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/web_contents.h"
#include "net/base/filename_util.h"
#include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h"

using content::DownloadManager;
using content::WebContents;
using download::DownloadItem;

namespace {

// Helper function to retrieve the CefDownloadHandler.
CefRefPtr<CefDownloadHandler> GetDownloadHandler(
    CefRefPtr<AlloyBrowserHostImpl> browser) {
  CefRefPtr<CefClient> client = browser->GetClient();
  if (client.get())
    return client->GetDownloadHandler();
  return nullptr;
}

// CefBeforeDownloadCallback implementation.
class CefBeforeDownloadCallbackImpl : public CefBeforeDownloadCallback {
 public:
  CefBeforeDownloadCallbackImpl(const base::WeakPtr<DownloadManager>& manager,
                                uint32 download_id,
                                const base::FilePath& suggested_name,
                                content::DownloadTargetCallback callback)
      : manager_(manager),
        download_id_(download_id),
        suggested_name_(suggested_name),
        callback_(std::move(callback)) {}

  void Continue(const CefString& download_path, bool show_dialog) override {
    if (CEF_CURRENTLY_ON_UIT()) {
      if (download_id_ <= 0)
        return;

      if (manager_) {
        base::FilePath path = base::FilePath(download_path);
        CEF_POST_USER_VISIBLE_TASK(
            base::BindOnce(&CefBeforeDownloadCallbackImpl::GenerateFilename,
                           manager_, download_id_, suggested_name_, path,
                           show_dialog, std::move(callback_)));
      }

      download_id_ = 0;
    } else {
      CEF_POST_TASK(CEF_UIT,
                    base::BindOnce(&CefBeforeDownloadCallbackImpl::Continue,
                                   this, download_path, show_dialog));
    }
  }

 private:
  static void GenerateFilename(base::WeakPtr<DownloadManager> manager,
                               uint32 download_id,
                               const base::FilePath& suggested_name,
                               const base::FilePath& download_path,
                               bool show_dialog,
                               content::DownloadTargetCallback callback) {
    CEF_REQUIRE_BLOCKING();

    base::FilePath suggested_path = download_path;
    if (!suggested_path.empty()) {
      // Create the directory if necessary.
      base::FilePath dir_path = suggested_path.DirName();
      if (!base::DirectoryExists(dir_path) &&
          !base::CreateDirectory(dir_path)) {
        NOTREACHED() << "failed to create the download directory";
        suggested_path.clear();
      }
    }

    if (suggested_path.empty()) {
      if (base::PathService::Get(base::DIR_TEMP, &suggested_path)) {
        // Use the temp directory.
        suggested_path = suggested_path.Append(suggested_name);
      } else {
        // Use the current working directory.
        suggested_path = suggested_name;
      }
    }

    CEF_POST_TASK(
        CEF_UIT,
        base::BindOnce(&CefBeforeDownloadCallbackImpl::ChooseDownloadPath,
                       manager, download_id, suggested_path, show_dialog,
                       std::move(callback)));
  }

  static void ChooseDownloadPath(base::WeakPtr<DownloadManager> manager,
                                 uint32 download_id,
                                 const base::FilePath& suggested_path,
                                 bool show_dialog,
                                 content::DownloadTargetCallback callback) {
    if (!manager)
      return;

    DownloadItem* item = manager->GetDownload(download_id);
    if (!item || item->GetState() != DownloadItem::IN_PROGRESS)
      return;

    bool handled = false;

    if (show_dialog) {
      WebContents* web_contents =
          content::DownloadItemUtils::GetWebContents(item);
      CefRefPtr<AlloyBrowserHostImpl> browser =
          AlloyBrowserHostImpl::GetBrowserForContents(web_contents);
      if (browser.get()) {
        handled = true;

        CefFileDialogRunner::FileChooserParams params;
        params.mode = blink::mojom::FileChooserParams::Mode::kSave;
        if (!suggested_path.empty()) {
          params.default_file_name = suggested_path;
          if (!suggested_path.Extension().empty()) {
            params.accept_types.push_back(
                CefString(suggested_path.Extension()));
          }
        }

        browser->RunFileChooser(
            params,
            base::BindOnce(
                &CefBeforeDownloadCallbackImpl::ChooseDownloadPathCallback,
                std::move(callback)));
      }
    }

    if (!handled) {
      std::move(callback).Run(
          suggested_path, DownloadItem::TARGET_DISPOSITION_OVERWRITE,
          download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
          download::DownloadItem::MixedContentStatus::UNKNOWN, suggested_path,
          absl::nullopt /*download_schedule*/,
          download::DOWNLOAD_INTERRUPT_REASON_NONE);
    }
  }

  static void ChooseDownloadPathCallback(
      content::DownloadTargetCallback callback,
      int selected_accept_filter,
      const std::vector<base::FilePath>& file_paths) {
    DCHECK_LE(file_paths.size(), (size_t)1);

    base::FilePath path;
    if (file_paths.size() > 0)
      path = file_paths.front();

    // The download will be cancelled if |path| is empty.
    std::move(callback).Run(path, DownloadItem::TARGET_DISPOSITION_OVERWRITE,
                            download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
                            download::DownloadItem::MixedContentStatus::UNKNOWN,
                            path, absl::nullopt /*download_schedule*/,
                            download::DOWNLOAD_INTERRUPT_REASON_NONE);
  }

  base::WeakPtr<DownloadManager> manager_;
  uint32 download_id_;
  base::FilePath suggested_name_;
  content::DownloadTargetCallback callback_;

  IMPLEMENT_REFCOUNTING(CefBeforeDownloadCallbackImpl);
  DISALLOW_COPY_AND_ASSIGN(CefBeforeDownloadCallbackImpl);
};

// CefDownloadItemCallback implementation.
class CefDownloadItemCallbackImpl : public CefDownloadItemCallback {
 public:
  explicit CefDownloadItemCallbackImpl(
      const base::WeakPtr<DownloadManager>& manager,
      uint32 download_id)
      : manager_(manager), download_id_(download_id) {}

  void Cancel() override {
    CEF_POST_TASK(CEF_UIT,
                  base::BindOnce(&CefDownloadItemCallbackImpl::DoCancel, this));
  }

  void Pause() override {
    CEF_POST_TASK(CEF_UIT,
                  base::BindOnce(&CefDownloadItemCallbackImpl::DoPause, this));
  }

  void Resume() override {
    CEF_POST_TASK(CEF_UIT,
                  base::BindOnce(&CefDownloadItemCallbackImpl::DoResume, this));
  }

 private:
  void DoCancel() {
    if (download_id_ <= 0)
      return;

    if (manager_) {
      DownloadItem* item = manager_->GetDownload(download_id_);
      if (item && item->GetState() == DownloadItem::IN_PROGRESS)
        item->Cancel(true);
    }

    download_id_ = 0;
  }

  void DoPause() {
    if (download_id_ <= 0)
      return;

    if (manager_) {
      DownloadItem* item = manager_->GetDownload(download_id_);
      if (item && item->GetState() == DownloadItem::IN_PROGRESS)
        item->Pause();
    }
  }

  void DoResume() {
    if (download_id_ <= 0)
      return;

    if (manager_) {
      DownloadItem* item = manager_->GetDownload(download_id_);
      if (item && item->CanResume())
        item->Resume(true);
    }
  }

  base::WeakPtr<DownloadManager> manager_;
  uint32 download_id_;

  IMPLEMENT_REFCOUNTING(CefDownloadItemCallbackImpl);
  DISALLOW_COPY_AND_ASSIGN(CefDownloadItemCallbackImpl);
};

}  // namespace

CefDownloadManagerDelegate::CefDownloadManagerDelegate(DownloadManager* manager)
    : manager_(manager), manager_ptr_factory_(manager) {
  DCHECK(manager);
  manager->AddObserver(this);

  DownloadManager::DownloadVector items;
  manager->GetAllDownloads(&items);
  DownloadManager::DownloadVector::const_iterator it = items.begin();
  for (; it != items.end(); ++it)
    OnDownloadCreated(manager, *it);
}

CefDownloadManagerDelegate::~CefDownloadManagerDelegate() {
  if (manager_) {
    manager_->SetDelegate(nullptr);
    manager_->RemoveObserver(this);
  }

  while (!item_browser_map_.empty())
    OnDownloadDestroyed(item_browser_map_.begin()->first);
}

void CefDownloadManagerDelegate::OnDownloadUpdated(DownloadItem* download) {
  CefRefPtr<AlloyBrowserHostImpl> browser = GetBrowser(download);
  CefRefPtr<CefDownloadHandler> handler;
  if (browser.get())
    handler = GetDownloadHandler(browser);

  if (handler.get()) {
    CefRefPtr<CefDownloadItemImpl> download_item(
        new CefDownloadItemImpl(download));
    CefRefPtr<CefDownloadItemCallback> callback(new CefDownloadItemCallbackImpl(
        manager_ptr_factory_.GetWeakPtr(), download->GetId()));

    handler->OnDownloadUpdated(browser.get(), download_item.get(), callback);

    ignore_result(download_item->Detach(nullptr));
  }
}

void CefDownloadManagerDelegate::OnDownloadDestroyed(DownloadItem* item) {
  item->RemoveObserver(this);

  AlloyBrowserHostImpl* browser = nullptr;

  ItemBrowserMap::iterator it = item_browser_map_.find(item);
  DCHECK(it != item_browser_map_.end());
  if (it != item_browser_map_.end()) {
    browser = it->second;
    item_browser_map_.erase(it);
  }

  if (browser) {
    // Determine if any remaining DownloadItems are associated with the same
    // browser. If not, then unregister as an observer.
    bool has_remaining = false;
    ItemBrowserMap::const_iterator it2 = item_browser_map_.begin();
    for (; it2 != item_browser_map_.end(); ++it2) {
      if (it2->second == browser) {
        has_remaining = true;
        break;
      }
    }

    if (!has_remaining)
      browser->RemoveObserver(this);
  }
}

void CefDownloadManagerDelegate::OnDownloadCreated(DownloadManager* manager,
                                                   DownloadItem* item) {
  // This callback may arrive after DetermineDownloadTarget, so we allow
  // association from either method.
  CefRefPtr<AlloyBrowserHostImpl> browser = GetOrAssociateBrowser(item);
  if (!browser) {
    // If the download is rejected (e.g. ALT+click on an invalid protocol link)
    // then an "interrupted" download will be started via DownloadManagerImpl::
    // StartDownloadWithId (originating from CreateInterruptedDownload) with no
    // associated WebContents and consequently no associated CEF browser. In
    // that case DetermineDownloadTarget will be called before this method.
    // TODO(cef): Figure out how to expose this via a client callback.
    const std::vector<GURL>& url_chain = item->GetUrlChain();
    if (!url_chain.empty()) {
      LOG(INFO) << "Rejected download of " << url_chain.back().spec();
    }
    item->Cancel(true);
  }
}

void CefDownloadManagerDelegate::ManagerGoingDown(DownloadManager* manager) {
  DCHECK_EQ(manager, manager_);
  manager->SetDelegate(nullptr);
  manager->RemoveObserver(this);
  manager_ptr_factory_.InvalidateWeakPtrs();
  manager_ = nullptr;
}

bool CefDownloadManagerDelegate::DetermineDownloadTarget(
    DownloadItem* item,
    content::DownloadTargetCallback* callback) {
  if (!item->GetForcedFilePath().empty()) {
    std::move(*callback).Run(
        item->GetForcedFilePath(), DownloadItem::TARGET_DISPOSITION_OVERWRITE,
        download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
        download::DownloadItem::MixedContentStatus::UNKNOWN,
        item->GetForcedFilePath(), absl::nullopt /*download_schedule*/,
        download::DOWNLOAD_INTERRUPT_REASON_NONE);
    return true;
  }

  // This callback may arrive before OnDownloadCreated, so we allow association
  // from either method.
  CefRefPtr<AlloyBrowserHostImpl> browser = GetOrAssociateBrowser(item);
  CefRefPtr<CefDownloadHandler> handler;
  if (browser.get())
    handler = GetDownloadHandler(browser);

  if (handler.get()) {
    base::FilePath suggested_name = net::GenerateFileName(
        item->GetURL(), item->GetContentDisposition(), std::string(),
        item->GetSuggestedFilename(), item->GetMimeType(), "download");

    CefRefPtr<CefDownloadItemImpl> download_item(new CefDownloadItemImpl(item));
    CefRefPtr<CefBeforeDownloadCallback> callbackObj(
        new CefBeforeDownloadCallbackImpl(manager_ptr_factory_.GetWeakPtr(),
                                          item->GetId(), suggested_name,
                                          std::move(*callback)));

    handler->OnBeforeDownload(browser.get(), download_item.get(),
                              suggested_name.value(), callbackObj);

    ignore_result(download_item->Detach(nullptr));
  }

  return true;
}

void CefDownloadManagerDelegate::GetNextId(
    content::DownloadIdCallback callback) {
  static uint32 next_id = DownloadItem::kInvalidId + 1;
  std::move(callback).Run(next_id++);
}

std::string CefDownloadManagerDelegate::ApplicationClientIdForFileScanning() {
  const CefSettings& settings = CefContext::Get()->settings();
  if (settings.application_client_id_for_file_scanning.length > 0) {
    return CefString(&settings.application_client_id_for_file_scanning)
        .ToString();
  }
  return std::string();
}

void CefDownloadManagerDelegate::OnBrowserDestroyed(
    CefBrowserHostBase* browser) {
  ItemBrowserMap::iterator it = item_browser_map_.begin();
  for (; it != item_browser_map_.end(); ++it) {
    if (it->second == browser) {
      // Don't call back into browsers that have been destroyed. We're not
      // canceling the download so it will continue silently until it completes
      // or until the associated browser context is destroyed.
      it->second = nullptr;
    }
  }
}

AlloyBrowserHostImpl* CefDownloadManagerDelegate::GetOrAssociateBrowser(
    download::DownloadItem* item) {
  ItemBrowserMap::const_iterator it = item_browser_map_.find(item);
  if (it != item_browser_map_.end())
    return it->second;

  AlloyBrowserHostImpl* browser = nullptr;
  content::WebContents* contents =
      content::DownloadItemUtils::GetWebContents(item);
  if (contents) {
    browser = AlloyBrowserHostImpl::GetBrowserForContents(contents).get();
    DCHECK(browser);
  }
  if (!browser)
    return nullptr;

  item->AddObserver(this);

  item_browser_map_.insert(std::make_pair(item, browser));

  // Register as an observer so that we can cancel associated DownloadItems when
  // the browser is destroyed.
  if (!browser->HasObserver(this))
    browser->AddObserver(this);

  return browser;
}

AlloyBrowserHostImpl* CefDownloadManagerDelegate::GetBrowser(
    DownloadItem* item) {
  ItemBrowserMap::const_iterator it = item_browser_map_.find(item);
  if (it != item_browser_map_.end())
    return it->second;

  // If the download is rejected (e.g. ALT+click on an invalid protocol link)
  // then an "interrupted" download will be started via DownloadManagerImpl::
  // StartDownloadWithId (originating from CreateInterruptedDownload) with no
  // associated WebContents and consequently no associated CEF browser. In that
  // case DetermineDownloadTarget will be called before OnDownloadCreated.
  DCHECK(!content::DownloadItemUtils::GetWebContents(item));
  return nullptr;
}