// Copyright 2023-present 650 Industries. All rights reserved.

import ExpoModulesCore
import Photos

private let assetIdentifier = "ph://"

internal func ensureFileDirectoryExists(_ fileUrl: URL) throws {
  let directoryPath = fileUrl.deletingLastPathComponent()

  if !FileManager.default.fileExists(atPath: directoryPath.path) {
    throw DirectoryNotExistsException(directoryPath.path)
  }
}

internal func readFileAsBase64(path: String, options: ReadingOptions) throws -> String {
  return try readFileData(path: path, options: options).base64EncodedString(options: .endLineWithLineFeed)
}

internal func readFileAsString(path: String, encoding: String.Encoding, options: ReadingOptions) throws -> String {
  guard let string = String(data: try readFileData(path: path, options: options), encoding: encoding) else {
    throw FileNotReadableException(path)
  }
  return string
}

private func readFileData(path: String, options: ReadingOptions) throws -> Data {
  let file = FileHandle(forReadingAtPath: path)

  guard let file else {
    throw FileNotExistsException(path)
  }
  defer {
    try? file.close()
  }
  if let position = options.position, position != 0 {
    // TODO: Handle these errors?
    try? file.seek(toOffset: UInt64(position))
  }
  if let length = options.length {
    return file.readData(ofLength: length)
  }
  return file.readDataToEndOfFile()
}

internal func writeFileAsBase64(path: String, string: String) throws {
  let data = Data(base64Encoded: string, options: .ignoreUnknownCharacters)

  if !FileManager.default.createFile(atPath: path, contents: data) {
    throw FileWriteFailedException(path)
  }
}

internal func removeFile(path: String, idempotent: Bool = false) throws {
  if FileManager.default.fileExists(atPath: path) {
    do {
      try FileManager.default.removeItem(atPath: path)
    } catch {
      throw FileCannotDeleteException(path)
        .causedBy(error)
    }
  } else if !idempotent {
    throw FileCannotDeleteException(path)
      .causedBy(FileNotExistsException(path))
  }
}

internal func getResourceValues(from directory: URL?, forKeys: Set<URLResourceKey>) throws -> URLResourceValues? {
  do {
    return try directory?.resourceValues(forKeys: forKeys)
  } catch {
    throw CannotDetermineDiskCapacity().causedBy(error)
  }
}

internal func ensurePathPermission(_ appContext: AppContext?, path: String, flag: EXFileSystemPermissionFlags) throws {
  guard let fileSystemManager = appContext?.fileSystem else {
    throw Exceptions.PermissionsModuleNotFound()
  }
  guard fileSystemManager.getPathPermissions(path).contains(flag) else {
    throw flag == .read ? FileNotReadableException(path) : FileNotWritableException(path)
  }
}

internal func isPHAsset(path: String) -> Bool {
  return path.contains(assetIdentifier)
}

internal func copyPHAsset(fromUrl: URL, toUrl: URL, with resourceManager: PHAssetResourceManager, promise: Promise) {
  if isPhotoLibraryStatusAuthorized() {
    if FileManager.default.fileExists(atPath: toUrl.path) {
      promise.reject(FileAlreadyExistsException(toUrl.path))
      return
    }

    let identifier = fromUrl.absoluteString.replacingOccurrences(of: assetIdentifier, with: "")

    guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject else {
      promise.reject(FailedToFindAssetException(fromUrl.absoluteString))
      return
    }

    // An edited asset carries both its original resource (`.photo`/`.video`) and the
    // rendered current version (`.fullSizePhoto`/`.fullSizeVideo`). Prefer the
    // rendition matching the asset's media type, since an edited video can also
    // include a `.fullSizePhoto` poster image.
    let resources = PHAssetResource.assetResources(for: asset)
    let isVideo = asset.mediaType == .video
    let editedResourceType: PHAssetResourceType = isVideo ? .fullSizeVideo : .fullSizePhoto
    let originalResourceType: PHAssetResourceType = isVideo ? .video : .photo
    let resource =
      resources.first { $0.type == editedResourceType }
      ?? resources.first { $0.type == originalResourceType }
      ?? resources.first
    if let resource {
      let resourceOptions = PHAssetResourceRequestOptions()
      // Assets that only exist in iCloud have no local resource data to write.
      resourceOptions.isNetworkAccessAllowed = true
      resourceManager.writeData(for: resource, toFile: toUrl, options: resourceOptions) { error in
        if error != nil {
          promise.reject(FailedToCopyAssetException(fromUrl.absoluteString))
          return
        }
        promise.resolve()
      }
    } else {
      promise.reject(FailedToCopyAssetException(fromUrl.absoluteString))
    }
  }
}

internal func isPhotoLibraryStatusAuthorized() -> Bool {
  if #available(iOS 14, tvOS 14, *) {
    let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
    return status == .authorized || status == .limited
  }
  return PHPhotoLibrary.authorizationStatus() == .authorized
}
