// 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/chromeos/drive/download_handler.h"

#include "base/bind.h"
#include "base/file_util.h"
#include "base/supports_user_data.h"
#include "chrome/browser/chromeos/drive/drive.pb.h"
#include "chrome/browser/chromeos/drive/drive_system_service.h"
#include "chrome/browser/chromeos/drive/file_system_interface.h"
#include "chrome/browser/chromeos/drive/file_system_util.h"
#include "chrome/browser/chromeos/drive/file_write_helper.h"
#include "content/public/browser/browser_thread.h"

using content::BrowserThread;
using content::DownloadManager;
using content::DownloadItem;

namespace drive {
namespace {

// Key for base::SupportsUserData::Data.
const char kDrivePathKey[] = "DrivePath";

// User Data stored in DownloadItem for drive path.
class DriveUserData : public base::SupportsUserData::Data {
 public:
  explicit DriveUserData(const base::FilePath& path) : file_path_(path),
                                                 is_complete_(false) {}
  virtual ~DriveUserData() {}

  const base::FilePath& file_path() const { return file_path_; }
  bool is_complete() const { return is_complete_; }
  void set_complete() { is_complete_ = true; }

 private:
  const base::FilePath file_path_;
  bool is_complete_;
};

// Extracts DriveUserData* from |download|.
const DriveUserData* GetDriveUserData(const DownloadItem* download) {
  return static_cast<const DriveUserData*>(
      download->GetUserData(&kDrivePathKey));
}

DriveUserData* GetDriveUserData(DownloadItem* download) {
  return static_cast<DriveUserData*>(download->GetUserData(&kDrivePathKey));
}

// Creates a temporary file |drive_tmp_download_path| in
// |drive_tmp_download_dir|. Must be called on a thread that allows file
// operations.
base::FilePath GetDriveTempDownloadPath(
    const base::FilePath& drive_tmp_download_dir) {
  bool created = file_util::CreateDirectory(drive_tmp_download_dir);
  DCHECK(created) << "Can not create temp download directory at "
                  << drive_tmp_download_dir.value();
  base::FilePath drive_tmp_download_path;
  created = file_util::CreateTemporaryFileInDir(drive_tmp_download_dir,
                                                &drive_tmp_download_path);
  DCHECK(created) << "Temporary download file creation failed";
  return drive_tmp_download_path;
}

// Moves downloaded file to Drive.
void MoveDownloadedFile(const base::FilePath& downloaded_file,
                        FileError error,
                        const base::FilePath& dest_path) {
  if (error != FILE_ERROR_OK)
    return;
  file_util::Move(downloaded_file, dest_path);
}

// Used to implement CheckForFileExistence().
void ContinueCheckingForFileExistence(
    const content::CheckForFileExistenceCallback& callback,
    FileError error,
    scoped_ptr<ResourceEntry> entry) {
  callback.Run(error == FILE_ERROR_OK);
}

// Returns true if |download| is a Drive download created from data persisted
// on the download history DB.
bool IsPersistedDriveDownload(const base::FilePath& drive_tmp_download_path,
                              DownloadItem* download) {
  // Persisted downloads are not in IN_PROGRESS state when created, while newly
  // created downloads are.
  return drive_tmp_download_path.IsParent(download->GetFullPath()) &&
      download->GetState() != DownloadItem::IN_PROGRESS;
}

}  // namespace

DownloadHandler::DownloadHandler(
    FileWriteHelper* file_write_helper,
    FileSystemInterface* file_system)
    : file_write_helper_(file_write_helper),
      file_system_(file_system),
      weak_ptr_factory_(this) {
}

DownloadHandler::~DownloadHandler() {
}

// static
DownloadHandler* DownloadHandler::GetForProfile(Profile* profile) {
  DriveSystemService* system_service =
      DriveSystemServiceFactory::FindForProfile(profile);
  return system_service ? system_service->download_handler() : NULL;
}

void DownloadHandler::Initialize(
    DownloadManager* download_manager,
    const base::FilePath& drive_tmp_download_path) {
  DCHECK(!drive_tmp_download_path.empty());

  drive_tmp_download_path_ = drive_tmp_download_path;

  if (download_manager) {
    notifier_.reset(new AllDownloadItemNotifier(download_manager, this));
    // Remove any persisted Drive DownloadItem. crbug.com/171384
    content::DownloadManager::DownloadVector downloads;
    download_manager->GetAllDownloads(&downloads);
    for (size_t i = 0; i < downloads.size(); ++i) {
      if (IsPersistedDriveDownload(drive_tmp_download_path_, downloads[i]))
        RemoveDownload(downloads[i]->GetId());
    }
  }
}

void DownloadHandler::SubstituteDriveDownloadPath(
    const base::FilePath& drive_path,
    content::DownloadItem* download,
    const SubstituteDriveDownloadPathCallback& callback) {
  DVLOG(1) << "SubstituteDriveDownloadPath " << drive_path.value();

  SetDownloadParams(drive_path, download);

  if (util::IsUnderDriveMountPoint(drive_path)) {
    // Can't access drive if the directory does not exist on Drive.
    // We set off a chain of callbacks as follows:
    // FileSystem::GetEntryInfoByPath
    //   OnEntryFound calls FileSystem::CreateDirectory (if necessary)
    //     OnCreateDirectory calls SubstituteDriveDownloadPathInternal
    const base::FilePath drive_dir_path =
        util::ExtractDrivePath(drive_path.DirName());
    // Ensure the directory exists. This also forces FileSystem to
    // initialize DriveRootDirectory.
    file_system_->GetEntryInfoByPath(
        drive_dir_path,
        base::Bind(&DownloadHandler::OnEntryFound,
                   weak_ptr_factory_.GetWeakPtr(),
                   drive_dir_path,
                   callback));
  } else {
    callback.Run(drive_path);
  }
}

void DownloadHandler::SetDownloadParams(const base::FilePath& drive_path,
                                        DownloadItem* download) {
  if (!download || (download->GetState() != DownloadItem::IN_PROGRESS))
    return;

  if (util::IsUnderDriveMountPoint(drive_path)) {
    download->SetUserData(&kDrivePathKey, new DriveUserData(drive_path));
    download->SetDisplayName(drive_path.BaseName());
  } else if (IsDriveDownload(download)) {
    // This may have been previously set if the default download folder is
    // /drive, and the user has now changed the download target to a local
    // folder.
    download->SetUserData(&kDrivePathKey, NULL);
    download->SetDisplayName(base::FilePath());
  }
}

base::FilePath DownloadHandler::GetTargetPath(
    const DownloadItem* download) {
  const DriveUserData* data = GetDriveUserData(download);
  // If data is NULL, we've somehow lost the drive path selected by the file
  // picker.
  DCHECK(data);
  return data ? data->file_path() : base::FilePath();
}

bool DownloadHandler::IsDriveDownload(const DownloadItem* download) {
  // We use the existence of the DriveUserData object in download as a
  // signal that this is a DriveDownload.
  return GetDriveUserData(download) != NULL;
}

void DownloadHandler::CheckForFileExistence(
    const DownloadItem* download,
    const content::CheckForFileExistenceCallback& callback) {
  file_system_->GetEntryInfoByPath(
      util::ExtractDrivePath(GetTargetPath(download)),
      base::Bind(&ContinueCheckingForFileExistence,
                 callback));
}

void DownloadHandler::OnDownloadCreated(DownloadManager* manager,
                                        DownloadItem* download) {
  // Remove any persisted Drive DownloadItem. crbug.com/171384
  if (IsPersistedDriveDownload(drive_tmp_download_path_, download)) {
    // Remove download later, since doing it here results in a crash.
    BrowserThread::PostTask(BrowserThread::UI,
                            FROM_HERE,
                            base::Bind(&DownloadHandler::RemoveDownload,
                                       weak_ptr_factory_.GetWeakPtr(),
                                       download->GetId()));
  }
}

void DownloadHandler::RemoveDownload(int id) {
  DownloadManager* manager = notifier_->GetManager();
  if (!manager)
    return;
  DownloadItem* download = manager->GetDownload(id);
  if (!download)
    return;
  download->Remove();
}

void DownloadHandler::OnDownloadUpdated(
    DownloadManager* manager, DownloadItem* download) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  // Only accept downloads that have the Drive meta data associated with them.
  DriveUserData* data = GetDriveUserData(download);
  if (!drive_tmp_download_path_.IsParent(download->GetTargetFilePath()) ||
      !data ||
      data->is_complete())
    return;

