// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/web/download/download_task_impl.h"

#import <WebKit/WebKit.h>

#import <limits>

#import "base/apple/foundation_util.h"
#import "base/files/file.h"
#import "base/files/file_util.h"
#import "base/functional/bind.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/bind_post_task.h"
#import "base/task/sequenced_task_runner.h"
#import "ios/web/common/features.h"
#import "ios/web/download/download_result.h"
#import "ios/web/public/download/download_task_observer.h"
#import "ios/web/public/web_state.h"
#import "net/base/filename_util.h"
#import "net/base/net_errors.h"

namespace web {
namespace download {
namespace internal {

// Helper struct that store the error code and the opened file object (in case
// of success).
struct CreateFileResult {
  int net_error_code = net::OK;
  base::FilePath file_path;

  explicit CreateFileResult(int error_code) : net_error_code(error_code) {
    DCHECK_NE(net_error_code, net::OK);
  }

  explicit CreateFileResult(base::FilePath path) : file_path(std::move(path)) {
    DCHECK(!file_path.empty());
  }

  CreateFileResult(CreateFileResult&& other) = default;
  CreateFileResult& operator=(CreateFileResult&& other) = default;

  ~CreateFileResult() = default;
};

namespace {

CreateFileResult CreateFileForDownload(base::FilePath path) {
  if (path.empty()) {
    if (!base::CreateTemporaryFile(&path)) {
      return CreateFileResult(
          net::MapSystemError(logging::GetLastSystemErrorCode()));
    }
    DCHECK(!path.empty());
  } else {
    base::File::Error error;
    if (!base::CreateDirectoryAndGetError(path.DirName(), &error)) {
      return CreateFileResult(net::FileErrorToNetError(error));
    }
  }

  // If `path` exists and is a directory, fail with an error as we don't
  // want the download task to delete an existing directory.
  if (base::DirectoryExists(path)) {
    return CreateFileResult(net::ERR_ACCESS_DENIED);
  }

  // Try to delete any existing file at `path` (deleting a non-existent
  // file is not an error for `base::DeleteFile(...)`). This is needed
  // as some sub-classes of DownloadTaskImpl fail if the destination
  // file already exists.
  if (!base::DeleteFile(path)) {
    return CreateFileResult(
        net::MapSystemError(logging::GetLastSystemErrorCode()));
  }

  return CreateFileResult(std::move(path));
}

NSData* ReadDataFromFile(base::FilePath path, int64_t bytes) {
  // base::ReadFile uses int for the count value, so it will fail if we
  // try to read more than INT_MAX bytes. Given that this is already 2GB
  // and we can't allocate that much memory, there is no point trying to
  // read the data in 2GB chunks, instead just fail.
  if (bytes < 0 || std::numeric_limits<int>::max() < bytes) {
    return nil;
  }

  NSMutableData* data = [NSMutableData dataWithLength:bytes];
  std::optional<uint64_t> bytes_read =
      base::ReadFile(path, base::apple::NSMutableDataToSpan(data));
  if (!bytes_read || *bytes_read != static_cast<uint64_t>(bytes)) {
    return nil;
  }

  return [data copy];
}

}  // anonymous namespace
}  // namespace internal
}  // namespace download

DownloadTaskImpl::DownloadTaskImpl(
    WebState* web_state,
    const GURL& original_url,
    NSString* originating_host,
    NSString* http_method,
    const std::string& content_disposition,
    int64_t total_bytes,
    const std::string& mime_type,
    NSString* identifier,
    const scoped_refptr<base::SequencedTaskRunner>& task_runner)
    : original_url_(original_url),
      originating_host_([originating_host copy]),
      http_method_(http_method),
      total_bytes_(total_bytes),
      content_disposition_(content_disposition),
      original_mime_type_(mime_type),
      mime_type_(mime_type),
      identifier_([identifier copy]),
      web_state_(web_state),
      task_runner_(task_runner) {
  DCHECK(web_state_);
  DCHECK(task_runner_);

  base::RepeatingClosure closure = base::BindPostTaskToCurrentDefault(
      base::BindRepeating(&DownloadTaskImpl::OnAppWillResignActive,
                          weak_factory_.GetWeakPtr()));

  base::WeakPtr<DownloadTaskImpl> weak_Task = weak_factory_.GetWeakPtr();
  observer_ = [NSNotificationCenter.defaultCenter
      addObserverForName:UIApplicationWillResignActiveNotification
                  object:nil
                   queue:nil
              usingBlock:^(NSNotification* _Nonnull) {
                closure.Run();
              }];
}

DownloadTaskImpl::~DownloadTaskImpl() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  [NSNotificationCenter.defaultCenter removeObserver:observer_];
  for (auto& observer : observers_) {
    observer.OnDownloadDestroyed(this);
  }

  // Delete the downloaded file if it was a temporary file or if the download
  // failed (it is not an error to delete a non-existent file).
  if (owns_file_ || state_ != State::kComplete) {
    task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(base::IgnoreResult(&base::DeleteFile), path_));
  }
}

WebState* DownloadTaskImpl::GetWebState() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return web_state_;
}

DownloadTask::State DownloadTaskImpl::GetState() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return state_;
}

