package org.limewire.ui.compose.integration

import com.sun.jna.Callback
import com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.Pointer
import com.sun.jna.ptr.PointerByReference
import org.limewire.ui.compose.runOnUi
import org.limewire.ui.compose.tr
import org.limewire.util.OSUtils
import java.awt.EventQueue
import java.awt.GraphicsEnvironment
import java.awt.SystemTray
import java.awt.Toolkit
import java.awt.TrayIcon
import java.awt.image.BufferedImage
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger

class NativeDesktopNotificationService : DesktopNotificationService {
    private val provider: PlatformNotificationProvider = when {
        GraphicsEnvironment.isHeadless() -> UnsupportedNotificationProvider(
            provider = "Desktop",
            message = tr("Notifications are unavailable in headless mode.")
        )
        OSUtils.isMacOSX() -> MacUserNotificationProvider()
        OSUtils.isWindows() -> WindowsNativeNotificationProvider()
        OSUtils.isLinux() -> LinuxLibnotifyNotificationProvider()
        else -> UnsupportedNotificationProvider(
            provider = System.getProperty("os.name", "Desktop"),
            message = tr("Native notifications are not available on this platform.")
        )
    }

    override fun supportsNotifications(): Boolean = provider.capability().available

    override fun capability(): NotificationCapability = provider.capability()

    override fun showNotification(request: NotificationRequest): NotificationDeliveryResult {
        val sanitized = request.copy(
            title = request.title.ifBlank { "WireShare" },
            body = request.body.replace('\r', ' ').replace('\n', ' ').trim()
        )
        return provider.show(sanitized)
    }

    override fun showChatNotification(
        senderName: String,
        body: String,
        onOpen: () -> Unit,
        onReply: ((String) -> Unit)?,
        playSound: Boolean
    ): NotificationDeliveryResult {
        return showNotification(
            NotificationRequest(
                title = tr("Chat from {0}", senderName),
                body = body,
                onOpen = onOpen,
                playSound = playSound,
                replyPlaceholder = tr("Reply to {0}", senderName),
                onReply = onReply
            )
        )
    }

    override fun playAttentionSound() {
        EventQueue.invokeLater {
            runCatching { Toolkit.getDefaultToolkit().beep() }
        }
    }
}

private interface PlatformNotificationProvider {
    fun capability(): NotificationCapability
    fun show(request: NotificationRequest): NotificationDeliveryResult
}

private class UnsupportedNotificationProvider(
    private val provider: String,
    private val message: String
) : PlatformNotificationProvider {
    override fun capability(): NotificationCapability =
        NotificationCapability(
            provider = provider,
            available = false,
            supportsInlineReply = false,
            message = message
        )

    override fun show(request: NotificationRequest): NotificationDeliveryResult =
        NotificationDeliveryResult(
            status = NotificationDeliveryStatus.UNSUPPORTED,
            provider = provider,
            message = message
        )
}

private class MacUserNotificationProvider : PlatformNotificationProvider {
    private val providerName = "macOS Notification Centre"
    private val loaded = MacUserNotificationBridge.isAvailable()

    init {
        if (loaded) {
            MacUserNotificationBridge.initialize()
        }
    }

    override fun capability(): NotificationCapability {
        if (!loaded) {
            return NotificationCapability(
                provider = providerName,
                available = false,
                supportsInlineReply = false,
                message = tr("The macOS notification bridge could not be loaded.")
            )
        }
        return when (MacUserNotificationBridge.authorizationStatus()) {
            MacUserNotificationBridge.STATUS_DENIED -> NotificationCapability(
                provider = providerName,
                available = false,
                supportsInlineReply = true,
                message = tr("Notifications are disabled by macOS for WireShare.")
            )
            MacUserNotificationBridge.STATUS_NOT_DETERMINED -> NotificationCapability(
                provider = providerName,
                available = true,
                supportsInlineReply = true,
                message = tr("macOS will ask for notification permission the first time WireShare sends one.")
            )
            MacUserNotificationBridge.STATUS_AUTHORIZED,
            MacUserNotificationBridge.STATUS_PROVISIONAL,
            MacUserNotificationBridge.STATUS_EPHEMERAL -> NotificationCapability(
                provider = providerName,
                available = true,
                supportsInlineReply = true,
                message = tr("Native macOS notifications are available, including inline replies for chat.")
            )
            else -> NotificationCapability(
                provider = providerName,
                available = false,
                supportsInlineReply = false,
                message = tr("macOS notification status could not be read.")
            )
        }
    }

