// 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 "webkit/fileapi/file_writer_delegate.h"

#include "base/file_util_proxy.h"
#include "base/message_loop.h"
#include "base/threading/thread_restrictions.h"
#include "net/base/net_errors.h"
#include "webkit/fileapi/file_system_context.h"
#include "webkit/fileapi/file_system_operation.h"
#include "webkit/fileapi/file_system_operation_context.h"
#include "webkit/fileapi/file_system_path_manager.h"
#include "webkit/fileapi/file_system_quota_util.h"
#include "webkit/fileapi/quota_file_util.h"

namespace fileapi {

static const int kReadBufSize = 32768;

namespace {

typedef Callback2<base::PlatformFileError /* error code */,
                  const base::PlatformFileInfo& /* file_info */
                  >::Type InitializeTaskCallback;

class InitializeTask : public base::RefCountedThreadSafe<InitializeTask> {
 public:
  InitializeTask(
      base::PlatformFile file,
      FileSystemOperationContext* context,
      InitializeTaskCallback* callback)
      : origin_message_loop_proxy_(
            base::MessageLoopProxy::current()),
        error_code_(base::PLATFORM_FILE_OK),
        file_(file),
        context_(*context),
        callback_(callback) {
    DCHECK(callback);
  }

  bool Start(scoped_refptr<base::MessageLoopProxy> message_loop_proxy,
             const tracked_objects::Location& from_here) {
    return message_loop_proxy->PostTask(from_here, NewRunnableMethod(this,
        &InitializeTask::ProcessOnTargetThread));
  }

 private:
  friend class base::RefCountedThreadSafe<InitializeTask>;

  void RunCallback() {
    callback_->Run(error_code_, file_info_);
    delete callback_;
  }

  void ProcessOnTargetThread() {
    DCHECK(context_.file_system_context());
    FileSystemQuotaUtil* quota_util = context_.file_system_context()->
        GetQuotaUtil(context_.src_type());
    if (quota_util) {
      DCHECK(quota_util->proxy());
      quota_util->proxy()->StartUpdateOrigin(
          context_.src_origin_url(), context_.src_type());
    }
    if (!base::GetPlatformFileInfo(file_, &file_info_))
      error_code_ = base::PLATFORM_FILE_ERROR_FAILED;
    origin_message_loop_proxy_->PostTask(FROM_HERE, NewRunnableMethod(this,
        &InitializeTask::RunCallback));
  }

  scoped_refptr<base::MessageLoopProxy> origin_message_loop_proxy_;
  base::PlatformFileError error_code_;

  base::PlatformFile file_;
  FileSystemOperationContext context_;
  InitializeTaskCallback* callback_;