  switch (download->GetState()) {
    case DownloadItem::IN_PROGRESS:
      break;

    case DownloadItem::COMPLETE:
      UploadDownloadItem(download);
      data->set_complete();
      break;

    case DownloadItem::CANCELLED:
    case DownloadItem::INTERRUPTED:
      download->SetUserData(&kDrivePathKey, NULL);
      break;

    default:
      NOTREACHED();
  }
}

void DownloadHandler::OnEntryFound(
    const base::FilePath& drive_dir_path,
    const SubstituteDriveDownloadPathCallback& callback,
    FileError error,
    scoped_ptr<ResourceEntry> entry) {
  if (error == FILE_ERROR_NOT_FOUND) {
    // Destination Drive directory doesn't exist, so create it.
    const bool is_exclusive = false, is_recursive = true;
    file_system_->CreateDirectory(
        drive_dir_path, is_exclusive, is_recursive,
        base::Bind(&DownloadHandler::OnCreateDirectory,
                   weak_ptr_factory_.GetWeakPtr(),
                   callback));
  } else if (error == FILE_ERROR_OK) {
    // Directory is already ready.
    OnCreateDirectory(callback, FILE_ERROR_OK);
  } else {
    LOG(WARNING) << "Failed to get entry info for path: "
                 << drive_dir_path.value() << ", error = "
                 << FileErrorToString(error);
    callback.Run(base::FilePath());
  }
}

void DownloadHandler::OnCreateDirectory(
    const SubstituteDriveDownloadPathCallback& callback,
    FileError error) {
  DVLOG(1) << "OnCreateDirectory " << FileErrorToString(error);
  if (error == FILE_ERROR_OK) {
    base::PostTaskAndReplyWithResult(
        BrowserThread::GetBlockingPool(),
        FROM_HERE,
        base::Bind(&GetDriveTempDownloadPath, drive_tmp_download_path_),
        callback);
  } else {
    LOG(WARNING) << "Failed to create directory, error = "
                 << FileErrorToString(error);
    callback.Run(base::FilePath());
  }
}

void DownloadHandler::UploadDownloadItem(DownloadItem* download) {
  DCHECK(download->IsComplete());
  file_write_helper_->PrepareWritableFileAndRun(
      util::ExtractDrivePath(GetTargetPath(download)),
      base::Bind(&MoveDownloadedFile, download->GetTargetFilePath()));
}

}  // namespace drive