// Copyright (c) 2011 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/download/download_item.h"

#include "base/basictypes.h"
#include "base/file_util.h"
#include "base/format_macros.h"
#include "base/logging.h"
#include "base/metrics/histogram.h"
#include "base/stringprintf.h"
#include "base/timer.h"
#include "base/utf_string_conversions.h"
#include "net/base/net_util.h"
#include "chrome/browser/download/download_extensions.h"
#include "chrome/browser/download/download_file_manager.h"
#include "chrome/browser/download/download_history.h"
#include "chrome/browser/download/download_manager.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/download/download_util.h"
#include "chrome/browser/history/download_create_info.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/prefs/pref_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/pref_names.h"
#include "content/browser/browser_thread.h"
#include "ui/base/l10n/l10n_util.h"

// A DownloadItem normally goes through the following states:
//      * Created (when download starts)
//      * Made visible to consumers (e.g. Javascript) after the
//        destination file has been determined.
//      * Entered into the history database.
//      * Made visible in the download shelf.
//      * All data is saved.  Note that the actual data download occurs
//        in parallel with the above steps, but until those steps are
//        complete, completion of the data download will be ignored.
//      * Download file is renamed to its final name, and possibly
//        auto-opened.
// TODO(rdsmith): This progress should be reflected in
// DownloadItem::DownloadState and a state transition table/state diagram.
//
// TODO(rdsmith): This description should be updated to reflect the cancel
// pathways.

namespace {

// Update frequency (milliseconds).
const int kUpdateTimeMs = 1000;

void DeleteDownloadedFile(const FilePath& path) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));

  // Make sure we only delete files.
  if (!file_util::DirectoryExists(path))
    file_util::Delete(path, false);
}

const char* DebugSafetyStateString(DownloadItem::SafetyState state) {
  switch (state) {
    case DownloadItem::SAFE:
      return "SAFE";
    case DownloadItem::DANGEROUS:
      return "DANGEROUS";
    case DownloadItem::DANGEROUS_BUT_VALIDATED:
      return "DANGEROUS_BUT_VALIDATED";
    default:
      NOTREACHED() << "Unknown safety state " << state;
      return "unknown";
  };
}

const char* DebugDownloadStateString(DownloadItem::DownloadState state) {
  switch (state) {
    case DownloadItem::IN_PROGRESS:
      return "IN_PROGRESS";
    case DownloadItem::COMPLETE:
      return "COMPLETE";
    case DownloadItem::CANCELLED:
      return "CANCELLED";
    case DownloadItem::REMOVING:
      return "REMOVING";
    case DownloadItem::INTERRUPTED:
      return "INTERRUPTED";
    default:
      NOTREACHED() << "Unknown download state " << state;
      return "unknown";
  };
}

DownloadItem::SafetyState GetSafetyState(bool dangerous_file,
                                         bool dangerous_url) {
  return (dangerous_url || dangerous_file) ?
      DownloadItem::DANGEROUS : DownloadItem::SAFE;
}

// Note: When a download has both |dangerous_file| and |dangerous_url| set,
// danger type is set to DANGEROUS_URL since the risk of dangerous URL
// overweights that of dangerous file type.
DownloadItem::DangerType GetDangerType(bool dangerous_file,
                                       bool dangerous_url) {
  if (dangerous_url) {
    // dangerous URL overweights dangerous file. We check dangerous URL first.
    return DownloadItem::DANGEROUS_URL;
  } else if (dangerous_file) {
    return DownloadItem::DANGEROUS_FILE;
  }
  return DownloadItem::NOT_DANGEROUS;
}

}  // namespace

// Constructor for reading from the history service.
DownloadItem::DownloadItem(DownloadManager* download_manager,
                           const DownloadCreateInfo& info)
    : id_(-1),
      full_path_(info.path),
      path_uniquifier_(0),
      url_chain_(info.url_chain),
      referrer_url_(info.referrer_url),
      mime_type_(info.mime_type),
      original_mime_type_(info.original_mime_type),
      total_bytes_(info.total_bytes),
      received_bytes_(info.received_bytes),
      start_tick_(base::TimeTicks()),
      state_(static_cast<DownloadState>(info.state)),
      start_time_(info.start_time),
      db_handle_(info.db_handle),
      download_manager_(download_manager),
      is_paused_(false),
      open_when_complete_(false),
      safety_state_(SAFE),
      danger_type_(NOT_DANGEROUS),
      auto_opened_(false),
      target_name_(info.original_name),
      render_process_id_(-1),
      request_id_(-1),
      save_as_(false),
      is_otr_(false),
      is_extension_install_(info.is_extension_install),
      name_finalized_(false),
      is_temporary_(false),
      all_data_saved_(false),
      opened_(false) {
  if (IsInProgress())
    state_ = CANCELLED;
  if (IsComplete())
    all_data_saved_ = true;
  Init(false /* don't start progress timer */);
}

