/*
 * StorageFile.kt
 * Copyright (C) 2009-2022 Ultrasonic developers
 *
 * Distributed under terms of the GNU GPLv3 license.
 */

package org.moire.ultrasonic.util

import android.content.res.AssetFileDescriptor
import android.net.Uri
import android.provider.DocumentsContract
import android.webkit.MimeTypeMap
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.ConcurrentHashMap
import org.moire.ultrasonic.app.UApp
import timber.log.Timber

/**
 * The DocumentsContract based implementation of AbstractFile
 * This class is used when a user selected directory is set as media storage
 */
class StorageFile(
    override val parent: StorageFile?,
    var uri: Uri,
    override val name: String,
    override val isDirectory: Boolean
) : AbstractFile() {
    override val isFile: Boolean = !isDirectory

    override val length: Long
        get() {
            try {
                val resolver = UApp.applicationContext().contentResolver
                val column = arrayOf(DocumentsContract.Document.COLUMN_SIZE)
                resolver.query(uri, column, null, null, null)?.use { cursor ->
                    if (cursor.moveToFirst()) {
                        return cursor.getLong(0)
                    }
                }
            } catch (_: IllegalArgumentException) {
                Timber.i("Tried to get length of $uri but it probably doesn't exists")
            }
            return 0
        }

    override val lastModified: Long
        get() {
            try {
                val resolver = UApp.applicationContext().contentResolver
                val column = arrayOf(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
                resolver.query(uri, column, null, null, null)?.use { cursor ->
                    if (cursor.moveToFirst()) {
                        return cursor.getLong(0)
                    }
                }
            } catch (_: IllegalArgumentException) {
                Timber.i("Tried to get length of $uri but it probably doesn't exists")
            }
            return 0
        }

    override val path: String
        get() {
            // We can't assume that the file's Uri is related to its path,
            // so we generate our own path by concatenating the names on the path.
            if (parent != null) return parent.path + "/" + name
            return uri.toString()
        }

    override fun delete(): Boolean {
        val deleted = DocumentsContract.deleteDocument(
            UApp.applicationContext().contentResolver,
            uri
        )
        if (!deleted) return false
        storageFilePathDictionary.remove(path)
        notExistingPathDictionary.putIfAbsent(path, path)
        return true
    }

    override fun listFiles(): Array<AbstractFile> {
        return getChildren().toTypedArray()
    }

    override fun getFileOutputStream(append: Boolean): OutputStream {
        val mode = if (append) "wa" else "w"
        return UApp.applicationContext().contentResolver.openOutputStream(
            uri,
            mode
        ) ?: throw IOException("Couldn't retrieve OutputStream")
    }

    override fun getFileInputStream(): InputStream {
        return UApp.applicationContext().contentResolver.openInputStream(uri)
            ?: throw IOException("Couldn't retrieve InputStream")
    }

    override fun getDocumentFileDescriptor(openMode: String): AssetFileDescriptor? {
        return UApp.applicationContext().contentResolver.openAssetFileDescriptor(
            uri,
            openMode
        )
    }

    @Synchronized
    override fun getOrCreateFileFromPath(path: String): AbstractFile {
        if (storageFilePathDictionary.containsKey(path)) {
            return storageFilePathDictionary[path]!!
        }

        val parent = getStorageFileForParentDirectory(path)
            ?: throw IOException("Parent directory doesn't exist")

        val name = FileUtil.getNameFromPath(path)
        val file = getFromParentAndName(parent, name)

        storageFilePathDictionary[path] = file
        notExistingPathDictionary.remove(path)
        return file
    }

    override fun isPathExists(path: String): Boolean {
        return getFromPath(path) != null
    }

    override fun getFromPath(path: String): StorageFile? {
        if (storageFilePathDictionary.containsKey(path)) {
            return storageFilePathDictionary[path]!!
        }
        if (notExistingPathDictionary.contains(path)) return null

        val parent = getStorageFileForParentDirectory(path)
        if (parent == null) {
            notExistingPathDictionary.putIfAbsent(path, path)
            return null
        }

        val fileName = FileUtil.getNameFromPath(path)
        var file: StorageFile? = null

        parent.listFiles().forEach {
            if (it.name == fileName) file = it as StorageFile
            storageFilePathDictionary[it.path] = it as StorageFile
            notExistingPathDictionary.remove(it.path)
        }

        if (file == null) {
            notExistingPathDictionary.putIfAbsent(path, path)
            return null
        }

        return file
    }

    @Synchronized
    override fun createDirsOnPath(path: String) {
        val segments = getUriSegments(path)
            ?: throw IOException("Can't get path because the root has changed")

        var file = Storage.mediaRoot.value as StorageFile
        segments.forEach { segment ->
            val foundFile = file.listFiles().singleOrNull { it.name == segment }
            if (foundFile != null) {
                file = foundFile as StorageFile
            } else {
                val createdUri = DocumentsContract.createDocument(
                    UApp.applicationContext().contentResolver,
                    file.uri,
                    DocumentsContract.Document.MIME_TYPE_DIR,
                    segment
                ) ?: throw IOException("Can't create directory")

                file = StorageFile(file, createdUri, segment, true)
            }
            notExistingPathDictionary.remove(file.path)
        }
    }

    @Synchronized
    override fun rename(pathFrom: AbstractFile, pathTo: String) {
        val fileFrom = pathFrom as StorageFile
        if (!fileFrom.exists()) throw IOException("File to rename doesn't exist")
        Timber.i("Renaming from %s to %s", fileFrom.path, pathTo)

        val parentTo = getFromPath(FileUtil.getParentPath(pathTo)!!)
            ?: throw IOException("Destination folder doesn't exist")
        val fileTo = getFromParentAndName(parentTo, FileUtil.getNameFromPath(pathTo))

        copyFileContents(fileFrom, fileTo)
        fileFrom.delete()

        notExistingPathDictionary.remove(pathTo)
        storageFilePathDictionary.remove(fileFrom.path)
    }

    private fun exists(): Boolean {
        val resolver = UApp.applicationContext().contentResolver
        val column = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
        resolver.query(uri, column, null, null, null)?.use { cursor ->
            if (cursor.count != 0) return true
        }
        return false
    }

    private fun getChildren(): List<StorageFile> {
        val resolver = UApp.applicationContext().contentResolver
        val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
            uri,
            DocumentsContract.getDocumentId(uri)
        )

        return resolver.query(childrenUri, columns, null, null, null)?.use { cursor ->
            val result = mutableListOf<StorageFile>()

            while (cursor.moveToNext()) {
                val documentId = cursor.getString(0)
                val displayName = cursor.getString(1)
                val mimeType = cursor.getString(2)

                val documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId)

                val storageFile = StorageFile(
                    this,
                    documentUri,
                    displayName,
                    (mimeType == DocumentsContract.Document.MIME_TYPE_DIR)
                )

                result += storageFile
            }
            return@use result
        } ?: emptyList()
    }

    companion object {
        // These caches are necessary because SAF is very slow.
        // Ultrasonic assumes that the files won't change while it is in the foreground.
        // TODO to really handle concurrency we'd need API24.
        // If this isn't good enough we can add locking.
        val storageFilePathDictionary = ConcurrentHashMap<String, StorageFile>()
        val notExistingPathDictionary = ConcurrentHashMap<String, String>()

        val mimeTypeMap: MimeTypeMap = MimeTypeMap.getSingleton()

        private val columns = arrayOf(
            DocumentsContract.Document.COLUMN_DOCUMENT_ID,
            DocumentsContract.Document.COLUMN_DISPLAY_NAME,
            DocumentsContract.Document.COLUMN_MIME_TYPE
        )

        private fun copyFileContents(sourceFile: AbstractFile, destinationFile: AbstractFile) {
            sourceFile.getFileInputStream().use { inputStream ->
                destinationFile.getFileOutputStream(false).use { outputStream ->
                    inputStream.copyInto(outputStream)
                }
            }
        }

        private fun getFromParentAndName(parent: StorageFile, name: String): StorageFile {
            val foundFile = parent.listFiles().firstOrNull { it.name == name }

            if (foundFile != null) return foundFile as StorageFile

            val createdUri = DocumentsContract.createDocument(
                UApp.applicationContext().contentResolver,
                parent.uri,
                "*/*",
                name
            ) ?: throw IOException("Can't create file")

            return StorageFile(parent, createdUri, name, false)
        }

        private fun getStorageFileForParentDirectory(path: String): StorageFile? {
            val parentPath = FileUtil.getParentPath(path) ?: return null
            if (storageFilePathDictionary.containsKey(parentPath)) {
                return storageFilePathDictionary[parentPath]!!
            }
            if (notExistingPathDictionary.contains(parentPath)) return null

            val parent = findStorageFileForParentDirectory(parentPath)

            if (parent == null) {
                storageFilePathDictionary.remove(parentPath)
                notExistingPathDictionary.putIfAbsent(parentPath, parentPath)
            } else {
                storageFilePathDictionary[parentPath] = parent
                notExistingPathDictionary.remove(parentPath)
            }

            return parent
        }

        @Suppress("NestedBlockDepth")
        private fun findStorageFileForParentDirectory(path: String): StorageFile? {
            val segments = getUriSegments(path)
                ?: throw IOException("Can't get path because the root has changed")

            var file = Storage.mediaRoot.value as StorageFile
            segments.forEach { segment ->
                val currentPath = file.path + "/" + segment
                if (notExistingPathDictionary.contains(currentPath)) return null
                if (storageFilePathDictionary.containsKey(currentPath)) {
                    file = storageFilePathDictionary[currentPath]!!
                } else {
                    var foundFile: StorageFile? = null
                    file.listFiles().forEach {
                        if (it.name == segment) foundFile = it as StorageFile
                        storageFilePathDictionary[it.path] = it as StorageFile
                        notExistingPathDictionary.remove(it.path)
                    }

                    if (foundFile == null) {
                        notExistingPathDictionary.putIfAbsent(path, path)
                        return null
                    }

                    file = foundFile!!
                }
            }
            return file
        }

        private fun getUriSegments(uri: String): List<String>? {
            val rootPath = Storage.mediaRoot.value.path
            if (!uri.startsWith(rootPath)) return null
            val pathWithoutRoot = uri.substringAfter(rootPath)
            return pathWithoutRoot.split('/').filter { it.isNotEmpty() }
        }
    }
}

fun String.extension(): String {
    val index = this.indexOfLast { ch -> ch == '.' }
    if (index == -1) return ""
    if (index == this.lastIndex) return ""
    return this.substring(index + 1)
}

fun String.withoutExtension(): String {
    val index = this.indexOfLast { ch -> ch == '.' }
    if (index == -1) return this
    return this.substring(0, index)
}

fun InputStream.copyInto(outputStream: OutputStream) {
    var read: Int
    val buffer = ByteArray(DEFAULT_BUFFER_SIZE)

    while (true) {
        read = this.read(buffer)
        if (read == -1) {
            break
        }
        outputStream.write(buffer, 0, read)
    }
}
