/*
 * Copyright 2019 Jeremy Jamet / Kunzisoft.
 *
 * This file is part of KeePassDX.
 *
 *  KeePassDX is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  KeePassDX is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with KeePassDX.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package com.kunzisoft.keepass.services

import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Intent
import android.net.Uri
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.ServiceCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.CopyOnWriteArrayList


class AttachmentFileNotificationService: LockNotificationService() {

    private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
    private var mDatabase: ContextualDatabase? = null
    private val mPendingCommands: MutableList<Intent?> = mutableListOf()

    override val notificationId: Int = 10000
    private val attachmentNotificationList = CopyOnWriteArrayList<AttachmentNotification>()

    private var mActionTaskBinder = ActionTaskBinder()
    private var mActionTaskListeners = mutableListOf<ActionTaskListener>()

    private val mainScope = CoroutineScope(Dispatchers.Main)

    override fun retrieveChannelId(): String {
        return CHANNEL_ATTACHMENT_ID
    }

    override fun retrieveChannelName(): String {
        return getString(R.string.entry_attachments)
    }

    inner class ActionTaskBinder: Binder() {

        fun getService(): AttachmentFileNotificationService = this@AttachmentFileNotificationService

        fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
            mActionTaskListeners.add(actionTaskListener)
        }

        fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
            mActionTaskListeners.remove(actionTaskListener)
        }
    }

    private val attachmentFileActionListener = object: AttachmentFileAction.AttachmentFileActionListener {
        override fun onUpdate(attachmentNotification: AttachmentNotification) {
            newNotification(attachmentNotification)
            mActionTaskListeners.forEach { actionListener ->
                actionListener.onAttachmentAction(attachmentNotification.uri,
                        attachmentNotification.entryAttachmentState)
            }
        }
    }

    override fun onCreate() {
        super.onCreate()

        mDatabaseTaskProvider = DatabaseTaskProvider(this)
        mDatabaseTaskProvider?.registerProgressTask()
        mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
            this.mDatabase = database
            // Execute each command in wait state
            val commandIterator = this.mPendingCommands.iterator()
            while (commandIterator.hasNext()) {
                val command = commandIterator.next()
                actionRequested(command)
                commandIterator.remove()
            }
        }
    }

    interface ActionTaskListener {
        fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState)
    }

    override fun onBind(intent: Intent): IBinder {
        return mActionTaskBinder
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        super.onStartCommand(intent, flags, startId)

        // Wait for database to execute action request
        if (mDatabase != null) {
            actionRequested(intent)
        } else {
            mPendingCommands.add(intent)
        }

        return START_REDELIVER_INTENT
    }

    private fun actionRequested(intent: Intent?) {

        val downloadFileUri: Uri? = if (intent?.hasExtra(FILE_URI_KEY) == true) {
            intent.getParcelableExtraCompat(FILE_URI_KEY)
        } else null

        when(intent?.action) {
            ACTION_ATTACHMENT_FILE_START_UPLOAD -> {
                actionStartUploadOrDownload(downloadFileUri,
                    intent,
                    StreamDirection.UPLOAD)
            }
            ACTION_ATTACHMENT_FILE_STOP_UPLOAD -> {
                actionStopUpload()
            }
            ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
                actionStartUploadOrDownload(downloadFileUri,
                    intent,
                    StreamDirection.DOWNLOAD)
            }
            ACTION_ATTACHMENT_REMOVE -> {
                intent.getParcelableExtraCompat<Attachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
                    attachmentNotificationList.firstOrNull { it.entryAttachmentState.attachment == entryAttachment }?.let { elementToRemove ->
                        attachmentNotificationList.remove(elementToRemove)
                    }
                }
            }
            else -> {
                if (downloadFileUri != null) {
                    attachmentNotificationList.firstOrNull { it.uri == downloadFileUri }?.let { elementToRemove ->
                        notificationManager?.cancel(elementToRemove.notificationId)
                        attachmentNotificationList.remove(elementToRemove)
                    }
                }
                if (attachmentNotificationList.isEmpty()) {
                    stopService()
                }
            }
        }
    }

    @Synchronized
    fun checkCurrentAttachmentProgress() {
        attachmentNotificationList.forEach { attachmentNotification ->
            mActionTaskListeners.forEach { actionListener ->
                actionListener.onAttachmentAction(
                        attachmentNotification.uri,
                        attachmentNotification.entryAttachmentState
                )
            }
        }
    }

    @Synchronized
    fun removeAttachmentAction(entryAttachment: EntryAttachmentState) {
        attachmentNotificationList.firstOrNull {
            it.entryAttachmentState == entryAttachment
        }?.let {
            attachmentNotificationList.remove(it)
        }
    }

    private fun newNotification(attachmentNotification: AttachmentNotification) {

        val pendingContentIntent = PendingIntent.getActivity(this,
            randomRequestCode(),
            Intent().apply {
                action = Intent.ACTION_VIEW
                setDataAndType(attachmentNotification.uri,
                        contentResolver.getType(attachmentNotification.uri))
                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
            } else {
                PendingIntent.FLAG_CANCEL_CURRENT
            }
        )

        val pendingDeleteIntent =  PendingIntent.getService(this,
            randomRequestCode(),
                Intent(this, AttachmentFileNotificationService::class.java).apply {
                    // No action to delete the service
                    putExtra(FILE_URI_KEY, attachmentNotification.uri)
                }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
            } else {
                PendingIntent.FLAG_CANCEL_CURRENT
            }
        )

        val fileName = attachmentNotification.uri.getDocumentFile(this)?.name
                ?: attachmentNotification.uri.path

        val builder = buildNewNotification().apply {
            when (attachmentNotification.entryAttachmentState.streamDirection) {
                StreamDirection.UPLOAD -> {
                    setSmallIcon(R.drawable.ic_file_upload_white_24dp)
                    setContentTitle(getString(R.string.upload_attachment, fileName))
                }
                StreamDirection.DOWNLOAD -> {
                    setSmallIcon(R.drawable.ic_file_download_white_24dp)
                    setContentTitle(getString(R.string.download_attachment, fileName))
                }
            }
            setAutoCancel(false)
            when (attachmentNotification.entryAttachmentState.downloadState) {
                AttachmentState.NULL, AttachmentState.START -> {
                    setContentText(getString(R.string.download_initialization))
                    setOngoing(true)
                }
                AttachmentState.IN_PROGRESS -> {
                    if (attachmentNotification.entryAttachmentState.downloadProgression > FILE_PROGRESSION_MAX) {
                        setContentText(getString(R.string.download_finalization))
                    } else {
                        setProgress(FILE_PROGRESSION_MAX,
                                attachmentNotification.entryAttachmentState.downloadProgression,
                                false)
                        setContentText(getString(R.string.download_progression,
                                attachmentNotification.entryAttachmentState.downloadProgression))
                    }
                    setOngoing(true)
                }
                AttachmentState.COMPLETE -> {
                    setContentText(getString(R.string.download_complete))
                    when (attachmentNotification.entryAttachmentState.streamDirection) {
                        StreamDirection.UPLOAD -> {

                        }
                        StreamDirection.DOWNLOAD -> {
                            setContentIntent(pendingContentIntent)
                        }
                    }
                    setDeleteIntent(pendingDeleteIntent)
                    setOngoing(false)
                }
                AttachmentState.CANCELED -> {
                    setContentText(getString(R.string.download_canceled))
                    setDeleteIntent(pendingDeleteIntent)
                    setOngoing(false)
                }
                AttachmentState.ERROR -> {
                    setContentText(getString(R.string.error_file_not_create))
                    setDeleteIntent(pendingDeleteIntent)
                    setOngoing(false)
                }
            }
        }
        when (attachmentNotification.entryAttachmentState.downloadState) {
            AttachmentState.COMPLETE,
            AttachmentState.CANCELED,
            AttachmentState.ERROR -> {
                ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
                try {
                    checkNotificationsPermission(this) {
                        notificationManager?.notify(
                            attachmentNotification.notificationId,
                            builder.build()
                        )
                    }
                } catch (e: SecurityException) {
                    Log.e(TAG, "Unable to notify the attachment state", e)
                }
            } else -> {
                startForegroundCompat(
                    attachmentNotification.notificationId,
                    builder,
                    NotificationServiceType.ATTACHMENT
                )
            }
        }
    }

    override fun onDestroy() {
        attachmentNotificationList.forEach { attachmentNotification ->
            attachmentNotification.attachmentFileAction?.cancel()
            attachmentNotification.attachmentFileAction?.listener = null
            notificationManager?.cancel(attachmentNotification.notificationId)
        }
        attachmentNotificationList.clear()
        mDatabaseTaskProvider?.unregisterProgressTask()

        super.onDestroy()
    }

    private data class AttachmentNotification(var uri: Uri,
                                              var notificationId: Int,
                                              var entryAttachmentState: EntryAttachmentState,
                                              var attachmentFileAction: AttachmentFileAction? = null) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as AttachmentNotification

            if (notificationId != other.notificationId) return false

            return true
        }

        override fun hashCode(): Int {
            return notificationId
        }
    }

    private fun actionStartUploadOrDownload(fileUri: Uri?,
                                            intent: Intent,
                                            streamDirection: StreamDirection) {
        if (fileUri != null
                && intent.hasExtra(ATTACHMENT_KEY)) {
            try {
                intent.getParcelableExtraCompat<Attachment>(ATTACHMENT_KEY)?.let { entryAttachment ->

                    val nextNotificationId = (attachmentNotificationList.maxByOrNull { it.notificationId }
                            ?.notificationId ?: notificationId) + 1
                    val entryAttachmentState = EntryAttachmentState(entryAttachment, streamDirection)
                    val attachmentNotification = AttachmentNotification(fileUri, nextNotificationId, entryAttachmentState)

                    // Add action to the list on start
                    attachmentNotificationList.add(attachmentNotification)

                    mDatabase?.let { database ->
                        mainScope.launch {
                            AttachmentFileAction(attachmentNotification,
                                    database,
                                    contentResolver).apply {
                                listener = attachmentFileActionListener
                            }.executeAction()
                        }
                    }
                }
            } catch (e: Exception) {
                Log.e(TAG, "Unable to upload/download $fileUri", e)
            }
        }
    }

    private fun actionStopUpload() {
        try {
            // Stop each upload
            attachmentNotificationList.filter {
                it.entryAttachmentState.streamDirection == StreamDirection.UPLOAD
            }.forEach {
                it.attachmentFileAction?.cancel()
            }
        } catch (e: Exception) {
            Log.e(TAG, "Unable to stop upload", e)
        }
    }

    private class AttachmentFileAction(
        private val attachmentNotification: AttachmentNotification,
        private val database: ContextualDatabase,
        private val contentResolver: ContentResolver) {

        private val updateMinFrequency = 1000
        private var previousSaveTime = System.currentTimeMillis()
        var listener: AttachmentFileActionListener? = null

        interface AttachmentFileActionListener {
            fun onUpdate(attachmentNotification: AttachmentNotification)
        }

        suspend fun executeAction() {

            // on pre execute
            CoroutineScope(Dispatchers.Main).launch {
                attachmentNotification.attachmentFileAction = this@AttachmentFileAction
                attachmentNotification.entryAttachmentState.apply {
                    downloadState = AttachmentState.START
                    downloadProgression = 0
                }
                listener?.onUpdate(attachmentNotification)
            }

            withContext(Dispatchers.IO) {
                // on Progress with thread
                val asyncAction = launch {
                    attachmentNotification.entryAttachmentState.apply {
                        try {
                                downloadState = AttachmentState.IN_PROGRESS

                                when (streamDirection) {
                                    StreamDirection.UPLOAD -> {
                                        BinaryDatabaseManager.uploadToDatabase(
                                                database,
                                                attachmentNotification.uri,
                                                attachment.binaryData,
                                                contentResolver,
                                                { percent ->
                                                    publishProgress(percent)
                                                },
                                                { // Cancellation
                                                    downloadState == AttachmentState.CANCELED
                                                }
                                        )
                                    }
                                    StreamDirection.DOWNLOAD -> {
                                        BinaryDatabaseManager.downloadFromDatabase(
                                                database,
                                                attachmentNotification.uri,
                                                attachment.binaryData,
                                                contentResolver,
                                                { percent ->
                                                    publishProgress(percent)
                                                }
                                        )
                                    }
                                }
                        } catch (e: Exception) {
                            e.printStackTrace()
                            downloadState = AttachmentState.ERROR
                        }
                    }
                    attachmentNotification.entryAttachmentState.downloadState
                }

                // on post execute
                withContext(Dispatchers.Main) {
                    asyncAction.join()
                    attachmentNotification.entryAttachmentState.apply {
                        if (downloadState != AttachmentState.CANCELED
                                && downloadState != AttachmentState.ERROR) {
                            downloadState = AttachmentState.COMPLETE
                            downloadProgression = FILE_PROGRESSION_MAX
                        }
                    }
                    attachmentNotification.attachmentFileAction = null
                    listener?.onUpdate(attachmentNotification)
                }

            }
        }

        fun cancel() {
            attachmentNotification.entryAttachmentState.downloadState = AttachmentState.CANCELED
        }

        private fun publishProgress(percent: Int) {
            // Publish progress
            val currentTime = System.currentTimeMillis()
            if (previousSaveTime + updateMinFrequency < currentTime) {
                attachmentNotification.entryAttachmentState.apply {
                    if (downloadState != AttachmentState.CANCELED) {
                        downloadState = AttachmentState.IN_PROGRESS
                    }
                    downloadProgression = percent
                }
                CoroutineScope(Dispatchers.Main).launch {
                    listener?.onUpdate(attachmentNotification)
                    Log.d(TAG, "Download file ${attachmentNotification.uri} : $percent%")
                }
                previousSaveTime = currentTime
            }
        }

        companion object {
            private val TAG = AttachmentFileAction::class.java.name
        }
    }

    companion object {
        private val TAG = AttachmentFileNotificationService::javaClass.name

        private const val CHANNEL_ATTACHMENT_ID = "com.kunzisoft.keepass.notification.channel.attachment"

        const val ACTION_ATTACHMENT_FILE_START_UPLOAD = "ACTION_ATTACHMENT_FILE_START_UPLOAD"
        const val ACTION_ATTACHMENT_FILE_STOP_UPLOAD = "ACTION_ATTACHMENT_FILE_STOP_UPLOAD"
        const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD"
        const val ACTION_ATTACHMENT_REMOVE = "ACTION_ATTACHMENT_REMOVE"

        const val FILE_URI_KEY = "FILE_URI_KEY"
        const val ATTACHMENT_KEY = "ATTACHMENT_KEY"

        const val FILE_PROGRESSION_MAX = 100
    }

}