// Constructing for a regular download:
DownloadItem::DownloadItem(DownloadManager* download_manager,
                           const DownloadCreateInfo& info,
                           bool is_otr)
    : id_(info.download_id),
      full_path_(info.path),
      path_uniquifier_(info.path_uniquifier),
      url_chain_(info.url_chain),
      referrer_url_(info.referrer_url),
      mime_type_(info.mime_type),
      original_mime_type_(info.original_mime_type),
      total_bytes_(info.total_bytes),
      received_bytes_(0),
      last_os_error_(0),
      start_tick_(base::TimeTicks::Now()),
      state_(IN_PROGRESS),
      start_time_(info.start_time),
      db_handle_(DownloadHistory::kUninitializedHandle),
      download_manager_(download_manager),
      is_paused_(false),
      open_when_complete_(false),
      safety_state_(GetSafetyState(info.is_dangerous_file,
                                   info.is_dangerous_url)),
      danger_type_(GetDangerType(info.is_dangerous_file,
                                 info.is_dangerous_url)),
      auto_opened_(false),
      target_name_(info.original_name),
      render_process_id_(info.child_id),
      request_id_(info.request_id),
      save_as_(info.prompt_user_for_save_location),
      is_otr_(is_otr),
      is_extension_install_(info.is_extension_install),
      name_finalized_(false),
      is_temporary_(!info.save_info.file_path.empty()),
      all_data_saved_(false),
      opened_(false) {
  Init(true /* start progress timer */);
}

// Constructing for the "Save Page As..." feature:
DownloadItem::DownloadItem(DownloadManager* download_manager,
                           const FilePath& path,
                           const GURL& url,
                           bool is_otr)
    : id_(1),
      full_path_(path),
      path_uniquifier_(0),
      url_chain_(1, url),
      referrer_url_(GURL()),
      mime_type_(std::string()),
      original_mime_type_(std::string()),
      total_bytes_(0),
      received_bytes_(0),
      last_os_error_(0),
      start_tick_(base::TimeTicks::Now()),
      state_(IN_PROGRESS),
      start_time_(base::Time::Now()),
      db_handle_(DownloadHistory::kUninitializedHandle),
      download_manager_(download_manager),
      is_paused_(false),
      open_when_complete_(false),
      safety_state_(SAFE),
      danger_type_(NOT_DANGEROUS),
      auto_opened_(false),
      render_process_id_(-1),
      request_id_(-1),
      save_as_(false),
      is_otr_(is_otr),
      is_extension_install_(false),
      name_finalized_(false),
      is_temporary_(false),
      all_data_saved_(false),
      opened_(false) {
  Init(true /* start progress timer */);
}

DownloadItem::~DownloadItem() {
  state_ = REMOVING;
  UpdateObservers();
}

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

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

void DownloadItem::UpdateObservers() {
  FOR_EACH_OBSERVER(Observer, observers_, OnDownloadUpdated(this));
}

bool DownloadItem::CanOpenDownload() {
  return !Extension::IsExtension(target_name_);
}

bool DownloadItem::ShouldOpenFileBasedOnExtension() {
  return download_manager_->ShouldOpenFileBasedOnExtension(
      GetUserVerifiedFilePath());
}

void DownloadItem::OpenFilesBasedOnExtension(bool open) {
  DownloadPrefs* prefs = download_manager_->download_prefs();
  if (open)
    prefs->EnableAutoOpenBasedOnExtension(GetUserVerifiedFilePath());
  else
    prefs->DisableAutoOpenBasedOnExtension(GetUserVerifiedFilePath());
}

void DownloadItem::OpenDownload() {
  if (IsPartialDownload()) {
    open_when_complete_ = !open_when_complete_;
  } else if (IsComplete()) {
    opened_ = true;
    FOR_EACH_OBSERVER(Observer, observers_, OnDownloadOpened(this));
    if (is_extension_install()) {
      download_util::OpenChromeExtension(download_manager_->profile(),
                                         download_manager_,
                                         *this);
      return;
    }
#if defined(OS_MACOSX)
    // Mac OS X requires opening downloads on the UI thread.
    platform_util::OpenItem(full_path());
#else
    BrowserThread::PostTask(
        BrowserThread::FILE, FROM_HERE,
        NewRunnableFunction(&platform_util::OpenItem, full_path()));
#endif
  }
}