void DownloadTaskImpl::Start(const base::FilePath& path) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK_NE(state_, State::kInProgress);

  state_ = State::kInProgress;
  percent_complete_ = 0;
  received_bytes_ = 0;
  owns_file_ = path.empty();

  using download::internal::CreateFileForDownload;
  task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&CreateFileForDownload, path),
      base::BindOnce(&DownloadTaskImpl::OnDownloadFileCreated,
                     weak_factory_.GetWeakPtr()));
}

void DownloadTaskImpl::Cancel() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  state_ = State::kCancelled;
  CancelInternal();
  OnDownloadUpdated();
}

NSString* DownloadTaskImpl::GetIdentifier() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return identifier_;
}

const GURL& DownloadTaskImpl::GetOriginalUrl() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return original_url_;
}

const GURL& DownloadTaskImpl::GetRedirectedUrl() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (redirected_url_.is_valid()) {
    return redirected_url_;
  }
  return original_url_;
}

NSString* DownloadTaskImpl::GetOriginatingHost() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return originating_host_;
}

NSString* DownloadTaskImpl::GetHttpMethod() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return http_method_;
}

bool DownloadTaskImpl::IsDone() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  switch (state_) {
    case State::kNotStarted:
    case State::kInProgress:
      return false;
    case State::kCancelled:
    case State::kComplete:
    case State::kFailed:
    case State::kFailedNotResumable:
      return true;
  }
}

int DownloadTaskImpl::GetErrorCode() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return download_result_.error_code();
}

int DownloadTaskImpl::GetHttpCode() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return http_code_;
}

int64_t DownloadTaskImpl::GetTotalBytes() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return total_bytes_;
}

int64_t DownloadTaskImpl::GetReceivedBytes() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return received_bytes_;
}

int DownloadTaskImpl::GetPercentComplete() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return percent_complete_;
}

std::string DownloadTaskImpl::GetContentDisposition() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return content_disposition_;
}

std::string DownloadTaskImpl::GetOriginalMimeType() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return original_mime_type_;
}

std::string DownloadTaskImpl::GetMimeType() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return mime_type_;
}

base::FilePath DownloadTaskImpl::GenerateFileName() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  base::FilePath generated_path =
      net::GenerateFileName(original_url_, content_disposition_,
                            /*referrer_charset=*/std::string(),
                            /*suggested_name=*/GetSuggestedName(),
                            /*mime_type=*/mime_type_,
                            /*default_name=*/"document");

  // iOS ShareSheet has a bug where it crashes when handling filenames with
  // Precomposed (NFC) Unicode characters (e.g., "ä" as \xC3\xA4). Converting
  // the string to the file system representation (which enforces NFD
  // normalization on iOS) and back ensures the filename is safe to use.
  if (base::FeatureList::IsEnabled(
          web::features::kIOSDownloadSanitizeFilename)) {
    NSString* path_string = base::SysUTF8ToNSString(generated_path.value());
    const char* fs_rep = [path_string fileSystemRepresentation];
    if (fs_rep) {
      return base::FilePath(fs_rep);
    }
  }
  return generated_path;
}

bool DownloadTaskImpl::HasPerformedBackgroundDownload() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return has_performed_background_download_;
}

void DownloadTaskImpl::AddObserver(DownloadTaskObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  observers_.AddObserver(observer);
}

void DownloadTaskImpl::RemoveObserver(DownloadTaskObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  observers_.RemoveObserver(observer);
}

void DownloadTaskImpl::GetResponseData(
    ResponseDataReadCallback callback) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(IsDone());
  using download::internal::ReadDataFromFile;
  task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&ReadDataFromFile, path_, received_bytes_),
      std::move(callback));
}

const base::FilePath& DownloadTaskImpl::GetResponsePath() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(IsDone());
  static const base::FilePath kEmptyPath;
  return owns_file_ ? kEmptyPath : path_;
}

base::WeakPtr<DownloadTask> DownloadTaskImpl::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

std::string DownloadTaskImpl::GetSuggestedName() const {
  return std::string();
}

void DownloadTaskImpl::OnAppWillResignActive() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (GetState() == DownloadTask::State::kInProgress) {
    has_performed_background_download_ = YES;
  }
}

void DownloadTaskImpl::OnDownloadFileCreated(
    download::internal::CreateFileResult result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (result.net_error_code == net::OK) {
    path_ = std::move(result.file_path);
    StartInternal(path_);
    return;
  }

  OnDownloadFinished(DownloadResult(result.net_error_code));
}

void DownloadTaskImpl::OnDownloadFinished(DownloadResult download_result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  download_result_ = download_result;
  if (download_result_.error_code()) {
    state_ = download_result_.can_retry() ? State::kFailed
                                          : State::kFailedNotResumable;
  } else {
    state_ = State::kComplete;
  }

  OnDownloadUpdated();
}

void DownloadTaskImpl::OnDownloadUpdated() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  observers_.Notify(&DownloadTaskObserver::OnDownloadUpdated, this);
}

void DownloadTaskImpl::OnRedirected(const GURL& redirected_url) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (redirected_url == GetRedirectedUrl()) {
    // If the redirected URL is the original one, or the redirection was already
    // known, ignore it.
    return;
  }
  redirected_url_ = redirected_url;
  observers_.Notify(&DownloadTaskObserver::OnDownloadUpdated, this);
}

}  // namespace web
