//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import SignalCoreKit

@objc
public extension OWSFileSystem {
    class func fileOrFolderExists(atPath filePath: String) -> Bool {
        FileManager.default.fileExists(atPath: filePath)
    }

    class func fileOrFolderExists(url: URL) -> Bool {
        fileOrFolderExists(atPath: url.path)
    }

    @discardableResult
    class func deleteFile(_ filePath: String) -> Bool {
        deleteFile(filePath, ignoreIfMissing: false)
    }

    @discardableResult
    class func deleteFileIfExists(_ filePath: String) -> Bool {
        return deleteFile(filePath, ignoreIfMissing: true)
    }

    class func deleteFile(url: URL) throws {
        try FileManager.default.removeItem(at: url)
    }

    class func deleteFileIfExists(url: URL) throws {
        guard FileManager.default.fileExists(atPath: url.path) else {
            return
        }
        try deleteFile(url: url)
    }

    class func moveFile(from fromUrl: URL, to toUrl: URL) throws {
        guard FileManager.default.fileExists(atPath: fromUrl.path) else {
            throw OWSAssertionError("Source file does not exist.")
        }
        guard !FileManager.default.fileExists(atPath: toUrl.path) else {
            throw OWSAssertionError("Destination file already exists.")
        }
        try FileManager.default.moveItem(at: fromUrl, to: toUrl)

        // Ensure all files moved have the proper data protection class.
        // On large directories this can take a while, so we dispatch async
        // since we're in the launch path.
        DispatchQueue.global().async {
            self.protectRecursiveContents(atPath: toUrl.path)
        }

        #if TESTABLE_BUILD
        guard !FileManager.default.fileExists(atPath: fromUrl.path) else {
            throw OWSAssertionError("Source file does not exist.")
        }
        guard FileManager.default.fileExists(atPath: toUrl.path) else {
            throw OWSAssertionError("Destination file already exists.")
        }
        #endif
    }

    class func copyFile(from fromUrl: URL, to toUrl: URL) throws {
        guard FileManager.default.fileExists(atPath: fromUrl.path) else {
            throw OWSAssertionError("Source file does not exist.")
        }
        guard !FileManager.default.fileExists(atPath: toUrl.path) else {
            throw OWSAssertionError("Destination file already exists.")
        }
        try FileManager.default.copyItem(at: fromUrl, to: toUrl)

        // Ensure all files copied have the proper data protection class.
        // On large directories this can take a while, so we dispatch async
        // since we're in the launch path.
        DispatchQueue.global().async {
            self.protectRecursiveContents(atPath: toUrl.path)
        }

        #if TESTABLE_BUILD
        guard FileManager.default.fileExists(atPath: toUrl.path) else {
            throw OWSAssertionError("Destination file not created.")
        }
        #endif
    }

    class func recursiveFilesInDirectory(_ dirPath: String) throws -> [String] {
        owsAssertDebug(!dirPath.isEmpty)

        do {
            return try FileManager.default.subpathsOfDirectory(atPath: dirPath)
                .map { (dirPath as NSString).appendingPathComponent($0) }
                .filter {
                    var isDirectory: ObjCBool = false
                    FileManager.default.fileExists(atPath: $0, isDirectory: &isDirectory)
                    return !isDirectory.boolValue
                }

        } catch CocoaError.fileReadNoSuchFile {
            return []
        }
    }
}

// MARK: - Temporary Files

@objc
public extension OWSFileSystem {

    class func temporaryFileUrl(
        fileExtension: String? = nil,
        isAvailableWhileDeviceLocked: Bool = false
    ) -> URL {
        return URL(fileURLWithPath: temporaryFilePath(
            fileName: nil,
            fileExtension: fileExtension,
            isAvailableWhileDeviceLocked: isAvailableWhileDeviceLocked
        ))
    }

    class func temporaryFileUrl(
        fileName: String,
        fileExtension: String? = nil,
        isAvailableWhileDeviceLocked: Bool = false
    ) -> URL {
        return URL(fileURLWithPath: temporaryFilePath(
            fileName: fileName,
            fileExtension: fileExtension,
            isAvailableWhileDeviceLocked: isAvailableWhileDeviceLocked
        ))
    }

    class func temporaryFilePath(
        fileName: String? = nil,
        fileExtension: String? = nil
    ) -> String {
        temporaryFilePath(
            fileName: fileName,
            fileExtension: fileExtension,
            isAvailableWhileDeviceLocked: false
        )
    }

    class func temporaryFilePath(
        fileName: String? = nil,
        fileExtension: String? = nil,
        isAvailableWhileDeviceLocked: Bool = false
    ) -> String {
        let tempDirPath = tempDirPath(availableWhileDeviceLocked: isAvailableWhileDeviceLocked)
        var fileName = fileName ?? UUID().uuidString
        if let fileExtension = fileExtension,
            !fileExtension.isEmpty {
            fileName = String(format: "\(fileName).\(fileExtension)")
        }
        let filePath = (tempDirPath as NSString).appendingPathComponent(fileName)
        return filePath
    }

    private class func tempDirPath(availableWhileDeviceLocked: Bool) -> String {
        return availableWhileDeviceLocked
            ? OWSTemporaryDirectoryAccessibleAfterFirstAuth()
            : OWSTemporaryDirectory()
    }
}

// MARK: -

public extension OWSFileSystem {
    @objc
    class func deleteFile(_ filePath: String, ignoreIfMissing: Bool = false) -> Bool {
        do {
            try FileManager.default.removeItem(atPath: filePath)
            return true
        } catch POSIXError.ENOENT where ignoreIfMissing, CocoaError.fileNoSuchFile where ignoreIfMissing {
            // Ignore "No such file or directory" error.
            return true
        } catch CocoaError.fileWriteNoPermission {
            let attemptedUrl = URL(fileURLWithPath: filePath)
            let knownNoWritePermissionUrls = [
                OWSFileSystem.appSharedDataDirectoryURL().appendingPathComponent(".com.apple.mobile_container_manager.metadata.plist")
            ]
            owsAssertDebug(knownNoWritePermissionUrls.contains(attemptedUrl))
            return false
        } catch {
            owsFailDebug("\(error.shortDescription)")
            return false
        }
    }
}

// MARK: - Remaining space

public extension OWSFileSystem {
    /// Get the remaining free space for a path's volume in bytes.
    ///
    /// See [Apple's example][0]. It checks "important" storage (versus "opportunistic" storage).
    ///
    /// [0]: https://developer.apple.com/documentation/foundation/nsurlresourcekey/checking_volume_storage_capacity
    class func freeSpaceInBytes(forPath path: URL) throws -> UInt64 {
        let resourceValues = try path.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])
        guard let result = resourceValues.volumeAvailableCapacityForImportantUsage else {
            throw OWSGenericError("Could not determine remaining disk space")
        }
        return UInt64(result)
    }
}

// MARK: - Creating Partial files

public extension OWSFileSystem {
    class func createTempFileSlice(url: URL, start: Int) throws -> (URL, Int) {
        // Resuming, slice attachment data in memory.
        let dataSliceFileUrl = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)

        // TODO: It'd be better if we could slice on disk.
        let entireFileData = try Data(contentsOf: url)
        let dataSlice = entireFileData.suffix(from: start)
        let dataSliceLength = dataSlice.count
        guard dataSliceLength + start == entireFileData.count else {
            throw OWSAssertionError("Could not slice the data.")
        }

        // Write the slice to a temporary file.
        try dataSlice.write(to: dataSliceFileUrl)

        return (dataSliceFileUrl, dataSliceLength)
    }
}