void DownloadItem::ShowDownloadInShell() {
#if defined(OS_MACOSX)
  // Mac needs to run this operation on the UI thread.
  platform_util::ShowItemInFolder(full_path());
#else
  BrowserThread::PostTask(
      BrowserThread::FILE, FROM_HERE,
      NewRunnableFunction(&platform_util::ShowItemInFolder,
                          full_path()));
#endif
}

void DownloadItem::DangerousDownloadValidated() {
  UMA_HISTOGRAM_ENUMERATION("Download.DangerousDownloadValidated",
                            danger_type_,
                            DANGEROUS_TYPE_MAX);
  download_manager_->DangerousDownloadValidated(this);
}

void DownloadItem::UpdateSize(int64 bytes_so_far) {
  received_bytes_ = bytes_so_far;

  // If we've received more data than we were expecting (bad server info?),
  // revert to 'unknown size mode'.
  if (received_bytes_ > total_bytes_)
    total_bytes_ = 0;
}

void DownloadItem::StartProgressTimer() {
  update_timer_.Start(base::TimeDelta::FromMilliseconds(kUpdateTimeMs), this,
                      &DownloadItem::UpdateObservers);
}

void DownloadItem::StopProgressTimer() {
  update_timer_.Stop();
}

// Updates from the download thread may have been posted while this download
// was being cancelled in the UI thread, so we'll accept them unless we're
// complete.
void DownloadItem::Update(int64 bytes_so_far) {
  if (!IsInProgress()) {
    NOTREACHED();
    return;
  }
  UpdateSize(bytes_so_far);
  UpdateObservers();
}

// Triggered by a user action.
void DownloadItem::Cancel(bool update_history) {
  VLOG(20) << __FUNCTION__ << "()" << " download = " << DebugString(true);
  if (!IsPartialDownload()) {
    // Small downloads might be complete before this method has
    // a chance to run.
    return;
  }

  download_util::RecordDownloadCount(download_util::CANCELLED_COUNT);

  state_ = CANCELLED;
  UpdateObservers();
  StopProgressTimer();
  if (update_history)
    download_manager_->DownloadCancelled(id_);
}

void DownloadItem::MarkAsComplete() {
  DCHECK(all_data_saved_);
  state_ = COMPLETE;
  UpdateObservers();
}

void DownloadItem::OnAllDataSaved(int64 size) {
  DCHECK(!all_data_saved_);
  all_data_saved_ = true;
  UpdateSize(size);
  StopProgressTimer();
}

void DownloadItem::Completed() {
  VLOG(20) << " " << __FUNCTION__ << "() "
           << DebugString(false);

  download_util::RecordDownloadCount(download_util::COMPLETED_COUNT);

  // Handle chrome extensions explicitly and skip the shell execute.
  if (is_extension_install()) {
    download_util::OpenChromeExtension(download_manager_->profile(),
                                       download_manager_,
                                       *this);
    auto_opened_ = true;
  } else if (open_when_complete() ||
             download_manager_->ShouldOpenFileBasedOnExtension(
                 GetUserVerifiedFilePath()) ||
             is_temporary()) {
    // If the download is temporary, like in drag-and-drop, do not open it but
    // we still need to set it auto-opened so that it can be removed from the
    // download shelf.
    if (!is_temporary())
      OpenDownload();
    auto_opened_ = true;
  }

  DCHECK(all_data_saved_);
  state_ = COMPLETE;
  UpdateObservers();
  download_manager_->DownloadCompleted(id());
}

void DownloadItem::Interrupted(int64 size, int os_error) {
  if (!IsInProgress())
    return;
  state_ = INTERRUPTED;
  last_os_error_ = os_error;
  UpdateSize(size);
  StopProgressTimer();
  UpdateObservers();
}

