// Copyright 2023 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/chrome/browser/sessions/model/session_internal_util.h"

#import "base/apple/foundation_util.h"
#import "base/files/file_path.h"
#import "base/logging.h"
#import "third_party/protobuf/src/google/protobuf/message_lite.h"

namespace ios::sessions {
namespace internal {

// Indicate the status for a path.
enum class PathStatus {
  kFile,
  kDirectory,
  kInexistent,
};

// Checks the status of `path`.
[[nodiscard]] PathStatus GetPathStatus(NSString* path) {
  BOOL is_directory = NO;
  if (![[NSFileManager defaultManager] fileExistsAtPath:path
                                            isDirectory:&is_directory]) {
    return PathStatus::kInexistent;
  }
  return is_directory ? PathStatus::kDirectory : PathStatus::kFile;
}

// Returns whether a file named `filename` exists.
[[nodiscard]] bool FileExists(NSString* filename) {
  return GetPathStatus(filename) == PathStatus::kFile;
}

// Returns whether a directory named `dirname` exists.
[[nodiscard]] bool DirectoryExists(NSString* dirname) {
  return GetPathStatus(dirname) == PathStatus::kDirectory;
}

// Creates `directory` including all intermediate directories and returns
// whether the operation was a success. Safe to call if `directory` exists.
[[nodiscard]] bool CreateDirectory(NSString* directory) {
  return [[NSFileManager defaultManager] createDirectoryAtPath:directory
                                   withIntermediateDirectories:YES
                                                    attributes:nil
                                                         error:nil];
}

// Renames a file from `from` to `dest`.
[[nodiscard]] bool RenameFile(NSString* from, NSString* dest) {
  if (!CreateDirectory([dest stringByDeletingLastPathComponent])) {
    return false;
  }

  return [[NSFileManager defaultManager] moveItemAtPath:from
                                                 toPath:dest
                                                  error:nil];
}

// Returns whether `directory` exists and is empty.
[[nodiscard]] bool DirectoryEmpty(NSString* directory) {
  if (!DirectoryExists(directory)) {
    return false;
  }

  NSDirectoryEnumerator<NSString*>* enumerator =
      [[NSFileManager defaultManager] enumeratorAtPath:directory];

  return [enumerator nextObject] == nil;
}

// Deletes recursively file or directory at `path`. Returns whether the
// operation was a success.
[[nodiscard]] bool DeleteRecursively(NSString* path) {
  return [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}

// Copies content of `from_dir` to `dest_dir` recursively. It is an error
// if `from_dir` is not an existing directory or if `dest_dir` exists and
// is not a directory.
[[nodiscard]] bool CopyDirectory(NSString* from_dir, NSString* dest_dir) {
  if (!DirectoryExists(from_dir)) {
    return false;
  }

  switch (GetPathStatus(dest_dir)) {
    case PathStatus::kDirectory:
      if (!DeleteRecursively(dest_dir)) {
        return false;
      }
      break;

    case PathStatus::kFile:
      return false;

    case PathStatus::kInexistent:
      break;
  }

  // Create parent directory of `dest_dir`.
  if (!CreateDirectory([dest_dir stringByDeletingLastPathComponent])) {
    return false;
  }

  // Use hardlink to perform the copy to reduce the impact on storage. The
  // documentation of -linkItemAtPath:toPath:error: explicitly explain that
  // if source is a directory, the method create the destination directory
  // and hard-link the content recursively.
  return [[NSFileManager defaultManager] linkItemAtPath:from_dir
                                                 toPath:dest_dir
                                                  error:nil];
}

// Copies file at `from_path` to `dest_path`. It is an error if `from_path`
// is not a file or if `dest_path` exists and is not a file.
[[nodiscard]] bool CopyFile(NSString* from_path, NSString* dest_path) {
  if (!FileExists(from_path)) {
    return false;
  }

  switch (GetPathStatus(dest_path)) {
    case PathStatus::kDirectory:
      break;

    case PathStatus::kFile:
      if (!DeleteRecursively(dest_path)) {
        return false;
      }
      break;

    case PathStatus::kInexistent:
      break;
  }

  // Create parent directory of `dest_path`.
  if (!CreateDirectory([dest_path stringByDeletingLastPathComponent])) {
    return false;
  }

  // Use hardlink to perform the copy to reduce the impact on storage.
  return [[NSFileManager defaultManager] linkItemAtPath:from_path
                                                 toPath:dest_path
                                                  error:nil];
}

// Writes `data` to `filename` and returns whether the operation was a success.
// The file is created with protection until first user authentication.
[[nodiscard]] bool WriteFile(NSString* filename, NSData* data) {
  if (!CreateDirectory([filename stringByDeletingLastPathComponent])) {
    return false;
  }

  // Options for writing data.
  constexpr NSDataWritingOptions options =
      NSDataWritingAtomic |
      NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication;
  return [data writeToFile:filename options:options error:nil];
}

// Reads content of `filename` and returns it as a `NSData*` or nil on error.
[[nodiscard]] NSData* ReadFile(NSString* filename) {
  return [NSData dataWithContentsOfFile:filename options:0 error:nil];
}

}  // namespace internal

bool FileExists(const base::FilePath& filename) {
  return internal::FileExists(base::apple::FilePathToNSString(filename));
}

bool DirectoryExists(const base::FilePath& dirname) {
  return internal::DirectoryExists(base::apple::FilePathToNSString(dirname));
}

bool RenameFile(const base::FilePath& from, const base::FilePath& dest) {
  return internal::RenameFile(base::apple::FilePathToNSString(from),
                              base::apple::FilePathToNSString(dest));
}

bool CreateDirectory(const base::FilePath& directory) {
  return internal::CreateDirectory(base::apple::FilePathToNSString(directory));
}

bool DirectoryEmpty(const base::FilePath& directory) {
  return internal::DirectoryEmpty(base::apple::FilePathToNSString(directory));
}

bool DeleteRecursively(const base::FilePath& path) {
  return internal::DeleteRecursively(base::apple::FilePathToNSString(path));
}

bool CopyDirectory(const base::FilePath& from_dir,
                   const base::FilePath& dest_dir) {
  return internal::CopyDirectory(base::apple::FilePathToNSString(from_dir),
                                 base::apple::FilePathToNSString(dest_dir));
}

bool CopyFile(const base::FilePath& from_path,
              const base::FilePath& dest_path) {
  return internal::CopyFile(base::apple::FilePathToNSString(from_path),
                            base::apple::FilePathToNSString(dest_path));
}

bool WriteFile(const base::FilePath& filename, NSData* data) {
  return internal::WriteFile(base::apple::FilePathToNSString(filename), data);
}

NSData* ReadFile(const base::FilePath& filename) {
  return internal::ReadFile(base::apple::FilePathToNSString(filename));
}

bool WriteProto(const base::FilePath& filename,
                const google::protobuf::MessageLite& proto) {
  @autoreleasepool {
    // Allocate a NSData object large enough to hold the serialized protobuf.
    const size_t serialized_size = proto.ByteSizeLong();
    NSMutableData* data = [NSMutableData dataWithLength:serialized_size];

    if (!proto.SerializeToArray(data.mutableBytes, data.length)) {
      return false;
    }

    return WriteFile(filename, data);
  }
}

bool ParseProto(const base::FilePath& filename,
                google::protobuf::MessageLite& proto) {
  NSData* data = ReadFile(filename);
  if (!data) {
    return false;
  }

  if (!proto.ParseFromArray(data.bytes, data.length)) {
    return false;
  }

  return true;
}

}  // namespace ios::sessions
