godot/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt

/**************************************************************************/
/*  DirectoryAccessHandler.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.directory

import android.content.Context
import android.util.Log
import org.godotengine.godot.Godot
import org.godotengine.godot.io.StorageScope
import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES

/**
 * Handles files and directories access and manipulation for the Android platform
 */
class DirectoryAccessHandler(context: Context) {

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

		internal const val INVALID_DIR_ID = -1
		internal const val STARTING_DIR_ID = 1
	}

	private enum class AccessType(val nativeValue: Int) {
		ACCESS_RESOURCES(0),

		/**
		 * Maps to [ACCESS_FILESYSTEM]
		 */
		ACCESS_USERDATA(1),
		ACCESS_FILESYSTEM(2);

		fun generateDirAccessId(dirId: Int) = (dirId * DIR_ACCESS_ID_MULTIPLIER) + nativeValue

		companion object {
			const val DIR_ACCESS_ID_MULTIPLIER = 10

			fun fromDirAccessId(dirAccessId: Int): Pair<AccessType?, Int> {
				val nativeValue = dirAccessId % DIR_ACCESS_ID_MULTIPLIER
				val dirId = dirAccessId / DIR_ACCESS_ID_MULTIPLIER
				return Pair(fromNative(nativeValue), dirId)
			}

			private fun fromNative(nativeAccessType: Int): AccessType? {
				for (accessType in entries) {
					if (accessType.nativeValue == nativeAccessType) {
						return accessType
					}
				}
				return null
			}

			fun fromNative(nativeAccessType: Int, storageScope: StorageScope? = null): AccessType? {
				val accessType = fromNative(nativeAccessType)
				if (accessType == null) {
					Log.w(TAG, "Unsupported access type $nativeAccessType")
					return null
				}

				// 'Resources' access type takes precedence as it is simple to handle:
				// if we receive a 'Resources' access type and this is a template build,
				// we provide a 'Resources' directory handler.
				// If this is an editor build, 'Resources' refers to the opened project resources
				// and so we provide a 'Filesystem' directory handler.
				if (accessType == ACCESS_RESOURCES) {
					return if (Godot.isEditorBuild()) {
						ACCESS_FILESYSTEM
					} else {
						ACCESS_RESOURCES
					}
				} else {
					// We've received a 'Filesystem' or 'Userdata' access type. On Android, this
					// may refer to:
					// - assets directory (path has 'assets:/' prefix)
					// - app directories
					// - device shared directories
					// As such we check the storage scope (if available) to figure what type of
					// directory handler to provide
					if (storageScope != null) {
						val accessTypeFromStorageScope = when (storageScope) {
							StorageScope.ASSETS -> ACCESS_RESOURCES
							StorageScope.APP, StorageScope.SHARED -> ACCESS_FILESYSTEM
							StorageScope.UNKNOWN -> null
						}

						if (accessTypeFromStorageScope != null) {
							return accessTypeFromStorageScope
						}
					}
					// If we're not able to infer the type of directory handler from the storage
					// scope, we fall-back to the 'Filesystem' directory handler as it's the default
					// for the 'Filesystem' access type.
					// Note that ACCESS_USERDATA also maps to ACCESS_FILESYSTEM
					return ACCESS_FILESYSTEM
				}
			}
		}
	}

	internal interface DirectoryAccess {
		fun dirOpen(path: String): Int
		fun dirNext(dirId: Int): String
		fun dirClose(dirId: Int)
		fun dirIsDir(dirId: Int): Boolean
		fun dirExists(path: String): Boolean
		fun fileExists(path: String): Boolean
		fun hasDirId(dirId: Int): Boolean
		fun isCurrentHidden(dirId: Int): Boolean
		fun getDriveCount() : Int
		fun getDrive(drive: Int): String
		fun makeDir(dir: String): Boolean
		fun getSpaceLeft(): Long
		fun rename(from: String, to: String): Boolean
		fun remove(filename: String): Boolean
	}

	private val storageScopeIdentifier = StorageScope.Identifier(context)

	private val assetsDirAccess = AssetsDirectoryAccess(context)
	private val fileSystemDirAccess = FilesystemDirectoryAccess(context, storageScopeIdentifier)

	fun assetsFileExists(assetsPath: String) = assetsDirAccess.fileExists(assetsPath)
	fun filesystemFileExists(path: String) = fileSystemDirAccess.fileExists(path)

	private fun hasDirId(accessType: AccessType, dirId: Int): Boolean {
		return when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId)
			else -> fileSystemDirAccess.hasDirId(dirId)
		}
	}

	fun dirOpen(nativeAccessType: Int, path: String?): Int {
		if (path == null) {
			return INVALID_DIR_ID
		}

		val storageScope = storageScopeIdentifier.identifyStorageScope(path)
		val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return INVALID_DIR_ID

		val dirId = when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path)
			else -> fileSystemDirAccess.dirOpen(path)
		}
		if (dirId == INVALID_DIR_ID) {
			return INVALID_DIR_ID
		}

		val dirAccessId = accessType.generateDirAccessId(dirId)
		return dirAccessId
	}

	fun dirNext(dirAccessId: Int): String {
		val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
		if (accessType == null || !hasDirId(accessType, dirId)) {
			Log.w(TAG, "dirNext: Invalid dir id: $dirId")
			return ""
		}

		return when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId)
			else -> fileSystemDirAccess.dirNext(dirId)
		}
	}

	fun dirClose(dirAccessId: Int) {
		val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
		if (accessType == null || !hasDirId(accessType, dirId)) {
			Log.w(TAG, "dirClose: Invalid dir id: $dirId")
			return
		}

		when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId)
			else -> fileSystemDirAccess.dirClose(dirId)
		}
	}

	fun dirIsDir(dirAccessId: Int): Boolean {
		val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
		if (accessType == null || !hasDirId(accessType, dirId)) {
			Log.w(TAG, "dirIsDir: Invalid dir id: $dirId")
			return false
		}

		return when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId)
			else -> fileSystemDirAccess.dirIsDir(dirId)
		}
	}

	fun isCurrentHidden(dirAccessId: Int): Boolean {
		val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
		if (accessType == null || !hasDirId(accessType, dirId)) {
			return false
		}

		return when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId)
			else -> fileSystemDirAccess.isCurrentHidden(dirId)
		}
	}

	fun dirExists(nativeAccessType: Int, path: String?): Boolean {
		if (path == null) {
			return false
		}

		val storageScope = storageScopeIdentifier.identifyStorageScope(path)
		val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false

		return when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.dirExists(path)
			else -> fileSystemDirAccess.dirExists(path)
		}
	}

	fun fileExists(nativeAccessType: Int, path: String?): Boolean {
		if (path == null) {
			return false
		}

		val storageScope = storageScopeIdentifier.identifyStorageScope(path)
		val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false

		return when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.fileExists(path)
			else -> fileSystemDirAccess.fileExists(path)
		}
	}

	fun getDriveCount(nativeAccessType: Int): Int {
		val accessType = AccessType.fromNative(nativeAccessType) ?: return 0
		return when(accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.getDriveCount()
			else -> fileSystemDirAccess.getDriveCount()
		}
	}

	fun getDrive(nativeAccessType: Int, drive: Int): String {
		val accessType = AccessType.fromNative(nativeAccessType) ?: return ""
		return when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive)
			else -> fileSystemDirAccess.getDrive(drive)
		}
	}

	fun makeDir(nativeAccessType: Int, dir: String?): Boolean {
		if (dir == null) {
			return false
		}

		val storageScope = storageScopeIdentifier.identifyStorageScope(dir)
		val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false

		return when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir)
			else -> fileSystemDirAccess.makeDir(dir)
		}
	}

	fun getSpaceLeft(nativeAccessType: Int): Long {
		val accessType = AccessType.fromNative(nativeAccessType) ?: return 0L
		return when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft()
			else -> fileSystemDirAccess.getSpaceLeft()
		}
	}

	fun rename(nativeAccessType: Int, from: String, to: String): Boolean {
		val accessType = AccessType.fromNative(nativeAccessType) ?: return false
		return when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.rename(from, to)
			else -> fileSystemDirAccess.rename(from, to)
		}
	}

	fun remove(nativeAccessType: Int, filename: String?): Boolean {
		if (filename == null) {
			return false
		}

		val storageScope = storageScopeIdentifier.identifyStorageScope(filename)
		val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
		return when (accessType) {
			ACCESS_RESOURCES -> assetsDirAccess.remove(filename)
			else -> fileSystemDirAccess.remove(filename)
		}
	}

}