void DownloadItem::Delete(DeleteReason reason) {
  switch (reason) {
    case DELETE_DUE_TO_USER_DISCARD:
      UMA_HISTOGRAM_ENUMERATION("Download.UserDiscard",
                                danger_type_,
                                DANGEROUS_TYPE_MAX);
      break;
    case DELETE_DUE_TO_BROWSER_SHUTDOWN:
      UMA_HISTOGRAM_ENUMERATION("Download.Discard",
                                danger_type_,
                                DANGEROUS_TYPE_MAX);
      break;
    default:
      NOTREACHED();
  }

  BrowserThread::PostTask(
      BrowserThread::FILE, FROM_HERE,
      NewRunnableFunction(&DeleteDownloadedFile, full_path_));
  Remove();
  // We have now been deleted.
}

void DownloadItem::Remove() {
  Cancel(true);
  state_ = REMOVING;
  download_manager_->RemoveDownload(db_handle_);
  // We have now been deleted.
}

bool DownloadItem::TimeRemaining(base::TimeDelta* remaining) const {
  if (total_bytes_ <= 0)
    return false;  // We never received the content_length for this download.

  int64 speed = CurrentSpeed();
  if (speed == 0)
    return false;

  *remaining =
      base::TimeDelta::FromSeconds((total_bytes_ - received_bytes_) / speed);
  return true;
}

int64 DownloadItem::CurrentSpeed() const {
  if (is_paused_)
    return 0;
  base::TimeDelta diff = base::TimeTicks::Now() - start_tick_;
  int64 diff_ms = diff.InMilliseconds();
  return diff_ms == 0 ? 0 : received_bytes_ * 1000 / diff_ms;
}

int DownloadItem::PercentComplete() const {
  int percent = -1;
  if (total_bytes_ > 0)
    percent = static_cast<int>(received_bytes_ * 100.0 / total_bytes_);
  return percent;
}

void DownloadItem::Rename(const FilePath& full_path) {
  VLOG(20) << " " << __FUNCTION__ << "()"
           << " full_path = \"" << full_path.value() << "\""
           << DebugString(true);
  DCHECK(!full_path.empty());
  full_path_ = full_path;
}

void DownloadItem::TogglePause() {
  DCHECK(IsInProgress());
  download_manager_->PauseDownload(id_, !is_paused_);
  is_paused_ = !is_paused_;
  UpdateObservers();
}

void DownloadItem::OnNameFinalized() {
  VLOG(20) << " " << __FUNCTION__ << "() "
           << DebugString(true);
  name_finalized_ = true;

  // We can't reach this point in the code without having received all the
  // data, so it's safe to move to the COMPLETE state.
  DCHECK(all_data_saved_);
  state_ = COMPLETE;
  UpdateObservers();
  download_manager_->DownloadCompleted(id());
}

void DownloadItem::OnDownloadCompleting(DownloadFileManager* file_manager) {
  VLOG(20) << " " << __FUNCTION__ << "() "
           << " needs rename = " << NeedsRename()
           << " " << DebugString(true);
  DCHECK_NE(DANGEROUS, safety_state());
  DCHECK(file_manager);

  if (NeedsRename()) {
    BrowserThread::PostTask(
        BrowserThread::FILE, FROM_HERE,
        NewRunnableMethod(
            file_manager, &DownloadFileManager::RenameCompletingDownloadFile,
            id(), GetTargetFilePath(), safety_state() == SAFE));
    return;
  } else {
    name_finalized_ = true;
  }

  Completed();

  BrowserThread::PostTask(
      BrowserThread::FILE, FROM_HERE,
      NewRunnableMethod(
          file_manager, &DownloadFileManager::CompleteDownload, id()));
}

void DownloadItem::OnDownloadRenamedToFinalName(const FilePath& full_path) {
  VLOG(20) << " " << __FUNCTION__ << "()"
           << " full_path = " << full_path.value()
           << " needed rename = " << NeedsRename()
           << " " << DebugString(false);
  DCHECK(NeedsRename());

  Rename(full_path);
  OnNameFinalized();

  Completed();
}

bool DownloadItem::MatchesQuery(const string16& query) const {
  if (query.empty())
    return true;

  DCHECK_EQ(query, l10n_util::ToLower(query));

  string16 url_raw(l10n_util::ToLower(UTF8ToUTF16(url().spec())));
  if (url_raw.find(query) != string16::npos)
    return true;

  // TODO(phajdan.jr): write a test case for the following code.
  // A good test case would be:
  //   "/\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xbd\xa0\xe5\xa5\xbd",
  //   L"/\x4f60\x597d\x4f60\x597d",
  //   "/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD"
  PrefService* prefs = download_manager_->profile()->GetPrefs();
  std::string languages(prefs->GetString(prefs::kAcceptLanguages));
  string16 url_formatted(l10n_util::ToLower(net::FormatUrl(url(), languages)));
  if (url_formatted.find(query) != string16::npos)
    return true;

  string16 path(l10n_util::ToLower(full_path().LossyDisplayName()));
  // This shouldn't just do a substring match; it is wrong for Unicode
  // due to normalization and we have a fancier search-query system
  // used elsewhere.
  // http://code.google.com/p/chromium/issues/detail?id=71982
  if (path.find(query) != string16::npos)
    return true;

  return false;
}