    override fun show(request: NotificationRequest): NotificationDeliveryResult {
        if (!loaded) {
            return NotificationDeliveryResult(
                status = NotificationDeliveryStatus.UNSUPPORTED,
                provider = providerName,
                message = tr("The macOS notification bridge could not be loaded.")
            )
        }
        val status = MacUserNotificationBridge.authorizationStatus()
        val authorized = when (status) {
            MacUserNotificationBridge.STATUS_AUTHORIZED,
            MacUserNotificationBridge.STATUS_PROVISIONAL,
            MacUserNotificationBridge.STATUS_EPHEMERAL -> true
            MacUserNotificationBridge.STATUS_NOT_DETERMINED ->
                MacUserNotificationBridge.requestAuthorization() == MacUserNotificationBridge.STATUS_AUTHORIZED
            else -> false
        }
        if (!authorized) {
            return NotificationDeliveryResult(
                status = NotificationDeliveryStatus.DENIED,
                provider = providerName,
                message = tr("macOS did not grant notification permission to WireShare.")
            )
        }
        val result = MacUserNotificationBridge.deliver(
            request.title,
            request.body,
            request.replyPlaceholder,
            request.playSound,
            { request.onOpen?.let { open -> runOnUi(open) } },
            request.onReply?.let { reply ->
                java.util.function.Consumer<String> { text ->
                    runOnUi { reply(text) }
                }
            }
        )
        return if (result == MacUserNotificationBridge.DELIVERED) {
            NotificationDeliveryResult(NotificationDeliveryStatus.DELIVERED, providerName)
        } else {
            NotificationDeliveryResult(
                status = NotificationDeliveryStatus.FAILED,
                provider = providerName,
                message = tr("macOS could not deliver the notification.")
            )
        }
    }
}

private class WindowsNativeNotificationProvider : PlatformNotificationProvider {
    private val providerName = "Windows notifications"
    private val tray = if (GraphicsEnvironment.isHeadless() || !SystemTray.isSupported()) null else SystemTray.getSystemTray()

    @Volatile
    private var trayIcon: TrayIcon? = null

    @Volatile
    private var notificationAction: (() -> Unit)? = null

    override fun capability(): NotificationCapability {
        if (tray == null) {
            return NotificationCapability(
                provider = providerName,
                available = false,
                supportsInlineReply = false,
                message = tr("Windows shell notifications are unavailable because the system tray is disabled.")
            )
        }
        return NotificationCapability(
            provider = providerName,
            available = true,
            supportsInlineReply = false,
            message = tr("Native Windows shell notifications are available. Inline replies require a Windows toast activator and are not yet available.")
        )
    }

    override fun show(request: NotificationRequest): NotificationDeliveryResult {
        val tray = tray ?: return NotificationDeliveryResult(
            status = NotificationDeliveryStatus.UNSUPPORTED,
            provider = providerName,
            message = tr("Windows shell notifications are unavailable because the system tray is disabled.")
        )
        val delivered = runCatching {
            runOnEventThreadAndWait {
                val icon = trayIcon ?: TrayIcon(
                    BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB),
                    "WireShare"
                ).apply {
                    isImageAutoSize = true
                    addActionListener {
                        val open = notificationAction
                        notificationAction = null
                        if (open != null) {
                            runOnUi(open)
                        }
                    }
                    tray.add(this)
                    trayIcon = this
                }
                notificationAction = request.onOpen
                icon.displayMessage(request.title, request.body, TrayIcon.MessageType.INFO)
            }
            true
        }.getOrDefault(false)
        return if (delivered) {
            NotificationDeliveryResult(NotificationDeliveryStatus.DELIVERED, providerName)
        } else {
            NotificationDeliveryResult(
                status = NotificationDeliveryStatus.FAILED,
                provider = providerName,
                message = tr("Windows could not display the notification.")
            )
        }
    }
}

