@file:Suppress("TooManyFunctions")

package co.electriccoin.zcash.spackle.io

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.util.UUID

suspend fun File.existsSuspend() =
    withContext(Dispatchers.IO) {
        exists()
    }

suspend fun File.mkdirsSuspend() =
    withContext(Dispatchers.IO) {
        mkdirs()
    }

suspend fun File.isDirectorySuspend() =
    withContext(Dispatchers.IO) {
        isDirectory
    }

suspend fun File.isFileSuspend() =
    withContext(Dispatchers.IO) {
        isFile
    }

suspend fun File.canWriteSuspend() =
    withContext(Dispatchers.IO) {
        canWrite()
    }

suspend fun File.deleteSuspend() =
    withContext(Dispatchers.IO) {
        delete()
    }

suspend fun File.renameToSuspend(destination: File) =
    withContext(Dispatchers.IO) {
        renameTo(destination)
    }

suspend fun File.listFilesSuspend(): Array<out File>? =
    withContext(Dispatchers.IO) {
        listFiles()
    }

/**
 * Given an ultimate output file destination, this generates a temporary file that [action] can write to.  After action
 * is complete, the temp file is renamed to the expected output destination.  Depending on the underlying filesystem,
 * this should effectively ensure that the file is perceived as being written atomically.
 *
 * @receiver Ultimate file that we desire to write to.  Must be a file and not a directory.
 * @param action Action to perform on the file, specifically this should be writing data.  This action should not
 * delete, rename, or do other operations in the filesystem.
 */
suspend fun File.writeAtomically(action: (suspend (File) -> Unit)) {
    val tempFile =
        withContext(Dispatchers.IO) {
            File(parentFile, name.newTempFileName()).also {
                it.deleteOnExit()
            }
        }

    var isWriteSuccessful = false

    try {
        action(tempFile)
        isWriteSuccessful = true
    } catch (
        @Suppress("TooGenericExceptionCaught") e: Exception
    ) {
        tempFile.deleteSuspend()
        throw e
    } finally {
        if (isWriteSuccessful) {
            tempFile.moveTo(destination = this)
        }
    }
}

private suspend fun File.moveTo(destination: File) {
    val isRenameSuccessful = renameToSuspend(destination)

    if (!isRenameSuccessful) {
        if (existsSuspend()) {
            throw IOException("Couldn't move file $path to ${destination.path}")
        }

        // Otherwise no data was written, so there's no file to rename.
    }
}

// Note that adding uuid and .tmp could theoretically go past file name length limits on some filesystems
private fun String.newTempFileName() = "$this-${UUID.randomUUID()}.tmp"