void DownloadItem::SetFileCheckResults(const FilePath& path,
                                       bool is_dangerous_file,
                                       bool is_dangerous_url,
                                       int path_uniquifier,
                                       bool prompt,
                                       bool is_extension_install,
                                       const FilePath& original_name) {
  VLOG(20) << " " << __FUNCTION__ << "()"
           << " path = \"" << path.value() << "\""
           << " is_dangerous_file = " << is_dangerous_file
           << " is_dangerous_url = " << is_dangerous_url
           << " path_uniquifier = " << path_uniquifier
           << " prompt = " << prompt
           << " is_extension_install = " << is_extension_install
           << " path = \"" << path.value() << "\""
           << " original_name = \"" << original_name.value() << "\""
           << " " << DebugString(true);
  // Make sure the initial file name is set only once.
  DCHECK(full_path_.empty());
  DCHECK(!path.empty());

  full_path_ = path;
  safety_state_ = GetSafetyState(is_dangerous_file, is_dangerous_url);
  danger_type_ = GetDangerType(is_dangerous_file, is_dangerous_url);
  path_uniquifier_ = path_uniquifier;
  save_as_ = prompt;
  is_extension_install_ = is_extension_install;
  target_name_ = original_name;

  if (target_name_.value().empty())
    target_name_ = full_path_.BaseName();
}

FilePath DownloadItem::GetTargetFilePath() const {
  return full_path_.DirName().Append(target_name_);
}

FilePath DownloadItem::GetFileNameToReportUser() const {
  if (path_uniquifier_ > 0) {
    FilePath name(target_name_);
    download_util::AppendNumberToPath(&name, path_uniquifier_);
    return name;
  }
  return target_name_;
}

FilePath DownloadItem::GetUserVerifiedFilePath() const {
  if (safety_state_ == DownloadItem::SAFE)
    return GetTargetFilePath();
  return full_path_;
}

void DownloadItem::Init(bool start_timer) {
  if (target_name_.value().empty())
    target_name_ = full_path_.BaseName();
  if (start_timer)
    StartProgressTimer();
  VLOG(20) << " " << __FUNCTION__ << "() " << DebugString(true);
}

// TODO(ahendrickson) -- Move |INTERRUPTED| from |IsCancelled()| to
// |IsPartialDownload()|, when resuming interrupted downloads is implemented.
bool DownloadItem::IsPartialDownload() const {
  return (state_ == IN_PROGRESS);
}

bool DownloadItem::IsInProgress() const {
  return (state_ == IN_PROGRESS);
}

bool DownloadItem::IsCancelled() const {
  return (state_ == CANCELLED) || (state_ == INTERRUPTED);
}

bool DownloadItem::IsInterrupted() const {
  return (state_ == INTERRUPTED);
}

bool DownloadItem::IsComplete() const {
  return state() == COMPLETE;
}

std::string DownloadItem::DebugString(bool verbose) const {
  std::string description =
      base::StringPrintf("{ id_ = %d"
                         " state = %s",
                         id_,
                         DebugDownloadStateString(state()));

  if (verbose) {
    description += base::StringPrintf(
        " db_handle = %" PRId64
        " total_bytes = %" PRId64
        " is_paused = " "%c"
        " is_extension_install = " "%c"
        " is_otr = " "%c"
        " safety_state = " "%s"
        " url = " "\"%s\""
        " target_name_ = \"%" PRFilePath "\""
        " full_path = \"%" PRFilePath "\"",
        db_handle(),
        total_bytes(),
        is_paused() ? 'T' : 'F',
        is_extension_install() ? 'T' : 'F',
        is_otr() ? 'T' : 'F',
        DebugSafetyStateString(safety_state()),
        url().spec().c_str(),
        target_name_.value().c_str(),
        full_path().value().c_str());
  } else {
    description += base::StringPrintf(" url = \"%s\"", url().spec().c_str());
  }

  description += " }";

  return description;
}