  base::PlatformFileInfo file_info_;
};

}  // namespace (anonymous)

FileWriterDelegate::FileWriterDelegate(
    FileSystemOperation* file_system_operation, int64 offset,
    scoped_refptr<base::MessageLoopProxy> proxy)
    : file_system_operation_(file_system_operation),
      file_(base::kInvalidPlatformFileValue),
      offset_(offset),
      proxy_(proxy),
      bytes_written_backlog_(0),
      bytes_written_(0),
      bytes_read_(0),
      total_bytes_written_(0),
      allowed_bytes_to_write_(0),
      io_buffer_(new net::IOBufferWithSize(kReadBufSize)),
      io_callback_(ALLOW_THIS_IN_INITIALIZER_LIST(this),
                   &FileWriterDelegate::OnDataWritten),
      method_factory_(ALLOW_THIS_IN_INITIALIZER_LIST(this)),
      callback_factory_(ALLOW_THIS_IN_INITIALIZER_LIST(this)) {
}

FileWriterDelegate::~FileWriterDelegate() {
}

void FileWriterDelegate::OnGetFileInfoAndCallStartUpdate(
    base::PlatformFileError error,
    const base::PlatformFileInfo& file_info) {
  if (error) {
    OnError(error);
    return;
  }
  int64 allowed_bytes_growth =
      file_system_operation_context()->allowed_bytes_growth();
  if (allowed_bytes_growth == QuotaFileUtil::kNoLimit ||
      file_system_operation_->file_system_context()->IsStorageUnlimited(
          file_system_operation_context()->src_origin_url())) {
    // TODO(kinuko): kNoLimit is kint64max therefore all the calculation/
    // comparison with the value should just work, but we should drop
    // such implicit assumption and should use an explicit boolean flag
    // or something.
    allowed_bytes_to_write_ = QuotaFileUtil::kNoLimit;
  } else {
    if (allowed_bytes_growth < 0)
      allowed_bytes_growth = 0;
    allowed_bytes_to_write_ = file_info.size - offset_ + allowed_bytes_growth;
  }
  size_ = file_info.size;
  file_stream_.reset(new net::FileStream(file_,
      base::PLATFORM_FILE_OPEN | base::PLATFORM_FILE_WRITE |
      base::PLATFORM_FILE_ASYNC));
  request_->Start();
}

void FileWriterDelegate::Start(base::PlatformFile file,
                               net::URLRequest* request) {
  file_ = file;
  request_ = request;

  scoped_refptr<InitializeTask> relay = new InitializeTask(
      file_, file_system_operation_context(),
      callback_factory_.NewCallback(
          &FileWriterDelegate::OnGetFileInfoAndCallStartUpdate));
  relay->Start(proxy_, FROM_HERE);
}

void FileWriterDelegate::OnReceivedRedirect(
    net::URLRequest* request, const GURL& new_url, bool* defer_redirect) {
  NOTREACHED();
  OnError(base::PLATFORM_FILE_ERROR_SECURITY);
}

void FileWriterDelegate::OnAuthRequired(
    net::URLRequest* request, net::AuthChallengeInfo* auth_info) {
  NOTREACHED();
  OnError(base::PLATFORM_FILE_ERROR_SECURITY);
}

void FileWriterDelegate::OnCertificateRequested(
    net::URLRequest* request, net::SSLCertRequestInfo* cert_request_info) {
  NOTREACHED();
  OnError(base::PLATFORM_FILE_ERROR_SECURITY);
}

void FileWriterDelegate::OnSSLCertificateError(
    net::URLRequest* request, int cert_error, net::X509Certificate* cert) {
  NOTREACHED();
  OnError(base::PLATFORM_FILE_ERROR_SECURITY);
}

void FileWriterDelegate::OnResponseStarted(net::URLRequest* request) {
  DCHECK_EQ(request_, request);
  // file_stream_->Seek() blocks the IO thread.
  // See http://crbug.com/75548.
  base::ThreadRestrictions::ScopedAllowIO allow_io;
  if (!request->status().is_success()) {
    OnError(base::PLATFORM_FILE_ERROR_FAILED);
    return;
  }
  int64 error = file_stream_->Seek(net::FROM_BEGIN, offset_);
  if (error != offset_) {
    OnError(base::PLATFORM_FILE_ERROR_FAILED);
    return;
  }
  Read();
}

void FileWriterDelegate::OnReadCompleted(net::URLRequest* request,
                                         int bytes_read) {
  DCHECK_EQ(request_, request);
  if (!request->status().is_success()) {
    OnError(base::PLATFORM_FILE_ERROR_FAILED);
    return;
  }
  OnDataReceived(bytes_read);
}

void FileWriterDelegate::Read() {
  bytes_written_ = 0;
  bytes_read_ = 0;
  if (request_->Read(io_buffer_.get(), io_buffer_->size(), &bytes_read_)) {
    MessageLoop::current()->PostTask(
        FROM_HERE,
        method_factory_.NewRunnableMethod(
            &FileWriterDelegate::OnDataReceived, bytes_read_));
  } else if (!request_->status().is_io_pending()) {
    OnError(base::PLATFORM_FILE_ERROR_FAILED);
  }
}

void FileWriterDelegate::OnDataReceived(int bytes_read) {
  bytes_read_ = bytes_read;
  if (!bytes_read_) {  // We're done.
    OnProgress(0, true);
  } else {
    // This could easily be optimized to rotate between a pool of buffers, so
    // that we could read and write at the same time.  It's not yet clear that
    // it's necessary.
    Write();
  }
}

void FileWriterDelegate::Write() {
  // allowed_bytes_to_write could be negative if the file size is
  // greater than the current (possibly new) quota.
  // (The UI should clear the entire origin data if the smaller quota size
  // is set in general, though the UI/deletion code is not there yet.)
  DCHECK(total_bytes_written_ <= allowed_bytes_to_write_ ||
         allowed_bytes_to_write_ < 0);
  if (total_bytes_written_ >= allowed_bytes_to_write_) {
    OnError(base::PLATFORM_FILE_ERROR_NO_SPACE);
    return;
  }

  int64 bytes_to_write = bytes_read_ - bytes_written_;
  if (bytes_to_write > allowed_bytes_to_write_ - total_bytes_written_)
    bytes_to_write = allowed_bytes_to_write_ - total_bytes_written_;

  int write_response = file_stream_->Write(io_buffer_->data() + bytes_written_,
                                           static_cast<int>(bytes_to_write),
                                           &io_callback_);
  if (write_response > 0)
    MessageLoop::current()->PostTask(
        FROM_HERE,
        method_factory_.NewRunnableMethod(
            &FileWriterDelegate::OnDataWritten, write_response));
  else if (net::ERR_IO_PENDING != write_response)
    OnError(base::PLATFORM_FILE_ERROR_FAILED);
}

void FileWriterDelegate::OnDataWritten(int write_response) {
  if (write_response > 0) {
    OnProgress(write_response, false);
    bytes_written_ += write_response;
    total_bytes_written_ += write_response;
    if (bytes_written_ == bytes_read_)
      Read();
    else
      Write();
  } else {
    OnError(base::PLATFORM_FILE_ERROR_FAILED);
  }
}

void FileWriterDelegate::OnError(base::PlatformFileError error) {
  request_->set_delegate(NULL);
  request_->Cancel();

  if (quota_util()) {
    quota_util()->proxy()->EndUpdateOrigin(
        file_system_operation_context()->src_origin_url(),
        file_system_operation_context()->src_type());
  }

  file_system_operation_->DidWrite(error, 0, true);
}

void FileWriterDelegate::OnProgress(int bytes_written, bool done) {
  DCHECK(bytes_written + bytes_written_backlog_ >= bytes_written_backlog_);
  if (quota_util() &&
      bytes_written > 0 &&
      total_bytes_written_ + bytes_written + offset_ > size_) {
    int overlapped = 0;
    if (total_bytes_written_ + offset_ < size_)
      overlapped = size_ - total_bytes_written_ - offset_;
    quota_util()->proxy()->UpdateOriginUsage(
        file_system_operation_->file_system_context()->quota_manager_proxy(),
        file_system_operation_context()->src_origin_url(),
        file_system_operation_context()->src_type(),
        bytes_written - overlapped);
  }
  static const int kMinProgressDelayMS = 200;
  base::Time currentTime = base::Time::Now();
  if (done || last_progress_event_time_.is_null() ||
      (currentTime - last_progress_event_time_).InMilliseconds() >
          kMinProgressDelayMS) {
    bytes_written += bytes_written_backlog_;
    last_progress_event_time_ = currentTime;
    bytes_written_backlog_ = 0;
    if (done && quota_util()) {
      if (quota_util()) {
        quota_util()->proxy()->EndUpdateOrigin(
            file_system_operation_context()->src_origin_url(),
            file_system_operation_context()->src_type());
      }
    }
    file_system_operation_->DidWrite(
        base::PLATFORM_FILE_OK, bytes_written, done);
    return;
  }
  bytes_written_backlog_ += bytes_written;
}

FileSystemOperationContext*
FileWriterDelegate::file_system_operation_context() const {
  DCHECK(file_system_operation_);
  DCHECK(file_system_operation_->file_system_operation_context());
  return file_system_operation_->file_system_operation_context();
}

FileSystemQuotaUtil* FileWriterDelegate::quota_util() const {
  DCHECK(file_system_operation_);
  DCHECK(file_system_operation_->file_system_context());
  DCHECK(file_system_operation_->file_system_operation_context());
  return file_system_operation_->file_system_context()->GetQuotaUtil(
      file_system_operation_context()->src_type());
}

}  // namespace fileapi