godot/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt

/**************************************************************************/
/*  FileAccessHandler.kt                                                  */
/**************************************************************************/
/*                         This file is part of:                          */
/*                             GODOT ENGINE                               */
/*                        https://godotengine.org                         */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
/*                                                                        */
/* Permission is hereby granted, free of charge, to any person obtaining  */
/* a copy of this software and associated documentation files (the        */
/* "Software"), to deal in the Software without restriction, including    */
/* without limitation the rights to use, copy, modify, merge, publish,    */
/* distribute, sublicense, and/or sell copies of the Software, and to     */
/* permit persons to whom the Software is furnished to do so, subject to  */
/* the following conditions:                                              */
/*                                                                        */
/* The above copyright notice and this permission notice shall be         */
/* included in all copies or substantial portions of the Software.        */
/*                                                                        */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
/**************************************************************************/

package org.godotengine.godot.io.file

import android.content.Context
import android.util.Log
import android.util.SparseArray
import org.godotengine.godot.error.Error
import org.godotengine.godot.io.StorageScope
import java.io.FileNotFoundException
import java.io.InputStream
import java.lang.UnsupportedOperationException
import java.nio.ByteBuffer

/**
 * Handles regular and media store file access and interactions.
 */
class FileAccessHandler(val context: Context) {

	companion object {
		private val TAG = FileAccessHandler::class.java.simpleName

		private const val INVALID_FILE_ID = 0
		private const val STARTING_FILE_ID = 1
		private val FILE_OPEN_FAILED = Pair(Error.FAILED, INVALID_FILE_ID)

		internal fun getInputStream(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): InputStream? {
			val storageScope = storageScopeIdentifier.identifyStorageScope(path)
			return try {
				path?.let {
					DataAccess.getInputStream(storageScope, context, path)
				}
			} catch (e: Exception) {
				null
			}
		}

		internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
			val storageScope = storageScopeIdentifier.identifyStorageScope(path)
			if (storageScope == StorageScope.UNKNOWN) {
				return false
			}

			return try {
				path?.let {
					DataAccess.fileExists(storageScope, context, it)
				} ?: false
			} catch (e: SecurityException) {
				false
			}
		}

		internal fun removeFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
			val storageScope = storageScopeIdentifier.identifyStorageScope(path)
			if (storageScope == StorageScope.UNKNOWN) {
				return false
			}

			return try {
				path?.let {
					DataAccess.removeFile(storageScope, context, it)
				} ?: false
			} catch (e: Exception) {
				false
			}
		}

		internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String, to: String): Boolean {
			val storageScope = storageScopeIdentifier.identifyStorageScope(from)
			if (storageScope == StorageScope.UNKNOWN) {
				return false
			}

			return try {
				DataAccess.renameFile(storageScope, context, from, to)
			} catch (e: Exception) {
				false
			}
		}
	}

	internal val storageScopeIdentifier = StorageScope.Identifier(context)
	private val files = SparseArray<DataAccess>()
	private var lastFileId = STARTING_FILE_ID

	private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0

	fun canAccess(filePath: String?): Boolean {
		return storageScopeIdentifier.canAccess(filePath)
	}

	/**
	 * Returns a positive (> 0) file id when the operation succeeds.
	 * Otherwise, returns a negative value of [Error].
	 */
	fun fileOpen(path: String?, modeFlags: Int): Int {
		val (fileError, fileId) = fileOpen(path, FileAccessFlags.fromNativeModeFlags(modeFlags))
		return if (fileError == Error.OK) {
			fileId
		} else {
			// Return the negative of the [Error#toNativeValue()] value to differentiate from the
			// positive file id.
			-fileError.toNativeValue()
		}
	}

	internal fun fileOpen(path: String?, accessFlag: FileAccessFlags?): Pair<Error, Int> {
		if (accessFlag == null) {
			return FILE_OPEN_FAILED
		}

		val storageScope = storageScopeIdentifier.identifyStorageScope(path)
		if (storageScope == StorageScope.UNKNOWN) {
			return FILE_OPEN_FAILED
		}

		return try {
			path?.let {
				val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return FILE_OPEN_FAILED

				files.put(++lastFileId, dataAccess)
				Pair(Error.OK, lastFileId)
			} ?: FILE_OPEN_FAILED
		} catch (e: FileNotFoundException) {
			Pair(Error.ERR_FILE_NOT_FOUND, INVALID_FILE_ID)
		} catch (e: UnsupportedOperationException) {
			Pair(Error.ERR_UNAVAILABLE, INVALID_FILE_ID)
		} catch (e: Exception) {
			Log.w(TAG, "Error while opening $path", e)
			FILE_OPEN_FAILED
		}
	}

	fun fileGetSize(fileId: Int): Long {
		if (!hasFileId(fileId)) {
			return 0L
		}

		return files[fileId].size()
	}

	fun fileSeek(fileId: Int, position: Long) {
		if (!hasFileId(fileId)) {
			return
		}

		files[fileId].seek(position)
	}

	fun fileSeekFromEnd(fileId: Int, position: Long) {
		if (!hasFileId(fileId)) {
			return
		}

		files[fileId].seekFromEnd(position)
	}

	fun fileRead(fileId: Int, byteBuffer: ByteBuffer?): Int {
		if (!hasFileId(fileId) || byteBuffer == null) {
			return 0
		}

		return files[fileId].read(byteBuffer)
	}

	fun fileWrite(fileId: Int, byteBuffer: ByteBuffer?) {
		if (!hasFileId(fileId) || byteBuffer == null) {
			return
		}

		files[fileId].write(byteBuffer)
	}

	fun fileFlush(fileId: Int) {
		if (!hasFileId(fileId)) {
			return
		}

		files[fileId].flush()
	}

	fun getInputStream(path: String?) = Companion.getInputStream(context, storageScopeIdentifier, path)

	fun renameFile(from: String, to: String) = Companion.renameFile(context, storageScopeIdentifier, from, to)

	fun fileExists(path: String?) = Companion.fileExists(context, storageScopeIdentifier, path)

	fun fileLastModified(filepath: String?): Long {
		val storageScope = storageScopeIdentifier.identifyStorageScope(filepath)
		if (storageScope == StorageScope.UNKNOWN) {
			return 0L
		}

		return try {
			filepath?.let {
				DataAccess.fileLastModified(storageScope, context, it)
			} ?: 0L
		} catch (e: SecurityException) {
			0L
		}
	}

	fun fileResize(fileId: Int, length: Long): Int {
		if (!hasFileId(fileId)) {
			return Error.FAILED.toNativeValue()
		}

		return files[fileId].resize(length).toNativeValue()
	}

	fun fileGetPosition(fileId: Int): Long {
		if (!hasFileId(fileId)) {
			return 0L
		}

		return files[fileId].position()
	}

	fun isFileEof(fileId: Int): Boolean {
		if (!hasFileId(fileId)) {
			return false
		}

		return files[fileId].endOfFile
	}

	fun setFileEof(fileId: Int, eof: Boolean) {
		val file = files[fileId] ?: return
		file.endOfFile = eof
	}

	fun fileClose(fileId: Int) {
		if (hasFileId(fileId)) {
			files[fileId].close()
			files.remove(fileId)
		}
	}
}