// Copyright 2014 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/file_manager/snapshot_manager.h"

#include "base/bind.h"
#include "base/sys_info.h"
#include "chrome/browser/chromeos/file_manager/app_id.h"
#include "chrome/browser/chromeos/file_manager/fileapi_util.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_thread.h"
#include "google_apis/drive/task_util.h"
#include "storage/browser/blob/shareable_file_reference.h"
#include "storage/browser/fileapi/file_system_context.h"
#include "third_party/cros_system_api/constants/cryptohome.h"

namespace file_manager {
namespace {

typedef base::Callback<void(int64)> GetNecessaryFreeSpaceCallback;

// Part of ComputeSpaceNeedToBeFreed.
int64 ComputeSpaceNeedToBeFreedAfterGetMetadataOnBlockingPool(
    const base::FilePath& path,
    int64 snapshot_size) {
  int64 free_size = base::SysInfo::AmountOfFreeDiskSpace(path);
  if (free_size < 0)
    return -1;

  // We need to keep cryptohome::kMinFreeSpaceInBytes free space even after
  // |snapshot_size| is occupied.
  free_size -= snapshot_size + cryptohome::kMinFreeSpaceInBytes;
  return (free_size < 0 ? -free_size : 0);
}

// Part of ComputeSpaceNeedToBeFreed.
void ComputeSpaceNeedToBeFreedAfterGetMetadata(
    const base::FilePath& path,
    const GetNecessaryFreeSpaceCallback& callback,
    base::File::Error result,
    const base::File::Info& file_info) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  if (result != base::File::FILE_OK) {
    callback.Run(-1);
    return;
  }

  base::PostTaskAndReplyWithResult(
      content::BrowserThread::GetBlockingPool(),
      FROM_HERE,
      base::Bind(&ComputeSpaceNeedToBeFreedAfterGetMetadataOnBlockingPool,
                 path, file_info.size),
      callback);
}

// Part of ComputeSpaceNeedToBeFreed.
void GetMetadataOnIOThread(const base::FilePath& path,
                           scoped_refptr<storage::FileSystemContext> context,
                           const storage::FileSystemURL& url,
                           const GetNecessaryFreeSpaceCallback& callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  context->operation_runner()->GetMetadata(
      url, storage::FileSystemOperation::GET_METADATA_FIELD_SIZE,
      base::Bind(&ComputeSpaceNeedToBeFreedAfterGetMetadata, path, callback));
}

// Computes the size of space that need to be __additionally__ made available
// in the |profile|'s data directory for taking the snapshot of |url|.
// Returns 0 if no additional space is required, or -1 in the case of an error.
void ComputeSpaceNeedToBeFreed(
    Profile* profile,
    scoped_refptr<storage::FileSystemContext> context,
    const storage::FileSystemURL& url,
    const GetNecessaryFreeSpaceCallback& callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  content::BrowserThread::PostTask(
      content::BrowserThread::IO,
      FROM_HERE,
      base::Bind(&GetMetadataOnIOThread, profile->GetPath(), context, url,
                 google_apis::CreateRelayCallback(callback)));
}

// Part of CreateManagedSnapshot. Runs CreateSnapshotFile method of fileapi.
void CreateSnapshotFileOnIOThread(
    scoped_refptr<storage::FileSystemContext> context,
    const storage::FileSystemURL& url,
    const storage::FileSystemOperation::SnapshotFileCallback& callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  context->operation_runner()->CreateSnapshotFile(url, callback);
}

// Utility for destructing the bound |file_refs| on IO thread. This is meant
// to be used together with base::Bind. After this function finishes, the
// Bind callback should destruct the bound argument.
void FreeReferenceOnIOThread(
    const std::deque<SnapshotManager::FileReferenceWithSizeInfo>& file_refs) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
}

}  // namespace

SnapshotManager::FileReferenceWithSizeInfo::FileReferenceWithSizeInfo(
    scoped_refptr<storage::ShareableFileReference> ref,
    int64 size)
    : file_ref(ref), file_size(size) {
}

SnapshotManager::FileReferenceWithSizeInfo::~FileReferenceWithSizeInfo() {
}

SnapshotManager::SnapshotManager(Profile* profile)
    : profile_(profile), weak_ptr_factory_(this) {
}

SnapshotManager::~SnapshotManager() {
  if (!file_refs_.empty()) {
    bool posted = content::BrowserThread::PostTask(
        content::BrowserThread::IO,
        FROM_HERE,
        base::Bind(&FreeReferenceOnIOThread, file_refs_));
    DCHECK(posted);
  }
}

void SnapshotManager::CreateManagedSnapshot(
    const base::FilePath& absolute_file_path,
    const LocalPathCallback& callback) {
  scoped_refptr<storage::FileSystemContext> context(
      util::GetFileSystemContextForExtensionId(profile_, kFileManagerAppId));
  DCHECK(context.get());

  GURL url;
  if (!util::ConvertAbsoluteFilePathToFileSystemUrl(
          profile_, absolute_file_path, kFileManagerAppId, &url)) {
    callback.Run(base::FilePath());
    return;
  }
  storage::FileSystemURL filesystem_url = context->CrackURL(url);

  ComputeSpaceNeedToBeFreed(profile_, context, filesystem_url,
      base::Bind(&SnapshotManager::CreateManagedSnapshotAfterSpaceComputed,
                 weak_ptr_factory_.GetWeakPtr(),
                 filesystem_url,
                 callback));
}

void SnapshotManager::CreateManagedSnapshotAfterSpaceComputed(
    const storage::FileSystemURL& filesystem_url,
    const LocalPathCallback& callback,
    int64 needed_space) {
  scoped_refptr<storage::FileSystemContext> context(
      util::GetFileSystemContextForExtensionId(profile_, kFileManagerAppId));
  DCHECK(context.get());

  if (needed_space < 0) {
    callback.Run(base::FilePath());
    return;
  }

  // Free up to the required size.
  std::deque<FileReferenceWithSizeInfo> to_free;
  while (needed_space > 0 && !file_refs_.empty()) {
    needed_space -= file_refs_.front().file_size;
    to_free.push_back(file_refs_.front());
    file_refs_.pop_front();
  }
  if (!to_free.empty()) {
    bool posted = content::BrowserThread::PostTask(
        content::BrowserThread::IO,
        FROM_HERE,
        base::Bind(&FreeReferenceOnIOThread, to_free));
    DCHECK(posted);
  }

  // If we still could not achieve the space requirement, abort with failure.
  if (needed_space > 0) {
    callback.Run(base::FilePath());
    return;
  }

  // Start creating the snapshot.
  content::BrowserThread::PostTask(
      content::BrowserThread::IO,
      FROM_HERE,
      base::Bind(&CreateSnapshotFileOnIOThread,
                 context,
                 filesystem_url,
                 google_apis::CreateRelayCallback(
                     base::Bind(&SnapshotManager::OnCreateSnapshotFile,
                                weak_ptr_factory_.GetWeakPtr(),
                                callback))));
}

void SnapshotManager::OnCreateSnapshotFile(
    const LocalPathCallback& callback,
    base::File::Error result,
    const base::File::Info& file_info,
    const base::FilePath& platform_path,
    const scoped_refptr<storage::ShareableFileReference>& file_ref) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (result != base::File::FILE_OK) {
    callback.Run(base::FilePath());
    return;
  }

  file_refs_.push_back(FileReferenceWithSizeInfo(file_ref, file_info.size));
  callback.Run(platform_path);
}

}  // namespace file_manager