private fun runOnEventThreadAndWait(block: () -> Unit) {
    if (EventQueue.isDispatchThread()) {
        block()
    } else {
        EventQueue.invokeAndWait { block() }
    }
}

private class LinuxLibnotifyNotificationProvider : PlatformNotificationProvider {
    private val providerName = "Freedesktop notifications"
    private val callbacks = ConcurrentHashMap<Int, LibNotifyActionCallback>()
    private val callbackIds = AtomicInteger(1)
    private val retainedNotifications = CopyOnWriteArrayList<Pointer>()
    private val notify: LibNotify? = runCatching {
        Native.load("notify", LibNotify::class.java) as LibNotify
    }.getOrNull()
    private val gObject: GObject? = runCatching {
        Native.load("gobject-2.0", GObject::class.java) as GObject
    }.getOrNull()
    private val initialized: Boolean = notify?.notify_init("WireShare") == true

    override fun capability(): NotificationCapability {
        if (!initialized || notify == null) {
            return NotificationCapability(
                provider = providerName,
                available = false,
                supportsInlineReply = false,
                message = tr("No native Linux notification service was found.")
            )
        }
        return NotificationCapability(
            provider = providerName,
            available = true,
            supportsInlineReply = false,
            message = tr("Native Linux notifications are available. Inline replies are not portable in the Freedesktop notification spec.")
        )
    }

    override fun show(request: NotificationRequest): NotificationDeliveryResult {
        val notify = notify ?: return NotificationDeliveryResult(
            status = NotificationDeliveryStatus.UNSUPPORTED,
            provider = providerName,
            message = tr("No native Linux notification service was found.")
        )
        if (!initialized) {
            return NotificationDeliveryResult(
                status = NotificationDeliveryStatus.UNSUPPORTED,
                provider = providerName,
                message = tr("No native Linux notification service was found.")
            )
        }
        val notification = notify.notify_notification_new(
            request.title,
            request.body,
            "org.teamhermes.WireShare"
        ) ?: return NotificationDeliveryResult(
            status = NotificationDeliveryStatus.FAILED,
            provider = providerName,
            message = tr("The Linux notification service rejected the notification.")
        )

        retainedNotifications += notification
        notify.notify_notification_set_timeout(notification, 10_000)
        request.onOpen?.let { open ->
            val token = callbackIds.getAndIncrement()
            val callback = LibNotifyActionCallback { _, _, _ ->
                callbacks.remove(token)
                retainedNotifications.remove(notification)
                gObject?.g_object_unref(notification)
                runOnUi(open)
            }
            callbacks[token] = callback
            notify.notify_notification_add_action(
                notification,
                "default",
                "Open",
                callback,
                Pointer.createConstant(token.toLong()),
                null
            )
        }
        val error = PointerByReference()
        val delivered = notify.notify_notification_show(notification, error)
        if (!delivered) {
            retainedNotifications.remove(notification)
            gObject?.g_object_unref(notification)
        }
        return if (delivered) {
            NotificationDeliveryResult(NotificationDeliveryStatus.DELIVERED, providerName)
        } else {
            NotificationDeliveryResult(
                status = NotificationDeliveryStatus.FAILED,
                provider = providerName,
                message = tr("The Linux notification service could not deliver the notification.")
            )
        }
    }

    private interface LibNotify : Library {
        fun notify_init(app_name: String): Boolean
        fun notify_notification_new(summary: String, body: String, icon: String): Pointer?
        fun notify_notification_set_timeout(notification: Pointer, timeout: Int)
        fun notify_notification_add_action(
            notification: Pointer,
            action: String,
            label: String,
            callback: LibNotifyActionCallback,
            user_data: Pointer?,
            free_func: Pointer?
        )
        fun notify_notification_show(notification: Pointer, error: PointerByReference): Boolean
    }

    private interface GObject : Library {
        fun g_object_unref(obj: Pointer)
    }
}

private fun interface LibNotifyActionCallback : Callback {
    fun invoke(notification: Pointer, action: String, userData: Pointer?)
}
